本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
次世代暗号認証・フィッシング確実な防止 W3C WebAuthn L3 / FIDO2 CTAP2 / COSE / P-256 & Ed25519 対象:経営者様・経理/総務ご担当者様・認証基盤エンジニア様

FIDO2 / WebAuthn Level 3 と CTAP2 公開鍵暗号による リアルタイム中間者フィッシング(AiTM)防止プロトコル実装

監修:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / 登録情報処理安全確保支援士(第007617号))

1. 基本と全体像

この記事の要点と4つの章のご案内

身近なたとえで見る仕組み:「伝言ゲームで盗まれる合言葉」と「本物の相手の目の前でしか押せない実印」に関するイメージ写真
身近なたとえで見る仕組み:「伝言ゲームで盗まれる合言葉」と「本物の相手の目の前でしか押せない実印」
現場で陥りやすい落とし穴と回避策:パスキー導入・実装時の3大アンチパターンに関するイメージ写真
現場で陥りやすい落とし穴と回避策:パスキー導入・実装時の3大アンチパターン
FIDO2 / WebAuthn Level 3 と CTAP2 公開鍵暗号による リアルタイム中間者フィッシング(AiTM)防止プロトコル実装 の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 身近なたとえで見る仕組み:「伝言ゲームで盗まれる合言葉」と「本物の相手の目の前でしか押せない実印」 国内の銀行・証券口座や社内メールで急増している不正アクセスは、偽サイトが本物のサイトを丸ごと中継して6桁の確認コードまで盗み取る「リアルタイム中間者フィッシング(AiTM)」によるものです。
2. 現場での進め方と対策 現場で陥りやすい落とし穴と回避策:パスキー導入・実装時の3大アンチパターン FIDO2 / WebAuthn(パスキー)は、秘密鍵を端末の専用金庫チップに閉じ込め、「接続先ドメイン(RP ID)のSHA-256ハッシュ」が一致したときしか署名を行いません。
3. 関連する制度とルール 「スマホに届く6桁の数字(SMS認証・ワンタイムパスワード)を入力しているから安心」ではないのですか? さらに37バイトの認証器データ(AuthenticatorData)に含まれる単調増加カウンタ(signCount)により、万一の鍵複製(クローン)も即座に検知・遮断します。
4. 詳しい技術仕様としくみ 「パスキー(FIDO2 / WebAuthn)」を使うと、自分の指紋や顔の画像データが会社のサーバーへ送られるのですか? 最近の巧妙なフィッシングサイト(リアルタイム中間者攻撃:AiTM)は、あなたが偽サイトにID・パスワード・6桁の数字を入力したその瞬間に、裏で本物の銀行や業務システムへ同じ数字を自動転送し、ログイン後の「通行証(セッションCookie)」を横取りします。人間が目で見て数字を入力する方式では、そっくりな偽サイトを見破れない限り防ぐことができません。
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
この記事のポイント
  • 01 国内の銀行・証券口座や社内メールで急増している不正アクセスは、偽サイトが本物のサイトを丸ごと中継して6桁の確認コードまで盗み取る「リアルタイム中間者フィッシング(AiTM)」によるものです。
  • 02 FIDO2 / WebAuthn(パスキー)は、秘密鍵を端末の専用金庫チップに閉じ込め、「接続先ドメイン(RP ID)のSHA-256ハッシュ」が一致したときしか署名を行いません。
  • 03 さらに37バイトの認証器データ(AuthenticatorData)に含まれる単調増加カウンタ(signCount)により、万一の鍵複製(クローン)も即座に検知・遮断します。
場面別の実務確認ポイント(ご自身の目的に合わせてお読みいただけます) 全訪問者対応・3段階の構成
01 経営者・経理担当者・個人でネット取引をされる方

なぜ「SMSの6桁コード」を入力しても被害に遭うのか、そしてスマホの指紋・顔認証(パスキー)に切り替えるだけでなぜ偽サイトを構造的に防げるのかがわかります。

02 情報システム・総務・SCS評価制度ご担当の方

パスキー導入後に「パスワードリセット窓口」が抜け穴になるリスクや、スマホ紛失時の予備キー(YubiKey等)の運用ルールを具体的に確認できます。

03 暗号技術者・認証基盤開発者の方

後半の「第3章 Monograph」にて、37バイト AuthenticatorData のバイナリ構造、COSE Key 形式、Origin 束縛数理、Rust 2024 検証器を詳解しています。

第1章:Overview & Visual Architecture

身近なたとえで見る仕組み:「伝言ゲームで盗まれる合言葉」と「本物の相手の目の前でしか押せない実印」

これまでの「パスワード」や「スマホに届く6桁の番号(ワンタイムパスワード)」は、いわば「口頭で伝える合言葉」です。もし本物の銀行窓口の手前に、銀行員の制服を着た詐欺師(偽サイト:Evilginx等のリバースプロキシ)が立っていて、あなたがその詐欺師に「合言葉は 123456 です!」と伝えてしまったらどうなるでしょうか?詐欺師は後ろを向いて、0.1秒で本物の銀行窓口へ「合言葉は 123456 です!」とそのまま伝言し、あなたになりすまして金庫を開けてしまいます。これが今、日本中のネットバンキングや証券口座、社内クラウドで被害を出している「リアルタイム中間者フィッシング(AiTM)」の仕組みです。

これに対して、私たちが採用・推奨している「FIDO2 / WebAuthn(パスキー)」は、合言葉を伝えるのを一切やめ、「あなたのスマホやパソコンの金庫室から外へは原則として出せない『実印(秘密鍵)』で、その場の契約書にハンコを押す仕組み」に変えます。しかもこの実印には賢い安全装置がついており、「契約書に印刷された相手の住所(Webサイトのドメイン名)が、登録した本物の住所と1文字でも違っていたら、そもそもハンコ自体が出てこない」ようになっています。だから、どんなに本物そっくりの偽サイトを開いてしまっても、盗まれる合言葉自体が存在しないのです。

ポイント 01 米国NIST SP 800-63B 認証基準
AAL3 最高強度準拠

米国国立標準技術研究所(NIST)およびCISAは、中間者攻撃を防ぐ唯一の手段として「フィッシング耐性のあるFIDO2/WebAuthn認証」への移行を強く勧告しています。

ポイント 02 ネットワーク上を流れる秘密情報
0 バイト(共有秘密なし)

サーバー側には「検証用の公開鍵」しか保存されず、通信路にも秘密鍵は一切流れないため、サーバーからのパスワード漏洩も通信傍受も原理的に不可能です。

ポイント 03 ドメイン束縛ハッシュ長(rpIdHash)
SHA-256(32 Bytes)

認証器の署名データ先頭32バイトに接続先ドメインのSHA-256ハッシュが焼き込まれるため、別ドメインの偽サイトが署名を転送しても100%拒絶されます。

図解:なぜ「SMS 6桁コード」は中継型の偽サイト(AiTM)に突破され、「パスキー(FIDO2)」は構造的に防げるのか
01 従来のパスワード+SMS 6桁コード(偽サイトがリアルタイム中継すると突破される) 利用者(人間が目視で判断) 本物そっくりの画面を見て ID・PW・6桁コードを入力してしまう 中継型の偽サイト(Evilginx等) ドメイン: fake-bank-login.example 0.1秒で本物へ転送しCookieを窃取 本物のサーバー(2026.ok.st) 正しい6桁コードが届いたため 本人のログインだと誤認して許可 02 FIDO2 / WebAuthn パスキー(ブラウザとTPM/Secure Enclaveがドメインを暗号検証) 端末内セキュリティチップ ブラウザから渡された接続先Originを SHA-256(rpId) で厳密照合 偽サイト上で開いた場合 fake-login.example の鍵は存在せず そもそも署名自体が1バイトも作られない Rust 2024 署名検証エンジン rpIdHash・UVフラグ・signCount・ P-256/Ed25519署名を定数時間検証
比較項目パスワード + SMS / アプリ6桁コード(TOTP)当社推奨・実装:FIDO2 / WebAuthn(パスキー)
リアルタイム中継型偽サイト(AiTM)利用者が偽サイトに6桁コードを入力するとそのまま突破されるドメイン名(RP ID)が一致しないため秘密鍵が起動せず構造的に無力化
サーバー情報漏洩時のリスクハッシュ化されたパスワードやTOTP共通鍵が盗まれる危険があるサーバーには「公開鍵」しかないため、万一漏れても悪用価値はゼロ
日々のログインにかかる手間複雑な文字列を打ち込み、スマホを開いて6桁の数字を転記する(約30秒)指紋センサーに触れるか画面を見るだけ(約1〜2秒、記憶・入力不要)
複製(クローン)検知機能同じパスワードやシードがコピーされてもサーバー側では気づけない署名ごとに増える signCount の逆転検知により複製を即座に遮断
第2章:Practical Field Guide & Self-Check

現場で陥りやすい落とし穴と回避策:パスキー導入・実装時の3大アンチパターン

落とし穴 01:せっかくパスキーを導入したのに、横のリンクに「従来のSMSやメールだけのパスワードログイン」を残したままにする

回避策:重要操作(送金・管理者設定・機密閲覧)では「ダウングレード認証」を禁止し、複数のパスキー登録を促す

玄関にどんなに頑丈な電子錠(パスキー)をつけても、勝手口に「昔の合言葉(パスワード+SMS)でも入れます」という抜け道が残っていたら、攻撃者は偽サイトでわざと「パスキーがエラーになったのでパスワードを入力してください」と誘導してきます(ダウングレード攻撃)。管理者アカウントやネットバンキング、会計システムでは、知識要素(パスワード)へのフォールバックを閉じ、代わりに「パソコン本体+スマートフォン(または予備のセキュリティキー YubiKey)」の2つ以上のパスキーを登録しておく運用が安全です。

落とし穴 02:サーバー側の検証プログラムで、User Presence(UP)だけを確認し、User Verification(UV:生体/PIN確認)の検査を省略する

回避策:37バイト目の flags バイトにおいて、UP (0x01) と UV (0x04) の双方が立っていることを必須とする

WebAuthnの仕様では、認証器が返すフラグの中に「人が物理的に触れたか(UP: ビット0)」と「指紋・顔・PINなどで本人確認したか(UV: ビット2)」が別々に記録されています。もしサーバー側が UV ビットの確認を省略してしまうと、離席中に机の上に置かれたままのセキュリティキーを第三者がポンと指で触っただけでログインが通ってしまいます。業務システムでは必ず userVerification: "required" を指定し、サーバー側でもビット2が 1 であることを検証します。

落とし穴 03:サーバーが発行したチャレンジ乱数(Challenge)をセッションと紐付けず、有効期限や1回使い切り(One-Time)を順守しない

回避策:最低128ビット以上の暗号論的乱数を生成し、検証完了と同時にサーバーメモリから即時破棄(Replay防止)する

公開鍵暗号がどれほど強力でも、毎回署名する対象となる「問題文(Challenge乱数)」が固定だったり、一度使った問題文をもう一度使えたりすると、過去の通信データを録音して再生する「リプレイ攻撃」が成立してしまいます。サーバーは認証要求のたびに予測不能な乱数を発行し、60秒〜120秒の短い有効期限を設け、1回検証に使われた瞬間に無効化する設計が必須です。

今日から確認できる!フィッシング・乗っ取り対策(パスキー) 3項目チェック

達成度: 0 / 3

クリックしてチェックを入れると、ご自身や社内のアカウント保護レベルに応じたアドバイスが表示されます。

上の3項目をクリックして、毎日のログイン方法の安全度を点検してみてください。

W3C WebAuthn Level 3 サーバー側検証(Relying Party Verification)監査マトリクス

パスキー認証において、中間者攻撃(AiTM)やクローン認証器をサーバー側で確実に排除するための必須検証ステップです。

検証対象フィールド 暗号学的・構造的不変条件 防御される攻撃手口
clientDataJSON.origin origin === "https://ok.st" フィッシングドメインからの認証レスポンス中継を遮断
authData.rpIdHash (32B) ct_eq(rpIdHash, SHA256("2026.ok.st")) Evilginx2等のリバースプロキシ型中間者攻撃(AiTM)を構造的に無力化
authData.flags & signCount (flags & 0x05) == 0x05 && signCount > prev 生体/PIN未検証バイパスおよびハードウェア鍵の複製(クローン)を検知
関連コラム・豆知識

なぜ「大文字・小文字・記号を混ぜて90日ごとにパスワードを変更するルール」は世界中で廃止されたの?

ひと昔前までは「パスワードは3ヶ月ごとに変更しましょう」とよく言われていました。しかし米国政府(NIST)や日本の総務省・NISCが調査した結果、人間に定期的な変更を強制すると「末尾の数字を 1 から 2 に変えただけの推測しやすいパスワードにする」「覚えきれなくなってパソコンの画面に付箋で貼る」「偽サイトから『定期変更のお知らせ』というメールが来たときに疑わずに入力してしまう」という逆効果ばかりが起きることがわかりました。現在は「定期変更は不要、その代わり使い回しをやめてパスキー(FIDO2)へ移行すること」が世界共通の新しい常識になっています。

技術仕様・実装解説:Technical Architecture & Implementation — W3C WebAuthn Level 3 AuthenticatorData Layout & CTAP2 Cryptographic Verification

W3C WebAuthn Level 3 における 37バイト AuthenticatorData 構造・Origin 束縛数理と Rust 2024 定数時間検証器

W3C WebAuthn L3 / FIDO CTAP 2.1 / RFC 9052 (COSE)
AuthData Header 37 Bytes (32B Hash + 1B + 4B)
COSE Algorithms ES256 (-7) / EdDSA (-8)
AiTM Proxy Defense SHA-256(rpId) + clientData.origin
Clone Detection Strict Monotonic signCount
1. 設計仕様・数理モデル(Cryptographic Binding against Reverse-Proxy AiTM & AuthenticatorData Bitfield Semantics)

W3C Web Authentication(WebAuthn)Level 3 Section 6.1 および FIDO CTAP 2.1 において、認証器(Authenticator)が authenticatorGetAssertion 要求に対して生成する署名 σ は、最低 37バイトのバイナリ構造体 authData と、ブラウザが生成した clientDataJSON の SHA-256 ダイジェスト(32バイト)を連結したバイト列 M に対して計算されます:

M = authData ∥ SHA-256(clientDataJSON),   σ = Signsk_RP(M)
authData[0..32] = SHA-256(rpId),   clientDataJSON = {"type":"webauthn.get","challenge":"...","origin":"https://ok.st"}

なぜ Evilginx2 や Modlishka などのリアルタイム中間者(AiTM)リバースプロキシが FIDO2 を一切突破できないのか?それは二重の暗号学的オリジン束縛にあります。攻撃者が被害者を偽ドメイン D_adv = login-ok.example へ誘導した場合、被害者のブラウザ(User Agent)は TLS 終端先の実際のオリジン D_adv を clientDataJSON.origin に書き込み、かつ CTAP2 層へ D_adv を渡します。第一に、被害者の TPM / Secure Enclave 内には (D_adv, CredentialID) に紐づく秘密鍵 sk が存在しないため、署名自体が生成されません。第二に、万一攻撃者がスクリプトを改変して自身のドメイン上で別鍵による署名を作らせ、それを正規サーバー D_legit = 2026.ok.st へ中継したとしても、正規サーバー側の検証器で authData[0..32] = SHA-256(D_adv) ≠ SHA-256(D_legit) となり、100% 拒絶されます。

さらにオフセット +0x20(33バイト目)の flags バイトは、ビット0 UP (0x01)、ビット2 UV (0x04)、ビット3 BE (0x08: Backup Eligible)、ビット4 BS (0x10: Backup State)、ビット6 AT (0x40)、ビット7 ED (0x80) を表現し、続く +0x21..+0x24 のビッグエンディアン 32ビット整数 signCount が前回記録値 C_prev に対して C_new > C_prev を満たすことを検証することで、ハードウェアトークンの物理クローンを検出します。

2. パラメータ・構成仕様(W3C WebAuthn AuthenticatorData & COSE Key Binary Specification) WebAuthn Level 3 Section 6.1 & RFC 9052
Byte Offset / FieldStructure / BitmaskLength / TypeVerification Invariant
authData +0x00..0x1FrpIdHash (SHA-256 of RP Domain)32 BytesConstant-time equality with SHA-256("2026.ok.st")
authData +0x20flags (UP=0x01, UV=0x04, BE=0x08, BS=0x10)1 Byte Bitfield(flags & 0x05) == 0x05 mandatory for AAL2/AAL3
authData +0x21..0x24signCount (Big-Endian uint32)4 Bytes (u32 BE)If (prev > 0 || curr > 0), require curr > prev
authData +0x25..0x34aaguid (Authenticator Attestation GUID)16 Bytes (if AT=1)Identifies hardware authenticator model against FIDO MDS3
COSE_Key (alg: -8)kty=1 (OKP), crv=6 (Ed25519), x=32B PubKeyCBOR Map (RFC 9052)Deterministic Ed25519 signature verification (RFC 8032)
COSE_Key (alg: -7)kty=2 (EC2), crv=1 (P-256), x=32B, y=32BCBOR Map (RFC 9052)ECDSA over NIST P-256 with SHA-256 digest
3. 実装コード・設定仕様(src/auth/webauthn_l3_verifier.rs)
Rust Edition 2024 (#![no_std] Constant-Time AuthData & Clone Guard)
#![no_std]
#![forbid(unsafe_code)]

pub const FLAG_USER_PRESENT: u8 = 0x01; — Bit 0: UP
pub const FLAG_USER_VERIFIED: u8 = 0x04; — Bit 2: UV
pub const FLAG_BACKUP_ELIGIBLE: u8 = 0x08; — Bit 3: BE
pub const FLAG_BACKUP_STATE: u8 = 0x10; — Bit 4: BS
pub const FLAG_ATTESTED_CRED_DATA: u8 = 0x40; — Bit 6: AT

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VerifiedAssertionHeader {
    pub rp_id_hash: [u8; 32],
    pub flags: u8,
    pub sign_count: u32,
    pub is_backup_synced: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WebAuthnVerifyError {
    AuthDataTooShort(usize),
    RpIdHashMismatch,
    UserPresenceMissing,
    UserVerificationMissing,
    BackupStateInconsistent,
    ClonedAuthenticatorDetected { previous: u32, received: u32 },
    ChallengeMismatch,
}

/// Constant-time 32-byte slice comparison to eliminate timing side-channels (CWE-208).
#[inline]
pub const fn ct_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
    let mut diff: u8 = 0;
    let mut i = 0;
    while i < 32 {
        diff |= a[i] ^ b[i];
        i += 1;
    }
    diff == 0
}

/// Verifies a W3C WebAuthn Level 3 `authenticatorData` binary payload (minimum 37 bytes)
/// against expected `SHA-256(rp_id)`, strict `UP | UV` requirements, `BE/BS` consistency,
/// and monotonic `signCount` anti-cloning invariants.
pub const fn verify_webauthn_authenticator_data(
    raw_auth_data: &[u8],
    expected_rp_id_sha256: &[u8; 32],
    stored_sign_count: u32,
    require_uv: bool,
) -> Result<VerifiedAssertionHeader, WebAuthnVerifyError> {
    if raw_auth_data.len() < 37 {
        return Err(WebAuthnVerifyError::AuthDataTooShort(raw_auth_data.len()));
    }

    let mut rp_id_hash = [0u8; 32];
    let mut i = 0;
    while i < 32 {
        rp_id_hash[i] = raw_auth_data[i];
        i += 1;
    }

    if !ct_eq_32(&rp_id_hash, expected_rp_id_sha256) {
        return Err(WebAuthnVerifyError::RpIdHashMismatch);
    }

    let flags = raw_auth_data[32];
    if (flags & FLAG_USER_PRESENT) == 0 {
        return Err(WebAuthnVerifyError::UserPresenceMissing);
    }
    if require_uv && (flags & FLAG_USER_VERIFIED) == 0 {
        return Err(WebAuthnVerifyError::UserVerificationMissing);
    }

    // WebAuthn Level 3 invariant: BS (Bit 4) cannot be 1 if BE (Bit 3) is 0
    let be = (flags & FLAG_BACKUP_ELIGIBLE) != 0;
    let bs = (flags & FLAG_BACKUP_STATE) != 0;
    if !be && bs {
        return Err(WebAuthnVerifyError::BackupStateInconsistent);
    }

    let sign_count = u32::from_be_bytes([
        raw_auth_data[33],
        raw_auth_data[34],
        raw_auth_data[35],
        raw_auth_data[36],
    ]);

    if (stored_sign_count != 0 || sign_count != 0) && sign_count <= stored_sign_count {
        return Err(WebAuthnVerifyError::ClonedAuthenticatorDetected {
            previous: stored_sign_count,
            received: sign_count,
        });
    }

    Ok(VerifiedAssertionHeader {
        rp_id_hash,
        flags,
        sign_count,
        is_backup_synced: bs,
    })
}
ご質問・公的制度の活用に関するお問い合わせ窓口

掲載資料に関するご質問・公的制度活用のご案内

本ページの図解や解説内容に関するご質問、経済産業省「SCS評価制度」やIPA「SECURITY ACTION・サイバーセキュリティお助け隊サービス」等の公的制度の確認方法について、落ち着いた参考窓口としてご案内しております。売り込みや営業連絡は一切行いませんので、社内勉強会やご家庭での確認資料としてご自由にご活用ください。

図解・ビジュアルの拡大表示

図表内のテキストもそのまま選択・検索可能です(ベクターSVG描画) 枠外クリックまたは ESC キーで閉じます