Это моя личная книга по аутентификации. Она представляет собой сборник руководств, рекомендаций и примеров по внедрению аутентификации в веб-приложениях, основанный на моем личном опыте. Книга совершенно бесплатна и не содержит рекламы. Надеюсь, она будет полезна всем, кто хочет узнать больше об аутентификации, безопасности и веб-технологиях в целом.

Как следует из названия, эта книга в значительной степени посвящена системе аутентификации и авторизации для веб-приложений. Более общие вопросы безопасности см. в серии “Шпаргалки OWASP” .

Если у вас возникнут какие-либо вопросы, не стесняйтесь задавать их на сервере Discord или в обсуждениях на GitHub.

Разработано и поддерживается Pilcrow. Исходный код доступен на GitHub.

Примеры на Go

Методы аутентификации

Простейший метод аутентификации - использование имени пользователя и пароля. При регистрации пользователь устанавливает то и другое и позже использует их для авторизации. Это просто и легко понять. Однако, с паролями связано несколько рисков. Пользователи могут использовать слабые или легко подбираемые пароли, использовать пароли повторно, забывать их или становиться жертвами фишинговых атак. Кроме того, если база данных, содержащая хеши (hashes) паролей, скомпроментирована, аккаунты пользователей могут оказаться в опасности не только на вашем веб-сайте.

Распространенный подход - позволять пользователям привязывать аккаунт к адресу электронной почты. Хотя это не улучшает безопасность как таковую, это позволяет восстанавливать пароли через временные ссылки или коды подтверждения, отправляемые на email. Также email часто используется как идентификатор аккаунта, поскольку он глобально уникален, и пользователю сложнее его забыть.

Поскольку пароли часто недостаточно сильны, все больше сайтов добавляет второй метод аутентификации, помимо пароля. Это называется двухфакторной аутентификацией (2FA) или мультифакторной аутентификацией (MFA). Вторые методы включают одноразовые коды, отправляемые на номер телефона пользователя через СМС, или коды, генерируемые приложением-аутентификатором (authenticator app) на мобильном устройстве. Хотя сами по себе эти методы относительно слабы, вместе с паролями они предоставляют разумный уровень безопасности для большинства пользователей. На практике, на мой взгляд, лучше использовать несколько вторичных методов аутентификации в качестве механизмов, спроектированных для замедления атакующего настолько, чтобы легитимный пользователь успел сбросить свои учетные данные. Если аутентификация на основе email используется для подтверждения личности пользователя в процессе сброса пароля, я бы не стал использовать его в качестве второго фактора. Если для авторизации требуются и пароль, и подтверждение через email, но доступа к email достаточно для сброса пароля, тогда сам пароль предоставляет незначительную дополнительную защиту. Также нужно подумать о том, является ли аутентификация на основе email достаточно безопасной для сброса пароля. Обычно, в процессе сброса пароля требуется второй фактор.

Другой вариант - совсем не использовать пароли. Распространенная реализация - использовать поток (flow), похожий на поток сброса пароля, когда пользователь получает одноразовый код или ссылку на свой email. Поскольку сервер контролирует сложность кода или ссылки, эти методы часто являются более безопасными, чем пароли. Однако, особенно при использовании менеджеров паролей, они могут быть более медленными и утомительными, чем традиционная аутентификация на основе пароля. Основной риск в том, что код или ссылка безопасны настолько, насколько безопасны аккаунт пользователя и устройство. Хотя эти методы не такие безопасные как двухфакторная аутентификация, они могут быть достаточно безопасными для большинства сайтов.

Наконец, существуют passkey. Это безопасные учетные данные, хранящиеся на устройстве пользователя или в менеджере паролей, которые используют криптографию на основе открытого ключа (public key) для аутентификации пользователя. Они позволяют пользователям авторизовываться на сайтах с помощью одного мастер-пароля или биометрик, хранящихся на их устройствах. Passkey предоставляют один из самых высоких уровней безопасности, поскольку они устойчивы к атакам грубой силы (brute force) и не делятся с сайтами учетными данными напрямую. Они также являются быстрыми и удобными. Однако passkey - новая возможность, и пользователи, не подкованные технически, могут быть с ними незнакомы.

Существуют также внешние аппаратные токены (external hardware tokens) или безопасные ключи, такие как YubiKeys. Самые современные из них поддерживают passkey, но они также часто используются в качестве второго метода аутентификации, наряду с паролями, в корпоративных средах. Пользователь подключает ключ к компьютеру для подтверждения наличия устройства и, опционально, подтверждает свою личность с помощью ПИН-кода или биометрик. При использовании их с подтверждением личности, они уже предоставляют намного более высокий уровень безопасности, чем пароли, поэтому на практике использовать их для дополнительной аутентификации не имеет особого смысла.

Выбор метода аутентификации полностью зависит от требований безопасности вашего приложения. Следует избегать метода аутентификации, который не дает уверенности в обеспечении достаточной безопасности для вашего приложения. Пользователи ленивы и почти всегда выбирают удобство вместо безопасности. Легче сразу реализовать более безопасный метод аутентификации, чем полагаться на обнаружение ботов, мониторинг подозрительной активности и другие эвристики после. Также следует учитывать ожидания пользователей. Насколько безопасным должен быть сайт с точки зрения пользователей? Будут ли пользователь винить платформу, если его аккаунт будет скомпроментирован из-за его небрежного отношения к безопасности?

Для неформального чата или браузерной игры аутентификации на основе пароля должно быть достаточно. Однако, если пользователи заходят на сайт ежедневно и у злоумышленников есть веские стимулы для захвата аккаунтов, вам следует использовать более безопасный метод авторизации. Здесь, обычно, существует два варианта: добавить второй фактор аутентификации или использовать беспарольный подход, такой как авторизация на основе email. Если аккаунты требуют очень высокий уровень безопасности, рассмотрите возможность использования passkey. Если вы разрабатываете систему с нуля, я рекомендую поддерживать как аутентификацию на основе email, так и passkey, позволяя пользователям выбирать метод авторизации самостоятельно. Аутентификация на основе email более знакома пользователям и ее легче понять, а passkey, как правило, быстрее и более безопасны. По умолчанию пользователи должны иметь возможность авторизовываться с помощью любого метода, но пользователи, нуждающиеся в повышенной безопасности, должны иметь возможность использовать для авторизации только passkey.

Сессии

Когда пользователь посещает ваш сайт, браузер отправляет несколько запросов HTTP на ваш сервер. Однако эти запросы не зависят друг от друга на уровне протокола, поскольку HTTP не хранит состояние (stateless). Сервер не может “сказать”, являются ли два запроса запросами одного клиента.

Простым решением является хранение данных на клиенте и их добавление в запросы. Однако, такой подход не работает для данных, которые клиент не должен видеть или модифицировать. Например, хранение идентификатора аутентифицированного пользователя на клиенте и доверие сервера к любым ID, отправленным клиентом, полностью ломает безопасность. Такие данные должны храниться в виде записи на сервере, а клиент может ссылаться на них в последующих запросах для продления активности пользователя.

В рамках этой книги под сессией (session) понимается запись на сервере, представляющая активность пользователя в приложении. Чтобы предотвратить обращение злоумышленников к произвольным сессиям, сервер выдает клиенту токен сессии (session token). Клиент включает этот токен в последующие запросы. Поскольку токен известен только клиенту, запустившему сессию, посторонний субъект не может перехватить (hijack) сессию.

Сессии, отслеживающие состояние аутентификации, именуются в книге “сессиями аутентификации”. Сервер может иметь несколько типов сессий. Например, я часто использую сессию регистрации для отслеживания email пользователя и процесса его подтверждения.

Если ID пользователя является произвольным и непредсказуемым, токен технически не нужен. Клиент может просто отправлять ID и все. Однако, я не рекомендую использовать такой подход. Использование ID как секрета - это ошибка, поскольку во многих случаях вам потребуется обращаться к сессии, не раскрывая конфиденциальные учетные данные. Я рекомендую генерировать ID и секрет для ваших сессий. По аналогии с аутентификацией пользователя, ID определяет сессию, а секрет выступает в роли подтверждения. ID и секрет объединяются в один токен.

Секрет сессии должен генерироваться с помощью криптографически защищенного источника случайных чисел. Не используйте быстрый предсказуемый генератор случайных чисел, часто предоставляемый стандартной математической библиотекой. Он также должен хешироваться перед сохранением. Это гарантирует, что даже если ваша база данных будет взломана, злоумышленник не сможет получить валидные учетные данные сессии. Для подтверждения секрета хешируйте введенное значение и сравните его с сохраненным хешем с помощью сравнения за постоянное время (constant-time comparison). Если секрет обладает достаточной энтропией, может быть использован быстрый алгоритм хеширования, в отличие от пользовательских паролей. Я рекомендую использовать 32-байтовый секрет и хешировать его с помощью SHA-256. Обратите внимание, что в случае с SHA-256 создавать секрет, превышающий 32 байта, не имеет особого смысла, поскольку итоговый хеш имеет размер 32 байта.

Для создания токена закодируйте секрет в строку и объедините его с ID, например, используя точку в качестве разделителя. Существуют разные схемы кодирования, но распространенными вариантами являются hex, base64 и base64url.

u9qerabnmqwfjrig.oIGZG+9w+flpwCIb5azPgXLdmmS+86KYkQbIag/wXB8=

Основная угроза для сессии - это ее перехват (hijack). Токен сессии может быть украден многими способами, включая вредоносное ПО и кражу устройства. Поэтому важно, чтобы сессии имели границы (boundaries). Злоумышленник не должен получать доступ ко всему, украв один токен. Во-первых, это означает установку разумного срока действия (expiration) токена. Он может быть как фиксированным, так и динамичным. Во-вторых, токен должен выдаваться после определенных действий, например, аутентификации пользователя. Можно установить дополнительные ограничения, например, привязывать токен к IP-адресу. Однако, я бы не рекомендовал этого делать, поскольку адреса IP могут часто меняться, особенно в мобильных сетях или при использовании VPN. Более практичной альтернативой является привязка сессии к стране или региону, полученным на основе IP-адреса.

Что касается хранения токена сессии, то у меня нет определенного мнения на этот счет. Просто храните его, используя самый безопасный вариант, предоставляемый платформой. Для браузеров все методы предоставляют примерно одинаковый уровень безопасности и рисков. См. раздел “Браузерное клиентское хранилище” для получения более подробной информации.

Сессии аутентификации

Сессия аутентификации (auth session) - это сессия, которая отслеживает состояние аутентификации пользователя путем хранения ID аутентифицированного пользователя. Она создается, когда клиент успешно авторизуется как пользователь приложения.

Срок действия (expiration) определяет, как часто пользователь должен повторно авторизовываться, и конкретная продолжительность сильно зависит от приложения. Сессии аутентификации могут быть настроены на окончание через час, в полночь каждого дня или через несколько недель. Другой подход заключается в продлении срока действия при каждом использовании токена сессии, что обеспечивает выход из системы только пользователей, не проявлявших активности в течение длительного периода. Если приложение не требует наивысший уровень безопасности и в приоритете положительный пользовательский опыт, вероятно, это лучшая стратегия. Во избежание лишних записей в хранилище, я бы рекомендовал периодически обновлять срок действия (например, раз в час или в день), а не при каждом запросе.

Выход из системы должен инвалидировать (invalidate) сессию, а не просто удалять токен из клиентского хранилища. Пользователи должны иметь возможность выходить из системы только на текущем или на всех устройствах. Это позволяет им инвалидировать потенциально перехваченные сессии или выходить из системы на украденных устройствах. Однако, пользователи не должны иметь возможность инвалидировать определенные сессии без подтверждения личности. Злоумышленник, перехвативший сессию, может мешать авторизации валидного пользователя и потенциально навсегда заблокировать доступ к их учетной записи. Управление сессиями должно расцениваться как высокопривилегированная операция.

Должны ли сессии инвалидироваться при обновлении пользователем пароля - отдельный разговор. На мой взгляд, хорошая идея - спрашивать пользователей, не хотят ли они выйти из системы на всех устройствах сразу после изменения пароля, но это не должно быть обязательным. Пользователи часто обновляют забытые пароли или просто в качестве превентивной меры. Также сессии, как правило, не инвалидируются после регистрации или удаления passkey. При инвалидации всех сессий после изменения пароля, явно сообщайте об этом пользователю.

Сессия аутентификации должна быть строго привязана к успешному событию аутентификации. Это актуально для проверки личности пользователя (user verification) после входа в систему. Основная цель подтверждения личности - предотвращение использования украденных токенов сессии злоумышленниками. Однако, если повторно используется существующая сессия, злоумышленник может просто дождаться аутентификации легитимного пользователя и затем взять контроль над процессом. Поэтому я рекомендую использовать специфичные для операций сессии для любой операции, требующей подтверждения личности, такой как обновление пароля, регистрация нового passkey или удаление аккаунта. Это также позволяет убедиться, что верификация используется один раз и строго привязана к определенной операции.

Адреса электронной почты

Адреса электронной почты прекрасно работают как идентификаторы аккаунтов, поскольку они глобально уникальны и пользователи их обычно помнят. Проверка принадлежности домена также позволяет избежать ошибок и опечаток. Однако помните, что это фактически превращает email в общедоступный идентификатор. Избегайте его использования, если вы (или ваши пользователи) предпочитаете скрывать людей, посещающих ваш сайт.

Структура email простая. Сначала идет имя пользователя, затем символ @ и домен. Но спецификации и правила, определяющие точный шаблон каждой части, сложны, а разработка строгой логики проверки их соблюдения не стоит затраченных усилий. Например, локальная часть может содержать пробелы или символы @, если они находятся в двойных кавычках. Домен может включать нелатинские символы (IDN) или даже быть литеральным IP-адресом. Локальная часть может быть чувствительной к регистру в зависимости от сервера электронной почты.

Если email требуется только для отправки писем, проверки на наличие @ должно быть достаточно.

Однако, если email является идентификатором аккаунта, мы должны быть более аккуратными. Это будет частью вашей системы и частью UI вашего сайта. Это означает, что к ним должны предъявляться более строгие требования. Я рекомендую накладывать следующие ограничения:

  1. Максимальная длина - 100 символов.

  2. Наличие только одного символа @ для разделения имени пользователя и пароля.

  3. Имя пользователя и домен должны состоять минимум из 1 символа.

  4. Имя пользователя может содержать только буквы в нижнем регистре, числа, точки (.), знаки плюс (+), нижние подчеркивания (_) и дефисы (-).

  5. Домен должен включать минимум одну точку.

  6. Домен может содержать только буквы в нижнем регистре, числа, точки и дефисы.

Я крайне не рекомендую молча модифицировать ввод пользователя, например, автоматически приводить адрес к нижнему регистру или удалять знак плюс для блокировки псевдонимов. Как упоминалось ранее, локальная часть email может быть чувствительной к регистру, а знак плюс не является универсальным и имеет специальное значение только для некоторых серверов электронной почты (например, Gmail).

Пароли

Аутентификация с помощью пароля - простейший метод аутентификации. Пользователь определяет свой пароль при создании аккаунта и подтверждает свою личность, вводя его.

Я считаю разумным ограничивать пароли печатаемыми (printable) символами ASCII и отклонять пароли, которые начинаются или заканчиваются пробелами. Даже за пределами США, в паролях редко используются нелатинские символы, и такие ограничения позволяют перехватывать потенциальные ошибки ввода. Никогда молча не модифицируйте или не обезвреживайте (sanitize) ввод пользователя. Отклоняйте ввод и явно информируйте пользователя о том, что пошло не так. Максимальный размер пароля должен устанавливаться в диапазоне от 50 до 100 символов для поддержки как случайно сгенерированных паролей, так и кодовых фраз (комбинаций слов).

Минимальная длина пароля должна составлять 8 или 10 символов. Однако, следует избегать других требований к сложности пароля. Лучше сравнивать пароли с известными утечками данных с помощью таких сервисов, как Haveibeenpwned API. Это позволяет отклонить часто используемые пароли и защищает пользователей от создания уязвимых аккаунтов.

Пароли пользователей должны хешироваться перед сохранением. хеширование, в отличие от шифрования, это однонаправленное, т.е. необратимое преобразование. Это гарантирует, что пароли пользователей не раскрываются при взломе БД. Это очень важно, поскольку пользователи часто используют одинаковые пароли на разных сайтах. Пароль подтверждается путем сравнения хеша введенного пароля с сохраненным хешем. Для предотвращения атак по времени (timing attacks) это сравнение следует проводить с использованием операции с постоянным временем выполнения (constant-time operation). Однако, пароли пользователей часто являются слабыми. Даже произвольные 8 символов легко подбираются компьютером. Поэтому следует использовать сильный алгоритм хеширования, специально предназначенный для паролей. Такие алгоритмы хеширования используют большое количество вычислительных ресурсов и существенно снижают вероятность успеха атаки методом грубой силы (brute force).

хеш необходимо “солить”. Соль (salt) - это произвольная строка, уникальная для каждого хеша, которая объединяется с паролем при хешировании. Процесс засаливания гарантирует, что даже 2 одинаковых пароля производят уникальные хеши, не позволяя атакующим предварительно вычислять хеши для часто используемых паролей (радужная таблица - rainbow table). Я рекомендую сгенерировать не менее 16 случайных байтов с помощью криптографически защищенного источника случайных чисел, чтобы использовать их в качестве соли. Соль - это не секрет. Ее не нужно шифровать перед сохранением. “Перчение” (peppering), с другой стороны, генерирует секретный ключ, называемый “перцем” (pepper), в процессе хеширования, который является общим для всех хешей. Однако, это эффективно, только если перец хранится отдельно от хешей в защищенной локации. Если вам требуется перчение, не разрабатывайте собственное решение поверх алгоритма хеширования, а используйте поддерживающий его алгоритм.

Если вы разрешаете все символы Юникода в паролях, нормализуйте текст с помощью NFC перед его передачей в алгоритм хеширования для обеспечения согласованного представления.

Часто используемыми алгоритмами хеширования являются Argon2, Bcrypt и Scrypt. Bcrypt - самый старый и широко используемый вариант, но я рекомендую использовать Argon2. В частности, Argon2id с, как минимум, 16 МБ памяти, 3 итерациями и 1 степенью паралеллизма. Не путайте Argon2id с Argon2i и Argon2d, которые являются разными вариантами Argon2. Смело увеличивайте размер памяти для усиления хеша, но не трогайте параметры итераций и паралеллизма. В целом, Argon2 предоставляет немного лучшую защиту по сравнению с Bcrypt с меньшим количеством ловушек. Для получения подробной информации см. разделы “Argon2” и Bcrypt. Я также публиковал подробное сравнение этих 2 алгоритмов в моем блоге: Is Argon2 actually better than Bcrypt?.

Заметьте, что хеширование пароля - тяжелая для ЦП операция, которая может занять весь поток ЦП во время выполнения. Как следствие, использование более медленного алгоритма уменьшает количество паролей, которые может обработать сервер. Если вы не контролируете число одновременных операций хеширования, они могут потребить все вычислительные ресурсы и ухудшить производительность всего приложения. Также может закончиться память при использовании ресурсоемкого алгоритма хеширования, такого как Argon2. В однопоточных средах, таких как JavaScript, выполняйте хеширование в отдельном процессе в порядке очереди (queue). В многопоточных средах ограничивайте количество одновременных операций хеширования с помощью мьютекса (mutex) или семафора (semaphore).

Все конечные точки, выполняющие хеширование, должны быть защищены строгим ограничением количества запросов для предотвращения как ресурсоемких атак типа “Отказ в обслуживании” (DoS), так и подбора учетных данных методом перебора. Для начала можно установить лимит в 1 попытку в минуту на пользователя. Например, используйте алгоритм “группы токенов” (token bucket) с максимальной емкостью в 5 токенов и скоростью пополнения в 1 токен в минуту. Это ограничивает количество попыток взлома пароля примерно 500 000 в год, чего, как правило, недостаточно для взлома среднестатистического пароля. Я не рекомендую внедрять блокировки учетных записей или экспоненциальное ограничение количества запросов, поскольку ими легко злоупотребить, чтобы навсегда заблокировать доступ законным пользователям. Я также не рекомендую устанавливать лимит на основе IP, поскольку это может случайно блокировать легитимных пользователей в общих сетях и легко обходится злоумышленниками с помощью прокси.

В конечном счете, безопасность - общая ответственность, и мы должны исходить из того, что пользователи выбирают достаточно надежные и уникальные пароли. хеширование и строгое ограничение количества запросов имеют важное значение, но они лишь частично компенсируют изначально слабые учетные данные.

Наконец, при авторизации с помощью пароля пользователь должен ввести идентификатор (например, ID или имя пользователя). Обычно рекомендуется возвращать одинаковое сообщение об ошибке как для несуществующего аккаунта, так и для неправильного пароля. Это призвано предотвратить перебор пользователей (user enumeration) - атаку, при которой злоумышленник может вывести действительные идентификаторы пользователей и использовать их для дальнейших атак. Однако я бы не стал полностью рекомендовать такой подход. Во-первых, полностью предотвратить перечисление пользователей на практике сложно. Поскольку хеширование паролей происходит очень медленно, различия во времени ответа могут быть использованы для определения существования пользователя. Хотя можно хешировать пароли независимо от того, существует ли пользователь, это усложняет ограничение количества запросов и не полностью исключает атаки, основанные на времени. Также необходимо внести изменения в другие связанные процессы, такие как регистрация и сброс пароля, чтобы избежать подобных проблем. Что еще важнее, этот подход может значительно ухудшить пользовательский опыт. Пользователю может быть непонятно, ввел он неправильные учетные данные или пытается получить доступ к несуществующей учетной записи. Думаю, здесь лучше предоставлять более подробные сообщения об ошибках. Если адрес электронной почты необходимо защитить, используйте вместо него непрозрачный идентификатор (opaque identifier), например, идентификатор пользователя или имя пользователя.

Браузерное клиентское хранилище

Браузеры предоставляют два основных механизма для хранения данных на стороне клиента: куки (cookies) и Web Storage API (localStorage и sessionStorage). С Web Storage API легче работать, но браузеры обычно более агрессивны в очистке данных, хранящихся в куки. Куки также автоматически включаются в запросы, что является важным, если сайт рендерится в HTML на сервере.

Куки требуют некоторой дополнительной защиты. При определенных условиях браузер автоматически включает куки в запросы, даже если они инициируются сторонними сайтами. Это означает, что если пользователь вошел в вашу систему и токен сессии хранится в куки, сайт злоумышленника может отправлять аутентифицированные запросы и действовать от имени пользователя. Это называется “межсайтовой подделкой запросов” (Cross-Site Request Forgery, CSRF) и является одной из старейших атак в вебе. Атрибут куки SameSite позволяет избежать многих межсайтовых атак, но одного этого атрибута недостаточно. Мы подробнее поговорим об этом в соответствующем разделе.

Атрибут HttpOnly позволяет управлять доступом к куки с помощью клиентского JavaScript. Включение этого флага запрещает клиентским скриптам читать куки напрямую. Однако, это не запрещает скриптам отправлять запросы с куки. Злоумышленник может отправлять аутентифицированные запросы, эксплуатируя уязвимость под названием “межсайтовый скриптинг” (Cross-Site Scripting, XSS). HttpOnly не позволяет скриптам воровать и экспортировать токены пользователей. Это также полезная защита от атак на цепочки поставок (supply-chain attacks). Например, использование этого флага защитит куки от вредоносных пакетов, пытающихся прочитать все хранящиеся куки. Web Storage API полностью доступен клиентскому JS.

Наконец, всегда добавляйте куки атрибут Secure. Этот флаг обеспечивает передачу куки только по “безопасным каналам”, определяемым браузером. Обычно это означает HTTPS. Некоторые браузеры, такие как Chrome, также разрешают localhost, но Safari, например, разрешает только HTTPS.

Хотя куки с флагом HttpOnly являются более безопасными, оба хранилища предоставляют примерно одинаковый уровень безопасности. В любой случае, следует защищаться от уязвимостей XSS путем надлежащего обезвреживания данных и политики безопасности контента (Content Security Policy, CSP). Кроме того, ни один из методов не шифрует данные, поэтому конфиденциальные персональные данные никогда не должны храниться на стороне клиента.

Межсайтовая подделка запросов (CSRF)

Межсайтовая подделка запросов (Cross-Site Request Forgery, CSRF) - это атака, при которой вредоносный сайт отправляет аутентифицированные запросы в приложение, в котором пользователь в данный момент авторизован, что позволяет злоумышленнику выполнять несанкционированные действия от его имени.

Браузеры автоматически добавляют соответствующие куки во все исходящие запросы, включая те, что были инициированы сторонними сайтами. Хотя политика одного источника (Same-Origin Policy, SOP) блокирует большую часть межсайтовых запросов, она прямо разрешает “простые запросы”. Это включает GET-, HEAD- и некоторые POST-запросы. Если сервер принимает такие запросы и записывает токен сессии в куки, вредоносный сайт может отправлять запросы от лица аутентифицированного пользователя. Обратите внимание, что уязвимость заключается именно в автоматическом использовании браузером куки пользователя. Ручная отправка запросов с помощью таких инструментов, как cURL, не является уязвимостью CSRF и не должна расцениваться как атака.

Во-первых, важно понимать разницу между “источником” (origin) и “сайтом” (site). В контексте same-origin и cross-origin источником считается конкретный домен, включая поддомены. foo.example.com и bar.example.com считаются разными источниками. Сайтом же считается корневой домен (root domain). foo.example.com и bar.example.com считаются одним сайтом. Заметьте, что браузер также учитывает список общедоступных суффиксов (Public Suffix List) при определении корневого домена. Например, github.io, который используется GitHub Pages для хостинга пользовательских сайтов, включен в этот список, поэтому foo.github.io и bar.github.io считаются разными сайтами.

Первое правило защиты от CSRF - никогда не принимать GET-запросы на модифицирующие состояния операции. Даже если вы блокируете GET-запросы, запускаемые с помощью JS, вредоносный сайт по-прежнему может перенаправлять пользователя на URL и запускать операции. Всегда используйте POST- или другие не GET-запросы для конечных точек, модифицирующих состояние.

Не все POST-запросы считаются простыми. Они считаются таковыми при полном отсутствии заголовка Content-Type или если он имеет одно из трех значений: application/x-www-form-urlencoded, multipart/form-data или text/plain. Заметьте, что браузер использует заголовок запроса, а не его тело для определения типа контента. Заголовок должен аккуратно парситься и валидироваться, даже если приложение принимает данные только в формате JSON. Запросы без Content-Type должны отклоняться. Не используйте метод совпадения со строкой для валидации этого заголовка. Например, значение заголовка text/plain; application/json совпадет со строкой text/plain и будет пропущено браузером. Даже с правильной валидацией, следует использовать и другие ограничения CSRF.

Простейший способ предотвращения CSRF - установка атрибута куки SameSite в значение Strict или Lax. Strict гарантирует, что куки не будет включаться в межсайтовые запросы, а Lax гарантирует, что куки будет включаться только в GET- и подобные (HEAD, OPTIONS, TRACE) запросы. Strict блокирует отправку куки при перенаправлении пользователя сторонним сайтом, что обычно является нежелательным поведением. Поэтому дефолтным значением должно быть Lax.

Хотя атрибут куки SameSite - хороший слой защиты, это не серебряная пуля. Во-первых, это не блокирует межсайтовые запросы, например, между поддоменами. Вы по-прежнему подвержены CSRF, если один из ваших поддоменов имеет уязвимость XSS или захвачен злоумышленником. Кроме того, этот атрибут был широко адаптирован браузерами только к 2019 году. Любом старый браузер игнорирует его, оставляя пользователей незащищенными.

Поэтому я рекомендую выполнять строгую проверку источника на сервере и отклонять не GET-запросы из ненадежных источников. Простейший способ сделать это - проверка заголовка Sec-Fetch-Site. Этот заголовок включается во все запросы браузера (не только те, которые выполняются с помощью Fetch Web API) и отражает отношение между сайтом-инициатором и целевым сервером. Поскольку это запрещенный (forbidden) заголовок, клиентский JS не может его модифицировать. Не GET-запросы без этого заголовка или со значением, отличным от same-origin, должны отклоняться. Хотя этот заголовок получил широкую поддержку браузеров только в 2023 году, в отличие от атрибута куки SameSite, старые браузеры могут блокироваться путем отклонения запросов, не содержащих этот заголовок. Заголовки с префиксом Sec- являются запрещенными с 2008 года, поэтому рассматриваемый заголовок нельзя подделать даже в старых браузерах. Заметьте, что подделка заголовка за пределами браузера не имеет значения, поскольку CSRF - это атака, основанная на браузере.

Для разрешения запросов из поддоменов в старых браузерах используйте заголовок Origin. Он включает источник запроса и является запрещенным с 2008 года. Все основные браузеры включают его во все запросы, примерно, с 2020 года. Блокируйте запросы без этого заголовка или со значением, не включенным в белый список (white list).

Наконец, самая старая защита от CSRF - анти-CSRF токены (anti-CSRF tokens). Сервер генерирует токен при первом посещении сайта, и этот токен является обязательным для всех модифицирующих состояние операций, таких как отправка формы. Сторонние сайты не имеют доступа к токену, поскольку SOP запрещает другим сайтам читать ответы и, следовательно, встроенные токены. Этот подход совместим со старыми браузерами и по-прежнему широко применяется. См. страницу CSRF OWASP для получения инструкций по его реализации.

Argon2

Argon2 - это новое семейство алгоритмов хеширования пароля. Он разработан с учетом интенсивного использования памяти для предотвращения атак с использованием графических процессоров и другого специализированного оборудования. Я рекомендую использовать Argon2id с объемом памяти не менее 16 МБ, 3 итерациями и 1 степенью параллелизма.

Argon2 имеет 3 варианта: Argon2i, Argon2d и Argon2id. Argon2i разработан для защиты от атак по побочным каналам (side-channel attacks), тогда как Argon2d обеспечивает превосходную защиту от атак на основе графических процессоров. Argon2id сочетает в себе механизмы обоих вариантов для обеспечения сбалансированной защиты.

Алгоритм настраивается с помощью трех основных параметров. Первый определяет размер памяти, используемой каждым процессом. Удвоение размера памяти удваивает время выполнения. В идеале он должен превышать 128 МБ, чтобы в полной мере ощутить преимущества Argon2, но для большинства веб-приложений это нереалистично. Следующий параметр - количество итераций. В случае Argon2id следует использовать минимум 3 итерации. Наконец, степень параллелизма. Она определяет количество параллельных потоков, используемых каждой операцией хеширования. Хотя увеличение количества одновременных операций хеширования ускоряет отдельное время выполнения, оно не влияет на количество операций, которые может выполнить сервер. Я рекомендую всегда использовать значение 1 для максимизации пропускной способности сервера.

Argon2 также может возвращать данные любого размера, но, как правило, устанавливаются 32 байта.

Стандартного формата результата не существует, но часто используется строковый формат PHC. Он выглядит так:

$argon2id$v=19$m=MEMORY,t=ITERATION,p=PARALLELISM$SALT$HASH

где MEMORY - это память в КБ, ITERATION - количество итераций, PARALLELISM - степень параллелизма, SALT - соль, закодированная в base64, а HASH - хеш также в base64.

Самая быстрая видеокарта потребительского класса (RTX 5090) может обработать около 10 000 хешей в секунду, а самая быстрая видеокарта корпоративного класса (H100) — около 20 000 хешей в секунду при рекомендуемой конфигурации. Аренда H100 стоит от 2 до 5 долларов в час.

Bcrypt

Bcrypt - один из самых старых алгоритмов хеширования паролей, который по-прежнему широко используется. Он настраивается с помощью одного параметра стоимости (работы), увеличение которого на единицу удваивает вычислительную нагрузку. Я рекомендую устанавливать фактор стоимости, как минимум, в значение 10.

Максимальная длина пароля в Bcrypt составляет 72 байта. Это включает в себя нулевой терминатор (null terminator), поэтому фактический лимит составляет 71 байт. Обратите внимание, что речь о байтах, а не о символах. Символ, закодированный в UTF-16 или UTF-8, может занимать до 4 байт. Поэтому я рекомендую ограничивать пароли только печатными символами ASCII, чтобы 1 символ всегда занимал 1 байт. Кроме того, поскольку Bcrypt ожидает строку с нулевым завершением, он не может обрабатывать сырые (raw) двоичные данные. Не передавайте в него напрямую результаты работы других алгоритмов хеширования и шифрования.

Большинство реализаций добавляют соль автоматически и возвращают форматированную строку, включающую хеш, соль и стоимость:

$2a$10$TEBu87xaQfRF3lrFq2mfxe.HScQPBKFpmuvhCywrIAri3gvTtwhwO

Как самая быстрая видеокарта потребительского класса (RTX 5090), так и самая быстрая видеокарта корпоративного класса (H100) способны вычислять около 20 000 хешей в секунду при стоимости, равной 9. Аренда H100 стоит от 2 до 5 долларов в час. Следует отметить, что специализированное оборудование на базе ASIC/FPGA может вычислять хеши в 10 раз эффективнее, хотя его сложно приобрести и запустить.

Коды подтверждения, отправляемые на email

Это одноразовые коды, отправляемые на email. Запрашивая у пользователя такой код, мы подтверждаем, что указанный email принадлежит этому пользователю. Я предпочитаю использовать этот способ, вместо одноразовых ссылок верификации.

Код подтверждения должен быть привязан к конкретной сессии и конкретному email. Привязка кода к сессии связывает попытку верификации с устройством-инициатором. Делая код уникальным для попытки, мы предотвращаем ситуацию, когда пользователь отправляет код на свой email, а затем использует его для подтверждения другого email.

Основная цель подтверждения email состоит в том, чтобы исключить использование email, которые не принадлежат пользователю. Я рекомендую использовать 8-значный цифровой код с ограничением в 1 попытку в минуту на один адрес электронной почты. Код должен оставаться действительным до одного часа, но лучше меньше. Использование email в качестве ключа ограничения количества запросов гарантирует, что злоумышленник не сможет обойти лимит, совершив несколько попыток или создав несколько учетных записей. Например, можно использовать “группу токенов” (token bucket) с максимальной вместимостью 5 токенов и скоростью пополнения 1 токен в минуту в качестве ограничения количества запросов. Код не нужно хешировать перед сохранением. Также не нужно отслеживать неудачные попытки. При таком ограничении количества запросов и истечении срока действия это не сильно снизит шансы на успешный перебор кода методом грубой силы (brute force).

Однако, если существуют веские причины для проверки конкретного адреса, следует использовать более длинный код и хешировать его с помощью алгоритма хеширования паролей, такого как Argon2.

Для генерации случайного 8-значного числового кода необходимо сгенерировать 4 случайных байта, отбросить 5 бит и интерпретировать оставшиеся 27 бит как целое число. Если полученное значение равно 100 000 000 или больше, повторяем процесс. В противном случае, добавляем нули слева к числу, чтобы получить 8-значную строку. Такой подход позволяет избежать внесения искажений, гарантируя, что все возможные коды появляются с равной вероятностью. Можно также использовать оператор деления по модулю, хотя это вносит небольшую статистическую погрешность, но не настолько значительную, чтобы существенно снизить безопасность.

Аутентификация по коду, отправленному на email

Аутентификация по коду, отправленному на email - это метод аутентификации, когда одноразовый код отправляется на email пользователя, и пользователь вводит этот код для подтверждения своей личности. Этот метод также можно использовать для сброса пароля. На мой взгляд, этот метод лучше одноразовых ссылок.

Для реализации этого подхода я рекомендую создавать сессию для процесса аутентификации и генерировать новый код для каждой попытки. Привязка кода к сессии связывает попытку аутентификации c устройством-инициатором. Я не рекомендую ограничивать количество попыток или кодов на один аккаунт, поскольку это может привести к отказу пользователя от входа в систему.

Код должен содержать не менее 40 бит энтропии. При использовании 32-символьного буквенно-цифрового алфавита это соответствует 8-символьному коду. Код должен быть валидным, как минимум, на протяжении часа. Ограничение количества запросов должно быть установлено в 1 запрос в минуту на пользователя. Например, можно использовать “группу токенов” (token bucket) с максимальной вместимостью 5 токенов и скоростью пополнения 1 токен в минуту. При такой конфигурации для перебора кода методом грубой силы (brute force) потребуется около 80 миллиардов попыток, чтобы иметь хотя бы 50% вероятности успеха. Отслеживать неудачные попытки не нужно, поскольку это существенно не влияет на вероятность угадывания кода.

Для генерации кода с энтропией в 40 бит сначала необходимо сгенерировать 8 произвольных байтов из криптографически защищенного источника произвольных чисел. Для каждого байта маскируем 3 бита, а оставшиеся 5 бит обрабатываем как целое число. Сопоставляем каждое полученное значение с буквенно-цифровым символом. Я рекомендую использовать заглавные буквы и цифры, за исключением I, O, 0 и 1. Вы также можете просто сгенерировать 5 байтов и использовать все биты, но это потребует больше работы.

Код должен быть хеширован с помощью сильного алгоритма хеширования пароля, такого как Argon2 или Bcrypt. Можно использовать менее сильную конфигурацию, чем для пароля. Хотя 40 бит энтропии - это относительно высокий показатель, он не сравним с секретами сессии и может быть подобран методом перебора, если используется быстрый хеш, например, SHA-256. Например, для алгоритма Argon2id с 16 МБ памяти, 3 итерациями и 1 степенью параллелизма злоумышленнику потребуется доступ примерно к 5000–10000 новейшим графическим процессорам, чтобы взломать его методом перебора за час.

Код должен быть инвалидирован (invalidate) сразу после использования. Он также должен быть инвалидирован при изменении email пользователем.

Passkey

Passkey - это секретные учетные данные, хранящиеся на устройстве пользователя или в менеджере паролей, которые используют криптографию на основе открытого ключа (public key) для аутентификации пользователей. Они позволяют пользователям авторизовываться на сайтах с помощью одного мастер-пароля или биометрик на их устройствах. Они существенно сильнее обычных паролей, поскольку их нельзя угадать, они привязаны к определенному домену и никогда не делятся секретом с сайтом в процессе аутентификации. Они предоставляют пользовательский опыт, похожий на использование паролей с менеджерами паролей, но намного более плавный и прямолинейный. Фактически, большинство менеджеров паролей уже поддерживают их, что избавляет от необходимости автозаполнения полей ввода с помощью обходных путей.

Passkey основаны на Web Authentication API (WebAuth). Строго говоря, не существует стандартного определения этого термина. Это общее дружелюбное (user-friendly) понятие, которое используется для описания учетных данных WebAuth.

Лично я определяю их как обнаруживаемые (discoverable) учетные данные WebAuthn, требующие подтверждения пользователя. Это означает, что закрытый ключ (private key) хранится аутентификатором (authenticator) (устройство, внешний аппаратный токен, менеджер паролей), а пользователь должен подтвердить свою личность (ПИН-код, биометрики). Думаю, много приложений определяют их именно таким образом. Другое популярное определение идет дальше и требует, чтобы учетные данные были синхронизированы между устройствами и облаком. Однако, поскольку UX их использования на сайте идентичен, я предпочитаю включать только учетные данные, хранящиеся на устройстве.

Passkey могут быть комбинацией 2 факторов. Если учетные данные привязаны к устройству, они подтверждают принадлежность физического устройства и знание ПИН-кода (или биометрик). Однако, мы теряем физическое устройство, когда passkey копируется в облако с помощью менеджера паролей. Поэтому большинство passkey по определению не являются двухфакторными. Но даже в этом случае они гораздо безопаснее традиционных методов аутентификации. Это также сильно ускоряет аутентификацию пользователей. Поэтому в данном случае я бы сильно не настаивал на многофакторной аутентификации.

Пользователи должны иметь возможность регистрировать несколько passkey. Как минимум, 5, но лучше 10. Пользователи также должны иметь возможность именовать passkey. Наследование имен с устройств или из ID аутентификатора WebAuth - отличное решение, но пользователи должны иметь возможность вручную устанавливать имена. Это особенно важно для пользователей с несколькими устройствами или внешними аппаратными токенами.

Если вы предоставляете аутентификацию с помощью passkey и двухфакторную аутентификацию с помощью пароля, пользователь не должен проходить через другие методы аутентификации. В большинстве случаев passkey намного сильнее традиционной основанной на паролях двухфакторной аутентификации.

Личность пользователя должна быть подтверждена перед регистрацией или удалением passkey. Обратите внимание, что на этапе верификации использовать passkey не обязательно.

Web Authentication API (WebAuthn)

Обзор

Web Authentication API (WebAuthn) - это API для работы с учетными данными на основе открытого ключа (public key). Как правило, закрытый ключ хранится на компьютере, мобильном устройстве пользователя, внешнем аппаратном токене или в менеджере паролей. Когда учетные данные хранятся на физическом устройстве, пользователь может подтвердить наличие у себя этого устройства. Это называется присутствием пользователя (user presence). Кроме того, учетные данные могут защищаться с помощью ПИН или биометрик, которые используются для подтверждения личности пользователя. Это называется верификацией пользователя (user verification).

В этот процесс вовлечено 3 актора: аутентификатор, проверяющая сторона (relying party) и клиент. Аутентификатор хранит закрытый ключ от учетных данных и отвечает за подтверждение личности пользователя при наличии поддержки. Каждый аутентификатор имеет глобально уникальный идентификатор - AAGUID. Проверяющая сторона - это сущность, которая может попросить аутентификатора создать учетные данные или подтвердить принадлежность существующих учетных данных. Она состоит из клиентского компонента, который взаимодействует с веб-API, и сервера, который хранит и проверяет соответствующие данные. По сути, это ваш сайт. Наконец, клиент - это браузер, выступающий в роли посредника между аутентификатором и проверяющей стороной.

В процессе регистрации аутентификатор генерирует и сохраняет закрытый ключ и делится соответствующим открытым ключом с проверяющей стороной, которая сохраняет его для будущей верификации. В процессе аутентификации пользователь выбирает учетные данные, аутентификатор опционально верифицирует пользователя, и затем подписывает сообщение (message) с помощью закрытого ключа учетных данных. ID учетных данных и подпись отправляются проверяющей стороне, которая верифицирует подпись с помощью открытого ключа.

Учетные данные, которые хранятся прямо в и доступны аутентификатору, называются обнаруживаемыми учетными данными (discoverable credentials) (ранее они назывались резидентными ключами (residential keys)). Существуют также необнаруживаемые учетные данные (undiscoverable credentials), когда закрытый ключ не хранится аутентификатором. Они часто используются устройствами с ограниченным хранилищем, такими как внешние аппаратные токены. Они обычно реализуются путем шифрования учетных данных и их экспорта в виде ID учетных данных, которые затем сохраняются проверяющей стороной. В процессе аутентификации проверяющая сторона предоставляет аутентификатору список ID учетных данных, а аутентификатор пытается расшифровать их и использовать первый распознанный. В этой модели пользователь должен сначала вручную выбрать аккаунт для аутентификации.

WebAuthn также опционально поддерживает аттестационные заявления (attestation statements), которые можно использовать для проверки подлинности и происхождения аутентификатора. В зависимости от формата и политики аттестации, может отображаться общая информация, такая как модель аутентификатора, или более специфичная информация, определяющая конкретное устройство. Аттестация чаще всего используется в корпоративных средах, например, для обеспечения использования только корпоративных ключей безопасности.

Проверяющая сторона определяется по ее ID, которым является домен. Сайт может идентифицировать себя в качестве проверяющей стороны либо с помощью своего текущего домена, либо с помощью родительского домена. Например, foo.example.com может использовать foo.example.com или example.com. Аналогично определению сайтов с одинаковым источником, браузеры учитывают список общедоступных суффиксов (Public Suffix List). Например, foo.github.io не может использовать github.io в качестве ID проверяющей стороны, поскольку это общедоступный суффикс.

Регистрация

Для каждой попытки проверяющая сторона генерирует на сервере уникальное одноразовое произвольное значение (nonce) - запрос (challenge) и возвращает его клиенту. Аутентификатор использует это значение для обеспечения уникальности каждого аттестационного заявления (attestation statement). Заметьте, что запрос требуется только при обязательной аттестации, и не имеет особого смысла, если аттестация не используется или не обеспечивается.

Учетные данные могут быть созданы с помощью navigator.credentials.create() путем передачи настройки publicKey:

const credential = await navigator.credentials.create({
	publicKey: {
		challenge: challenge,
		rp: {
			id: new URL(window.location.href).hostname,
		},
		user: {
			id: new TextEncoder().encode(userId),
			displayName: userEmailAddress,
		},
		pubKeyCredParams: [
			{ type: "public-key", alg: -8 },
			{ type: "public-key", alg: -7 },
			{ type: "public-key", alg: -257 },
		],
		excludeCredentials: [{
			type: "public-key",
			id: existingCredentialId,
		}],
		authenticatorSelection: {
			residentKey: "required",
			requireResidentKey: true,
			userVerification: "required",
		},
		attestation: "none",
		extensions: {
			credentialProtectionPolicy: "userVerificationRequired",
			enforceCredentialProtectionPolicy: false,
		},
	},
});

Во-первых, он принимает ID проверяющей стороны как rp.id. Раньше он также принимал человекочитаемое имя rp.name, но это свойство было признано устаревшим.

Далее свойство user. Учетные данные будут привязаны к ID пользователя, определенному параметром user.id. Если новые учетные данные создаются с тем же ID пользователя, они заменяют существующие. ID также возвращается аутентификатором при аутентификации. Поэтому я рекомендую использовать внутренний ID пользователя приложения, несмотря на то, что технически это может быть любая произвольная последовательность байтов. Если вы хотите скрыть реальный ID пользователя, используйте произвольную строку байтов. user.displayName должно содержать человекочитаемый идентификатор пользователя, такой как email, ID пользователя, имя пользователя или его полное имя. Раньше он также принимал user.name, но это свойство также было признано устаревшим.

Настройка challenge принимает сгенерированный запрос. Если запрос не используется, передается пустой массив.

Массив pubKeyCredParams определяет, какие типы учетных данных и алгоритмы подписания поддерживает проверяющая сторона. Поскольку существует только один тип учетных данных public-key, в нем определяются алгоритмы подписания. Эти алгоритмы идентифицируются с помощью идентификаторов алгоритмов COSE, определенных в реестре IANA. Что сбивает с толку, так это то, что алгоритм подписи также определяет тип открытого ключа, хотя теоретически несколько типов открытых ключей могут поддерживать один и тот же алгоритм подписи. Некоторые часто используемые алгоритмы подписи:

  • -7 (ES256): ECDSA с SHA-256. Спецификация WebAuth требует, чтобы аутентификатор использовал кривую P-256. Аутентификатор возвращает открытый ключ ECDSA с помощью P-256. Не используйте -9 (ESP256).

  • -257 (RS256): RSASSA-PKCS1-v1_5 с SHA-256. Аутентификатор возвращает открытый ключ RSA.

  • -8 (EdDSA): EdDSA. Спецификация WebAuth требует, чтобы аутентификатор использовал кривую Ed25519. Аутентификатор возвращает открытый ключ Ed25519 и использует подписи Ed25519. Не используйте -19 (Ed25519).

Объект authenticatorSelection определяет тип создаваемых учетных данных. residentKey должно быть установлено в "required" при создании обнаруживаемых учетных данных и "discouraged", в противном случае. Настройка requireResidentKey признана устаревшей, но должна включаться для обратной совместимости. Настройка userVerification определяет, должен ли аутентификатор верифицировать пользователя и принимает значения "required", "preferred" или "discouraged". На практике это используется, в основном, для определения поддержки верификации пользователей аутентификатором. Требование верификации пользователя при регистрации не означает, что она будет обязательной при аутентификации.

Свойство attestation определяет, какой тип аттестации требует проверяющая сторона. "none" означает, что аттестация не требуется. Заметьте, что с этой настройкой аутентификатор может опустить всю информацию об устройстве, включая AAGUID. "direct" означает, что проверяющая сторона требует аттестации от самого аутентификатора, что, как правило, позволяет получить информацию о модели аутентификатора без идентификации конкретного устройства. "indirect" похоже на "direct", за исключением того, что клиент (браузер) может заменить аттестацию более конфиденциальным форматом, например, выданным сторонним центром сертификации. В этом случае подробная информация об устройстве может быть недоступна, хотя аттестация по-прежнему будет использоваться для верификации возможностей и надежности аутентификатора. Однако я не знаю ни одного современного браузера, который вел бы себя иначе, чем в режимах "none" или "direct". Наконец, "enterprise" означает, что проверяющая сторона требует корпоративную аттестацию, которая позволяет однозначно идентифицировать конкретное устройство. Аутентификатор должен явно разрешить проверяющую сторону, прежде чем такое подтверждение может быть возвращено.

Используйте настройку excludeCredentials для предоставления списка существующих учетных данных и предотвращения создания новых учетных данных аутентификатором, уже владеющим ими.

Наконец, свойство extensions предназначено для определения входных параметров расширений (extension inputs).

Возвращаемый промис отклоняется с Error с названием NotAllowedError, если пользователь отменяет процесс, или NotSupportedError, если аутентификатор не может создать учетные данные, удовлетворяющие указанным требованиям (например, не поддерживает указанные алгоритмы, не поддерживает верификацию пользователей и т.д.).

Если учетные данные успешно созданы, возвращаемый промис разрешается в PublicKeyCredential, свойство response которого содержит AuthenticatorAttestationResponse. Это включает в себя карту объектов (object map) аттестации в кодировке CBOR, представленную как AuthenticatorAttestationResponse.attestationObject, и объект клиентских данных, сериализованный в UTF-8 и закодированный в JSON, как AuthenticatorAttestationResponse.clientDataJSON.

Объект аттестации включает в себя аттестационное заявление (attestation statement), его формат и данные аутентификатора. Аттестационное заявление должно разбираться (parse) на основе формата. Данные аутентификатора - это двоичная структура данных, включающая ID учетных данных, AAGUID, открытый ключ и другие метаданные об аутентификаторе. См. раздел “Данные аутентификатора WebAuth” для получения более подробной информации. Не забывайте проверять валидность открытого ключа и поддержку его типа.

Данные клиента - это объект JSON, содержащий информацию о клиенте. См. раздел “Данные клиента WebAuth” для получения более подробной информации.

Если определено аттестационное заявление, верифицируйте его относительно данных аутентификатора и данных клиента. Затем инвалидируйте текущий запрос. Запрос может инвалидироваться после успешной аттестации или после каждой попытки. Оба подхода примерно одинаково безопасны.

Наконец, сохраните ID учетных данных, открытый ключ и алгоритм подписи. Убедитесь, что ID учетных данных уникален и доступен для запроса, и отклоняйте его, если он уже используется. Опционально, свяжите ID учетных данных с ID пользователя, передаваемым в navigator.credentials.create(), поскольку аутентификатор возвращает оба в процессе аутентификации. Поскольку размер запроса (challenge) может достигать 1023 байт, можно сначала выполнить его хеширование, если хранить его непосредственного слишком дорого для памяти.

Опционально, можно поддерживать счетчики подписей (signature counters), которые увеличиваются после каждой операции подписания. На практике это используется, в основном, внешними аппаратными токенами. Значение 0 означает, что аутентификатор не поддерживает счетчики подписей. Хотя эти счетчики предназначены для помощи в обнаружении потенциальной кражи закрытого ключа, они не предоставляют неопровержимых доказательств, что компрометация имела место. В лучшем случае, несовпадение счетчиков должно расцениваться как сигнал для логирования события и ручного анализа аккаунта пользователя. Следует отметить, что извлечение закрытого ключа из внешнего аппаратного токена является очень сложной задачей и почти никогда не случается. При реализации данного функционала, храните последнее значение счетчика рядом с учетными данными.

Если аттестация не требуется, данные клиента можно игнорировать, поскольку они не могут быть верифицированы. Веб-API также позволяет получить прямой доступ к данным аутентификатора с помощью AuthenticatorAttestationResponse.getAuthenticatorData(), что позволяет пропустить разбор объекта аттестации, закодированного в формате CBOR.

Как вариант, можно полностью пропустить разбор данных аутентификатора на сервере. ID учетных данных доступен как PublicKeyCredential.rawId, а открытый ключ можно получить с помощью AuthenticatorAttestationResponse.getPublicKey(). Ключ возвращается как структура SubjectPublicKeyInfo ASN.1, закодированная в формате DER, как определено в RFC 5280. Следует также извлекать ID алгоритма COSE с помощью AuthenticatorAttestationResponse.getPublicKeyAlgorithm(), поскольку открытый ключ не включает алгоритм подписи. Заметьте, что Safari поддерживает только алгоритм -7 (ES256) с кривой P-256 для getPublicKey(), поэтому для поддержки других алгоритмов может все-таки потребоваться разбор открытого ключа COSE в данных аутентификатора. AAGUID можно извлечь только при разборе данных аутентификатора. Затем можно отправить ID учетных данных, открытый ключ, алгоритм подписи и, опционально, AAGUID на сервер.

Аутентификация

По аналогии с регистрацией, нужно сначала сгенерировать запрос (challenge). В отличие от заявлений аттестации (attestation statements), которые являются опциональными, запрос в данном случае является обязательным, поскольку он используется для генерации и верификации подписей.

Для верификации аутентификатора используйте navigator.credentials.get() с настройкой publicKey:

const credential = await navigator.credentials.get({
	publicKey: {
		challenge: challenge,
		allowCredentials: [{
			type: "public-key",
			id: registeredCredentialId,
		}],
		userVerification: "required",
	},
});

Настройка challenge содержит сгенерированный запрос.

Настройка allowCredentials может использоваться для ограничения учетных данных и указания списка ID необнаруживаемых учетных данных.

Настройка userVerification определяет, как строго аутентификатор применяет верификацию пользователей. Значениями могут быть "required", "preferred" или "discouraged".

Метод возвращает промис, который либо отклоняется, если пользователь отменяет операцию, либо разрешается в PublicKeyCredential, свойство response которого содержит AuthenticatorAssertionResponse.

В отличие от регистрации, необходимо отправлять полные бинарные данные аутентификатора и данные клиента на сервер. Они доступны как AuthenticatorAssertionResponse.authenticatorData и AuthenticatorAssertionResponse.clientDataJSON. Также нужно отправлять ID учетных данных из PublicKeyCredential.rawId вместе с подписью из AuthenticatorAssertionResponse.signature. Идентификатор пользователя (user handle) для учетных данных доступен через AuthenticatorAssertionResponse.userHandle.

После разбора и валидации данных аутентификатора и данных клиента извлеките учетные данные с помощью ID учетных данных. Для верификации подписи сначала вычислите хеш SHA-256 данных клиента в формате JSON. Затем сформируйте подписанное сообщение (signed message) путем объединения данных аутентификатора с хешем данных клиента. Верифицируйте подпись относительно этого сообщения. Если верификация провалилась, значит, открытый ключ невалиден, процесс должен быть прерван. Заметьте, что для ECDSA форматом подписи является ANSI X9.62 (ASN.1).

При поддержке счетчиков подписей и ненулевом значении счетчика для сохраненных учетных данных, проверьте, что текущая попытка подписи больше сохраненного значения, и обновите сохраненное значение перед верификацией подписи. Если счетчик меньше, просто запишите событие для будущего анализа. Не блокируйте попытку аутентификации, поскольку в случае кражи закрытого ключа, аутентификация законного пользователя также может привести к несовпадению счетчиков. Я бы также не рекомендовал блокировать учетную запись, поскольку такие ошибки могут возникать естественным образом.

Наконец, инвалидируйте запрос. Как в случае регистрации, можно инвалидировать его после успешной аутентификации или после каждой попытки.

Данные аутентификатора WebAuth

Подсчет байтов и битов осуществляется с использованием индекса, начинающегося с 0.

Данные аутентификатора - это двоичные данные, содержащие информацию об аутентификаторе и, при регистрации, о созданных учетных данных. Они должны быть надлежащим образом валидированы в процессе регистрации и аутентификации.

Первые 32 байта представляют собой хеш SHA-256 ID проверяющей стороны (relying side). Он должен валидироваться относительно ожидаемого значения.

Байт 32 содержит несколько флагов:

  • бит 0 (самый левый (left-most) бит): флаг присутствия пользователя. Устанавливается в 1, если пользователь активно вовлечен в регистрацию или аутентификацию (например, ввел ключ безопасности или подтвердил запрос). В большинстве случаев устанавливается в 1;

  • бит 2: флаг верификации пользователя. Устанавливается в 1, если личность пользователя была подтверждена (например, через ПИН или биометрики). Проверяйте наличие этого флага, если верификация пользователя обязательна;

  • бит 3: флаг возможности резервного копирования. Устанавливается в 1, если аутентификатор поддерживает резервное копирование учетных данных (например, синхронизацию с облаком);

  • бит 4: флаг состояния резервного копирования. Устанавливается в 1, если учетные данные скопированы для страховки (например, в облако);

  • бит 6: флаг наличия подтвержденных (attested) учетных данных. Устанавливается в 1, если учетные данные включены в данные аутентификатора. Этот флаг должен быть включен при регистрации;

  • бит 7: флаг наличия данных расширений.

Следующие 4 байта - 32-битное целое число без знака, представляющее счетчик подписей (signature counter).

Если флаг наличия подтвержденных (attested) учетных данных установлен в 1, следующие байты содержат подтвержденные учетные данные, которые могут быть разной длины. Первые 16 байтов - AAGUID аутентификатора. Значение, состоящее из одних 0, означает, что аутентификатор не определен (undefined). Следующие 2 байта - 16-битное целое число без знака, определяющее длину ID учетных данных, за которыми следует сам ID учетных данных. Согласно спецификации, максимальная длина ID составляет 1023 байта (включительно). Оставшиеся байты содержат открытый ключ учетных данных, кодированный как ключ COSE в формате CBOR. Это последний элемент подтвержденных учетных данных. См. разделы для каждого алгоритма: ECDSA, схема подписи RSA и EdDSA для получения более подробной информации. Также см. раздел “Кодирование CBOR WebAuth” для получения деталей парсинга CBOR. Заметьте, что алгоритм подписи внедряется (embed) в открытый ключ COSE.

Наконец, если флаг наличия данных расширений установлен в 1, оставшиеся байты содержат кодированную в CBOR карту расширений (map of extensions). В целом, следует отклонять любые данные расширений, который вы явно не запрашивали или не ожидаете.

Кодирование CBOR WebAuth

CBOR можно встретить в двух местах: открытый ключ COSE и данные расширений в данных аутентификатора. Также его можно встретить при ручном разборе объекта аттестации в процессе регистрации.

CBOR - это бинарный формат с типами данных, похожими на JSON. Он определен в RFC 8949. Он поддерживает карты (похожие на объекты JSON), массивы, текстовые строки, бинарные строки, целые числа, числа с плавающей запятой и простые значения (включая true, false, undefined и null).

В WebAuthn все кодировки CBOR соответствуют канонической форме кодировки CBOR CTAP2, которая определяет строгие правила, гарантирующие, что каждая структура данных имеет единую кодировку. Это делает парсинг карт CBOR относительно простым. В частности, элементы неопределенной длины запрещены, а ключи карт должны быть отсортированы. Таким образом, карту можно анализировать, ожидая определенный порядок ключей, а когда ожидается определенное значение CBOR (включая карты), его можно проверить с помощью прямого сравнения на уровне байтов.

Данные клиента WebAuth

Данные клиента - это объект JSON, содержащий информацию о клиенте и контексте операции WebAuth. Ключ type определяет тип операции: "webauthn.create" для регистрации и "webauthn.get" для аутентификации. Значение challenge - это строка запроса в кодировке Base64url, выписанная проверяющей стороной. Далее, ключ origin определяет источник URL, инициировавший запрос учетных данных, и должен всегда валидироваться относительно доверенных источников. Наконец, опциональное значение crossOrigin устанавливается в true, когда запрос выполняется в iframe, и опускается, если запрос поступил из того же источника. Все значения должны валидироваться и совпадать с ожидаемыми.

{
	"type": "webauthn.get",
	"challenge": "q83vEjR9mK2Y7pT4cW1BQg",
	"origin": "https://example.com",
	"crossOrigin": false
}

Регистрация passkey

Перед тем, как пользователь сможет зарегистрировать passkey, он должен подтвердить свою личность с помощью любого доступного метода аутентификации.

Passkey являются обнаруживаемыми (discoverable) учетными данными WebAuth, которые используют верификацию пользователей и не требуют аттестации.

Поскольку аттестация не требуется, предоставление запроса (challenge) опционально. В качестве запроса можно просто передать пустой массив.

Должны поддерживаться алгоритмы -7 (ES256) и -257 (RS256). ES256 - это, безусловно, самый широко поддерживаемый алгоритм. RS256 требуется для обратной совместимости со старым устройствами Windows, поддерживающими только учетные данные RSA. Даже устройства с новыми версиями Windows могут поддерживать только RS256, если пользователь создал passkey до обновления. Можно также добавить поддержку алгоритма -8 (EdDSA), который широко поддерживается ключами безопасности.

const credential = await navigator.credentials.create({
	challenge: new Uint8Array(0),
	rp: {
		id: new URL(window.location.href).hostname,
	},
	user: {
		id: new TextEncoder().encode(userId),
		displayName: userEmailAddress,
	},
	pubKeyCredParams: [
		{ type: "public-key", alg: -8 },
		{ type: "public-key", alg: -7 },
		{ type: "public-key", alg: -257 },
	],
	excludeCredentials: [{
		id: existingCredentialId,
		type: "public-key",
	}],
	authenticatorSelection: {
		residentKey: "required",
		requireResidentKey: true,
		userVerification: "required",
	},
	attestation: "none",
	extensions: {
		credentialProtectionPolicy: "userVerificationRequired",
		enforceCredentialProtectionPolicy: false,
	},
})

Параметр расширений credentialProtectionPolicy можно установить в значение "userVerificationRequired". Это предотвращает перечисление учетных данных (credential enumeration) на переносимых аутентификаторах (roaming authenticators), таких как ключи безопасности, а также требует подтверждения пользователя, даже если предоставлен ID учетных данных. Если вы планируете сделать верификацию пользователей опциональной, когда passkey используется в качестве второго фактора, используйте "userVerificationOptionalWithCredentialIDList". Для предотвращения блокировки устройств, не поддерживающих это расширение, таких как программные аутентификаторы, параметр enforceCredentialProtectionPolicy должен быть установлен в значение false.

Открытый ключ должен надлежаще валидироваться на сервере:

  • ES256: убедитесь, что открытый ключ использует кривую P-256 и SHA-256 для подписей. Опционально валидируйте открытый ключ путем проверки того, что точка лежит на кривой и не является точкой на бесконечности. См. раздел 3.2.2.1 SEC1. При пропуске этой валидации, убедитесь, что библиотека верификации подписи не “паникует” при передаче невалидного открытого ключа при регистрации.

  • RS256: убедитесь, что модуль N представляет собой 2048-битное целое число без ведущих нулей, а показатель степени e равен 65537. Хотя допустимы и другие значения, они встречаются редко, и использование больших чисел может увеличить вычислительные затраты. Дополнительно можно поддерживать модули размером 3072 и 4096 бит.

  • EdDSA: убедитесь, что кривая - это Ed25519. Опционально валидируйте, что точка открытого ключа лежит на кривой. В отличие от ECDSA, спецификация EdDSA явно требует, чтобы реализации отклоняли невалидные открытые ключи, хотя многие реализации этого не делают. При пропуске этой валидации, убедитесь, что используемая вами библиотека не выдает ошибку при получении невалидного открытого ключа.

Данные расширений в данных аутентификатора обычно пусты, если в navigator.credentials.create() явно не передаются входные параметры расширений. Однако, Chrome автоматически включает параметр расширений credentialProtectionPolicy, если он не определен, поэтому данные расширений могут включаться, несмотря на то, что вы их не предоставляли. Если такие данные определены, карта CBOR будет содержать ключ credProtect с одним из трех значений: целое число 1 для userVerificationOptional, 2 для userVerificationOptionalWithCredentialIDList и 3 для userVerificationRequired. При явной установке параметра расширений в значение "userVerificationRequired", можно просто проверить, что данные расширений имеют значение

A16B6372656450726F7465637403

Обратите внимание, что даже при явном определении входных параметров расширений, поддержка политики защиты учетных данных будет ограничена перемещаемыми аутентификаторами, реализующими последний стандарт. Программные средства аутентификации, такие как менеджеры паролей, обычно не поддерживают это и не включают данные расширений в данные аутентификатора. Поэтому использование данных аутентификатора, не содержащих данные расширений, также должно поддерживаться.

Не забывайте проверять количество passkey пользователя и отклонять запрос при достижении лимита.

После создания passkey пользователю должно быть предложено указать его имя. Для упрощения процесса можно установить имя, полученное из AAGUID, в качестве значения по умолчанию. Однако, как правило, это необходимо делать только для программных средств аутентификации, таких как менеджеры паролей. Ключи безопасности обычно не возвращают AAGUID, когда аттестация отключена, а пользователи часто носят с собой несколько аппаратных ключей. Поскольку глобального реестра AAGUID не существует, AAGUID часто используемых аутентификаторов нужно собирать в единый список.

Аутентификация passkey

Генерируем запрос (challenge) и вызываем navigator.credentials.get() с обязательной верификацией пользователя:

const credential = await navigator.credentials.get({
	publicKey: {
		challenge: challenge,
		userVerification: "required",
	},
});

На сервере проверяем наличие флагов присутствия пользователя и верификации пользователя в данных аутентификатора. Затем проверяем подпись с помощью открытого ключа.

При поддержке методов аутентификации, требующих от пользователя ввода email (таких как авторизация на основе пароля или авторизация по коду, отправленному на email), добавьте автозаполнение passkey. Это возможность, когда браузер отображает выпадающий список с passkey, доступными пользователю, когда пользователь фокусируется на поле ввода, по аналогии с менеджерами паролей. Это значительно упрощает процесс, поскольку пользователю не нужно понимать, что такое passkey, или искать кнопку “Войти с помощью passkey”.

Сначала устанавливаем атрибут autocomplete поля ввода email в значение "webauth":

<input name="email_address" type="email" autocomplete="webauthn">

Затем, после загрузки страницы, вызываем navigator.credentials.get() с mediation, установленным в "conditional":

const credential = await navigator.credentials.get({
	mediation: "conditional",
	publicKey: {
		// ...
	},
});

Промис разрешается, только когда пользователь выбирает passkey из выпадающего списка, и отклоняется, если пользователь отменяет процесс аутентификации. Заметьте, что промис может “висеть” (pending) бесконечно, пока пользователь не выполнит действие. Поэтому этот метод должен вызываться в конце скрипта или с помощью “висящих” (floating) промисов.

Если ваши запросы (challenges) имеют срок действия, убедитесь, что запрос (request) прерывается, если срок действия запроса (challenge) истек, или автоматически обновляйте запрос (challenge) периодически. Пользователи могут оставлять страницу открытой на неопределенный срок перед взаимодействием с выпадающим меню.

Алгоритм цифровой подписи на эллиптических кривых (ECDSA)

Алгоритм цифровой подписи на эллиптических кривых (Elliptic Curve Digital Signature Algorithm, ECDSA) - это асимметричная схема цифровой подписи, определенная в SEC 1, которая использует криптографию на эллиптических кривых. Она обеспечивает безопасность, сравнимую со схемами на основе RSA, при этом используя значительно меньшие ключи и подписи.

Эллиптическая кривая, используемая в ECDSA, является настраиваемой. Часто используются кривые, рекомендованные NIST, причем P-256 (secp256r1) является наиболее распространенной.

Закрытый ключ представляет собой случайно сгенерированное целое число d. Соответствующий открытый ключ - это точка на эллиптической кривой, полученная из этого значения, представленная в виде пары целых чисел (x, y). Подпись состоит из двух целых чисел, r и s, значения которых зависят как от сообщения, так и от закрытого ключа.

При генерации и проверке подписи для хеширования сообщения используется хеш-функция. Часто выбирают хеш-функцию SHA-256 с кривой P-256. Однако хеш-функция не зависит от кривой, поэтому с любой парой ключей может быть использован любой алгоритм хеширования.

Форматы открытых ключей

SEC 1

Этот открытый ключ, первоначально определенный в спецификации SEC 1, представляет собой конкатенацию пары целых чисел с байтом заголовка. Для кривой P-256 координаты x и y занимают по 32 байта каждая:

0x04 || x || y

Открытый ключ также может быть представлен в сжатом формате, который хранит только координату x. Заголовочный байт равен 0x02, если y - четное число, или 0x03, если оно нечетное:

0x02 || x
0x03 || x

ANSI X9.62

Также известен как формат X.509, SubjectPublicKeyInfo или PKIX. Открытый ключ представлен в виде последовательности SubjectPublicKeyInfo в формате ASN.1, закодированной в DER. AlgorithmIdentifier.algorithm равен 1.2.840.10045.2.1. SubjectPublicKey представляет собой либо несжатый, либо сжатый открытый ключ в формате SEC1.

Для P-256 идентификатор объекта AlgorithmIdentifier.namedCurve равен 1.2.840.10045.3.1.7.

SubjectPublicKeyInfo := SEQUENCE {
	algorithm			AlgorithmIdentifier,
	subjectPublicKey	BIT STRING
}

AlgorithmIdentifier := SEQUENCE {
	algorithm	OBJECT IDENTIFIER
	namedCurve	OBJECT IDENTIFIER
}

COSE

В RFC 8152 открытый ключ представлен в виде карты ключей EC2, закодированной в CBOR. Хотя в спецификации COSE значение алгоритма (3) является необязательным, оно всегда будет определено в WebAuthn. Значение алгоритма представляет собой идентификатор алгоритма COSE, зарегистрированный в реестре IANA (обычно ECDSA с хеш-функцией). Значение кривой (-1) - это один из идентификаторов кривых, также зарегистрированных в реестре IANA. Значения x и y (-2, -3) - это целые числа, закодированные в виде двоичных строк в WebAuthn.

Для кривой P-256 идентификатор кривой равен 1, а значения x и y занимают ровно 32 байта каждое:

{
	1: 2,
	3: -7,
	-1: 1,
	-2: h'0000000000000000000000000000000000000000000000000000000000000000',
	-3: h'0000000000000000000000000000000000000000000000000000000000000000'
}

Форматы подписей

IEEE P1363

В этом формате подпись представляется в виде простой конкатенации пар целых чисел (r, s), каждое из которых закодировано в виде двоичной строки в порядке байтов big-endian. Для кривой P-256 каждая пара занимает 32 байта:

r || s

ANSI X9.62

Также называется просто форматом ASN.1. В этом формате подпись представляется в виде закодированной в DER последовательности ASN.1, содержащей пару целых чисел (r, s):

SEQUENCE {
	r	INTEGER,
	s	INTEGER
}

Схема подписи RSA

RSA - это широко используемое семейство криптосистем с открытым ключом, основанных на математике модульного возведения в степень. Две распространенные схемы этой подписи - это RSASSA-PKCS1-v1_5, первоначально определенная в PKCS #1 v1.5, и RSASSA-PSS, первоначально определенная в PKCS #1 v2.1. Оба алгоритма в настоящее время определены в PKCS #1 v2.2 (RFC 8017). Они используют разные алгоритмы дополнения (padding algorithms), но имеют одинаковую структуру открытого ключа RSA. RSASSA-PSS - более новый и сложный алгоритм, но более старый RSASSA-PKCS1-v1_5 по-прежнему широко используется.

Закрытый показатель степени d - это целое число, полученное из двух больших простых чисел. Размер этих простых чисел определяет размер и безопасность ключа. Например, выбор двух 1024-битных простых чисел приведет к созданию 2048-битного закрытого ключа. В процессе генерации ключа открытый ключ создается в виде пары, состоящей из модуля n и открытого показателя степени e. Модуль имеет ту же битовую длину, что и закрытый ключ, а открытый показатель степени обычно устанавливается равным 65537.

При создании или проверке подписи для хеширования сообщения используется криптографическая хеш-функция. Чаще всего используется SHA-256.

Форматы открытых ключей

PKCS#1

В этом формате подпись представляется в виде последовательности ASN.1, закодированной в формате DER, содержащей модуль и показатель степени открытого ключа:

RSAPublicKey ::= SEQUENCE {
	modulus			INTEGER,
	publicExponent	INTEGER
}

ANSI X9.6

Также известен как формат X.509, SubjectPublicKeyInfo или PKIX. Открытый ключ представлен в виде последовательности SubjectPublicKeyInfo в формате ASN.1, закодированной в DER. AlgorithmIdentifier.algorithm равен 1.2.840.113549.1.1.1 для RSA. SubjectPublicKey - это открытый ключ в формате PKCS#1:

SubjectPublicKeyInfo := SEQUENCE {
	algorithm			AlgorithmIdentifier,
	subjectPublicKey	BIT STRING
}

AlgorithmIdentifier := SEQUENCE {
	algorithm	OBJECT IDENTIFIER
	parameter	NULL
}

COSE

В RFC 8152 открытый ключ представлен в виде карты ключей RSA, закодированной в формате CBOR. Значение алгоритма представляет собой идентификатор COSE, зарегистрированный в реестре IANA:

{
	1: 3,
	3: -257,
	-1: h'...',
	-2: h'010001'
}

Алгоритм цифровой подписи на основе кривой Эдвардса (EdDSA)

Алгоритм цифровой подписи на основе кривой Эдвардса (Edwards Curve Digital Signature Algorithm, EdDSA) - это асимметричная схема цифровой подписи, основанная на эллиптических кривых. Он служит современной высокопроизводительной альтернативой ECDSA.

Закрытый ключ представляет собой случайное целое число k, закодированное в виде битовой строки, а соответствующий открытый ключ - это точка на кривой A, сжатая и закодированная в виде битовой строки. Подпись состоит из пары целых чисел (r, s), которые также сжаты и закодированы в одну битовую строку.

В отличие от ECDSA, параметры схем EdDSA обычно предопределены. Ed25519 использует кривую Curve25519 с хеш-функцией SHA-512 и имеет размер ключа 256 бит. Ed448 использует кривую Curve448 с хеш-функцией SHAKE256 и имеет размер ключа 456 бит. Их не следует путать с X25519 и X448, которые являются вариантами алгоритма Диффи-Хеллмана на эллиптических кривых (ECDH).

Форматы открытых ключей

ANSI X9.62

Также известен как формат X.509, SubjectPublicKeyInfo или PKIX. Открытый ключ представлен в виде последовательности SubjectPublicKeyInfo в формате ASN.1, закодированной в DER. AlgorithmIdentifier.algorithm равен 1.3.101.112 для Ed25519 или 1.3.101.113 для Ed448. SubjectPublicKey - это исходный открытый ключ:

SubjectPublicKeyInfo := SEQUENCE {
	algorithm			AlgorithmIdentifier,
	subjectPublicKey	BIT STRING
}

AlgorithmIdentifier := SEQUENCE {
	algorithm	OBJECT IDENTIFIER
}

COSE

В RFC 8152 открытый ключ представлен в виде карты OKP, закодированной в CBOR. Хотя в спецификации COSE значение алгоритма (3) является необязательным, оно всегда присутствует в WebAuthn и содержит идентификатор алгоритма COSE, зарегистрированный в реестре IANA, который обычно равен -8 (EdDSA). Значение кривой (-1) равно либо 6 для Ed25519, либо 7 для Ed448, а значение x (-2) содержит исходный открытый ключ:

{
	1: 4,
	3: -7,
	-1: 6,
	-2: h'fjuLyU4qH41sWp8bPnwtWotOHJ8tajtcfh1Ki5wuXxo',
}

Комментарии (1)


  1. kompilainenn2
    28.07.2026 06:51

    На гитхаб было бы гуманно добавить книгу в PDF , чтобы можно было скачать без лишних приседаний готовый документ