Хабрахабр

FIDO2 — Пароли must die

Думаю, все вы неоднократно слышали о том что «пароли мертвы», «пароли вымирают», «новая технология убьет пароли» и тому подобное.

Мы в FIDO Alliance как раз-таки пришли сообщить вам о том, что пароли все-таки вымрут… в аутентификации.
Прежде чем мы начнем разговор о массовом уничтожении многословных гадов, давайте вернемся к нашему самому популярному протоколу FIDO U2F о коем я писал ранее.

То есть сценарий аутентификации с U2F таков:

Регистрация:

— Пользователь регистрируется, используя имя пользователя (может лучше поменять на пароль, а то пользователь-пользователь?) и пароль
— Сервер хеширует пароль с использованием scrypt, argon2, bcrypt и сохраняет в БД
— Пользователь добавляет свое устройство, храня на сервере публичный ключ и id пары ключей на устройстве

Аутентификация:

— Пользователь вводит логин и пароль
— Сервер хеширует пароль и сравнивает с хешем в базе данных
— Сервер генерирует случайный «вызов» и отправляет клиенту вместе с ID
— Клиент передает вызов, id, token binding, и origin сессии U2F токену
— Аутентификатор все это подписывает приватным ключом и отправляет клиенту, а тот серверу
— Профит

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

Какие же?

Пароли!

Пароли все еще можно украсть на всех стадиях аутентификации:

— Можно украсть с помощью кейлоггера;
— Можно украсть при проблемах с TLS(см Heartbleed);
— Можно украсть если сервер уязвим к инъекции кода, или при ошибках в коде(см Twitter)
— Можно украсть если пароли слабо хешированы или не были хешированы вовсе.

Пока есть пароль, его можно украсть

А что если убрать пароль вообще?

Собственно, так подумали мы в FIDO Alliance (мы не имеем ничего общего с гипертекстовым фидонетом, хотя потенциально можем быть там использованы) и сделали FIDO2

Выше на картинке вы можете видеть наших членов совета, которые собственно и разрабатывают стандарты. Для тех, кто о нас не слышал, FIDO Alliance — это консорциум, разрабатывающий открытые стандарты безопасной аутентификации.

Он состоит из двух частей: FIDO2 — это наш последний проект, который мы разрабатывали более трех лет.

Это W3C стандарт, так что будет обязателен для всех браузеров. — WebAuthn — JS API для менеджмента учетных записей на публичных ключах. Собственно Chrome, Firefox и Edge уже публично заявили о работе над его поддержкой.
— CTAP2 — Client-to-Authenticator 2 — это стандарт, описывающий CBOR протокол для общения с аутентификатором по USB, NFC и BLE.

Пользователь

Для пользователя это просто:

— Пользователь вводит имя
— Проходит верификацию
— Профит!

Собственно, демонстрация:

Протокол

FIDO2 — это член семейства вызов-ответ протоколов FIDO. В протоколе есть шесть основных механизмов безопасности:

1. Вызов-ответ

Сервер генерирует случайный вызов и отправляет его клиенту. Первый механизм это вызов-ответ. Сервер, зная вызов, подпись и публичный ключ, может подтвердить подпись и таким образом аутентифицировать клиента. Клиент передает данный вызов аутентификатору, который подписывает вызов и возвращает его клиенту, а тот в свою очередь серверу.

2. Фишинг-защита

Когда клиент получает вызов от сервера, он добавляет к вызову информацию о сессии: origin (прим: example.com), token binding. Второй механизм это сессионно-зависимая транзакция. Сервер декодирует ответ и смотрит или в источником был он сам, и если пользователь подвергся фишинг атаке то сервер сможет увидеть что источником был на пример attacker.com а не example.com и таким образом предотвратить атаку. Эта информация подписывается аутентификатором и возвращается серверу.

3. Защита от атаки повторного воспроизведения

Если сервер получил ответ со счетчиком, значение которого меньше или равно последнему сохраненному значению, то сервер может точно сказать что это атака повторного воспроизведения. Чтобы защитить от атаки повторного воспроизведения, в транзакции присутствует счетчик, который увеличивается на единицу при каждой операции.

4. Приватность

При каждой регистрации аутентификатор генерирует случайную пару ключей со случайным идентификатором. Четвертый механизм защиты это регистрационно-уникальные пары ключей. Таким образом даже если вы и ваш партнер будете использовать один аутентификатор для обоих аккаунтов, то сервер не сможет узнать этого.

При аутентификации идентификатор пары ключей передается аутентификатору, и тот, используя origin и идентификатор, найдет вашу пару ключей в своей защищенной БД и вернет подпись.

При аутентификации аутентификатор вернет подписи для всех RK пар ключей. Альтернатива к передаче идентификатора это использование резидентных ключей (RK, resident keys). Более подробно RK я распишу ниже. Клиент предоставляет пользователю выбор пары и вернет подпись серверу.

5. Аттестация

Иногда сайту нужно узнать тип устройства у пользователя. Пятый механизм это аттестация. Подобные требования есть во Франции, Израиле, России и других странах. Так например в США все государственные учреждения обязаны использовать только FIPS (Federal Information Processing Standards) сертифицированные устройства. Для всего этого и существует аттестация.

При регистрации устройство подписывает всю информацию приватным ключом партийного сертификата и возвращает сертификат вместо публичного ключа. При производстве аутентификаторов, на каждые сто тысяч устройств компания генерирует «сертификат партии» и ключи к нему и устанавливает их на каждое устройство. Также при получении FIDO сертификации компания выпускает специальный документ метадаты, или просто метадату, и помещает в наше хранилище метадаты доступное всем. В сертификате содержится информация о аутентификаторе, и его AAGUID(Authenticator Attestation GUID). а основе имеющейся информации сервер может судить о том, стоит ли ему принимать аутентификатор или нет. В метадате содержится информация о аутентификаторе, включая его криптографические характеристики, биометрические характеристики если он поддерживает биометрику, алгоритмы, уровни безопасности, базовое описание и много другой информации.

Существует полная версия аттестации, в которой сервер возвращает сертификат. Аттестация бывает разная. Есть само-аттестация, при которой устройство не поддерживает сертификаты, а вместо этого возвращает публичный ключ только что сгенерированной пары.

Так например пользователь может вообще запретить возвращать аттестацию и клиент просто удалит её, вернув регистрационный ответ без аттестации. Также пользователь может сам настроить метод, с помощью которого он хочет вернуть аттестацию. Об этом я расскажу позже.

6. Тест на наличие пользователя

Одно из фундаментальных требований FIDO это тест на наличие пользователя. В данном случае имеется в виду, что аутентификатор обязан удостоверится в присутствии пользователя… Это может быть сделано по-разному: простое нажатие на кнопку, отпечаток, пин-код и другое.

WebAuthn

В этой секции мы рассмотрим JS API для работы с FIDO2.

Credential Management API

WebAuthn это дополнения к Credential Management API для менеджмента учетных записей публичных ключей. CredMan API это API(неожиданно!) для менеджмента учетных данных пользователя или если выразиться проще: это API для доступа к автозаполнению в браузере.

Далее мы рассмотрим пример создания учетных данных:

navigator.credentials.store({ 'type': 'password', 'id': 'alice', 'password': 'VeryRandomPassword123456'
})

А вот пример запроса учетных данных и последующего использования их для аутентификации:

navigator.credentials .get() .then(credential => { if (!credential) { throw new Error('No credentials returned!') } let credentials = { 'username': credential.id, 'password': credential.password } return fetch('https://example.com/loginEndpoint', { method: 'POST', body: JSON.stringify(credentials), credentials: 'include' }) }) .then((response) => { ... })

Джеймс Аллардайс сделал прекрасную презентацию в прошлом году по CredManAPI
pusher.com/sessions/meetup/js-monthly-london/building-a-better-login-with-the-credential-management-api

Create public-key credential

Для создания Public-Key Credential мы будем использовать navigator.credentials.create метод, передав ему CreateCredential объект типа «publicKey».

var randomChallengeBuffer = new Uint8Array(32); window.crypto.getRandomValues(randomChallengeBuffer); var base64id = 'MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII=' var idBuffer = Uint8Array.from(window.atob(base64id), c=>c.charCodeAt(0)) var publicKey = { challenge: randomChallengeBuffer, rp: { name: "FIDO Example Corporation" }, user: { id: idBuffer, name: "alice@example.com", displayName: "Alice von Wunderland" }, attestation: 'direct', pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -257 } // RS256 ] } // Note: The following call will cause the authenticator to display UI. navigator.credentials.create({ publicKey }) .then((newCredentialInfo) => { console.log('SUCCESS', newCredentialInfo) }) .catch((error) => { console.log('FAIL', error) })

Думаю, код говорит сам за себя, но отмечу при этом несколько моментов:
— challenge и user.id должны быть буферами
— pubKeyCredParams — это список алгоритмов, которые поддерживает сервер. В данный момент сертифицированные сервера FIDO2 обязаны поддерживать: RSA-PSS 2048 с SHA256/384/512, RSA-PKCS1_3 с SHA256/384/512/1, EC с SHA256/384/512, а также кривые secp256/384/521p1(NIST) и secp256k1, и EdDSA ED25519.
— attestation — это собственно вид аттестации, возвращаемой клиентом. Есть три возможные значения: none, direct, indirect. Direct это, собственно, классическая аттестация. None это полное отсутствие аттестации, при котором клиент просто удалит всю аттестационную секцию и заменит AAGUID в ответе на 0x00000000000000000000000000000000. Indirect это аттестация через privacy-ca. По-умолчанию attestation установлена на none. То есть если вы хотите аттестацию, то надо это чётко указать в параметрах. При этом необходимо учитывать, что пользователь все равно может вам отказать в аттестации и вернуть 'none' если он того захочет.

Вот пример пример того как пользователь может контролировать аттестацию в Firefox Nightly

В ответ вы получите аттестационный объект:

Здесь вы можете видеть:

id это base64url закодированная версия буфера rawId.
— response.clientDataJSON — это буфер CollectedDataJSON, JSON объекта содержащего сессионную информацию и сам вызов. — id/rawId — это идентификатор пары ключей на устройстве или credId. Пример декодированного clientDataJSON:

{ "challenge": "1KnytoY-it44IiEMx0lK5GBlBvAJeVULPSCw5E-5qpA", "origin": "http://localhost:3000", "tokenBinding": { "status": "not-supported" }, "type": "webauthn.create"
}

— attestationObject — это CBOR объект, содержащий аттестацию, информацию об аутентификаторе, флаги, счётчик и публичный ключ.

В данный момент WebAuthn поддерживает «packed», «fido-u2f», «android», «tpm» и «safety-net» аттестации.
— attStmt — это собственно информация по аттестации. — authData — это сырой буфер, содержащий хеш источника или rpIdHash, флаги, счетчик, идентификатор пары ключей, блок с аттестационной информацией и информацию от расширений.
— fmt — это формат аттестации. Подпись, сертификаты и так далее.

Затем мы объединяем authData и clientDataHash и получаем signatureBase. Для верификации подписи мы высчитываем хеш clientDataJSON и получаем clientDataHash. И в конце мы подтверждаем подпись с помощью attStmt.sig, сертификата или публичного ключа пользователя и signatureBase.

Get assertion

Для того чтобы получить аутентификационую подпись мы будем использовать navigator.credentials.create метод, передав ему объект типа «publicKey».

var randomChallengeBuffer = new Uint8Array(32);
window.crypto.getRandomValues(randomChallengeBuffer);

var idBuffer = encoder.encode("mFuXJYt-0qYZDUBFyN_SW_Cach6U56mrnHA_ZuxtlOnjHLhjfWjYVftbxr4WMmxp1-MG3nRRNQoI7WvvAM0pmw") var options = { challenge: randomChallengeBuffer, allowCredentials: [{ type: "public-key", id: idBuffer }] } navigator.credentials.get({ "publicKey": options }) .then((assertion) => { console.log('SUCCESS', assertion) }) .catch((error) => { console.log('FAIL', error) })

allowCredentials — это список разрешенных учетных записей, которые идентифицируются идентификатором пары ключей на устройстве (credId). Для двухфакторной аутентификации вы всегда обязаны передавать allowCredentials c credId в запросе. Если во время создания учетной записи мы установили флаг authenticatorSelection.requireResidentialKey, то в дальнейшем обязаны не добавлять поле allowCredentials.

В ответ получаем вот такой объект:

Как вы видите, структура очень похожа, но есть несколько различий:
— clientDataJSON.type теперь типа «webauthn.get»

{ "challenge": "-0NPkC-1zxpcGanUJMbKFdd7Ze0kRphWFy1_Sgc4FpI", "origin": "http://localhost:3000", "tokenBinding": { "status": "not-supported" }, "type": "webauthn.get"
}

— attestationObject отсутствует
— authenticatorData это authData, но теперь без authenticatorAttestation секции
— signature это подпись
— userHandle — это идентификатор пользователя, который вы передали во время регистрации в поле user.id

Вычисление подписи работает так же, как и при создании учетной записи.

UPD: Демо webauthn.org (Нужен U2F токен)

В следующей части мы разберем протокол общения клиента и аутентификатора CTAP2(Client-to-Authenticator 2). На этом я закончу первую часть из цикла статей.

Большая благодарность Павлу Замятину за редактуру и орфографию.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»