open atlas
↑ К треку
Основы безопасности SECF · 03 · 03

Passkey и WebAuthn

Passkey — это привязанные к origin публично-ключевые учётные данные; браузер подписывает истинный origin страницы, поэтому фишинговый или пересланный вход отвергается. Но WebAuthn защищает лишь событие входа — сессия и восстановление остаются на вас.

SECF Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Вылизанный прокси на 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. 1 Найти хранимый публичный ключ для предъявленного credential id
  2. 2 Проверить подпись над authenticatorData + hash(clientDataJSON) этим публичным ключом
  3. 3 Сверить, что подписанный origin равен ожидаемому, а challenge совпадает с выданным (затем погасить его)
  4. 4 Применить флаги политики (бит user-verified, если требуется), затем выдать сессию
Вспомните перед уходом
  1. 01
    Объясните точно, почему вход через passkey переживает пересылку «противник посередине», а пароль и TOTP-код — нет.
  2. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки
углубляется в

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.