【テクニカル・上級編】 多要素認証(MFA)のフィッシング耐性強化とFIDO2/WebAuthnの導入 – ガバナンス・リスク管理 & 生成AIセキュリティ防御ガイド

MFA神話の崩壊とAitM(Adversary-in-the-Middle)の現実

長年、セキュリティ業界は「単一のパスワードは悪であり、MFA(多要素認証)を導入すれば大半の攻撃を防げる」と唱え続けてきた。しかし、その「MFA神話」はすでに過去のものだ。今日の洗練された脅威アクターは、標的がSMS OTP(ワンタイムパスワード)やTOTP(Authenticatorアプリ)、あるいはモバイルプッシュ通知によるMFAを有効にしていることを前提として攻撃を組み立てている。

その中心にあるのが、AitM(Adversary-in-the-Middle:中間者)フィッシングである。

Evilginx3 や Mitmproxy を悪用したAitM攻撃は、ユーザーと本物のアイデンティティプロバイダ(IdP)の間に、攻撃者が制御するリバースプロキシを介在させる。

[被害者のブラウザ] 
       │
       ▼ (1) 偽のドメイン (例: login.microsoft.security-update.com) へアクセス
[AitM プロキシ (Evilginx3)] 
       │
       ▼ (2) 本物のドメイン (例: login.microsoftonline.com) へプロキシ
[本物の IdP サーバ]

この構成において、ユーザーがID、パスワード、そしてTOTPトークンを入力すると、プロキシはそれをそのまま本物のIdPに転送する。IdPは正当な認証と判断し、セッションCookie(OAuthトークン等)を発行する。AitMプロキシはこの認証完了後のセッショントークン(Cookie)を横取り(ハイジャック)し、攻撃者のブラウザにインポートする。

この時点で、MFAプロトコルがどれだけ強力な暗号アルゴリズム(SHA-256等)を内部で使っていようが無意味となる。なぜなら、認証プロセスそのものは正常に終了しており、攻撃者が盗んだのは「認証済みの状態」そのものだからだ。

これが、従来のMFAが「フィッシング耐性(Phishing-Resistant)」を持たないと定義される根本的な理由である。この致命的な設計欠陥を、アプリケーションレイヤの暗号論的結合によって完全に解決するのが、FIDO2およびWebAuthn(Web Authentication)である。

—

AitMを無力化するWebAuthnのコアプロトコル仕様

WebAuthnがAitMフィッシングを原理的に不可能にする理由は、認証プロセスが「ドメイン(Origin)と暗号鍵の強固なバインド」および「ブラウザ(プラットフォーム)による強制的なOrigin検証」の上に成り立っているからだ。

WebAuthnの仕様では、認証時にブラウザが以下の2つの重要なデータを取得・生成し、クライアント(Authenticator:セキュリティキーやTPM)に渡す。

1. RP ID(Relying Party Identifier): 通常はサービスのドメイン名(例: money.portal.co.jp)。
2. Client Data JSON: チャレンジ、オリジン、その他のクライアント状態を含むJSON構造体。

AitMフィッシングの現場で何が起きるか、低レイヤのシーケンスに沿って解説する。

1. 攻撃ドメインでの認証試行

ユーザーがフィッシングサイト https://login.evildomain.com にアクセスさせられたとする。このサイトは本物のサイト https://secure.bank.com をプロキシしている。

2. ブラウザによる厳格なOrigin取得

WebAuthn API(navigator.credentials.get())が呼び出されると、ブラウザのUA(User Agent)カーネルは、JavaScriptから渡されたパラメータではなく、現在アクティブなタブの実際のアドレスバーのドメイン(login.evildomain.com)をOriginとして強制的に取得する。これはサンドボックス化されたブラウザの特権セグメントで実行されるため、JavaScriptによる改ざんは不可能である。

3. Authenticator(セキュリティキー)内でのアサーション生成

ブラウザはAuthenticatorに対し、以下のハッシュ値を渡して署名を要求する。
$$\text{ClientDataHash} = \text{SHA-256}(\text{clientDataJSON})$$

この clientDataJSON の内部構造は以下のようになっている。

{
  "type": "webauthn.get",
  "challenge": "D8Z_7u...[暗号論的に安全なランダム値]",
  "origin": "https://login.evildomain.com",
  "crossOrigin": false
}

Authenticatorは、自身が安全なストレージ内に保持しているクレデンシャルキー(秘密鍵)のリストから、対応する RP ID (login.evildomain.com) に紐づく鍵を選択し、署名を生成する。

4. 本物のIdPでの検証失敗

AitMプロキシは、Authenticatorから返ってきた署名データと clientDataJSON を本物のIdP(https://secure.bank.com)に転送する。

本物のIdP(Relying Party:RP)は、送られてきた clientDataJSON をデコードし、以下のチェックを行う。

$$\text{DecodedOrigin} \stackrel{?}{=} \text{“https://secure.bank.com”}$$

ここで、デコードされた origin は https://login.evildomain.com であるため、検証エンジンは即座に認証を拒否する。

[本物のIdP検証部]
  EXPECTED_ORIGIN = "https://secure.bank.com"
  RECEIVED_ORIGIN = "https://login.evildomain.com" -> 不一致! 認証を即時破棄(SIGABRT)

仮に攻撃者が clientDataJSON の中身を https://secure.bank.com に書き換えてIdPに送ろうとしても、Authenticatorが生成した署名は書き換え前の clientDataJSON のハッシュ値を対象に生成されているため、署名の検証段階(Signature Verification)で数学的に失敗する。

これが、FIDO2/WebAuthnが「フィッシング耐性を持つ」とされる数学的かつアーキテクチャ上の根拠である。

—

WebAuthnアサーション(認証)の低レイヤ・パケット解析

WebAuthnを安全に実装するためには、抽象化されたライブラリの裏で動くバイナリ構造を理解しなければならない。特に、認証時にAuthenticatorから返却されるアサーション(PublicKeyCredential.response)の解析は、多くの実装者が脆弱性を埋め込みやすいポイントである。

WebAuthn認証レスポンスのコアは、以下の2つのバイナリバッファである。

1. clientDataJSON (UTF-8文字列のバイナリ)
2. authenticatorData (構造化されたバイナリデータ:通称 authData)

authenticatorData のバイナリ構造

authenticatorData は、Authenticatorの状態を伝えるためにパックされたバイト配列である。パースの際は、1バイト単位でのビット演算が必要となる。

| オフセット (Byte) | サイズ (Byte) | 名称 | 説明 |
| :— | :— | :— | :— |
| 0 | 32 | rpIdHash | RP ID(ドメイン)のSHA-256ハッシュ値。 |
| 32 | 1 | flags | 状態を示すフラグ(UP, UV, AT, EDなど)。後述。 |
| 33 | 4 | signCount | 署名カウンタ(32bit 符号なしビッグエンディアン整数)。 |
| 37 | 可変 | attestedCredData | 新規登録時のみ存在。Credential IDと公開鍵を含む。 |
| 可変 | 可変 | extensions | 拡張データ(オプション)。 |

flags バイトのビット構成

flags(32バイト目、0-indexedでOffset 32)の各ビットは、アサーションの信頼性を担保する極めて重要なセキュリティインジケータである。

Bit 0: UP (User Present) - ユーザーの物理的な存在確認(ボタンタッチ等)
Bit 2: UV (User Verified) - ユーザーの本人確認(PIN入力、指紋認証等)
Bit 6: AT (Attestation data present) - 構成データが含まれているか
Bit 7: ED (Extension data present) - 拡張データが含まれているか
  • UP (User Present) [Bit 0]: これが 0 の場合、人間が介在しない自動化されたリプレイ攻撃の可能性が高いため、サーバーは認証を拒否しなければならない。
  • UV (User Verified) [Bit 2]: 企業の機密セグメントへのアクセスなど、高セキュリティ要件下では、このビットが 1 であることを検証(PINや生体認証の強制)する必要がある。

—

セキュアなWebAuthn検証エンジン(Server-side)の実装

一般的なライブラリを盲信せず、攻撃者のバイナリ改ざんを見抜くための、Node.js/TypeScriptによるWebAuthnアサーション検証エンジンのリファレンス実装を以下に示す。

このコードは、生バイトのパース、flags の検証、および公開鍵(COSE形式)を用いた署名検証を厳格に行う。

import * as crypto from 'crypto';

interface AssertionVerificationPayload {
    credentialId: string;           // Base64URL
    clientDataJSON: string;         // Base64URL
    authenticatorData: string;      // Base64URL
    signature: string;              // Base64URL
    publicKeyJwk: jose.JWK;         // 登録時に保存されたユーザーの公開鍵 (JWK形式と仮定)
    expectedChallenge: string;      // セッションに保存されていたチャレンジ
    expectedOrigin: string;         // 本物のオリジン (例: "https://secure.bank.com")
    expectedRpId: string;           // 本物のRP ID (例: "secure.bank.com")
    storedSignCount: number;        // DBに記録されている前回の署名カウンタ
}

export function verifyWebAuthnAssertion(payload: AssertionVerificationPayload): boolean {
    // 1. Base64URLのデコード
    const clientDataBuffer = Buffer.from(payload.clientDataJSON, 'base64url');
    const authDataBuffer = Buffer.from(payload.authenticatorData, 'base64url');
    const signatureBuffer = Buffer.from(payload.signature, 'base64url');
    
    // 2. clientDataJSON の検証
    const clientDataStr = clientDataBuffer.toString('utf-8');
    const clientData = JSON.parse(clientDataStr);
    
    if (clientData.type !== 'webauthn.get') {
        throw new Error('Invalid credential type in clientDataJSON');
    }
    
    if (clientData.challenge !== payload.expectedChallenge) {
        throw new Error('Challenge mismatch. Replay attack suspected.');
    }
    
    if (clientData.origin !== payload.expectedOrigin) {
        throw new Error(`Origin mismatch. Expected ${payload.expectedOrigin}, got ${clientData.origin}`);
    }

    // 3. authenticatorData の解析
    if (authDataBuffer.length < 37) {
        throw new Error('authenticatorData is too short');
    }

    // RP ID ハッシュの検証
    const expectedRpIdHash = crypto.createHash('sha256').update(payload.expectedRpId).digest();
    const actualRpIdHash = authDataBuffer.subarray(0, 32);
    if (!expectedRpIdHash.equals(actualRpIdHash)) {
        throw new Error('RP ID Hash mismatch. Credential is not registered for this domain.');
    }

    // フラグの検証
    const flags = authDataBuffer.readUInt8(32);
    const UP = (flags & 0x01) !== 0; // Bit 0
    const UV = (flags & 0x04) !== 0; // Bit 2

    if (!UP) {
        throw new Error('User Presence (UP) flag is not set.');
    }
    
    // 高セキュリティモード:User Verification(PIN/生体認証)を要求する場合
    // if (!UV) {
    //     throw new Error('User Verification (UV) flag is required but not set.');
    // }

    // 署名カウンタの検証(クローンキー検出)
    const signCount = authDataBuffer.readUInt32BE(33);
    if (signCount > 0 || payload.storedSignCount > 0) {
        if (signCount <= payload.storedSignCount) {
            // 物理的クローン、またはリプレイ攻撃の検知
            throw new Error(`Cloned authenticator detected! Stored: ${payload.storedSignCount}, Received: ${signCount}`);
        }
    }

    // 4. 署名の検証
    // WebAuthnのアサーション署名は、以下の連結バッファに対して生成される:
    // Signature = Sign( PrivateKey, authenticatorData + SHA-256(clientDataJSON) )
    const clientDataHash = crypto.createHash('sha256').update(clientDataBuffer).digest();
    const verifyBuffer = Buffer.concat([authDataBuffer, clientDataHash]);

    // 保存されているJWK形式の公開鍵をPEM/CryptoKeyに変換(ここでは簡略化のためNode.jsのcryptoモジュールを使用)
    const publicKey = crypto.createPublicKey({
        key: payload.publicKeyJwk,
        format: 'jwk'
    });

    // 署名アルゴリズムは通常ECDSA(ES256)またはEdDSA。ここではES256(SHA-256 with ECDSA)を前提とする
    const verifier = crypto.createVerify('SHA256');
    verifier.update(verifyBuffer);
    
    // ASN.1 DER形式の署名であることを確認して検証を実行
    const isVerified = verifier.verify(publicKey, signatureBuffer);
    if (!isVerified) {
        throw new Error('Cryptographic signature verification failed.');
    }

    // 認証成功:新しい署名カウンタをデータベースに保存すること
    // updateStoredSignCountInDatabase(payload.credentialId, signCount);

    return true;
}

実装上のクリティカルな死角:署名カウンタ(signCount)のバイパス

多くのテックリードが「FIDO2を使っていれば安全」と誤解し、署名カウンタの検証をスキップしている。これは重大なセキュリティホールである。

ハードウェアキーの中には、物理的にファームウェアを吸い出され、秘密鍵がクローン(複製)されるリスクがゼロではない。もしクローンされたキーが存在する場合、本物のキーとクローンキーで交互にログインが試行される。

このとき、サーバー側で signCount の単調増加チェックを行っていれば、前回ログイン時のカウンタ値(例: 105)に対して、クローンキーから送られてきたカウンタ値(例: 98)が下回るため、システムは即座に「クローンキーによる不正アクセス」を検知し、該当クレデンシャルを無効化できる。この監査ログは、SOC(Security Operations Center)にとって最高精度のインジケータとなる。

—

耐量子暗号(PQC)への移行ロードマップ

FIDO2/WebAuthnは現時点で最もセキュアな認証方式であるが、その暗号学的基盤は主に楕円曲線暗号(ECDSA P-256、Ed25519)に依存している。これらは、十分な規模を持つ量子コンピュータ(Shorのアルゴリズムを実装したもの)の実用化によって、一瞬で解読される運命にある。

WebAuthnにおける耐量子暗号(PQC: Post-Quantum Cryptography)への移行は、すでにフロントラインで設計が始まっている。

PQ-WebAuthnのアーキテクチャ

NIST(米国国立標準技術研究所)によるPQC標準化プロジェクト(FIPS 204)において、デジタル署名規格として ML-DSA (Dilithium) が選定された。FIDOアライアンスおよびW3C WebAuthnワーキンググループは、これに伴い以下の「ハイブリッド署名(Hybrid Signature)」方式の導入を進めている。

[WebAuthn 署名バッファ]
       │
       ├─► 古典暗号署名 (ECDSA P-256) ───┐
       │                                  ├─► [結合アサーション]
       └─► 耐量子暗号署名 (ML-DSA-65) ───┘

なぜハイブリッドなのか。理由は、既存のブラウザ、OS、セキュリティキー(TPM/セキュアエレメント)が、一晩でPQCに対応することは不可能だからだ。

移行期のパケットサイズ問題

セキュリティアーキテクトが直面する現実的な問題は、「鍵および署名のサイズ肥大化」である。

| アルゴリズム | 公開鍵サイズ (Byte) | 署名サイズ (Byte) |
| :— | :— | :— |
| Ed25519 (古典) | 32 | 64 |
| ML-DSA-65 (耐量子) | 1,952 | 3,300 |

FIDOハードウェアキー(YubiKey等)の超制限されたメモリ(SRAM/EEPROM)領域において、数キロバイトに及ぶ署名データを処理・保持することは極めて困難である。このため、当面は以下の2つのアプローチが並行して進められる。

1. Lattice-based (格子暗号) ハードウェアアクセラレータ搭載チップへの刷新: セキュリティキー自体のハードウェア更新。
2. Stateful Hash-Based Signatures (LMS/XMSS): 鍵サイズは小さいが署名回数に制限があるハッシュベース署名の、特定ユースケースでの採用。

インフラ設計者は、将来的にWebAuthnのアサーションパケットが数バイトから数キロバイトへと肥大化することを見越し、WAF(Web Application Firewall)のペイロードサイズ制限や、認証エンドポイントのバッファ上限をあらかじめチューニングしておく必要がある。

—

ゼロトラスト監査・アーキテクチャ設計チェックリスト

WebAuthnの導入を成功させ、監査に耐えうるゼロトラストな認証基盤を構築するためのアーキテクチャ設計チェックリストを提示する。

1. レガシーMFAへのダウングレード攻撃を完全に遮断しているか?

攻撃者は、WebAuthnが有効化されているアカウントに対し、あえて「パスワードの初期化」や「SMSによるリカバリー」をリクエストすることで、セキュリティ強度の低い認証フローへバイパス(ダウングレード攻撃)しようとする。

  • 対策: WebAuthnを有効にしたユーザーに対しては、SMS/TOTPによるリカバリー手段を強制的に無効化、またはリカバリー時にも同等強度のFIDO2デバイス(予備キー)を要求する設計にせよ。

2. 「Device-bound Passkeys」と「Synced Passkeys」を識別して評価しているか?

iCloudキーチェーンやGoogleパスワードマネージャーを介してデバイス間で同期される「Synced Passkeys(同期型パスキー)」は、利便性は高いが、鍵のコピーが可能である。一方、物理的なYubiKey等に格納される「Device-bound Passkeys(デバイスバインドパスキー)」は、ハードウェアから鍵を抽出できない。

  • 対策: 金融インフラや特権アクセス(本番DBサーバーへのSSH等)においては、WebAuthnの attestation(構成証明)を要求し、ハードウェアが「FIPS 140-3 レベル3」等の認定を受けた物理キー(Device-bound)であることをサーバー側で検証せよ。

3. AAGUID(Authenticator Attestation Globally Unique Identifier)によるホワイトリスト制御を行っているか?

AAGUIDは、Authenticatorのモデル(例: 「YubiKey 5 Series」)を識別するための128ビットの識別子である。

  • 対策: 社内システムにおいては、私物の安価なスマートウォッチや信頼性の低いデバイスによるFIDO登録を拒否するため、検証エンジン側でAAGUIDのホワイトリストを保持し、認可されたデバイスからの登録のみを許可する制御を実装せよ。

FIDO2/WebAuthnは、認証における「人間」という最大の脆弱性を暗号学的にカバーする唯一の解である。その実装においては、高レイヤのフレームワークが隠蔽しているバイナリ仕様とドメインバインドのロジックを正確に把握し、妥協のない検証コードを書き上げることが、我々セキュリティアーキテクトに課された使命である。

コメント

タイトルとURLをコピーしました