SimpleWebAuthn разделяет WebAuthn на два пакета: @simplewebauthn/browser служит обёрткой над navigator.credentials, а @simplewebauthn/server формирует параметры, которые отправляет доверяющая сторона, и проверяет то, что приходит в ответ: форматы аттестации, ключи COSE, счётчики и подписи. В ходе проверки серверный пакет получал данные от программного аутентификатора: стандартный JSON регистрации и assertion, те же байты, что и для страниц о библиотеках для Python, Go и Java.
Самая частая ошибка интеграции — расхождение между тем, что запрашивают параметры, и тем, что требует проверка. По умолчанию generateRegistrationOptions() задаёт userVerification: "preferred", а обе функции проверки по умолчанию используют requireUserVerification: true: assertion от аутентификатора, который подтвердил только присутствие пользователя (ключ безопасности без PIN), был отклонён с ошибкой User verification required, but user could not be verified. Примите решение один раз и задайте одну и ту же политику с обеих сторон, как в примере.
Подписи assertion для ES256 должны быть в ASN.1 DER, как того требует спецификация WebAuthn. 64-байтовая подпись raw r‖s всё равно попадает в парсер ASN.1, и разбор завершается ошибкой, текст которой зависит от байтов подписи: для 60 новых учётных данных чаще всего встречались Too big integer, End of input reached before message was fully decoded и Data does not match to ECDSASigValue ASN1 schema., и ни одно из этих сообщений не упоминает кодировку. Аутентификаторы и поставщики ключей доступа создают DER, поэтому подпись raw обычно означает самописный клиент или тестовую обвязку, которая вызвала sign() из WebCrypto напрямую; преобразуйте такие подписи в конвертере DER ⇄ raw. Подпись DER с high-S прошла проверку.
Счётчик подписей меньше сохранённого — это ошибка, а не предупреждение: проверка выбросила Response counter value 3 was lower than expected 5, а счётчик 0 при сохранённом 5 привёл к такой же ошибке. Значение 0 проходит только у учётных данных, которые всегда сообщали 0, как синхронизируемые ключи доступа. Сохраняйте полученный счётчик и сами решайте, блокировать ли учётные данные при откате счётчика или только оповещать о нём. Версия 14 ставит ML-DSA-44 (COSE -48) первым в списке pubKeyCredParams по умолчанию, перед Ed25519 (-8), ES256 (-7) и RS256 (-257), а учётные данные Ed25519 прошли сквозную проверку.
Декодируйте то, что браузер действительно отправил, в инспекторе WebAuthn, пробуйте сценарии без настоящего аутентификатора в песочнице ключей доступа и определяйте модели аутентификаторов по AAGUID через поиск по AAGUID.