Сравнение ключей доступа и паролей — это сравнение общего секрета с аутентификацией по открытому ключу. Пароль известен пользователю и, в виде хеша, серверу, и он срабатывает на любой странице, которая его запросит, в том числе на поддельной. Ключ доступа (passkey) — это учётные данные WebAuthn: пара ключей, созданная для одного сайта, которую пользователь разблокирует на своём устройстве отпечатком пальца, лицом или PIN-кодом и которой подписывается каждый раз новый challenge, привязанный к идентификатору сайта.
Коротко о главном
| Пароль | Ключ доступа | |
|---|---|---|
| Что доказывает пользователь | Знание общего секрета | Владение закрытым ключом, который разблокируется локально биометрией или PIN-кодом |
| Что хранит сервер | Соль и медленный хеш (Argon2id, scrypt, PBKDF2) | Идентификатор учётных данных и открытый ключ (91 байт для P-256 в примере ниже) |
| Фишинговый сайт-двойник | Получает пароль, если пользователь его введёт | Не получает ничего, чем можно воспользоваться: учётные данные привязаны к RP ID и источнику (origin) сайта |
| Повторное использование на разных сайтах | Обычное дело, что и делает возможной подстановку учётных данных (credential stuffing) | Невозможно: одна пара ключей на сайт и аккаунт |
| Утечка базы данных | Офлайн-подбор по хешам | Утекают только открытые ключи; повторно предъявить нечего |
| NIST SP 800-63B-4 (июль 2025 г.) | Однофакторный: не менее 15 символов, AAL1 | Многофакторный криптографический: AAL2; аппаратные ключи, привязанные к устройству, могут достигать AAL3, синхронизируемые ключи доступа — нет |
| Потеря устройства | Не влияет | Синхронизируемые ключи доступа восстанавливаются вместе с аккаунтом у поставщика; для ключей, привязанных к устройству, нужны резервные учётные данные |
| Другие устройства | Вводится где угодно | Синхронизируются между устройствами поставщика; для остальных — QR-код и проверка близости по Bluetooth (гибридный транспорт) |
| Стандарты | Только HTML-формы и HTTP-аутентификация | W3C WebAuthn Level 3 (рекомендация с 25 авг. 2026 г.), FIDO CTAP 2.2 |
Что происходит при входе
При регистрации аутентификатор (телефон, ноутбук, менеджер паролей или ключ безопасности) создаёт пару ключей для идентификатора доверяющей стороны (relying party ID, RP ID) сайта, например example.com, а сайт сохраняет открытый ключ. При входе сайт отправляет случайный challenge; браузер добавляет источник (origin) страницы в подписываемые клиентские данные, а аутентификатор после проверки пользователя подписывает их вместе с хешем RP ID. Сервер проверяет challenge, источник, хеш RP ID и подпись.
Скрипт ниже (Node.js 26.10, Apple M4 Max) показывает, что каждый из подходов оставляет на сервере и какие проверки останавливают сайт-двойник.
import { argon2Sync, createHash, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
const sha256 = (b) => createHash("sha256").update(b).digest();
// What a password database stores: an Argon2id hash (OWASP minimum: 19 MiB, t=2, p=1)
const params = { nonce: randomBytes(16), memory: 19456, passes: 2, parallelism: 1, tagLength: 32 };
const t0 = performance.now();
const tag = argon2Sync("argon2id", { message: "correct horse battery staple", ...params });
const ms = performance.now() - t0;
console.log(`password: stores salt + ${tag.length}-byte Argon2id hash; one guess costs ${ms.toFixed(0)} ms`);
// What a passkey database stores: the credential's public key
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const spki = publicKey.export({ type: "spki", format: "der" });
console.log(`passkey: stores a ${spki.length}-byte public key; a leak lets nobody sign in`);
// Authenticator: signs authenticatorData || SHA-256(clientDataJSON)
function getAssertion(rpId, origin, challenge) {
const clientDataJSON = Buffer.from(JSON.stringify({ type: "webauthn.get", challenge, origin }));
const authData = Buffer.concat([sha256(rpId), Buffer.from([0x05]), Buffer.alloc(4)]);
const signature = sign("sha256", Buffer.concat([authData, sha256(clientDataJSON)]), privateKey);
return { clientDataJSON, authData, signature };
}
// Relying party: the checks from WebAuthn §7.2
function check(a, challenge) {
const c = JSON.parse(a.clientDataJSON);
return {
challenge: c.challenge === challenge,
origin: c.origin === "https://example.com",
rpIdHash: a.authData.subarray(0, 32).equals(sha256("example.com")),
signature: verify("sha256", Buffer.concat([a.authData, sha256(a.clientDataJSON)]), publicKey, a.signature),
};
}
const challenge = randomBytes(32).toString("base64url");
console.log("real site:", check(getAssertion("example.com", "https://example.com", challenge), challenge));
console.log("look-alike:", check(getAssertion("examp1e.com", "https://examp1e.com", challenge), challenge));
password: stores salt + 32-byte Argon2id hash; one guess costs 25 ms
passkey: stores a 91-byte public key; a leak lets nobody sign in
real site: { challenge: true, origin: true, rpIdHash: true, signature: true }
look-alike: { challenge: true, origin: false, rpIdHash: false, signature: true }
Случай с двойником намеренно даёт злоумышленнику фору: на практике браузер вообще не станет использовать учётные данные example.com на другом сайте, так что подпись даже не будет создана. Но и подпись, полученная ретрансляцией сеанса, содержала бы чужой источник и хеш RP ID. Поэтому NIST относит WebAuthn к устойчивым к фишингу методам благодаря привязке к имени проверяющей стороны (verifier name binding), а пароли, одноразовые коды и подтверждения через push-уведомления к ним не относит. С Argon2id каждая попытка угадать пароль обходится в десятки миллисекунд, но если пользователь выбрал распространённый или повторно используемый пароль, его всё равно быстро подберут; открытый ключ не даёт злоумышленнику ничего, что можно угадать.
Где у каждого способа слабые места
Пароли подводят из-за повторного использования (подстановка учётных данных по утёкшим спискам), фишинговых страниц и фишинговых прокси, которые работают в реальном времени и перехватывают заодно одноразовые коды, а также из-за неудачно выбранных паролей. Поэтому NIST SP 800-63B-4 требует не менее 15 символов для однофакторных паролей (8 — в составе многофакторной аутентификации), запрещает правила состава и периодическую смену, требует проверять новые пароли по спискам утёкших и распространённых значений и рекомендует разрешать пароли длиной не менее 64 символов. Менеджер паролей с автозаполнением, привязанным к домену, решает проблему повторного использования и отчасти фишинга, но он есть не у каждого пользователя.
Ключи доступа переносят риск в два других места. Первое — аккаунт у поставщика: синхронизируемые ключи доступа защищены ровно настолько, насколько защищён аккаунт Apple, Google, Microsoft или менеджера паролей, в котором они хранятся, поэтому NIST не допускает синхронизируемые экспортируемые ключи на уровне AAL3. Второе — восстановление доступа к аккаунту на вашем сайте: если потерянный ключ доступа можно заменить по ссылке из письма или коду из SMS, злоумышленники пойдут этим путём. Кроме того, чтобы не потерять доступ к аккаунту, пользователям нужны вторые учётные данные или поставщик с синхронизацией.
Что когда выбирать
- Потребительские сайты и приложения: ключи доступа как основной способ, их создают сразу после очередного входа по паролю; пароли — по желанию.
- Вход сотрудников: ключи доступа через вашего поставщика удостоверений; привязанные к устройству ключи безопасности для администраторов и других ролей уровня AAL3.
- Ценные потребительские аккаунты (банки, почта, облачные консоли): ключи доступа плюс надёжная процедура восстановления, в идеале — второй зарегистрированный ключ доступа.
- Общие устройства и киоски: гибридный вход с телефона пользователя, а не хранение учётных данных на общем устройстве.
- Протоколы без WebAuthn (IMAP, устаревшие VPN, SSH): пароли приложений или потоки OAuth, которые начинаются в браузере, где ключи доступа работают.
Заметки о миграции
- Добавьте ключи доступа рядом с паролями: после успешного входа по паролю предложите создать ключ доступа или используйте условное создание (conditional creation) из WebAuthn Level 3, чтобы ключ добавлялся автоматически, если менеджер паролей браузера это поддерживает.
- Используйте интерфейс автозаполнения: на странице входа вызывайте
navigator.credentials.getсmediation: "conditional", чтобы ключи доступа появлялись в подсказках поля имени пользователя. - Храните то, что понадобится: идентификатор учётных данных, открытый ключ, счётчик подписей, транспорты, флаги резервного копирования и AAGUID, который указывает на поставщика; поиск по AAGUID сопоставляет его с конкретным продуктом.
- Наладьте восстановление, прежде чем убирать пароли: требуйте уже существующий ключ доступа, второй зарегистрированный ключ доступа или подтверждение личности, а не просто ссылку из письма.
- Поддерживайте синхронизацию с поставщиками с помощью сигнальных методов (signal methods) из Level 3, когда пользователи удаляют учётные данные или меняют имена, и объявите связанные источники (related origins), если вход у вас работает на нескольких доменах.
- Перед развёртыванием протестируйте всё в песочнице ключей доступа и разберите реальные ответы в инспекторе WebAuthn.
Источники
- W3C Web Authentication Level 3 (§ 7.1: регистрация; § 7.2: церемония аутентификации)
- NIST SP 800-63B-4, аутентификация и управление аутентификаторами (§ 3.1.1.2: пароли; § 3.2.5: устойчивость к фишингу; приложение B: синхронизируемые аутентификаторы)
- FIDO Alliance: протокол взаимодействия клиента с аутентификатором (CTAP) 2.2
- OWASP: шпаргалка по хранению паролей (Password Storage Cheat Sheet)
- RFC 9106, Argon2