Passkey и WebAuthn
Passkey — это привязанные к origin публично-ключевые учётные данные; браузер подписывает истинный origin страницы, поэтому фишинговый или пересланный вход отвергается. Но WebAuthn защищает лишь событие входа — сессия и восстановление остаются на вас.
Вылизанный прокси на accounts-secure-login.com зеркалит вашу настоящую страницу входа пиксель в пиксель. Жертва приходит по убедительному письму, страница вызывает navigator.credentials.get, жертва делает Face ID, и атакующий перехватывает подлинную, криптографически валидную подпись — пересланную прямо от настоящего аутентификатора жертвы. С паролем он бы завладел аккаунтом. С TOTP-кодом у него был бы рабочий 30-секундный токен. С passkey у него подпись, которую ваш сервер отвергает на первой же проверке, потому что браузер жертвы вписал истинный origin — accounts-secure-login.com — в подписанные байты, а ваш сервер ждёт accounts.example.com. Атакующий сделал всё правильно и всё равно получил ничего. Это единственное, заполняемое браузером поле — и есть вся причина, по которой passkey называют устойчивыми к фишингу. Понять точно, почему это работает и чего оно не покрывает, — и есть разница между «развернуть WebAuthn» и «всё равно быть взломанным».
К концу урока вы будете знать механизм, делающий вход через passkey неподдающимся фишингу, компромиссы, которые вы принимаете при внедрении (восстановление, доверие синхронизируемым учётным данным, кража устройства), и режимы отказа, кусающие команды, которые думают, что passkey защищает весь аккаунт.
Что такое passkey на самом деле
Passkey — это учётные данные WebAuthn: асимметричная пара ключей, где приватный ключ никогда не покидает аутентификатор (защищённый анклав телефона, TPM ноутбука или роуминговый аппаратный ключ вроде YubiKey), а сервер хранит лишь соответствующий публичный ключ. Общего секрета нет. У сервера нет ничего, что атакующий мог бы выфишить, нет базы паролей для утечки, нет seed, который можно зеркалить. Это уже категориальное отличие от паролей и TOTP, где сервер держит проверяемый секрет — и его же держит любой, кто его украл.
Протокол задают две церемонии. Регистрация (navigator.credentials.create) генерирует свежую пару ключей, привязанную к rpId вашего сайта (регистрируемый домен, например example.com), и отправляет публичный ключ на ваш сервер, который сохраняет его за пользователем. Аутентификация (navigator.credentials.get) доказывает владение приватным ключом: сервер шлёт одноразовый случайный challenge, аутентификатор подписывает его (плюс контекст), а сервер проверяет подпись хранимым публичным ключом. Владение ключом, защищённое локальным жестом проверки пользователя (биометрия или PIN), и есть доказательство личности.
Ключевая часть — что именно подписывается. Аутентификатор не подписывает голый challenge. Он подписывает структуру, включающую clientDataJSON, — и браузер, а не JavaScript страницы, заполняет этот объект настоящим origin страницы и серверным challenge. Поэтому подпись привязывает утверждение к тому самому origin, на котором браузер пользователя реально находился. Эта привязка — вся суть.
Почему атака-пересылка проваливается: origin подписан, и подписан браузером
Пройдём атаку «противник посередине» (AiTM), которая побеждает пароли и TOTP, и посмотрим, как она умирает против passkey. Атакующий держит обратный прокси на похожем домене, честно пересылающий ваши настоящие страницы. Жертва попадает туда и аутентифицируется. С паролем прокси читает введённый секрет и переигрывает его на настоящем сайте — конец. С TOTP прокси читает введённый 6-значный код и переигрывает его в окне валидности — конец. Это работает, потому что секрет — читаемое значение на предъявителя: кто его перехватил, тот предъявит его где угодно.
Passkey ломает этот шаблон на уровне байтов. Когда браузер жертвы строит clientDataJSON, он вписывает туда origin страницы — а страница это accounts-secure-login.com. Аутентификатор подписывает эти байты. Прокси пересылает получившееся утверждение вашему настоящему серверу, который ждёт origin accounts.example.com. При проверке подпись криптографически валидна, но привязана к неверному origin, поэтому сверка origin на сервере отвергает её. Прокси не исправит это: он не заставит браузер солгать про origin (поле заполняет браузер, а не JS страницы) и не переподпишет под верный origin, потому что приватного ключа у него никогда не было. На практике аутентификатор обычно даже не покажет учётные данные, потому что rpId похожего домена не совпадёт с тем, на который passkey зарегистрирован, — и церемония провалится ещё до появления подписи. Атакующий остаётся с валидной подписью, бесполезной везде, кроме фишингового домена, к которому она привязана.
▸lesson.inset.note
Это и есть точный смысл «устойчивости к фишингу». Дело не в том, что пользователи умнее или клонировать страницу труднее, — клон может быть идеальным. Дело в том, что доказательство криптографически ограничено origin, который атакующий не контролирует, и это ограничение применяет браузер и перепроверяет сервер — две стороны, которых атакующий не может выдать за себя. Сравните с TOTP, который «устойчив к фишингу в маркетинге, фишабелен на практике»: код — это читаемое значение, которое пользователь отдаёт, и пересылка прокидывает его без проблем.
Какие компромиссы вы реально принимаете
Внедрение passkey не бесплатно, и цены не там, где их ищут новички.
Восстановление становится самой трудной частью, а не вход. Passkey живёт на устройстве или в фабрике синхронизации. Пользователи теряют телефоны, вайпят ноутбуки и меняют экосистемы. Поэтому вы обязаны предложить восстановление — и именно в восстановлении тихо решается безопасность всего аккаунта. Если ваш путь восстановления — «пришлём ссылку для сброса», а тот email защищён лишь паролем, то истинная сила аккаунта равна фишабельному паролю на стороннем ящике. Вы построили неподдающуюся фишингу парадную дверь и оставили нараспашку фишабельную боковую. Правило прямое: восстановление должно быть не слабее основного фактора. Самый чистый ответ — регистрировать два passkey при онбординге (один платформенный, один роуминговый аппаратный ключ), чтобы второй credential и был методом восстановления — без более слабого канала.
Синхронизируемые против привязанных к устройству passkey — это реальное решение о доверии. Современные потребительские passkey синхронизируются между устройствами пользователя через его платформенный аккаунт (iCloud Keychain, Google Password Manager). Это прекрасно для внедрения — потеряешь телефон, твои passkey уже на новом, — но это значит, что безопасность credential теперь зависит ещё и от безопасности платформенного аккаунта, и что старое допущение «приватный ключ живёт в одном защищённом от вскрытия чипе» больше не верно. Привязанные к устройству passkey (аппаратный ключ безопасности или платформенные ключи с политикой без синхронизации) остаются в одном месте и сильнее против компрометации уровня аккаунта, но делают потерю катастрофичной, если вы не зарегистрировали бэкап. Для контекстов высокой надёжности (админ, финансы, регулируемые системы) часто требуют привязанные к устройству ключи и аттестацию; для потребительского масштаба принимают синхронизируемые ключи и опираются на безопасность платформенного аккаунта. Знать, что из этого у вас, — часть вашей модели угроз.
signCount больше не та защита от вскрытия, что прежде. В authenticatorData WebAuthn есть счётчик подписей, призванный обнаруживать клонированный аппаратный аутентификатор: если счётчик хоть раз пошёл назад — вы видели клон. Этот сигнал реален для одноустройственных аппаратных ключей. Но синхронизируемые passkey по замыслу копируются между устройствами, поэтому их счётчик часто 0 или бессмыслен в парке — регресс не доказательство компрометации. Считайте signCount мягким сигналом, никогда не жёстким гейтом и никогда не тем, что стоит между вами и клонированным credential на синхронизируемой платформе.
| Измерение | Пароль / TOTP (общий секрет) | Passkey (WebAuthn) |
|---|---|---|
| Что хранит сервер | Проверяемый секрет (хеш / общий seed) — утекаем | Только публичный ключ — нечего фишить или сливать |
| Фишинг / AiTM-пересылка | Пересылается насквозь — секрет на предъявителя | Отвергается — подпись привязана к неверному origin |
| Повтор перехваченного доказательства | Работает в окне валидности (TOTP) / вечно (пароль) | Блокируется одноразовым серверным challenge |
| Слабейший достижимый путь | Сам секрет | Восстановление, работа с сессией или кража устройства — не вход |
| Обнаружение клона | Неприменимо | signCount — реален для аппаратных ключей, 0/бессмыслен при синхронизации |
Где ломается: WebAuthn защищает только событие входа
Самая дорогая ошибка с passkey — поверить, что credential защищает аккаунт. Он защищает ровно одно: событие аутентификации. Всё вокруг по-прежнему на вас, и именно там команды, которые «перешли на passwordless», всё равно теряют аккаунты.
Сессия по-прежнему токен на предъявителя. Вход через passkey выпускает сессионную cookie — и эта cookie наличные, ровно как раньше. Если вы никогда её не ротируете, даёте долгий срок жизни и пускаете сторонний скрипт на аутентифицированную страницу, одна XSS выносит живую сессию, и атакующий внутри вообще не касаясь passkey. WebAuthn сделал вход неподдающимся фишингу; про то, что после входа, он ничего не сказал. Вам всё ещё нужна ротация сессии при входе и смене привилегий, короткие сроки с повторной аутентификацией, cookie HttpOnly/Secure/SameSite и CSP, не дающий инжектированному скрипту прочитать сессию.
Владение на разблокированном устройстве побеждает проверку пользователя. WebAuthn доказывает владение ключом, защищённое локальным UV-жестом, — но на уже разблокированном ноутбуке ОС может держать жест в кеше или удовлетворять его автоматически. Вор с украденным разблокированным устройством может войти через синхронизируемый passkey, и криптография работает ровно как задумано. Изъяна в крипто нет. Защита — наслоённые контроли над входом: явная step-up повторная аутентификация (userVerification: 'required') для чувствительных действий, риск-сигналы (новое устройство, новая гео, невозможное перемещение), короткие привязанные сессии и — критично — держать аутентификацию отдельно от авторизации, чтобы привилегированное действие перепроверяло и «тот ли это человек прямо сейчас», и «разрешено ли этому субъекту», а не доверяло пассивной сессии, выпущенной часы назад.
▸Частая ошибка
Миграция-ловушка — оставить пароль постоянным параллельным входом «на всякий случай». Теперь у аккаунта две парадные двери: неподдающийся фишингу passkey и фишабельный пароль — и атакующие всегда берут слабейшую, поэтому ваша реальная безопасность — это пароль, от которого вы думали, что ушли. Фикс — миграция с зубами: как только у пользователя 2+ passkey, понизьте или удалите пароль для этого аккаунта и никогда не оставляйте email-OTP восстановление как сброс без трения, тихо становящийся истинной силой аккаунта.
Как зрелый инженер реально внедряет passkey
Относитесь к passkey как к сильному аутентификатору, а не полной системе идентичности, и укрепляйте цепочку вокруг него. Регистрируйте два credential при онбординге, чтобы восстановлением был второй passkey, а не фишабельная ссылка сброса. Уводите пароль как только покрытие реально, а не держите постоянный слабый параллельный путь. Делайте восстановление не слабее входа — сначала второй passkey или аппаратный ключ, а где внеполосный сброс всё же нужен, добавляйте трение (задержку, уведомление на все устройства, step-up, код восстановления, выданный при онбординге). Добавьте step-up для чувствительных действий, чтобы пассивная сессия не могла действовать, и ротируйте и привязывайте сессии, чтобы украденная быстро истекала. Решайте синхронизируемые против привязанных к устройству осознанно, по уровню надёжности. И запишите постоянное допущение — passkey защищает вход, а не сессию, не восстановление и не устройство, — чтобы никто заново не сделал ошибку «входа достаточно». Тест любого внедрения passkey — один проход: пройдите каждый путь, который может привести атакующего в аккаунт, и подтвердите, что ни один не более фишабелен, чем сам passkey.
Вы добавляете passkey в приложение, где сейчас вход по паролю + email-OTP. Аккаунт должен остаться не менее устойчивым к фишингу по каждому пути, включая восстановление. Выберите дизайн.
AiTM-прокси на похожем домене пересылает полную церемонию WebAuthn жертвы и перехватывает криптографически валидную подпись. Почему она бесполезна против вашего настоящего сервера?
Команда мигрировала на passkey и всё равно получила угоны аккаунтов, причём ни один passkey не украден. Какова самая вероятная причина?
Упорядочьте серверные проверки утверждения аутентификации WebAuthn, от его получения до выдачи сессии:
- 1 Найти хранимый публичный ключ для предъявленного credential id
- 2 Проверить подпись над authenticatorData + hash(clientDataJSON) этим публичным ключом
- 3 Сверить, что подписанный origin равен ожидаемому, а challenge совпадает с выданным (затем погасить его)
- 4 Применить флаги политики (бит user-verified, если требуется), затем выдать сессию
- 01Объясните точно, почему вход через passkey переживает пересылку «противник посередине», а пароль и TOTP-код — нет.
- 02Вход через passkey неподдающийся фишингу. Перечислите, что WebAuthn НЕ защищает и что нужно добавить для каждого.
Passkey — это учётные данные WebAuthn: асимметричная пара ключей, чья приватная половина никогда не покидает аутентификатор, а сервер держит лишь публичный ключ, поэтому нет общего секрета для фишинга или утечки. Аутентификация доказывает владение, подписывая одноразовый серверный challenge, и решающая деталь в том, что браузер вписывает истинный origin страницы и challenge в clientDataJSON до того, как аутентификатор подпишет. Это привязывает доказательство к конкретному origin, поэтому прокси «противник посередине», пересылающий настоящую валидную подпись, всё равно проваливает сверку origin на сервере — подпись привязана к домену атакующего, и прокси не переподпишет под ваш без приватного ключа. Это и значит «устойчивый к фишингу», и поэтому passkey переживает пересылку, которую пароль или TOTP-код не переживают. Но WebAuthn защищает лишь событие входа. Сессия — всё ещё cookie на предъявителя, которую надо ротировать, привязывать и укорачивать; восстановление становится самой трудной частью и должно быть не слабее входа (предпочтите второй зарегистрированный passkey, никогда не email-сброс без трения на ящике только с паролем); украденное разблокированное устройство может использовать валидный синхронизируемый passkey без изъяна в крипто, поэтому вы добавляете step-up, риск-сигналы и разделённую авторизацию; а signCount — сигнал клона для аппаратных ключей, но 0/бессмыслен на синхронизируемых passkey, поэтому он мягкий, никогда не гейт. Внедряйте passkey как сильный аутентификатор, регистрируйте два при онбординге, уводите пароль и помните: аккаунт силён лишь настолько, насколько слаб его слабейший достижимый путь.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.