Page tree

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Current »

Общая информация

Данный документ предназначен для инженеров и разработчиков, которые добавляют поддержку устройств Рутокен MFA в серверное приложение, клиентский портал или другой ресурс для реализации стандартизированной технологии FIDO2. 

Рутокен MFA — это семейство USB-токенов, предназначенных для аутентификации в веб-приложениях и поддерживающих режим беспарольной защиты («passkey»). Данное семейство продуктов обладает рядом ключевых преимуществ:

  • не требует драйверов и дополнительного ПО;
  • обеспечивает высокий уровень безопасности благодаря строгой аутентификации:
    • использует асимметричную криптографию;
    • защищает от фишинга и атак Man-in-the-middle (MitM);
    • исключает передачу паролей по сети интернет;
  • простота использования для обычных пользователей;
  • один аутентификатор используется для множества ресурсов.

Стандарты и компоненты

В основе Рутокен MFA лежит набор стандартов FIDO2, который включает в себя следующие спецификации:

  • к ключам безопасности (CTAP), текущая версия CTAP 2.1;

    Спецификация CTAP относится к двум версиям протокола: CTAP1 и CTAP2. 

    CTAP1 — это новое название FIDO U2F. CTAP1 можно использовать только в качестве второго фактора, но он не поддерживает проверку пользователя с помощью PIN-кода, в отличие от CTAP2.

    Рутокен MFA поддерживает CTAP2 и CTAP1/U2F в режиме обратной совместимости.

    CTAP2 также имеет различные подверсии протокола. Аутентификаторы могут сообщать о поддерживаемой ими версии CTAP2 с помощью идентификаторов версий, таких как FIDO_2_0, FIDO_2_1_PRE, или FIDO_2_1. Аутентификаторы FIDO2, которые также поддерживают U2F (то есть CTAP1), сообщают о U2F_V2 как о поддерживаемой версии.

    Эту информацию можно получить в Linux при помощи утилиты fido2-tools, выполнив следующую команду (если он имеет идентификатор USB HID /dev/hidraw0):

    fido2-token -I /dev/hidraw0
    ...
    version strings: FIDO_2_0, FIDO_2_1_PRE, FIDO_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 входит процесс регистрации ключа и процесс аутентификации пользователя.

Процесс регистрации ключа

  1. Пользователь выбирает регистрацию на веб-ресурсе и инициирует обмен данными.
  2. Сервер отправляет пользователю случайную последовательность (challenge), а также идентификатор веб-ресурса (доменное имя сервера). Браузер взаимодействует с ключом безопасности, токен проверяет данные, а также создает идентификатор пользователя и ключевую пару (открытый и закрытый ключ).
  3. Случайная последовательность подписывается закрытым ключом и передается вместе с открытым ключом и другими данными на сервер. Сервер:
    1. Расшифровывает данные открытым ключом.
    2. Проверяет, что полученная от пользователя и отправленная ему ранее случайная последовательность совпадают.
    3. Устанавливает счетчик операций для этого пользователя в 0.
    4. Сохраняет все данные, включая открытый ключ и идентификатор пользователя.

Ключ безопасности привязан к пользователю. 

Процесс аутентификации пользователя

  1. Сервер отправляет ключу безопасности через браузер перечень идентификаторов учетных записей, а также случайную последовательность для подписи. Ключ безопасности находит идентификатор учетной записи в своей памяти, подписывает случайную числовую последовательность соответствующим закрытым ключом пользователя и увеличивает значение счетчика операций.
  2. Данные передаются серверу. Сервер:
    1. Расшифровывает случайную последовательность.
    2. Проверяет значение счетчика и соответствие открытого ключа данной учетной записи.
    3. Сохраняет новое значение счетчика и аутентифицирует пользователя в его учетную запись.  

Аутентификация пользователя прошла успешно.

Особенности интеграции FIDO

Поддержка аутентификаторов по стандарту FIDO может быть реализована непосредственно или опосредованно через Identity & Access Management (IAM)/Identity Provider (IdP).

Непосредственная интеграция аутентификаторов

С помощью стандарта CTAP разработчики могут использовать различные варианты аутентификации FIDO для защиты своих приложений:

Архитектура для реализации непосредственной интеграции с ресурсом показана на схеме:

Fido2 Architecture

Для непосредственной интеграции в веб-порталах используются следующие компоненты архитектуры 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 в веб-приложения доступны библиотеки для разных языков программирования:

Список библиотек дополняется на ресурсах WebAuthn.io и https://passkeys.dev/docs/tools-libraries/libraries/.

Для реализации протокола CTAP2 в приложениях (добавление поддержки FIDO-токенов) можно использовать кросс-платформенную библиотеку libfido2.

Альтернативные сценарии использования Рутокен MFA

Использовать Рутокен 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 представлена ниже.

Краткое описание логики:

  1. Инициализируется запрос на регистрацию FIDO2, далее на Back End формируются параметры, которые отвечают за формат аутентификации, требования к ключам и другие, в частности:
    • isPasswordLess (параметр RequireResidentKey);
    • Challenge — случайная последовательность, которая будет записана в подпись (создается на устройстве), для дальнейшей проверки публичного ключа, сгенерированного на устройстве.
  2. Сформированные параметры сохраняются в сессии (можно хранить любым другим удобным способом, главное иметь сопоставление options и текущей сессии).
  3. Далее на клиенте сформированный CredentialOptions передается в качестве параметра для метода navigator.credentials.create.
  4. Если в результате выполнения метода не возникли ошибки, то формирование ключевой пары и подписи (проверятся при завершении регистрации) на устройстве считается завершенным.
  5. Полученный результат метода navigator.credentials.create передается обратно на Back End, верифицируется его содержимое с тем, что было записано в сессии, после чего сформированный публичный ключ FIDO2 сохраняется в БД. На этом этапе регистрация FIDO2 считается завершенной.

Шаг 1. Инициализация регистрации 

На этом шаге полученный CredentialOptions записывается в сессию для дальнейшей проверки при подтверждении регистрации на втором шаге.

Back End. Файл: /Services/MfaService.cs
// Метод создания 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):

Front End. Файл: /ClientApp/src/redux/actions/index.js, методы: registerFido и confirmRegisterFido
// Логика обработки результатов инициализации регистрации 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 (который был создан и записан в сессию на первом шаге), так как регистрация завершена.

Back End. Файл: /Services/MfaService.cs
// Метод подтверждения регистрации 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 представлена ниже.

Краткое описание логики:

  1. Аутентификация пользователя первым фактором, например, login/password (необязательный пункт при Passwordless).
  2. Запрос на создание AssertionOptions. На Back End формируется AssertionOption, содержащий информацию по работе с устройством и подпись для проверки. В данном пункте, при включенном параметре PasswordLess, публичный ключ не включается в AssertionOption (аутентификация на устройстве).
  3. Далее на Front End из полученного AssertionOptions формируется объект publicKey, который передается в метод navigator.credentials.get.
    Результатом выполнения этого метода, являются подписанные данные, которые обратно возвращаются на Back End.
    В случае Passwordless при завершении верификации получаем UserId, к которому принадлежит публичный ключ.
  4. На Back End проверяется сформированная подпись тем ключом, который записан в БД. Если проверка успешна, то аутентификация считается завершенной.

Шаг 1. Инициализация аутентификации

На этом шаге полученный AssertionOptions записывается в сессию для дальнейшей проверки при подтверждении аутентификации на втором шаге.

Back End. Файл: /Services/MfaService.cs
// Метод создания 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):

Front End. Файл: /ClientApp/src/redux/actions/index.js, метод: loginFido
// Логика обработки аутентификации и взаимодействие с устройством.
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 (который был создан и записан в сессию на первом шаге), так как аутентификация завершена.

Back End. Файл: /Services/MfaService.cs
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;
}

Примеры реализации у других вендоров

Ссылки на документацию

  • No labels