PublicKeyCredential в веб-приложениях

Дата публикации: 2023-05-19

Этот пример показывает аутентификацию пользователя через PublicKeyCredential из Web API. Оба слоя демонстрации написаны на JavaScript; исходники доступны в flancer64/demo-webauthn-pubkey, а демонстрация — по адресу pk.auth.demo.teqfw.com.

Участники WebAuthn

Во взаимодействии участвуют пользователь, браузер, backend (Relying Party), хранилище и аутентификатор. Аутентификатор — физическое устройство или программа с асимметричной парой ключей, доступная браузеру: встроенный biometric/PIN-модуль устройства, security key или виртуальный authenticator для разработки.

В Chrome виртуальный authenticator включается в DevTools → More tools → WebAuthn. Он удобен для изучения потока, но не заменяет тестирование на реальных устройствах и браузерах.

Два процесса: attestation и assertion

Attestation регистрирует credential и публичный ключ на сервере:

  1. браузер запрашивает challenge для регистрации;
  2. сервер создаёт случайный challenge и сохраняет его;
  3. браузер просит authenticator создать ключи или использовать существующие;
  4. пользователь подтверждает действие PIN-кодом, отпечатком или Face ID;
  5. браузер отправляет attestation-данные серверу;
  6. сервер проверяет данные и связывает публичный ключ с пользователем.

Assertion подтверждает владение уже зарегистрированным credential:

  1. браузер запрашивает challenge для входа;
  2. сервер создаёт и сохраняет challenge;
  3. authenticator подписывает его приватным ключом после проверки пользователя;
  4. браузер передаёт assertion серверу;
  5. сервер проверяет подпись ранее сохранённым публичным ключом.

Приватный ключ не покидает authenticator. Сервер хранит только credential ID, публичный ключ и метаданные, достаточные для проверки.

Регистрация ключа

Сервер генерирует криптографически случайный challenge, например 32 байта через crypto.randomBytes, хранит его с ограниченным временем жизни и передаёт браузеру в base64url-представлении вместе с данными пользователя.

const attestation = await navigator.credentials.create({
  publicKey: {
    rp: { name: 'WebAuthn Demo' },
    user: { id: userIdBytes, name: email, displayName: email },
    challenge: challengeBytes,
    pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'preferred',
    },
  },
});

Бинарные attestationObject и clientDataJSON передаются на сервер в base64url. Сервер обязан проверить challenge, origin, rpId, тип операции, флаги user verification и формат attestation. Из данных извлекается публичный ключ, например в JWK, и сохраняется вместе с credential ID. Библиотеки cbor и parse-cosekey помогают разобрать демонстрационные данные.

Вход по assertion

Для входа backend создаёт новый одноразовый challenge для нужного credential. Браузер вызывает:

const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: challengeBytes,
    allowCredentials: [{ id: credentialIdBytes, type: 'public-key', transports: ['internal'] }],
  },
});

На сервер отправляются authenticatorData, clientDataJSON и signature. Проверка должна сопоставить сохранённый challenge, origin и rpId, убедиться в корректности флагов, проверить подпись публичным ключом и обработать signature counter в соответствии с политикой продукта. После этого приложение создаёт собственную сессию — WebAuthn не заменяет управление сессиями и авторизацию.

Практические требования

Вывод

PublicKeyCredential позволяет построить вход без пароля, где секрет остаётся в authenticator, а сервер проверяет криптографическое доказательство. Демонстрация полезна для понимания обмена challenge и credential. В реальном продукте важны также UX регистрации, управление ключами, безопасное восстановление и строгая проверка на сервере. Wiredgeese может спроектировать и внедрить такой поток в существующее веб-приложение.

Дополнительные фрагменты исходного кода

} * Demo_Back_Web_Api_Sign_Up * Fl32_Auth_Back_Mod_PubKey.attestChallengeCreate
/** @type {PublicKeyCredential} */
const attestation = await navigator.credentials.create({publicKey});
{
},
},
{
}
],
}
}
} * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.composeOptPkCreate
{
}
} * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.attest * Fl32_Auth_Back_Web_Api_Attest
{
} On the frontend, the attestation ID (DDn…6XA) is stored in local storage for later use in assertions.
{“attestationId”: “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.assertChallenge * Fl32_Auth_Back_Web_Api_Assert_Challenge
{
} * Fl32_Auth_Back_Web_Api_Assert_Challenge * Fl32_Auth_Back_Mod_PubKey.assertChallengeCreate
/** @type {PublicKeyCredential} */
const assertion = await navigator.credentials.get({publicKey});
{
{
]
}
]
}
} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.composeOptPkGet
{
} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.validate * Fl32_Auth_Back_Web_Api_Assert_Validate
{
} Then, retrieve the user’s public key from the database for the corresponding challenge (refer to step 2).