SimpleWebAuthn は WebAuthn を 2 つのパッケージに分けています。@simplewebauthn/browser は navigator.credentials をラップし、@simplewebauthn/server はリライングパーティが送るオプションを生成し、返ってきたもの(アテステーション形式、COSE 鍵、カウンター、署名)を検証します。チェックでは、標準的な登録とアサーションの JSON を生成するソフトウェア認証器でサーバー側のパッケージを動かしました。これは Python、Go、Java のページで使ったものと同じバイト列です。
最もよくある統合時のバグは、オプションで求める内容と検証で要求する内容の食い違いです。generateRegistrationOptions() のデフォルトは userVerification: "preferred" ですが、2 つの検証関数はどちらもデフォルトで requireUserVerification: true です。ユーザー存在確認しか行っていない認証器(PIN のないセキュリティキー)のアサーションは、User verification required, but user could not be verified で拒否されました。サンプルのように方針を一度決め、両側で同じ設定にしてください。
WebAuthn 仕様が定めるとおり、ES256 のアサーション署名は ASN.1 DER でなければなりません。64 バイトの生の 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 を生成するため、生の署名は通常、手書きのクライアントか、WebCrypto の sign() を直接呼び出したテストハーネスによるものです。DER ⇄ 生形式の変換ツールで変換してください。high-S の DER 署名は検証を通りました。
保存済みの値より小さい署名カウンターは、警告ではなくエラーになります。検証では Response counter value 3 was lower than expected 5 がスローされ、保存済みの値が 5 のときのカウンター 0 も同様に失敗しました。カウンター 0 で通過するのは、同期パスキーのように常に 0 を報告してきた認証情報だけです。受け取ったカウンターを保存し、巻き戻りが起きたときに認証情報をロックするか、アラートを出すだけにするかは自分で決めてください。バージョン 14 はデフォルトの pubKeyCredParams で ML-DSA-44(COSE -48)を Ed25519(-8)、ES256(-7)、RS256(-257)より先に並べており、Ed25519 の認証情報は登録から認証まで一通り検証できました。
ブラウザーが実際に送信した内容は WebAuthn インスペクターでデコードし、実際の認証器なしでのフローはパスキーサンドボックスで試し、認証器の機種は AAGUID 検索で AAGUID から特定してください。