Titanセキュリティキーが登録はできるのにログインだけ失敗する: VaultwardenのWebAuthn 2FAで踏んだ罠

セルフホストのVaultwardenにGoogle Titanセキュリティキーを2段階認証(FIDO2 WebAuthn)として登録したら、登録自体は一発で成功するのに、その後のログインが必ず失敗する、という不可解な症状にはまった。同じ物理キーはGoogleやwebauthn.ioでは何の問題もなく使える。原因を突き止めるのに数週間かかったので、記録として残しておく。 症状 Titanキーを2FAとして登録すると成功する(タッチ要求 → 登録完了、というふつうの流れ) ログイン時、ブラウザの「セキュリティキーを挿入してタッチしてください」ダイアログが出たまま反応がなく、最終的にNotAllowedError: The operation either timed out or was not allowedでタイムアウトする 同じ現象がLinux(Chrome)、Mac mini(Chrome/Firefox)、スマートフォンでも再現。OSやマシンには依存しない 同じキーをGoogleアカウントやwebauthn.ioに登録すると問題なく動く。キー自体は壊れていない 除外していった仮説 原因を絞り込むまでに、以下をひとつずつ潰していった。 RP ID/ドメインの不一致 — ブラウザのネイティブダイアログには正しいドメインが表示されている ブラウザ拡張機能・広告ブロッカーの干渉 — 無効化しても変化なし webauthn-connector.htmlのPermissions-Policyヘッダーにpublickey-credentials-getが含まれていない — Caddy側でヘッダーを上書きするテストを本番で実施したが変化なし サーバー側での認証情報の保存/シリアライズ不具合 — DBに保存されたcred_idはログイン時にそのままバイト単位で一致しており、サーバー側で壊れている様子はない U2F互換用のappid拡張を無条件に送っていることが原因では? — 移行済み(migrated: true)のクレデンシャルにだけappidを送るようパッチしたVaultwardenをビルドして本番にデプロイし、Chromeの内部FIDO/CTAPログ(--vmodule="*fido*=3,*webauthn*=3")で比較したが、タイムアウトまでの挙動パターンは無変化。この仮説は外れ 本家Bitwarden(C#実装)との差異 — WebAuthnTokenProvider.csを確認したが、Vaultwardenとほぼ同じ流れ(UserVerificationRequirement.Discouraged + 無条件のAppID拡張)で組まれており、Vaultwarden固有のバグという線も薄い 突破口: Firefoxで登録し直すとPINを聞かれた 外堀を埋めていく中で、ふと「登録済みのキーを消して、Firefoxで登録し直してみたらどうなるか」を試した。すると、Chromeでは一度も出なかったセキュリティキーのPIN入力を、Firefoxは要求してきた。 このFirefoxで登録し直したクレデンシャルでログインすると、Chrome・Firefoxどちらからでも成功する。念のため2回繰り返して確認した。 Chromeで再登録 → 失敗(何度やっても同じ) Firefoxで再登録 → 成功(Chrome/Firefoxどちらのログインも通る) 「登録時にPINによるユーザー検証(UV)が実際に行われたかどうか」が明暗を分けているのは間違いなさそうだった。 DBレコードを直接比較する 同じ物理キーを、登録に使うブラウザだけ変えて2回登録し、Vaultwardenのtwofactorテーブル(SQLite)に保存されるレコードを直接比較した。 Chromeで登録(PINなし) Firefoxで登録(PINあり) user_verified false true counter(ログイン後) 0(一度も成功していない) ログインのたびに増分 クレデンシャルIDの長さ 160 bytes 288 bytes registration_policyはどちらもdiscouragedのまま変わらないが、実際にUVが行われたかどうかでクレデンシャルIDの長さそのものが変わっている。Titanキーは、UVなしで作られた短い方のクレデンシャルではログインのGetAssertionを完走できない、という個体差(あるいは仕様)を持っているらしい。 ...