Passkey (WebAuthn) 登録フローとPublicKeyCredentialの検証手順

Security WebAuthn Authentication Frontend
結論

チャレンジ生成 → 生体認証呼び出し → サーバーでの clientDataJSON 3大要素検証の順で実装します。

// W3C WebAuthn Level 2 公式仕様:ブラウザ側登録呼び出し
async function registerPasskey(challengeBuffer: ArrayBuffer) {
  const credential = await navigator.credentials.create({
    publicKey: {
      challenge: challengeBuffer,
      rp: { name: "Example App", id: "example.com" },
      user: {
        id: new Uint8Array([1, 2, 3, 4]),
        name: "[email protected]",
        displayName: "山田 太郎",
      },
      pubKeyCredParams: [{ alg: -7, type: "public-key" }], // ES256 (ECDSA P-256)
      authenticatorSelection: { userVerification: "preferred" },
      timeout: 60000,
    },
  });

  return credential;
}

W3C WebAuthn公式仕様:サーバー側検証の必須チェックリスト

W3C WebAuthn Level 2 規格(w3.org/TR/webauthn-2)の規定通り、サーバー(Relying Party)がブラウザから受信した PublicKeyCredential を検証する際、以下の項目を順番にチェックしなければなりません。

検証項目チェック内容防御するセキュリティ攻撃
1. type の検証clientDataJSON.type === "webauthn.create" であることログイン用(webauthn.get)レスポンスの流用防止
2. challenge の検証セッション保存したチャレンジと完全一致&ワンタイム消費リプレイ攻撃(盗聴データの再利用)の完全防止
3. origin の検証clientDataJSON.origin === "https://example.com" であることフィッシングサイト(似せドメイン)からの不正登録防止

実際に起こる事故:チャレンジ使い回しと Origin 検証モレ

事故1: チャレンジのワンタイム無効化モレによるリプレイ攻撃

サーバー側で一回利用した challenge をセッションから即座に破棄(Clear)しない実装をしている場合、攻撃者がネットワーク上で盗聴した過去の登録レスポンス(clientDataJSON+署名)をそのまま再送信する リプレイ攻撃 を許し、第三者による勝手な鍵追加が発生します。

事故2: origin 検証漏れによるフィッシング成功

偽のフィッシングサイト(https://examp1e.com)経由でユーザーが生体認証を行った際、バックエンドで clientDataJSON.origin の完全一致チェックを怠っていると、フィッシングサイト上で生成された公開鍵が本物サーバーへそのまま登録される重大事故 が発生します。


安全な実装手順

  1. サーバー側で暗号学的に安全なランダムバイト列(crypto.randomBytes(32))を生成し、ユーザーセッションへ一次保存する
  2. フロントエンドで navigator.credentials.create() を呼び出し、Touch ID / Face ID 生体認証を実行する
  3. バックエンドへレスポンスを送信し、clientDataJSON をデコードして type, challenge, origin の3つを厳格に照合・検証した後に公開鍵をDBへ保管する