WebAuthn(FIDO2)の仕組みと実装

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

WebAuthn(FIDO2)の仕組みと実装

WebAuthn(Web Authentication)は、W3Cによって標準化されたWeb認証APIであり、FIDO2プロジェクトの中核技術です。パスワードに代わる強力な認証手段として、フィッシング耐性やユーザー体験の向上を目指しています。本稿では、WebAuthn/FIDO2のセキュリティメカニズム、潜在的な脅威モデル、具体的な攻撃シナリオ、そして安全な実装と運用のための対策について、実務家の視点から解説します。

脅威モデル

WebAuthnはパスワード認証に比べて強固ですが、そのセキュリティは実装と運用に依存します。主要な資産と脅威アクター、攻撃目標は以下の通りです。

  • 資産: ユーザーアカウント、ユーザーの公開鍵認証情報、サービスプロバイダ(Relying Party: RP)サーバーの秘密鍵(TLS証明書など)、ユーザーの個人データ。

  • 脅威アクター:

    • 外部攻撃者: フィッシング詐欺師、中間者攻撃者、認証情報の窃取を狙う者。

    • 内部不正者: 管理者権限の悪用、データベースへの不正アクセス。

  • 攻撃目標: ユーザー認証のバイパス、アカウント乗っ取り、RPサーバーの侵害、個人情報の窃取。

WebAuthnは、認証情報のサーバーへの送信を不要とし、特定のオリジンに紐付けられた公開鍵暗号技術を用いることで、フィッシングや認証情報窃取のリスクを大幅に軽減します。しかし、RPサーバーの実装に脆弱性があれば、これらのメリットは失われかねません。

攻撃シナリオ

WebAuthnが適切に実装されていない場合、以下のような攻撃シナリオが考えられます。

1. リプレイ攻撃

概要: RPサーバーが発行するチャレンジ値が適切に管理されていない場合、攻撃者が過去の認証応答を再利用して認証を試みる。 詳細:

  • RPサーバーが毎回ユニークで予測不可能なチャレンジを生成しない。

  • RPサーバーが、すでに使用されたチャレンジを無効化しない、または有効期限が長すぎる。

  • オーセンティケーターから送信される署名カウンターの検証を怠る。

2. 不適切なオリジン検証によるフィッシング誘導

概要: WebAuthnはオリジンバインディングによりフィッシング耐性が高いですが、RPサーバー側の検証が不完全な場合、悪性オリジンへの登録や認証を誘導される可能性があります。 詳細:

  • RPサーバーがAssertion検証時に response.clientDataJSON.origin と期待されるオリジンを厳密に比較しない。

  • サブドメインの取り扱いや、HTTPS以外のプロトコルでの認証を許容する設定ミス。

  • 攻撃者がRPサーバーの登録フローを迂回し、悪意のあるオーセンティケーターをユーザーアカウントに登録させる。

3. 公開鍵認証情報の偽装/改ざん

概要: RPサーバーのデータベースに保存されたユーザーの公開鍵認証情報が改ざんされた場合、攻撃者の公開鍵が登録され、ユーザーが認証できなくなる、あるいは攻撃者が認証できてしまう。 詳細:

  • RPサーバーのデータベースにSQLインジェクションなどの脆弱性があり、攻撃者がユーザーの公開鍵を自身の公開鍵に置き換える。

  • 認証情報データベースの整合性保護が不十分な場合。

4. RPサーバー実装の脆弱性を悪用したWebAuthnバイパス

WebAuthn自体のセキュリティは高いですが、RPサーバー側の実装に脆弱性がある場合、それを悪用してWebAuthnの保護を迂回する可能性があります。

graph TD
    subgraph 攻撃フェーズ
        A["偵察: RPサーバーの脆弱性特定"] --> |情報収集 (例: XSS, CSRF)| B{"RPサーバー"};
        B --> |脆弱性発見| C["攻撃者"];
        C --> |初回アクセス: XSSペイロード注入| D["ユーザーブラウザ (セッション乗っ取り可能)"];
        D --> |持続性: 偽のWebAuthn登録要求生成| E["不正なWebAuthn登録フロー"];
        E --> |RPサーバーへ送信 (不適切なチャレンジ生成/検証)| F["RPサーバー (脆弱な実装)"];
        F --> |攻撃者のオーセンティケーター登録| G["攻撃者の認証情報とユーザーアカウント紐付け"];
        G --> |影響: ユーザーアカウント乗っ取り| H["アカウント侵害"];
    end

このシナリオでは、RPサーバー側のクロスサイトスクリプティング(XSS)などの脆弱性が起点となり、攻撃者がユーザーのブラウザ上で不正なWebAuthn登録フローを生成し、自身のオーセンティケーターをユーザーのアカウントに紐付けることで、WebAuthnの認証をバイパスしてアカウントを乗っ取ります。これはWebAuthnそのものの弱点ではなく、RPサーバーの堅牢な実装がいかに重要かを示しています。

検出/緩和策

1. WebAuthnの基本メカニズムの理解と適切な検証

WebAuthnのフローは大きく「登録(Registration)」と「認証(Assertion)」に分けられます。それぞれの段階でRPサーバーは厳格な検証を行う必要があります。

  • 登録:

    1. RPサーバーは、セッションと紐付けられたユニークなチャレンジ値を生成。

    2. クライアント(ブラウザ)は、オーセンティケーターを通じて公開鍵とAttestation(鍵の正当性証明)を生成。

    3. RPサーバーは、公開鍵、Attestation、および署名カウンターの初期値を検証し、公開鍵とオーセンティケーターIDをユーザーに紐付けて保存。

  • 認証:

    1. RPサーバーは、登録時と同様にユニークなチャレンジ値を生成。

    2. クライアントは、オーセンティケーターを通じて秘密鍵でチャレンジに署名したAssertionを生成。

    3. RPサーバーは、Assertionの署名検証、クライアントデータ(オリジン、チャレンジ、タイプ)の検証、フラグ(uv: User Verifiedup: User Present)の検証、そして署名カウンター値が前回の値より増加しているかを検証。

2. 安全なWebAuthn実装のためのコード例と考慮事項

チャレンジ値の生成と管理

チャレンジ値は、リプレイ攻撃を防ぐために、セキュアな乱数生成器で生成され、一度しか使われず、短い有効期限を持つ必要があります。

  • 誤用例(PHP):

    <?php
    // 予測可能なチャレンジ値や使い回し
    session_start();
    if (!isset($_SESSION['challenge'])) {
        $_SESSION['challenge'] = substr(md5(time()), 0, 16); // 予測可能かつ短い
    }
    // ... 後で検証せず、複数回使用される可能性
    ?>
    
    • 問題点: md5(time())はエントロピーが低く、substrで短くすることでさらに予測が容易になります。また、セッションでの管理だけでは使い回しのリスクがあります。
  • 安全な代替(Python – Flask/FastAPIの例):

    import os
    import base64
    from datetime import datetime, timedelta
    
    # チャレンジ値の生成
    
    def generate_challenge():
        challenge_bytes = os.urandom(32)  # 32バイト (256ビット) の強力な乱数
        return base64.urlsafe_b64encode(challenge_bytes).decode('utf-8').rstrip("=")
    
    # チャレンジ値をセッションに保存し、有効期限を設定
    
    def store_challenge(session_id, challenge):
    
        # RedisやDBなどに保存し、有効期限 (例: 2分) を設定する
    
    
        # {'challenge': challenge, 'expires': datetime.utcnow() + timedelta(minutes=2)}
    
        print(f"Challenge '{challenge}' stored for session '{session_id}' with expiry.")
    
        # ここではセッション変数として仮置きするが、実運用では永続化層へ
    
        return {"challenge": challenge, "expires": datetime.utcnow() + timedelta(minutes=2)}
    
    # チャレンジ値の検証と破棄
    
    def verify_and_consume_challenge(session_id, received_challenge, stored_challenge_data):
        if not stored_challenge_data:
            return False, "Challenge not found or expired."
    
        if datetime.utcnow() > stored_challenge_data['expires']:
            return False, "Challenge expired."
    
        if received_challenge != stored_challenge_data['challenge']:
            return False, "Challenge mismatch."
    
        # 使用済みチャレンジは直ちに破棄(Redisから削除するなど)
    
        print(f"Challenge '{received_challenge}' consumed for session '{session_id}'.")
        return True, "Challenge verified."
    
    # 使用例:
    
    
    # challenge = generate_challenge()
    
    
    # stored_data = store_challenge("user_session_abc", challenge)
    
    
    # # ... クライアントからの応答 ...
    
    
    # is_valid, msg = verify_and_consume_challenge("user_session_abc", challenge, stored_data)
    
    
    # print(f"Verification: {is_valid}, Message: {msg}")
    
    • ポイント:

      • os.urandom(32) で十分なエントロピーを持つランダムなバイト列を生成します。

      • チャレンジ値はBase64 URL Safeでエンコードされ、URLやJSONでの扱いが容易になります。

      • セッションIDと紐付けてサーバーサイドに保存し、検証後に即座に破棄することでリプレイ攻撃を防止します。

      • 短い有効期限(例:2分)を設定し、期限切れのチャレンジを拒否します。

    • 計算量/メモリ: os.urandom は定数時間の操作。チャレンジ保存はキーバリューストアで管理され、通常はO(1)のアクセス。メモリ消費は微小。

オリジンとRP IDの検証

RPサーバーは、clientDataJSONに含まれるoriginrpIdが、自身の期待する値と厳密に一致するかを確認する必要があります。

  • 誤用例:

    # RP_ID = "example.com"
    
    
    # received_rp_id = "sub.example.com"
    
    
    # if received_rp_id.endswith(RP_ID): # ドメインサフィックスマッチングは不適切
    
    
    #     print("RP ID matches!")
    
    • 問題点: 厳密な比較を行わないと、攻撃者がサブドメインを利用して認証を偽装する可能性があります。
  • 安全な代替(Python – 概念コード):

    # RP_ID = "example.com"
    
    
    # EXPECTED_ORIGIN = "https://example.com"
    
    def verify_rp_id_and_origin(received_rp_id, received_origin):
    
        # received_rp_id と EXPECTED_RP_ID は厳密に一致させる
    
    
        # received_origin と EXPECTED_ORIGIN は厳密に一致させる
    
        if received_rp_id != RP_ID:
            print(f"Error: RP ID mismatch. Expected '{RP_ID}', got '{received_rp_id}'")
            return False
        if received_origin != EXPECTED_ORIGIN:
            print(f"Error: Origin mismatch. Expected '{EXPECTED_ORIGIN}', got '{received_origin}'")
            return False
        return True
    
    # 実際の呼び出しでは、WebAuthnライブラリが提供する検証関数を使用する
    
    
    # 例: pywebauthn.verify_registration_response(..., expected_rp_id=RP_ID, expected_origin=EXPECTED_ORIGIN, ...)
    
    • ポイント: rpIdorigin は登録/認証設定時に指定された値と厳密に一致させる必要があります。これは Google Developers WebAuthn2024年4月26日 に更新したガイドラインでも強調されています。

3. 鍵/秘匿情報の取り扱い、ローテーション、最小権限、監査

  • 鍵/秘匿情報の取り扱い:

    • RPサーバーの秘密鍵: TLS証明書の秘密鍵は、ハードウェアセキュリティモジュール(HSM)またはクラウドの鍵管理サービス(KMS)で厳重に保護します。RPサーバーの公開鍵データベース自体は公開鍵しか含まないため、秘匿情報ではありませんが、データの完全性(integrity)は確保される必要があります。

    • ユーザーの公開鍵: ユーザーのWebAuthn公開鍵は秘密情報ではありませんが、改ざんされて攻撃者の鍵に置き換えられないよう、データベースの整合性を保護する必要があります。

  • ローテーション:

    • RPサーバーの秘密鍵/証明書: 定期的に(例: 四半期ごと、または年1回)ローテーションします。

    • ユーザーの認証情報(公開鍵): WebAuthnの特性上、公開鍵ペアの直接的なローテーションは一般的ではありません。ユーザーが新しいオーセンティケーターを登録したり、既存の認証情報を削除して再登録したりすることで、間接的にローテーション(更新)が行われます。ユーザーに定期的な認証情報更新を促すことも検討します。

  • 最小権限:

    • WebAuthn認証情報を保存するデータベースへのアクセスは、最小限の権限に制限します。

    • 認証情報の追加、参照、削除を行うアプリケーションコードのみがアクセス権を持つべきです。

    • データベース管理者も直接のデータ改ざんができないような監査体制と技術的制約を設けます。

  • 監査:

    • WebAuthnの登録、認証試行(成功/失敗)、認証情報削除などの全てのイベントを詳細なログとして記録します。

    • ログには、タイムスタンプ、ユーザーID、RP ID、オーセンティケーターID、クライアントIPアドレス、結果(成功/失敗)などを含めます。

    • これらのログは、セキュリティ情報イベント管理(SIEM)システムに転送し、異常検知やフォレンジック分析に活用します。

4. その他

  • Attestationの検証: Attestationはオプションですが、オーセンティケーターの信頼性を確認するために有効です。信頼できるオーセンティケーターからのものか、FIDO Allianceのメタデータサービス(MDS)と照合するなどして検証することが推奨されます。W3CのWeb Authentication: An API for accessing Strong Assertion Credentials (Level 2)2023年1月26日公開)で詳細が定義されています。

  • User Verification (UV): authenticatorSelection.userVerification オプションを "required" に設定し、PINや生体認証などによるユーザー検証を強制します。

運用対策

1. モニタリングとログ監査

  • 異常検知: RPサーバーのWebAuthn関連ログ(登録、認証、エラー)をリアルタイムで監視し、失敗した認証試行の増加、未承認の認証情報登録、通常とは異なるIPアドレスからの認証試行など、異常なパターンを検知します。

  • 定期レビュー: 2024年05月28日 時点での最新セキュリティベストプラクティスに基づき、監査ログを定期的にレビューし、設定変更や不審なアクティビティがないか確認します。

2. インシデント対応計画

  • WebAuthn認証情報の悪用やRPサーバーの侵害が発生した場合に備え、明確なインシデント対応計画を策定します。

  • 計画には、侵害された認証情報の無効化、ユーザーへの通知、フォレンジック調査、システム復旧の手順を含めます。

3. ユーザー教育

  • ユーザーに対し、フィッシング詐欺の手口、正規サイトでのWebAuthn利用の重要性、不審な登録要求への対応方法などを教育します。

  • 物理的なセキュリティキーを利用している場合、その紛失・盗難時の対応手順(WebAuthn認証情報の削除と再登録)を周知します。

4. オーセンティケーター管理

  • ユーザーが紛失・盗難したオーセンティケーターの認証情報をRPサーバーから速やかに削除できるメカニズムを提供します。

  • 管理画面での認証情報の一覧表示と削除機能、緊急時のサポート対応フローを整備します。

現場の落とし穴

WebAuthnの実装と運用においては、以下のような「落とし穴」に注意が必要です。

  • 誤検知と検出遅延: 不審な認証試行の閾値設定は難しく、厳しすぎると正当なユーザーをブロックし、緩すぎると攻撃を見逃します。リアルタイム監視システムの調整には熟練した経験と継続的な調整が必要です。

  • 可用性とのトレードオフ: 強固なセキュリティポリシー(例: 常にUVを要求)はユーザーの利便性を損ねる可能性があります。ユーザー体験とセキュリティ要件のバランスを見極めることが重要です。

  • 古いオーセンティケーターへの対応: 市場には様々な種類のオーセンティケーターが存在し、古いデバイスは新しいセキュリティ標準や機能に対応していない場合があります。全てのユーザーが最新のオーセンティケーターを利用しているとは限らないため、後方互換性やサポートポリシーの検討が必要です。OWASPのWebAuthn Testing Guide2023年9月8日更新)にも、実装上の注意点が指摘されています。

  • 複数デバイス対応の複雑さ: ユーザーが複数のオーセンティケーターを登録する場合、それらの管理(追加、削除、ラベル付け)を容易にするUI/UX設計が求められます。

まとめ

WebAuthn(FIDO2)は、現代のWebアプリケーションにおける認証セキュリティを根本から強化する強力な技術です。しかし、その恩恵を最大限に享受するためには、RPサーバー側でチャレンジ値の適切な生成と管理、厳格なオリジン・RP ID検証、AttestationとAssertionの包括的な検証といった安全な実装が不可欠です。また、鍵管理、最小権限、詳細な監査、そしてユーザー教育を含む堅牢な運用対策も同様に重要です。これらの対策を講じることで、パスワードが抱える多くの問題を克服し、フィッシングに強く、より安全で使いやすい認証システムを構築できます。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

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