Общая информация
Рутокен MFA — это семейство USB-токенов, предназначенных для аутентификации в веб-приложениях и поддерживающих режим беспарольной защиты («passkey»). Данное семейство продуктов обладает рядом ключевых преимуществ:
- не требует драйверов и дополнительного ПО;
- обеспечивает высокий уровень безопасности благодаря строгой аутентификации:
- использует асимметричную криптографию;
- защищает от фишинга и атак Man-in-the-middle (MitM);
- исключает передачу паролей по сети интернет;
- простота использования для обычных пользователей;
- один аутентификатор используется для множества ресурсов.
Стандарты и компоненты
В основе Рутокен MFA лежит набор стандартов FIDO2, который включает в себя следующие спецификации:
к ключам безопасности (CTAP), текущая версия CTAP 2.1;
- к серверу и браузеру (W3C WebAuthn), текущая версия — 2.
Разница между стандартами WebAuthn и CTAP показана на рисунке ниже.
Веб-разработчикам не требуется использовать спецификацию CTAP. Вместо этого следует использовать API WebAuthn, который поддерживается всеми основными браузерами.
В процессах регистрации ключа и аутентификации пользователя задействованы следующие компоненты FIDO2:
- аутентификатор FIDO2 (ключ безопасности, подключается по USB или NFC). В данном случае — это Рутокен MFA;
- клиент FIDO2 — прикладное ПО, которое взаимодействует с аутентификатором. Это может быть операционная система (Windows, iOS, Android), браузер (см. список браузеров, поддерживающих FIDO2 ниже) или клиентское приложение (например, SSH-клиент или PAM-модуль);
- сервер FIDO2 — веб-сервер, поддерживающий спецификацию WebAuthn. Реализует операции регистрации и аутентификации пользователя (подробнее в разделе FIDO2 (Рутокен MFA)#Инструменты для интеграции).
Браузеры, поддерживающие FIDO2
- Google Chrome 67+
- Mozilla Firefox 60+
- Microsoft Edge 18+
- Apple Safari 13+
- Яндекс Браузер 16.7+
Cхема аутентификации
В схему аутентификации FIDO входит процесс регистрации ключа и процесс аутентификации пользователя.
Процесс регистрации ключа
- Пользователь выбирает регистрацию на веб-ресурсе и инициирует обмен данными.
- Сервер отправляет пользователю случайную последовательность (challenge), а также идентификатор веб-ресурса (доменное имя сервера). Браузер взаимодействует с ключом безопасности, токен проверяет данные, а также создает идентификатор пользователя и ключевую пару (открытый и закрытый ключ).
- Случайная последовательность подписывается закрытым ключом и передается вместе с открытым ключом и другими данными на сервер. Сервер:
- Расшифровывает данные открытым ключом.
- Проверяет, что полученная от пользователя и отправленная ему ранее случайная последовательность совпадают.
- Устанавливает счетчик операций для этого пользователя в 0.
- Сохраняет все данные, включая открытый ключ и идентификатор пользователя.
Ключ безопасности привязан к пользователю.
Процесс аутентификации пользователя
- Сервер отправляет ключу безопасности через браузер перечень идентификаторов учетных записей, а также случайную последовательность для подписи. Ключ безопасности находит идентификатор учетной записи в своей памяти, подписывает случайную числовую последовательность соответствующим закрытым ключом пользователя и увеличивает значение счетчика операций.
- Данные передаются серверу. Сервер:
- Расшифровывает случайную последовательность.
- Проверяет значение счетчика и соответствие открытого ключа данной учетной записи.
- Сохраняет новое значение счетчика и аутентифицирует пользователя в его учетную запись.
Аутентификация пользователя прошла успешно.
Особенности интеграции FIDO
Поддержка аутентификаторов по стандарту FIDO может быть реализована непосредственно или опосредованно через Identity & Access Management (IAM)/Identity Provider (IdP).
Непосредственная интеграция аутентификаторов
С помощью стандарта CTAP разработчики могут использовать различные варианты аутентификации FIDO для защиты своих приложений:
используйте WebAuthn API (Web Authentication: An API for accessing Public Key Credentials - Level 2), если приложение является веб-приложением и пользователь работает с веб-браузером в качестве клиента;
используйте API-платформы, если приложение является настольным или мобильным и работает на платформе, которая предлагает такой API. В качестве примера можно привести API WebAuthn в Microsoft Windows (API WebAuthn | Microsoft Learn);
используйте библиотеку, реализующую протокол CTAP, если приложение является настольным или мобильным и API платформы недоступно или не подходит для решаемой задачи (GitHub - Yubico/libfido2: Provides library functionality for FIDO2, including communication with a device over USB or NFC).
Архитектура для реализации непосредственной интеграции с ресурсом показана на схеме:
Для непосредственной интеграции в веб-порталах используются следующие компоненты архитектуры FIDO2:
- Client-Side JS, Server-Side App & User Store:
- Client-Side JS — набор JavaScript методов для взаимодействия с WebAuthn API и c REST API приложения;
- Server-Side App — набор REST API методов (генерация Random, выпуск сертификата и т.д.);
- User Store — список пользователей сервиса;
- FIDO2 Server — серверный компонент, отвечающий за аутентификацию пользователей. Он инициирует процесс регистрации и аутентификации, проверяет подлинность ответов от аутентификатора и хранит необходимые данные для будущих проверок.
Архитектура FIDO2 позволяет реализовать аутентификацию в режиме Passwordless. Для этого устройство должно поддерживать верификацию пользователя. Passwordless аутентификация включается на сервере путем установки параметров (конфигурация для создания учетных данных для беспарольной аутентификации):
var authenticatorSelection = new AuthenticatorSelection
{
RequireResidentKey = true,
UserVerification = UserVerificationRequirement.Required
};
authenticatorSelection — это объект, который задает требования к выбору аутентификаторов в WebAuthn. Он включает в себя способ привязки аутентификатора и требования к проверке пользователя. Подробности об использовании и настройке аутентификации с помощью authenticatorSelection можно найти на странице.
Опосредованная интеграция через Identity & Access Managment (IAM)/Identity Provider (IdP)
Для реализации опосредованной интеграции целевое приложение должно поддерживать инструменты федеративной аутентификации. Чаще всего, это протоколы OpenID Connect + oAth 2.0, SAML или ADFS.
Примеры:
- Яндекс ID;
AvanPost FAM/MFA+;
Blitz Identity Provider.
Инструменты для интеграции
Для интеграции аутентификаторов FIDO в веб-приложения доступны библиотеки для разных языков программирования:
- TypeScript — SimpleWebAuthn;
- JS — WebAuth API;
- Rust — webauthn_rs: WebAuthn for Rust Server Applications;
- Java — java-webauthn-server;
- Python — py_webauthn;
- Go — Go WebAuthn Library;
- Ruby:
- Swift — Swift WebAuthn;
- .NET — FIDO2 .NET Library.
Список библиотек дополняется на ресурсах WebAuthn.io и https://passkeys.dev/docs/tools-libraries/libraries/.
Для реализации протокола CTAP2 в приложениях (добавление поддержки FIDO-токенов) можно использовать кросс-платформенную библиотеку libfido2.
Альтернативные сценарии использования Рутокен MFA
Использовать Рутокен MFA можно также для:
- Доступа в локальные учетные записи ОС Linux (PAM-модуль):
- ОС Альт (Локальная аутентификация по Рутокен MFA в ОС Альт);
- РЕД ОС (Локальная аутентификация по Рутокен MFA в РЕД ОС);
Ubuntu (Локальная аутентификации по Рутокен MFA в Ubuntu);
Для работы понадобится три пакета:
- pam_u2f — модуль PAM с поддержкой технологий аутентификации U2F и FIDO2;
- libfido2 — пакет для создания конфигурации для модуля pam_u2f и осуществления процедуры регистрации аутентификатора U2F/FIDO2;
- fido2 — пакет для управления аутентификатором U2F/FIDO2 (например, сброс токена или назначение PIN-кода).
- Хранения ключа SSH (Настройка OpenSSH доступа по Рутокен MFA);
- Шифрования дисков Luks (Инструкция по настройке шифрования LUKS с использованием Рутокен MFA).
Управление Рутокен MFA
Для управления токенами (задание/смена PIN-кода и сброс устройства) используются следующие инструменты:
- в ОС Windows: Windows Hello и Google Chrome;
- в ОС Linux и macOS: Google Chrome.
Управление с помощью командной строки возможно в утилитах Fido2-tools для ОС Linux и MFA Мanager для ОС Windows, Linux и macOS (Руководство по использованию MFA Мanager).
Примеры реализации
Разработаны специальные ресурсы, демонстрирующие реализацию технологии FIDO2 с использованием Рутокен MFA.
Пример реализации FIDO2 на портале demoauth.rutoken.ru
Компания «Актив» реализовала демо-проект demoauth.rutoken.ru, который эмулирует аутентификацию на портале с использованием Рутокен ЭЦП, Рутокен OTP и Рутокен MFA.
Полезные ссылки
В серверной части демо-портала используется библиотека FIDO2 .NET Library. Ниже представлен пример реализации с помощью библиотеки Fido2NetLib в приложении ASP .Net Core из демо-портала.
Регистрация служб в Program.cs
Для реализации технологии необходима настройка cookie, сессий и библиотеки Fido2NetLib.
Настройка cookie
Cookie используются в демонстрационном варианте для хранения данных об аутентифицированном пользователе (первый фактор — login/password, второй фактор — FIDO2).
Для настройки cookie достаточно будет добавить стандартную схему аутентификации:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
Настройка сессий
Сессии потребуются для сохранения временной информации (attestationOptions и assertionOptions), которая необходима для регистрации/аутентификации пользователя.
В данном случае, для параметра SameSite указан SameSiteMode.Unspecified, так как для демонстрационного варианта не принципиальны ограничения на передачу cookie.
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(2);
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Unspecified;
});
Настройка библиотеки Fido2NetLib
Так как доверяющей стороной в данном случае является само приложение демонстрации FIDO2, то для параметров:
- ServerDomain,
- ServerName,
- Origins (хост и порт сервера доверяющей стороны)
указываются данные текущего приложения.
В демонстрационном варианте все конфигурационные параметры находятся в файле appsettings.json.
builder.Services.AddFido2(options =>
{
options.ServerDomain = builder.Configuration["fido2:serverDomain"];
options.ServerName = "FIDO2 Test";
options.Origins = builder.Configuration.GetSection("fido2:origins").Get<HashSet<string>>();
options.TimestampDriftTolerance = builder.Configuration.GetValue<int>("fido2:timestampDriftTolerance");
options.MDSCacheDirPath = builder.Configuration["fido2:MDSCacheDirPath"];
});
Регистрация FIDO2
Упрошенная схема регистрации FIDO2 представлена ниже.
Краткое описание логики:
- Инициализируется запрос на регистрацию FIDO2, далее на Back End формируются параметры, которые отвечают за формат аутентификации, требования к ключам и другие, в частности:
- isPasswordLess (параметр RequireResidentKey);
- Challenge — случайная последовательность, которая будет записана в подпись (создается на устройстве), для дальнейшей проверки публичного ключа, сгенерированного на устройстве.
- Сформированные параметры сохраняются в сессии (можно хранить любым другим удобным способом, главное иметь сопоставление options и текущей сессии).
- Далее на клиенте сформированный CredentialOptions передается в качестве параметра для метода navigator.credentials.create.
- Если в результате выполнения метода не возникли ошибки, то формирование ключевой пары и подписи (проверятся при завершении регистрации) на устройстве считается завершенным.
- Полученный результат метода navigator.credentials.create передается обратно на Back End, верифицируется его содержимое с тем, что было записано в сессии, после чего сформированный публичный ключ FIDO2 сохраняется в БД. На этом этапе регистрация FIDO2 считается завершенной.
Шаг 1. Инициализация регистрации
На этом шаге полученный CredentialOptions записывается в сессию для дальнейшей проверки при подтверждении регистрации на втором шаге.
// Метод создания CredentialOptions для регистрации FIDO2 (Back End).
public string MakeCredentialOptions(int userId, bool isPasswordLess) {
// Получение пользователя из БД по userId из cookie.
var user = _context.Users.FirstOrDefault(usr => usr.Id == userId);
if (user == null)
throw new RTFDException("Пользователь не найден");
var fidoUser = new Fido2User{
Id = BitConverter.GetBytes(user.Id), DisplayName = user.UserName, Name = user.UserName
};
// Получение всех существующих ключей у данного пользователя.
var existingKeys = GetCredentialsByUser(user)
.Select(c => new PublicKeyCredentialDescriptor(c.CredentialId.HexStringToByteArray()))
.ToList();
// Объект, позволяющий регулировать настройки создания Attestation. В демонстрационном варианте не используется.
var attestation = new AttestationResponse();
// Настройки WebAuthn и требований к атрибутам устройства.
var authenticatorSelection = new AuthenticatorSelection {
// При значении true указывает для устройства принудительное создание публичного ключа на клиенте.
RequireResidentKey = isPasswordLess,
// Установление признака, который "рекомендует" проверку пользователя на устройстве,
// но в случае сбоя не завершит операцию.
UserVerification = UserVerificationRequirement.Preferred
};
// В демонстрационном варианте не используется,
// но может пригодиться для фильтрации устройств по типу подключения. Доступно: platform, crossPlatform.
if (!string.IsNullOrEmpty(attestation.AuthType))
authenticatorSelection.AuthenticatorAttachment = attestation.AuthType.ToEnum<AuthenticatorAttachment>();
// Настройки WebAuthn.
var exts = new AuthenticationExtensionsClientInputs() {
// Позволяет WebAuthn определять, какие расширения поддерживает устройство.
Extensions = true,
// Указывает о необходимости проверки пользователя (нажатие кнопки, pin-code и т.д.).
UserVerificationMethod = true,
};
// Создание объекта, который содержит необходимую информацию для WebAuthn
// при взаимодействии с устройством, а также дополнительные данные (в частности challenge,
// который будет проверятся при завершении регистрации полученным публичным ключом).
// Рекомендуется использовать метод из библиотеки: _fido2.RequestNewCredential.
var options = RequestNewCredential(fidoUser, existingKeys, authenticatorSelection,
attestation.AttType.ToEnum<AttestationConveyancePreference>(), exts);
return options.ToJson();
}
Регистрация на клиенте (Front End):
// Логика обработки результатов инициализации регистрации FIDO2 с Back End,
// создание ключевой пары на устройстве и создание запроса на подтверждение регистрации на Back End.
sequense = sequense.then(() => axios.post('/mfa/makecredentialoptions', {isWithoutLogin}));
sequense = sequense.then((response) => {
let option = JSON.parse(response.data);
if (option.status !== "ok") throw new Error("Ошибка регистрации токена");
// Преобразование Uint8Array в Base64.
option.challenge = coerceToArrayBuffer(option.challenge);
option.user.id = coerceToArrayBuffer(option.user.id);
option.excludeCredentials = option.excludeCredentials.map((x) => {
x.id = coerceToArrayBuffer(x.id);
return x;
});
if (option.authenticatorSelection.authenticatorAttachment === null)
option.authenticatorSelection.authenticatorAttachment = undefined;
return option;
});
// Происходит создание ключевой пары на устройстве, в том числе проводятся доп. проверки, например: нажатие кнопки, ввод pin-code и т.д.
// Результатом выполнения работы является объект с публичным ключом и подписанным challenge.
sequense = sequense.then((response) => {
return navigator.credentials.create({
publicKey: response
});
});
sequense = sequense.catch((error) => {
throw error;
});
// Подтверждение регистрации FIDO2.
return (dispatch) => {
let sequense = Promise.resolve();
// Преобразование Uint8Array в Base64.
let attestationObject = new Uint8Array(credential.response.attestationObject);
let clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
let rawId = new Uint8Array(credential.rawId);
const attestationResponse = {
id: credential.id,
rawId: coerceToBase64Url(rawId),
type: credential.type,
extensions: credential.getClientExtensionResults(),
response: {
AttestationObject: coerceToBase64Url(attestationObject),
clientDataJson: coerceToBase64Url(clientDataJSON)
}
};
// Запрос на подтверждение регистрации с полученным ранее публичным ключом и подписью attestationObject.
sequense = sequense.then(() => axios.post('/mfa/makecredential', {attestationResponse, label, isWithoutLogin}));
sequense = sequense.then((response) => {
let result = response.data;
if (result.status !== "ok")
throw new Error("Ошибка подтверждения регистрации токена");
return result;
});
sequense = sequense.then((response) => getUserInfo()(dispatch));
return sequense;
};
Шаг 2. Верификация и подтверждение регистрации
На этом шаге можно удалить из сессии CredentialOptions (который был создан и записан в сессию на первом шаге), так как регистрация завершена.
// Метод подтверждения регистрации FIDO2 (Back End).
public async Task<CredentialMakeResult> MakeCredential(int userId, LabelData labelData, string? jsonOptions,
CancellationToken cancellationToken) {
// Парсинг полученного из сессии CredentialOptions, созданного на первом шаге.
var options = CredentialCreateOptions.FromJson(jsonOptions);
IsCredentialIdUniqueToUserAsyncDelegate callback = async (args, cancellationToken) => {
// Проверка на существование этого ключа в БД.
return await GetUsersByCredentialIdAsync(args.CredentialId);
};
// Верификация options, созданного на первом шаге, и attestation, полученного с устройства.
// *Рекомендуется использовать метод из библиотеки: _fido2.RequestNewCredential.
var success = await MakeNewCredentialAsync(labelData.AttestationResponse, options, callback,
cancellationToken: cancellationToken);
// При успешной верификации производить запись в базу публичного ключа FIDO2.
var credentialId = AddCredentialToUser(new FidoKey {
UserId = userId,
CredentialId = success?.Result.CredentialId.ByteArrayToHexString(),
PublicKey = success?.Result.PublicKey.ByteArrayToHexString(),
SignatureCounter = success.Result.Counter,
RegDate = DateTime.UtcNow,
Label = labelData.Label,
LastLogin = DateTime.UtcNow,
IsPasswordLess = labelData.IsWithoutLogin
});
return success;
}
Аутентификация FIDO2
Упрошенная схема аутентификации FIDO2 представлена ниже.
Краткое описание логики:
- Аутентификация пользователя первым фактором, например, login/password (необязательный пункт при Passwordless).
- Запрос на создание AssertionOptions. На Back End формируется AssertionOption, содержащий информацию по работе с устройством и подпись для проверки. В данном пункте, при включенном параметре PasswordLess, публичный ключ не включается в AssertionOption (аутентификация на устройстве).
- Далее на Front End из полученного AssertionOptions формируется объект publicKey, который передается в метод navigator.credentials.get.
Результатом выполнения этого метода, являются подписанные данные, которые обратно возвращаются на Back End.
В случае Passwordless при завершении верификации получаем UserId, к которому принадлежит публичный ключ. - На Back End проверяется сформированная подпись тем ключом, который записан в БД. Если проверка успешна, то аутентификация считается завершенной.
Шаг 1. Инициализация аутентификации
На этом шаге полученный AssertionOptions записывается в сессию для дальнейшей проверки при подтверждении аутентификации на втором шаге.
// Метод создания AssertionOptions для аутентификации по FIDO2
public AssertionOptions AssertionOptionsPost(int? userId) {
var credentials = new List<PublicKeyCredentialDescriptor>();
var isPasswordLess = !userId.HasValue;
if (!isPasswordLess) {
// При is not PasswordLess необходимо получить пользовательские
// публичные ключи по userId (userId - получаем из cookie после прохождения первого фактора).
credentials =
_context.FidoKeys.Where(c => c.UserId == userId)
.Select(c => new PublicKeyCredentialDescriptor(c.CredentialId.HexStringToByteArray()))
.ToList();
}
// Настройки WebAuthn.
var extensions = new AuthenticationExtensionsClientInputs() {
// Указывает о необходимости проверки пользователя (нажатие кнопки, pin-code и т.д.).
UserVerificationMethod = true,
// Позволяет WebAuthn определять, какие расширения поддерживает устройство.
Extensions = true
};
// Установления признака, который "рекомендует" проверку пользователя на устройстве,
// но в случае сбоя не завершит операцию.
var uv = UserVerificationRequirement.Preferred;
// Создание AssertionOptions.
// Рекомендуется использовать метод из библиотеки: _fido2.GetAssertionOptions.
var options = GetAssertionOptions(
credentials,
uv,
extensions
);
return options;
}
Аутентификация на клиенте (Front End):
// Логика обработки аутентификации и взаимодействие с устройством.
let sequense = axios.get('/mfa/assertionoptions');
sequense = sequense.then((result) => {
const data = result.data;
// Создание объекта publicKey и преобразование Uint8Array в Base64.
const publicKey = {
...data,
allowCredentials: data.allowCredentials.map(el => ({
id: coerceToArrayBuffer(el.id),
type: el.type
})),
challenge: coerceToArrayBuffer(data.challenge)
};
// Запрос на устройство о необходимости начала процесса аутентификации.
// Определяется устройство и метод работы с ним.
// Создается специальная подпись, которая потом будет проверяться на Back End.
return navigator.credentials.get({publicKey});
});
sequense = sequense.then((assertedCredential) => {
// Преобразование Uint8Array в Base64.
let authData = new Uint8Array(assertedCredential.response.authenticatorData);
let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
let rawId = new Uint8Array(assertedCredential.rawId);
let sig = new Uint8Array(assertedCredential.response.signature);
const data = {
id: assertedCredential.id,
rawId: coerceToBase64Url(rawId),
type: assertedCredential.type,
extensions: assertedCredential.getClientExtensionResults(),
response: {
authenticatorData: coerceToBase64Url(authData),
clientDataJSON: coerceToBase64Url(clientDataJSON),
signature: coerceToBase64Url(sig)
}
};
// Отправка подписанных данных на Back End.
return axios.post('/mfa/makeassertion', data);
});
sequense = sequense.then((result) => {
// Вход пользователя после успешной аутентификации вторым фактором FIDO2.
setTimeout(() => dispatch(setLoginState(true)), 1000);
});
sequense = sequense.catch((error) => {
throw error;
});
return sequense;
Шаг 2. Верификация и подтверждение аутентификации
На этом шаге можно удалить из сессии AssertionOptions (который был создан и записан в сессию на первом шаге), так как аутентификация завершена.
public async Task<int> MakeAssertion(string? jsonOptions,
AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) {
// Парсинг полученного из сессии AssertionOptions, созданного на первом шаге.
var options = AssertionOptions.FromJson(jsonOptions);
// Получение публичного ключа из БД по clientResponse.Id.
// Получен из Front End из navigator.credentials.get.
var fidoKey = GetCredentialById(clientResponse.Id) ?? throw new RTFDException("Неизвестный токен");
var storedCounter = fidoKey.SignatureCounter;
// Проверка принадлежности публичного ключа к аутентифицированному пользователю.
IsUserHandleOwnerOfCredentialIdAsync callback = async (args, token) => {
var storedCreds = await GetCredentialsByUserHandleAsync(args.UserHandle);
var hexCredentialId = args.CredentialId.ByteArrayToHexString();
return storedCreds.Exists(c => c.CredentialId == hexCredentialId);
};
// Проверка подписи. В случае успеха (завершилось без ошибок) аутентификация считается завершенной.
// Рекомендуется использовать метод из библиотеки: _fido2.MakeAssertionAsync.
var res =
await MakeAssertionAsync(
clientResponse,
options,
fidoKey.PublicKey.HexStringToByteArray(),
storedCounter,
callback,
cancellationToken: cancellationToken
);
UpdateCounter(fidoKey, res.Counter);
return fidoKey.UserId;
}




