21 февраля 2026
Passkey позволяет входить без передачи общего секрета серверу. Аутентификатор использует приватный ключ, а сервер хранит публичный и проверяет подпись. Проверки RP ID, origin и challenge обеспечивают устойчивость к фишингу и повторному использованию ответа. При этом нужно отдельно защищать сессии, восстановление аккаунта и регистрацию новых ключей.
Эволюция криптографии в аутентификации
Чтобы понять, почему passkey работает именно так, полезно увидеть его как очередной шаг сорокалетней траектории public-key cryptography. До 1976 года вся криптография была симметричной: обе стороны должны были заранее знать общий секрет. Это удобно для личной переписки между двумя людьми, но катастрофично для аутентификации миллионов пользователей в открытой сети - общий секрет нужно как-то безопасно передать, и любая утечка с одной стороны компрометирует обе.
Прорыв сделали Whitfield Diffie и Martin Hellman в статье «New Directions in Cryptography» (IEEE Transactions on Information Theory, 1976). Они показали, что две стороны могут договориться об общем секрете, обмениваясь только публично наблюдаемыми сообщениями - механизм, который сегодня известен как Diffie-Hellman key exchange. Возможность получить общий секрет через обмен публичными сообщениями изменила подход к криптографии: безопасность теперь опиралась не на охрану канала передачи, а на математическую сложность дискретного логарифма.
Через год Ron Rivest, Adi Shamir и Leonard Adleman опубликовали алгоритм RSA («A Method for Obtaining Digital Signatures and Public-Key Cryptosystems», Communications of the ACM, 1978), реализовавший идею пары ключей в практической форме: один ключ шифрует или верифицирует подпись (публичный), другой расшифровывает или подписывает (приватный). RSA сделал возможным то, что в passkey используется буквально - сервер хранит публичный ключ, клиент подписывает приватным, никакого общего секрета на двоих больше не существует. Утечка серверной базы перестаёт компрометировать пользователей; это и есть архитектурное основание passkey, заложенное за полвека до самого WebAuthn.
Следующий концептуальный шаг сделали Neal Koblitz и Victor Miller, независимо предложившие в 1985 году elliptic curve cryptography (ECC). RSA при сравнимом уровне безопасности требует ключи в 2048-3072 бита; ECC даёт ту же стойкость на 256 битах. Для реализации passkey это важно: подпись на 256-битной кривой укладывается в десятки байт, генерируется и проверяется за миллисекунды на телефоне без специальных ускорителей. Daniel J. Bernstein в 2005 году предложил Curve25519 - конкретную кривую, спроектированную с учётом устойчивости к timing-атакам и простоты реализации. Современный WebAuthn в большинстве реализаций по умолчанию использует ES256 (ECDSA на P-256, NIST-кривая), но идейно стоит на той же линии: ECC - это то, что сделало asymmetric cryptography пригодной для массового потребительского устройства.
Параллельно индустрия искала формат для пользовательской аутентификации. FIDO Alliance был основан в 2012 году консорциумом, включавшим PayPal, Lenovo и Nok Nok Labs, с задачей стандартизировать сильную аутентификацию без паролей. Первый стандарт - U2F (Universal 2nd Factor) 2014 года - решал только задачу второго фактора с физическим ключом. Второй - UAF (Universal Authentication Framework) - локальную биометрию. В 2018-2019 годах эти линии слились в FIDO2 (CTAP2 - протокол общения с аутентификатором) и WebAuthn (W3C-стандарт для браузера), который в марте 2019 года получил статус W3C Recommendation. Это и есть моментальный снимок, на котором вырос passkey.
Следующий этап - совместное заявление Apple, Google и Microsoft 5 мая 2022 года (World Password Day) о синхронизируемых passkey. До этого WebAuthn-credential по умолчанию был привязан к конкретному устройству; потеря телефона означала потерю credential. Соглашение трёх платформ ввело cross-device sync через iCloud Keychain, Google Password Manager и (с задержкой) Windows Hello, что превратило WebAuthn из «удобной альтернативы для гиков» в массовую технологию, доступную обычному пользователю смартфона.
Проблемы паролей
Пароли существуют с 1960-х годов и до сих пор остаются основным способом аутентификации - несмотря на то, что за это время стало очевидно: они плохо работают в реальном мире.
Повторное использование
Среднестатистический пользователь имеет десятки, а то и сотни аккаунтов. Запомнить уникальный сложный пароль для каждого невозможно. Результат предсказуем: один пароль используется на многих сервисах. Когда утекает один сервис - злоумышленники проверяют учётные данные на всех остальных (credential stuffing).
Фишинг
Пользователь не может надёжно отличить настоящий сайт от поддельного. Фишинговые страницы, имитирующие популярные сервисы, воруют пароли даже у технически грамотных людей. По данным Google, фишинг - причина большинства захватов аккаунтов.
Утечки баз данных
Даже если сервис хранит пароли правильно - в виде bcrypt или Argon2 хэшей - при утечке базы злоумышленники могут перебрать слабые пароли офлайн на GPU. Хэш "password123" будет взломан за секунды.
Усталость от паролей и слабые пароли
Требования к сложности паролей (заглавная буква, цифра, спецсимвол) не делают их безопаснее - они делают их неудобными. Пользователи приспосабливаются предсказуемо: Password1! формально удовлетворяет всем требованиям, но взламывается мгновенно.
Трение от MFA
Двухфакторная аутентификация снижает риски, но добавляет трение: нужно доставать телефон, переключаться между приложениями, вводить одноразовый код. Часть пользователей отключает MFA из-за неудобства. SMS-коды вдобавок уязвимы к SIM-swapping и перехвату.
Что такое Passkey
Passkey - это учётные данные на основе стандарта FIDO2 / WebAuthn, разработанного альянсом FIDO (Fast IDentity Online) совместно с W3C. Вместо пароля используется пара криптографических ключей.
Асимметричная криптография
При создании passkey устройство генерирует пару ключей:
- Приватный ключ используется аутентификатором и не передается серверу приложения. У синхронизируемых passkey перенос ключевого материала зависит от провайдера credential; аппаратная защита зависит от реализации.
- Публичный ключ - передаётся на сервер при регистрации и хранится там
При аутентификации сервер отправляет случайный вызов (challenge). Устройство подписывает его приватным ключом. Сервер проверяет подпись публичным ключом. Приватный ключ при этом не передается серверу приложения.
Пользователь Устройство Сервер
| | |
| Вход по биометрии | |
|------------------------->| |
| | (приватный ключ в |
| | Secure Enclave) |
| | |
| | Подписать challenge |
| |<---------------------------|
| | |
| | Подпись(challenge) |
| |--------------------------->|
| | |
| | Проверить подпись |
| | публичным ключом |
| | OK |
|<----------------------------------------------- 200 --|
Привязка к домену (origin binding)
Credential связан с RP ID, а ответ WebAuthn - с origin и challenge. Это разные проверки: RP ID задает область применения credential, origin обозначает сайт, вызвавший API. Сервер должен проверить оба значения и подпись с помощью поддерживаемой WebAuthn-библиотеки.
Верификация пользователя
Перед использованием приватного ключа устройство требует подтвердить личность пользователя через:
- Биометрию: Touch ID, Face ID, распознавание лица или отпечатка на Android
- PIN-код устройства (как резервный вариант)
Биометрические данные обрабатываются локально на устройстве и никогда не покидают его - ни на сервер, ни к Apple/Google.
Синхронизация между устройствами
Платформенные passkey синхронизируются через облако платформы:
- Apple: iCloud Keychain (сквозное шифрование)
- Google: Google Password Manager
- Windows: доступность и синхронизация зависят от версии системы и провайдера passkey. QR-вход с другого устройства не означает экспорт ключа.
Как WebAuthn обеспечивает устойчивость к фишингу
Фраза «passkey устойчив к фишингу» звучит маркетингово, но за ней стоит конкретный протокольный механизм, который полезно разобрать формально. Ключевая разница с TOTP, SMS, magic link и любой другой схемой, переносящей секрет через пользователя - в том, что подпись passkey включает в себя имя сайта, на котором эта подпись была создана, и сервер проверяет это имя криптографически.
Что подписывает аутентификатор
При вызове navigator.credentials.get() браузер собирает структуру clientDataJSON, которая включает три поля, критичных для anti-phishing:
type-"webauthn.get"для аутентификации (или"webauthn.create"для регистрации). Это разделяет контексты: подпись, сделанная для регистрации, не пройдёт верификацию аутентификации, и наоборот.challenge- случайные байты от сервера; защищают от replay (повторного использования старой подписи).origin- полный origin страницы, напримерhttps://github.com. Это значение браузер берёт не от сайта, а от самого себя - оно соответствует window.location.origin того контекста, в котором WebAuthn API был вызван.
Затем аутентификатор отдельно проверяет: rp.id, переданный сервером в запросе, должен соответствовать domain или быть его суффиксом. Если страница на shop.example.com просит подписать challenge с rp.id: example.com - можно (suffix совпадает); если просит подписать с rp.id: other.com - аутентификатор отказывает. Хеш rp.id (rpIdHash) включается в подписываемые данные authenticatorData.
В итоге подпись создаётся над комбинацией authenticatorData || SHA-256(clientDataJSON), и эта подпись доказывает: пользователь аутентифицировался на сайте, чей origin равен https://github.com, а не на каком-либо другом.
Что проверяет сервер
На стороне сервера верификация (которую за вас сделает любая нормальная webauthn-библиотека) включает:
- Распарсить clientDataJSON.
- Сверить
clientDataJSON.originс ожидаемым origin сервиса. Если ожидаетсяhttps://github.com, а пришлоhttps://github.evil.com- отказ. - Сверить
clientDataJSON.challengeс ранее выданным challenge из сессии. - Проверить, что
authenticatorData.rpIdHash == SHA-256(rp.id). - Проверить подпись публичным ключом из БД.
Контраст: как ломается TOTP
Сравним с реалистичной фишинговой атакой на TOTP. Пользователь получает письмо «ваш аккаунт под угрозой, войдите» со ссылкой на github.evil.com. Сайт-фишер отлично имитирует логин-форму. Пользователь вводит логин и пароль; сайт-фишер в реальном времени передаёт их на настоящий github.com. Настоящий сайт просит TOTP-код. Сайт-фишер просит TOTP-код у пользователя. Пользователь смотрит в Google Authenticator, видит шесть цифр (в которых нет имени домена), вводит их. Сайт-фишер передаёт код на github.com и получает сессию. Атака занимает несколько секунд - явно меньше 30-секундного окна валидности кода.
В мире passkey тот же сценарий ломается на четвёртом шаге. Пользователь нажимает «Войти через passkey» на github.evil.com. Браузер вызывает WebAuthn с rp.id github.evil.com (или evil.com - произвольно, что сайт указал). Аутентификатор смотрит в свою базу: для этого rp.id у меня credential нет. Возможны два варианта: либо аутентификатор честно отвечает «нет credential» и атака просто не проходит, либо (если фишер пытается выдать страницу за github.com) браузер обнаруживает несоответствие origin и rp.id и отказывает. Никакого «пользователь должен внимательно смотреть на адресную строку» - проверка делается криптографией.
То же касается magic link с TLS-сертификатом на похожем домене (g1thub.com) - пользователь кликает, попадает на фишерный сайт, тот переотправляет magic-токен на настоящий сайт. Passkey не имеет такой уязвимости, потому что credential привязан к origin, а origin браузер защищает от подмены сертификатами и SOP (same-origin policy).
Как работает регистрация
Регистрация passkey - это создание пары ключей на устройстве и передача публичного ключа на сервер. Используется API navigator.credentials.create().
Схема регистрации
Клиент Сервер
| |
|--- GET /register/start --------->|
| |
|<-- challenge, rp, user ----------|
| (опции регистрации) |
| |
| navigator.credentials.create() |
| (пользователь проходит |
| биометрию/PIN) |
| |
|--- POST /register/finish ------->|
| publicKey, attestation |
| |
| Сервер сохраняет публичный |
| ключ, привязывает к userId |
| |
|<-- 200 OK -----------------------|
JavaScript: создание credential
const publicKeyCredentialCreationOptions = {
// Случайный вызов от сервера - защита от replay-атак
challenge: Uint8Array.from("random_challenge_from_server", c => c.charCodeAt(0)),
// Информация о сервисе (Relying Party)
rp: {
name: "MyApp",
// id по умолчанию = текущий домен
// Можно задать явно: id: "myapp.com"
},
// Информация о пользователе
user: {
id: Uint8Array.from("user_id", c => c.charCodeAt(0)),
name: "user@example.com",
displayName: "John Doe",
},
// Поддерживаемые алгоритмы подписи
// -7 = ES256 (ECDSA с SHA-256) - стандарт де-факто
// -257 = RS256 (RSA с SHA-256) - для совместимости с Windows Hello
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 },
],
authenticatorSelection: {
// "required" - обязательная верификация пользователя (биометрия/PIN)
userVerification: "required",
// "required" - создать resident key (discoverable credential)
// позволяет входить без предварительного ввода email
residentKey: "required",
},
timeout: 60000,
// "none" - не требовать аттестацию аутентификатора
// Для обычных приложений достаточно; enterprise может требовать "direct"
attestation: "none",
};
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions,
});
Ключевые параметры
| Параметр | Назначение | Типичное значение |
|---|---|---|
challenge |
Случайные байты от сервера, защищают от replay-атак | 16-32 байт, криптографически случайные |
rp.id |
Домен сервиса, к которому привязан passkey | example.com (без протокола и пути) |
user.id |
Уникальный идентификатор пользователя | Случайный UUID, не email и не username |
pubKeyCredParams |
Список поддерживаемых алгоритмов подписи | ES256 (-7) и RS256 (-257) |
userVerification |
Обязательность верификации пользователя | required для passkey |
residentKey |
Хранить credential на аутентификаторе (discoverable) | required для passkey без username |
attestation |
Аттестация устройства (проверка производителя) | none для большинства приложений |
Что возвращает credential
// credential.response содержит:
{
clientDataJSON, // JSON с challenge, origin, type - подписывается
attestationObject, // Содержит публичный ключ и данные аутентификатора
}
// credential.id - уникальный идентификатор credential
// Сохраняется на сервере, используется при аутентификации
Как работает аутентификация
При входе сервер отправляет новый случайный challenge. Устройство подписывает его приватным ключом. Сервер проверяет подпись публичным ключом, который хранится в базе с момента регистрации.
Схема аутентификации
Клиент Сервер
| |
|--- GET /auth/start ------------->|
| (опционально: email) |
| |
|<-- challenge, allowCredentials --|
| (список допустимых credential |
| для данного пользователя) |
| |
| navigator.credentials.get() |
| (пользователь проходит |
| биометрию/PIN) |
| |
|--- POST /auth/finish ----------->|
| assertion (подпись challenge) |
| |
| Сервер: найти публичный ключ |
| по credential.id, проверить |
| подпись, сверить challenge |
| |
|<-- 200 OK (сессия создана) ------|
JavaScript: подтверждение credential
const publicKeyCredentialRequestOptions = {
// Новый случайный challenge от сервера
challenge: Uint8Array.from("server_challenge", c => c.charCodeAt(0)),
timeout: 60000,
// Домен сервиса - должен совпадать с rp.id при регистрации
rpId: "yourdomain.com",
// Список допустимых credential для данного пользователя
// Если пользователь ввёл email - сервер возвращает его credential IDs
// Если нет (passwordless) - передаём пустой массив или опускаем поле
allowCredentials: [
{
id: Uint8Array.from("credential_id_from_server", c => c.charCodeAt(0)),
type: "public-key",
},
],
// Обязательная верификация пользователя
userVerification: "required",
};
const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
});
Что содержит assertion
// assertion.response содержит:
{
clientDataJSON, // JSON с challenge, origin, type
authenticatorData, // Метаданные: rpIdHash, флаги, счётчик подписей
signature, // Подпись authenticatorData + clientDataJSON
userHandle, // user.id из момента регистрации (если discoverable)
}
// assertion.id - идентификатор credential
// Сервер использует его для поиска соответствующего публичного ключа
Серверная часть
Сервер отвечает за три задачи: генерацию и хранение challenge, верификацию ответа аутентификатора, и хранение публичных ключей.
Что хранит сервер
-- Таблица credential'ов (публичных ключей)
CREATE TABLE passkey_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
credential_id BYTEA NOT NULL UNIQUE, -- assertion.id от клиента
public_key BYTEA NOT NULL, -- COSE-encoded публичный ключ
sign_count BIGINT NOT NULL DEFAULT 0,
-- Мета-информация
aaguid UUID, -- Идентификатор модели аутентификатора
transports TEXT[], -- ["internal", "hybrid"]
created_at TIMESTAMPTZ DEFAULT now(),
last_used_at TIMESTAMPTZ
);
-- Таблица challenge'ов (TTL: ~5 минут)
CREATE TABLE webauthn_challenges (
challenge BYTEA PRIMARY KEY,
user_id UUID REFERENCES users(id), -- NULL при passwordless
expires_at TIMESTAMPTZ NOT NULL
);
Верификация на сервере
Вручную реализовывать верификацию WebAuthn не нужно - это сложно и чревато ошибками. Используйте готовые библиотеки:
- Ruby: webauthn-ruby (cedarcode)
- Node.js: SimpleWebAuthn
- Python: py_webauthn (Duo Labs)
- Go: go-webauthn/webauthn
- Java: java-webauthn-server (Yubico)
Пример верификации (Ruby + webauthn-ruby)
# Регистрация - генерация опций
def registration_options
options = WebAuthn::Credential.options_for_create(
user: {
id: current_user.webauthn_id,
name: current_user.email,
display_name: current_user.name
},
authenticator_selection: {
user_verification: "required",
resident_key: "required"
},
attestation: "none"
)
session[:webauthn_challenge] = options.challenge
render json: options
end
# Регистрация - верификация ответа
def registration_finish
webauthn_credential = WebAuthn::Credential.from_create(params)
webauthn_credential.verify(session.delete(:webauthn_challenge))
current_user.passkey_credentials.create!(
external_id: Base64.strict_encode64(webauthn_credential.raw_id),
public_key: webauthn_credential.public_key,
sign_count: webauthn_credential.sign_count
)
rescue WebAuthn::Error => e
render json: { error: e.message }, status: :unprocessable_entity
end
# Аутентификация - верификация
def authentication_finish
stored_credential = PasskeyCredential.find_by!(
external_id: Base64.strict_encode64(params[:rawId])
)
webauthn_credential = WebAuthn::Credential.from_get(params)
webauthn_credential.verify(
session.delete(:webauthn_challenge),
public_key: stored_credential.public_key,
sign_count: stored_credential.sign_count
)
stored_credential.update!(sign_count: webauthn_credential.sign_count)
sign_in(stored_credential.user)
rescue WebAuthn::Error => e
render json: { error: e.message }, status: :unauthorized
end
Что проверяет библиотека при верификации
- clientDataJSON.type: должен быть
"webauthn.create"или"webauthn.get" - clientDataJSON.challenge: должен совпадать с challenge из сессии
- clientDataJSON.origin: должен совпадать с ожидаемым доменом - защита от фишинга
- authenticatorData.rpIdHash: SHA-256 от rp.id - должен совпадать
- Флаги UP/UV: проверяют присутствие пользователя и требуемую политикой локальную верификацию. UP и UV не взаимозаменяемы.
- Подпись: проверяется по публичному ключу из базы данных
Совместимость
Браузеры и платформы
| Платформа | Браузер / Среда | Поддержка | Хранилище ключей |
|---|---|---|---|
| macOS 13+ | Safari, Chrome, Firefox, Edge | Полная | iCloud Keychain |
| iOS / iPadOS 16+ | Safari, Chrome, Firefox (через Safari WebKit) | Полная | iCloud Keychain |
| Android 9+ | Chrome, Firefox, Edge | Полная | Google Password Manager |
| Windows 10/11 | Chrome, Firefox, Edge | Полная | Windows Hello (локально) |
| Linux | Chrome, Firefox | Частичная | Роуминговые ключи (USB), нет платформенного |
Ограничения
- Требуется HTTPS - WebAuthn работает только на HTTPS-сайтах. Исключение:
localhostдля разработки. - Инкогнито-режим - passkey недоступны в инкогнито-вкладках Chrome и приватных окнах Safari (нет доступа к хранилищу). Роуминговые ключи (YubiKey) работают везде.
- Несколько устройств - платформенные passkey синхронизируются только в рамках одной экосистемы. Пользователь с iPhone и Windows-ноутбуком на разных экосистемах может столкнуться с трудностями.
Cross-device authentication (Hybrid transport)
WebAuthn поддерживает аутентификацию через другое устройство по QR-коду. Сценарий: пользователь заходит с Windows-ноутбука, сканирует QR телефоном с Android, подтверждает биометрией на телефоне. Связь установлена по Bluetooth (для proximity check) + облачный relay. Никакие секреты не передаются по Bluetooth - только зашифрованный туннель.
Windows (Chrome) Android (Chrome)
| |
| Показать QR-код |
|--------------------------------->| (сканировать QR)
| |
|<-- Bluetooth proximity check --->|
| |
| | (биометрия)
| |
|<-- Подписанный assertion --------|
| (через зашифрованный |
| облачный канал) |
| |
| Верификация подписи |
| Сессия создана |
Passkey vs другие методы
| Метод | Безопасность | UX | Фишинг-устойчивость | Сложность внедрения |
|---|---|---|---|---|
| Passkey | Высокая | Высокий | Полная | Средняя |
| Пароль | Низкая | Низкий | Нет | Простая |
| Пароль + TOTP (Google Authenticator) | Средняя | Низкий | Частичная | Средняя |
| SMS OTP | Низкая | Средний | Нет | Средняя |
| Magic Link (email) | Средняя | Средний | Частичная | Простая |
| OAuth (Google/GitHub Sign-In) | Средняя | Высокий | Частичная | Простая |
| Hardware key (YubiKey) | Максимальная | Средний | Полная | Высокая |
Почему Passkey лучше TOTP
TOTP (Time-based One-Time Password, коды из Google Authenticator) - значительно лучше SMS, но уязвим к real-time фишингу. Атакующий создаёт поддельный сайт, пользователь вводит код, атакующий мгновенно использует его на настоящем сайте. Код действителен 30 секунд - достаточно для автоматизированной атаки. Подробный механический разбор - в разделе об устойчивости к фишингу.
Почему Passkey лучше OAuth
OAuth (Sign in with Google/GitHub) удобен, но создаёт зависимость от провайдера. Если Google заблокирует аккаунт или закроет OAuth API для вашего приложения - пользователи потеряют доступ. Passkey хранится на устройстве пользователя и не зависит от третьих сторон.
Истории внедрения
Абстрактные аргументы в пользу passkey стоят меньше, чем разобранные истории компаний, которые прошли путь внедрения и публично об этом написали. Ниже - три кейса с разным контекстом и разными уроками. Источники приведены в разделе «Источники»; если факт указан без ссылки, это стилизованный пример на основе типичных паттернов.
Внедрение passkey можно начинать с дополнительного способа входа и регистрации нескольких ключей. Конкретные даты и этапы внедрения у других сервисов нужно проверять по их публикациям; они не задают обязательный порядок для вашего продукта.
Корпоративный доступ. При выборе passkey для организации нужно решить, допустима ли синхронизация или требуется привязанный к устройству аутентификатор. authenticatorAttachment различает платформенные и внешние аутентификаторы, но сам по себе не запрещает синхронизацию. Требования к устройствам, аттестации и отзыву credential задаются отдельной политикой.
Стилизованный пример: B2B SaaS на 50 000 активных пользователей. Это не задокументированный кейс, а стилизованный пример на основе типичных паттернов, которые приходится наблюдать у B2B-команд уровня Series-B. Команда добавила passkey как дополнительный способ входа в дополнение к existing email/password + TOTP. Promo-баннер в личном кабинете с предложением «привязать устройство для быстрого входа». За первые шесть месяцев passkey включили около 8% пользователей; средний support-load по запросам «не могу войти» снизился на 12% именно в этой когорте. Важный урок - команда сделала следующее намеренно: passkey добавлен в дополнение, не вместо пароля; в момент регистрации passkey пользователю явно сказано «вы можете отвязать его в любой момент, ваш пароль продолжит работать»; recovery-flow остался прежним (email magic link), и не было ни одного локаута. Когда через год команда попыталась сделать «вход только по passkey» для новых аккаунтов, конверсия регистрации упала на 18%, и эксперимент откатили. Стилизация, но шаблон такого отката хорошо знаком.
Все три истории сходятся на одном: passkey - не «новая система входа, сметающая старую», а добавочный фактор, который выигрывает за счёт UX-преимущества там, где он включается, и не должен ломать вход для тех, кто его пока не включил. Никто из перечисленных - даже GitHub - не пошёл по пути «passkey only, password больше нет».
Антипаттерны и их следствия
Большинство неудачных внедрений passkey ломаются не на этапе «не работает API», а на этапе «работает, но в продакшне начинаются жалобы». Ниже - типичные failure modes; каждый из них стоит пройти как чеклист перед публичным запуском.
Отсутствие fallback-метода. Самый частый антипаттерн в рассказах команд, впервые внедряющих passkey. Логика выглядит соблазнительно: «зачем нам пароль, если есть passkey - давайте сделаем passkey-only». Реальность: пользователь меняет телефон, не успел настроить iCloud, потерял YubiKey, переехал с Android на iPhone, потерял доступ к старому Apple ID. Без recovery-канала (email magic link, recovery codes, бэкап-passkey на втором устройстве) такие пользователи становятся локаутами. Поскольку passkey не подразумевает «service desk видит ваш пароль» - сбросить нечего. Правильно: всегда оставлять как минимум один независимый recovery-канал, и проектировать его до запуска passkey, а не «после первого инцидента».
Слепая опора на платформенный sync. Команда исходит из «Apple/Google синхронизируют passkey через своё облако, нам ничего проектировать не надо». Пользователь делает factory reset телефона без настроенного iCloud Keychain - все passkey исчезают. Пользователь меняет основной Apple ID (например, переезжает между странами и создаёт новый) - passkey остаются на старом ID. Пользователь использует enterprise-устройство, где iCloud Keychain отключён политикой - синхронизация не работает в принципе. Платформенный sync - это удобство для большинства, но не гарантия для всех. Сервис, который полагается на него как на «backup», ловит локауты на узкой, но регулярной долей пользователей.
Misunderstanding resident keys vs server-side keys. WebAuthn различает discoverable credentials (resident keys, хранятся на аутентификаторе) и non-resident keys (хранятся серверной частью credential ID, аутентификатор только умеет их подписывать, если ему credentialId передадут). Passkey по соглашению - это discoverable credential (residentKey: required), потому что только тогда пользователь может войти без предварительного ввода email («Войти через passkey» прямо на стартовом экране). Но многие команды по умолчанию ставят residentKey: preferred или забывают этот параметр. Результат - в часть аутентификаторов credential попадает как non-resident, и обещанный «passwordless без логина» не работает: пользователь обязан сначала ввести email, чтобы сервер вернул allowCredentials. UX-обещание сломано на этапе integration, не на этапе фронтенда.
Счетчик подписей. Некоторые аутентификаторы не ведут signCount; у синхронизируемых ключей он не обязательно растет строго последовательно. Неувеличившийся счетчик - сигнал риска, а не универсальное доказательство клонирования. Решение принимают с учетом свойств credential и других сигналов. От повторного использования ответа защищают одноразовый challenge и его проверка.
Passkey и биометрия. PIN или биометрия могут использоваться для локальной проверки пользователя. Поддержка Face ID не определяет поддержку passkey. Проверка доступности платформенного аутентификатора также не описывает все варианты входа: отдельно учитывают внешние ключи и вход с другого устройства.
No recovery flow. Связанный с первым, но более узкий: команда добавила recovery-канал (например, email), но не протестировала пользовательский путь до конца. Пользователь нажимает «забыл passkey», получает email, кликает - попадает на страницу, где требуется ввести пароль (которого нет, потому что passwordless), или предлагается войти через passkey, который пользователь как раз потерял. Recovery-flow проектируется отдельно как первый-класс пользовательский путь, не как «ну, отправим magic link, дальше разберутся». Тестировать его надо на реальных сценариях: «новый телефон без аккаунтов», «утерянный YubiKey», «старый Apple ID без доступа».
Регистрация только с passkey. Такой выбор нужно проверить на поддерживаемых устройствах, вариантах восстановления и результатах тестирования сценария регистрации. Универсального процента потери конверсии из этого не следует.
Mishandled attestation. Параметр attestation определяет, требует ли сервер криптографического доказательства модели аутентификатора. Для большинства потребительских приложений правильное значение - "none": вы доверяете факту, что пользователь успешно прошёл верификацию, и не хотите видеть AAGUID конкретного устройства. Корпоративные сервисы иногда требуют "direct", чтобы фильтровать по разрешённым моделям (только корпоративные YubiKey, не любые consumer-аутентификаторы). Антипаттерн - запросить "direct" «на всякий случай»: часть платформенных аутентификаторов вернёт self-attestation или вообще откажет, и регистрация молча сломается в части браузеров. Attestation - инструмент с конкретной целью, а не «сделать систему более безопасной».
Гибридная аутентификация: переходный период
Если у вас новый продукт - вы можете спроектировать вход вокруг passkey с самого начала. Если у вас существующая user base в десятки тысяч аккаунтов с email/password, OAuth, magic link, TOTP - passkey появляется не как замена, а как ещё один фактор в наборе, и задача product-команды - провести transition без потерь конверсии и без локаутов.
Полезная рамка: рассматривать вход как набор из пяти возможных факторов, а не как один «выбранный метод». Эти факторы - email/password, OAuth (Google/Apple/GitHub), magic link, TOTP, passkey. Большинство пользователей будут включать два-три из них; product-задача - не выбрать «правильный» метод, а спроектировать UX так, чтобы пользователь понимал, что у него есть, и не оказывался без вариантов в момент потери одного из них.
Стратегия 1: passkey как primary, OAuth как fallback
Подходит для сервисов, где значительная часть аудитории уже привыкла к Sign in with Google/Apple. Passkey предлагается на главном экране входа («войти быстро, биометрией»); OAuth и пароль доступны через «другие способы». Преимущества: пользователь без passkey проходит знакомым путём; пользователь с passkey получает преимущество в скорости; нет принудительной миграции. Недостаток: вы остаётесь зависимы от OAuth-провайдеров для части аудитории.
Стратегия 2: magic link как переходный мост к passkey
Подходит для сервисов с историей passwordless через email magic link (Slack, Notion, Substack использовали этот паттерн годами). Magic link становится «дефолтным» путём для существующих пользователей, passkey - предложением «давайте сделаем это быстрее: привяжите устройство». Через несколько успешных входов сервис показывает one-tap promo «зарегистрируйте passkey за 30 секунд». Преимущество: пользователь уже привык к passwordless, переход психологически прост. Недостаток: magic link сам по себе уязвим к фишингу (ссылка из письма ведёт на любой сайт), так что переход критичен с security-перспективы.
Стратегия 3: progressive enhancement на следующем входе
Самый осторожный подход. Существующий пользователь продолжает заходить так же, как раньше - email/password или OAuth. После успешного входа сервис показывает one-time баннер «хотите включить passkey? одна биометрия вместо ввода пароля». Если пользователь нажал yes - регистрируется новый passkey, пароль сохраняется как fallback. Если no - больше не показываем минимум 30 дней. Такой подход ловит органическую конверсию, не создаёт давления, не ломает existing flow.
Стратегия 4: device-bound vs synced - сегментация по чувствительности
Для сервисов с разными уровнями чувствительности (бесплатный личный кабинет vs admin-консоль с финансовыми правами) полезно различать типы passkey. Платформенный синхронизируемый passkey (через iCloud/Google) удобен пользователю, но имеет theoretical attack surface на уровне облачного аккаунта платформы. Device-bound passkey (residentKey + authenticatorAttachment: cross-platform с YubiKey, или platform с required HW-binding на тех аутентификаторах, что это поддерживают) сильнее, но менее удобен. Стратегия: для обычного входа достаточно synced; для admin-операций требуется отдельный hardware-bound passkey. Это уже close к WebAuthn step-up authentication, поддерживается через параметр user_verification и через перепроверку при чувствительных действиях.
Стратегия 5: passkey + TOTP как два независимых фактора
В корпоративном/regulated сценарии (финтех, healthcare) одного passkey может быть недостаточно с compliance-перспективы - регулятор требует «two factors». В таких случаях passkey + TOTP даёт одновременно сильную фишинг-устойчивость (passkey не передаётся по фишинговому каналу) и формальное соответствие 2FA-требованиям. UX-цена - один лишний шаг, но в корпоративном сегменте это терпимо.
Общее правило для transition: не убирайте старые методы аутентификации до тех пор, пока не убедитесь, что 95%+ активных пользователей успешно используют новый метод как минимум 30 дней. Раннее отключение пароля или magic link создаёт класс пользователей, которые откроют сервис через полгода и обнаружат, что войти не могут - и поскольку «забыл passkey» не покрывается обычным password-reset flow, support получит поток жалоб.
Резюме
- Passkey - конец сорокалетней траектории public-key cryptography: Diffie-Hellman (1976), RSA (1978), ECC (Koblitz / Miller, 1985), Curve25519 (Bernstein, 2005), FIDO2/WebAuthn (W3C Recommendation, 2019), синхронизируемые passkey (Apple/Google/Microsoft, 2022).
- Асимметричная криптография: приватный ключ никогда не покидает устройство.
- Проверки RP ID, origin, challenge и подписи обеспечивают устойчивость WebAuthn к фишингу. Защита аккаунта также зависит от сессий, регистрации ключей и восстановления доступа.
- Биометрия верифицирует пользователя локально - данные не уходят на сервер.
- Утечка базы данных бесполезна - там хранится только публичный ключ.
- Синхронизация через iCloud Keychain / Google Password Manager - удобство для большинства, но не гарантия для всех; нужен fallback.
- Cross-device auth через QR + Bluetooth proximity - работает между экосистемами.
- Стандарт FIDO2/WebAuthn поддерживается всеми основными браузерами и платформами.
- Главные антипаттерны: отсутствие fallback, слепая опора на платформенный sync, игнорирование sign count, mandatory passkey на регистрации.
- Стратегия внедрения: passkey как additive method, не замена; progressive enhancement; recovery-flow проектируется до запуска.
Источники
Криптография: первоисточники
- Whitfield Diffie, Martin Hellman. New Directions in Cryptography (IEEE Transactions on Information Theory, 1976). Введение public-key cryptography; Diffie-Hellman key exchange.
- Ron Rivest, Adi Shamir, Leonard Adleman. A Method for Obtaining Digital Signatures and Public-Key Cryptosystems (Communications of the ACM, 1978). RSA - первая практическая реализация asymmetric crypto.
- Neal Koblitz. Elliptic Curve Cryptosystems (Mathematics of Computation, 1987) и Victor Miller. Use of Elliptic Curves in Cryptography (CRYPTO '85). Независимое предложение ECC, давшее технологическое основание для passkey-on-mobile.
- Daniel J. Bernstein. Curve25519: new Diffie-Hellman speed records (PKC 2006). Современная ECC-кривая, оптимизированная для безопасности и скорости.
FIDO/WebAuthn: стандарты
- W3C WebAuthn Level 2 - спецификация (Recommendation, апрель 2021; Level 1 был в марте 2019).
- FIDO2 overview - FIDO Alliance.
- CTAP2 specification - Client to Authenticator Protocol, дополняющий WebAuthn.
- Apple, Google и Microsoft о синхронизируемых passkey (FIDO Alliance, 5 мая 2022).
Реальные внедрения и кейсы
- GitHub - Introducing passwordless authentication on GitHub.com (12 июля 2023).
- Cloudflare - passkey support in Cloudflare Access.
- Have I Been Pwned - база скомпрометированных credentials, статистика по утечкам.
- passkeys.dev - developer-портал FIDO Alliance с примерами интеграции.
Серверные библиотеки и tooling
- webauthn-ruby (cedarcode) - Ruby.
- SimpleWebAuthn - Node.js / TypeScript.
- py_webauthn (Duo Labs) - Python.
- go-webauthn/webauthn - Go.
- java-webauthn-server (Yubico) - Java.
- OWASP: Passkey Security
- W3C: Web Authentication Level 3