В этой статье мы узнаем как происходит проверка аттестационного и авторизационного ответов на стороне сервера. Как работает сам WebAuthn API мы обсудим в другой раз.
Кратко объясним эти понятия:
Аттестационный ответ / Attestation - содержит свежеиспеченную учетную запись с публичным ключом, и аттестационную информацию. Аттестация используется для регистрации аутентификатора.
Авторизационный ответ / Assertion - содержит доказательство владением приватным ключом, чей ответ мы можем проверить с помощью ранее зарегистрированного публичного ключа. Авторизационный ответ используется для логина.
Типичный ответ на сервере выглядит так:
id/rawId - Это идентификатор учетной записи. id это base64url кодированный буфер rawId. В данном примере оба, id и rawId base64url кодированы, так как сервер получает пре-кодированный ответ. Этот id как раз таки используется в excludeCredentials, так и в allowList
type - Это тип, кой в данном случае “public-key”. В будущем возможны иные типы но на данный момент это единственный.
response - Это собственно сам ответ устройства.
response.clientDataJSON - Это созданный браузером объект содержащий информацию о сессии. Сервер декодируя этот объект имеет возможность узнать какой домен и протокол видел пользователь проверяя origin. Проверяя challenge с ранее сохраненным вызовом, сервер может узнать или была совершена MITM атака. clientDataJSON криптографически защищен, так что атакующий не сможет подменить информацию о сессии, и соответсвенно совершить фишинг атаку.
Для декодирования base64url можно использовать библиотеку “base64url” в npm.
type - Тип ответа. webauthn.create для аттестации, и webauthn.get для авторизации.
challenge - Base64url кодированный вызов(challenge) установленный во время вызова navigator.credentials.create/get.
origin - Фактически протокол + домен откуда был сделан вызов WebAuthn API. Так же в себя включает порт если он не стандартный. origin это очень важно поле для фишинг безопасности, так как любые отклонения в значении этого поля означает потенциально фишинг атака. Модель безопасности WebAuthn API строится на стандартной модели W3C WebAppSec, “лимитируй доменное пространство, и проверяй ресурсы”. Здесь же тот же самый подход. Так что строгая проверка origin, обязательная. https://example.com не тоже самое что https://example.com:8181. Специфику контекстов безопасности мы обсудим дальше в статье.
crossOrigin - Опциональное поле, дающее знать или вызов был произведен через iframe. Имеет значение только для авторизационного ответа.
Нужно иметь ввиду что ClientDataJSON может в будущем получить новые поля, так что учитывайте это когда пишете свой десериализатор.
response может содержать два типа ответов: аттестационный и авторизационный.
Аттестационный когда пользователь добавляет устройство в свой аккаунт.
Авторизационный когда пользователь хочет зайти в свой аккаунт.
Аттестационный ответ / Attestation result
Типичный аттестационный ответ вы уже видели:
У него есть уникальное поле response.attestationObject - это CBOR кодированный аттестационный объект. Для декодирования base64url можно использовать библиотеку “base64url” в npm. Для CBOR же можно использовать “node-cbor”.
Декодировав мы получим:
fmt - Формат аттестации. На данный момент есть семь видов аттестации: “packed”, “fido-u2f”, “none”, “android-key”, “android-safetynet”, “tpm” и “apple”.
attStmt - Объект содержащий саму аттестацию. В данном случае пустой.
authData - Буфер содержащий некоторую сессионную информацию а также и информацию о новой учетной записи.
В данной статье я не буду разбирать как производить проверку различный аттестационных форматов. Для этого у меня есть огромная серия статей на английском. Возможно в будущем их я тоже переведу:
Packed: https://medium.com/webauthnworks/verifying-fido2-packed-attestation-a067a9b2facd
FIDO-U2F: https://medium.com/webauthnworks/verifying-fido-u2f-attestations-in-fido2-f83fab80c355
Android Keystore: https://medium.com/webauthnworks/webauthn-fido2-verifying-android-keystore-attestation-4a8835b33e9d
Android SafetyNet: https://medium.com/webauthnworks/verifying-fido2-safetynet-attestation-bd261ce1978d
TPM: https://medium.com/webauthnworks/verifying-fido-tpm2-0-attestation-fc7243847498
Apple Anonymous Attestation: https://medium.com/webauthnworks/webauthn-fido2-verifying-apple-anonymous-attestation-5eaff334c849
В целом обычному сервису аттестация не нужна. Аттестация это довольно сложный функционал, которой полезен банкам, гос услугам и т.д. WebAuthn API по умолчанию вырезает аттестацию что облегчает нам жизнь. Если вам интересно понять зачем аттестация и как ею пользоваться советую почитать мою статью на английском "Demystifying attestation and MDS”.
Возвращаемся к authData. AuthData это просто склеенный буфер:
Тут у нас есть:
RPID hash - Это 32х байтовый SHA256 хеш RPID
Flags - Это восьмибитный битовый массив содержащий информацию о аутентификационном событии. Нулевой бит отвечает за проверку присутствия пользователя UP. Второй бит отвечает за верификацию пользователя UV. Шестой(AT) и седьмой(ED) биты описывают наличие аттестационных данных и ответов от расширений.
Counter - uint32 счетчик. Этот счетчик используется для защиты от атаки повтором. Сервер хранит значение счетчика, и если при следующей аутентификации счетчик не увеличился то возможно это атака повтором.
Хеш RPID, флаги, и счетчик одинаковы для аттестационного и авторизационного ответов. В аттестационном ответе еще присутствует новая учетная запись и публичный ключ, Attested Credential Data, а также шестой бит в флагах AT будет высоким или 1.
AAGUID - Это уникальный идентификатор модели устройства. Все устройства одной модели будут иметь одинаковый AAGUID. В случае если вы не запросили аттестацию, то AAGUID будет заменен на нулевой GUID, или 00000000-0000-0000-0000-000000000000. Сам же AAGUID используется для поиска метаданных и проверку аттестации устройства.
CredId length - uint16 длина идентификатора учетной записи, credId.
COSE Public Key - Публичный ключ кодированный в формате COSE.
Декодирование AuthData делается обычной резкой буфера:
Кратко о контекстах безопасности в WebAuthn/FIDO2
В модели безопасности WebAuthn/FIDO2 есть два понятия: origin и rpid, или контекст для пользователя и контекст для аутентификатора.
RPID же это фактически домен родителя: “example.com”. Аутентификатор хранит учетные записи у себя парой: id + хеш RPID. Это защищает учетную запись от несанкционированного доступа со стороны посторонних сайтов. pupkin.com не сможет запросить доступ к учетной записи example.com, так как даже зная id, хеш RPID не совпадет.
Origin это описание контекста браузера. Фактически это просто URL который видит пользователь, “https://example.com”, но с отсутствующим путем. Еще он может включать в себя порт если тот не стандартный 80 или 443. Origin используется для защиты от фишинга, так как если origin не совпадает со строгим списком разрешенных то значит что это была фишинг атака.
Домен в origin и RPID могу различаться. Иногда нам приходиться менять откуда пользователь хочет зайти в свой аккаунт: auth.example.com, потом станет login.example.com, который потом переезжает на example.com. Собственно для этого и есть RPID. Во время создания учетной записи можно использовать RPID для регистрации учетной записи в специфичном контексте: https://login.example.com может запросить RPID example.com. Это возможно потому что субдомен может запросить доступ к контексту родительского домена. Ребенок выше. Так что субдомен login.example.com сможет запросить example.com, но не наоборот. Во время логина, в вызове WebAuthn API сервер просто установит RPID “example.com” и таким методом получит доступ к учетной записи. То есть пользователь у себя в будет видеть адрес “https://login.example.com” но использовать учетную запись с “example.com”. Это очень удобно для того чтобы иметь общий контекст для учетных записей, чтобы быть независимым от структуры субдоменов, но при этом иметь возможность контролировать с каких доменов можно производить аутентификацию.
Авторизационный ответ / Assertion result
Типичный авторизационый ответ выглядит так:
Главное отличие от аттестационного ответа это отсутствие attestationObject, и наличие новых полей:
AuthenticatorData - Это тот же attestationObject.authData, только без аттестационных данных и новой учетке
Signature - Подпись
UserHandle - Собственно это и есть user.id.
Пошаговый процесс проверки:
Декодируем ClientDataJSON
Проверяем чтобы вызов, challenge, совпадал с тем что мы сгенерировали. Если не совпадает то это скорее всего была MITM атака.
Проверяем чтобы origin был в списке разрешенных. Если не совпадает то мы словили фишинг атаку!
Проверяем чтобы ClientDataJSON тип был “webauthn.create” или “webauthn.get”
Если у нас аттестационный ответ, то парсим attestationObject
Декодируем attestationObject.authData или authenticatorData.
Проверяем чтобы UP и UV бит флаги былы высокими. Если UV низкий, то мы получили только один фактор(владения устройством) и соответственно нужен еще и пароль.
Берем RPID который мы установили. Если rp.id не был установлен значит ваш RPID это ваш домен вашего сайта: example.com. Хешируем с помощью SHA256 и проверяем чтобы хеш RPID совпадал с authData.RPIDHash.
Если это webauthn.create:
Если формат, fmt, у нас “none”, то просто успешно завершаем возвращая новый публичный ключ, идентификатор, и счетчик.
Если же мы запросили аттестацию, то проводим её проверку:
Packed: https://medium.com/webauthnworks/verifying-fido2-packed-attestation-a067a9b2facd
FIDO-U2F: https://medium.com/webauthnworks/verifying-fido-u2f-attestations-in-fido2-f83fab80c355
Android Keystore: https://medium.com/webauthnworks/webauthn-fido2-verifying-android-keystore-attestation-4a8835b33e9d
Android SafetyNet: https://medium.com/webauthnworks/verifying-fido2-safetynet-attestation-bd261ce1978d
TPM: https://medium.com/webauthnworks/verifying-fido-tpm2-0-attestation-fc7243847498
Apple Anonymous Attestation: https://medium.com/webauthnworks/webauthn-fido2-verifying-apple-anonymous-attestation-5eaff334c849
Если это webauthn.get
Хешируем ClientDataJSON с помощью SHA256
Конкатенируем authData/authenticatorData с хешем clientDataJSON чтобы получить изначальное сообщение
Используя ранее сохраненный публичный ключ, проверяем подпись
Проверяем счетчик: если сохраненное значение счетчика было ноль, и значение счетчика в авторизационном ответе тоже ноль, значит устройство не поддерживает счетчик, пропускаем.
Если же сохраненное значение не ноль, или значение счетчика в авторизационном не ноль, то проверяем чтобы новое значение счетчика было больше чем сохраненное.
Сохраняем новое значение счетчика в базе данных.
Полезные ресурсы
Список полезных ресурсов по WebAuthn API и FIDO2 - https://github.com/herrjemand/awesome-webauthn
WebAuthn API спецификация - https://w3c.github.io/webauthn/
Моя библиотека для работы с COSE - https://github.com/WebauthnWorks/webauthn-cose
Пример серверного API используемого FIDO Alliance - https://github.com/fido-alliance/conformance-test-tools-resources/blob/master/docs/FIDO2/Server/Conformance-Test-API.md