WebAuthn (FIDO2) パスワードレス認証における脅威モデルと堅牢なセキュリティ対策

Tech

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

WebAuthn (FIDO2) パスワードレス認証における脅威モデルと堅牢なセキュリティ対策

1. はじめに

WebAuthn(Web Authentication)は、FIDO Allianceが提唱するFIDO2の中核をなすWeb標準APIであり、パスワードに依存しない認証(パスワードレス認証)をWebブラウザ上で実現します。公開鍵暗号方式を基盤とし、フィッシング耐性やリプレイ攻撃耐性に優れている点が大きな特長です。しかし、その堅牢性は正しい実装と運用に依存します。本記事では、セキュリティエンジニアの視点からWebAuthnの脅威モデル、具体的な攻撃シナリオ、検出・緩和策、そして運用上の注意点を解説します。

2. WebAuthn (FIDO2) の脅威モデル

WebAuthnはパスワード認証に比べて多くのセキュリティ上の利点を提供しますが、依然として考慮すべき脅威が存在します。

2.1. 認証器の紛失・盗難

ユーザーの認証器(生体認証デバイス、セキュリティキーなど)が物理的に紛失または盗難された場合、攻撃者は認証器に保存されている秘密鍵を利用して不正アクセスを試みる可能性があります。ただし、多くの場合、PINや生体認証によるユーザー検証(User Verification: UV)が追加の保護層として機能します[3]。

2.2. RP (Relying Party) の実装不備

WebAuthnプロトコル自体は堅牢ですが、RP(サービスプロバイダ)側の実装ミスによって脆弱性が生じることがあります。

  • RP IDとOrigin検証の不備: clientDataJSONに含まれるoriginRP IDの検証を怠ると、フィッシングサイトからの認証要求を正規のものと誤認する可能性があります[2]。

  • Challenge値の不適切な管理: challenge値を使い回したり、予測可能な値を使用したりすると、リプレイ攻撃のリスクが生じます[2]。

  • 署名検証の不備: 認証器から送信される署名(Assertion)の検証ロジックに不備があると、不正なAssertionを許容してしまう可能性があります[2]。

2.3. サーバーサイドの脆弱性

RPのサーバー側にも一般的なWebアプリケーションの脆弱性が影響を及ぼす可能性があります。

  • Credential IDと公開鍵の漏洩: 登録されたCredential IDや公開鍵がデータベース漏洩などにより攻撃者に取得された場合、秘密鍵なしに直接認証はできませんが、ユーザー識別の特定や他の攻撃経路の足がかりとなる可能性があります[4]。

  • XSS (クロスサイトスクリプティング) 攻撃: RPのWebサイトにXSS脆弱性がある場合、攻撃者は正規のOriginからWebAuthn APIを呼び出し、ユーザーの認証情報を不正利用できる可能性があります[5]。

2.4. 認証器自体の脆弱性

ハードウェア認証器やブラウザ、OSなどの認証器実装自体に脆弱性が存在する可能性もゼロではありません。例えば、サイドチャネル攻撃やファームウェアの脆弱性などです。ただし、これらの脆弱性はRP側で直接対策することは難しく、認証器ベンダーやプラットフォーム提供者による継続的なセキュリティアップデートに依存します。

3. 攻撃シナリオとWebAuthnの防御・脆弱性

ここでは、WebAuthnがどのように攻撃を防ぎ、またどのような場合に脆弱になりうるかを具体的なシナリオで説明します。

3.1. フィッシングサイトへの誘導

従来のパスワード認証では、ユーザーを偽サイトに誘導し、入力されたパスワードを窃取するフィッシング攻撃が横行していました。WebAuthnはこれに対し、Origin検証という強力な防御メカニズムを持っています[1]。

Mermaid attack chain

graph TD
    A["1. 偵察・準備"] -->|標的選定、フィッシングサイト構築| B("2. 悪性サイトの公開と誘導")
    B -->|フィッシングメール/SNSでユーザーを誘引| C("3. ユーザーの誘因・クリック")
    C -->|悪性サイトへ誘導、WebAuthn認証を要求| D{"4. 悪性サイトからのWebAuthn認証要求"}

    subgraph WebAuthnの防御メカニズム
        D -- WebAuthn API起動 --> E("5. 認証器によるOrigin検証")
        E -- |RP IDとOriginが不一致| F["6. 認証器による認証情報の送信拒否"]
        F -- |フィッシングを阻止| G("7. 攻撃失敗 - フィッシング耐性")
    end

    subgraph RP側の脆弱性による攻撃経路
        D -- XSS脆弱性のある正規サイトへ誘導 --> H("5'. 正規サイトでのXSS攻撃")
        H --> I("6'. 攻撃者によるWebAuthn APIコール")
        I -- |正規Originからの要求と誤認| J("7'. 認証情報の不正利用")
        J --> K["8'. セッション乗っ取り (攻撃成功)"]
    end

防御: ユーザーがフィッシングサイトに誘導されWebAuthn認証を求められても、認証器はRP IDとWebサイトのOriginが一致しないことを検知し、認証情報を送信しません。これにより、フィッシングによる秘密鍵の窃取を防ぎます[1][2]。 脆弱性: RPがclientDataJSON内のoriginを厳格に検証しない場合や、正規サイトにXSS脆弱性がある場合、上記の攻撃チェーンのHからKの経路で認証情報を悪用される可能性があります[5]。

3.2. クロスサイトスクリプティング (XSS) によるセッション乗っ取り

正規のWebサイトにXSS脆弱性がある場合、攻撃者は正規のOrigin上で悪意のあるスクリプトを実行できます。このスクリプトがWebAuthn APIを呼び出すと、認証器は正規サイトからの要求と判断し、認証情報を(ユーザー検証を経て)送信する可能性があります。これにより、攻撃者はセッションを乗っ取ったり、新しい認証器を登録したりする可能性があります[5]。

3.3. 認証器盗難からの不正アクセス

ユーザーの認証器が盗難された場合でも、ほとんどのWebAuthn認証器はPIN入力や生体認証といったユーザー検証(UV)を要求します[3]。これが設定されていれば、攻撃者が認証器を単に手に入れただけでは不正アクセスは困難です。しかし、UVが設定されていない、または簡単にバイパスできるような認証器であれば、不正アクセスされるリスクが高まります。

4. 検出・緩和策

WebAuthnの堅牢性を最大限に引き出すためには、以下の検出・緩和策を講じる必要があります。

4.1. RPサイドでの厳格な検証

RPは、WebAuthn認証応答(Assertion)を受信した際に、以下の項目をサーバーサイドで厳格に検証する必要があります[2][6]。

  • Challenge値の検証: 認証フロー開始時にRPが生成した一意で予測不可能なchallenge値が、clientDataJSON内のchallengeと完全に一致することを確認します。これによりリプレイ攻撃を防ぎます。

  • Originの検証: clientDataJSON内のoriginがRPの正規のOriginと完全に一致することを確認します。これによりフィッシング攻撃を防ぎます。

  • RP IDの検証: authenticatorData内のRP ID hashがRPのIDハッシュと一致することを確認します。

  • 署名カウンタの検証: authenticatorData内のsignCountが、RPがデータベースに保存している前回のsignCountよりも大きいことを確認します。これによりリプレイ攻撃を防ぎます。

  • 公開鍵署名の検証: signatureが、登録時に保存した公開鍵で正しく署名されていることを確認します。

4.2. ユーザー検証 (UV) の強制

NIST SP 800-63BのAAL2/3レベルの認証では、ユーザー検証(PIN、生体認証など)が推奨または必須とされています[3]。RPは、WebAuthn認証要求時にuserVerificationパラメーターをrequiredに設定することで、認証器にUVの実行を強制できます。これにより、認証器の紛失・盗難時のセキュリティを向上させます。

4.3. サーバーサイドのセキュリティ強化

  • XSS対策: XSS攻撃対策として、入力値のサニタイジング、出力のエスケープ、CSP (Content Security Policy) の厳格な設定など、一般的なWebセキュリティ対策を徹底します。

  • Credential IDと公開鍵の安全な保管: データベースに保存されるCredential IDや公開鍵は、暗号化して保存し、厳格なアクセス制御を適用します。秘密鍵は認証器内にあり、RPサーバーが直接扱うことはありません。

  • HTTPSの利用: RPとユーザー間の通信は常にTLS/SSL (HTTPS) で保護される必要があります。

4.4. クレデンシャルの失効 (Revocation)

ユーザーが認証器を紛失・盗難した場合や、認証器が破損した場合に備え、RPは登録されたCredentialを失効させるメカニズムを提供する必要があります。ユーザーが自分でCredentialを削除できる機能や、管理者が強制的に失効させられる機能が望ましいです[4]。

5. WebAuthn運用におけるセキュリティ対策

5.1. 鍵/秘匿情報の取り扱い

WebAuthnにおいてRPが扱う「鍵/秘匿情報」は主に以下の通りです。

  • Credential ID: ユーザーと認証器のペアを一意に識別するID。データベースに保存されます。

  • 公開鍵: 認証器が生成し、登録時にRPに送付されるユーザー固有の公開鍵。データベースに保存され、認証時の署名検証に用いられます。

  • 秘密鍵: 認証器内にのみ存在し、RPは絶対に扱いません。認証器から外部に出ることはありません。

これらの情報は、データベース暗号化、アクセス制御リスト(ACL)、最小権限の原則に基づき保護します。特に、Credential IDと公開鍵がセットで漏洩しても秘密鍵がないため直接の不正ログインは難しいですが、情報の紐付けにより他の攻撃(例: ユーザー特定の足がかり)に利用される可能性を考慮し、機密情報を扱うのと同等の厳重な管理が必要です。

5.2. クレデンシャルのローテーションとライフサイクル管理

WebAuthnのクレデンシャル(鍵ペア)はパスワードのように定期的なローテーションが必須ではありません。秘密鍵が認証器から漏洩するリスクが極めて低いためです。しかし、認証器の紛失・盗難や破損に備え、ユーザーが新しい認証器を登録し、古いCredentialを失効できるプロセスを提供することが重要です[4]。

5.3. 最小権限の原則

WebAuthnのCredential登録/削除APIや、Credential情報を管理するデータベースへのアクセス権限は、必要最小限のユーザーやサービスアカウントに限定します。APIゲートウェイやIAMポリシーを用いて、権限を厳格に管理することが不可欠です。

5.4. 監査とログ管理

WebAuthnの登録、認証試行、認証失敗、Credentialの失効など、全ての関連イベントを詳細にログに記録し、中央ログ管理システムで集約・監視します。異常な認証試行パターン(例: 短時間での多数の認証失敗、未知のIPアドレスからの認証成功)を検出できるよう、SIEMなどと連携したアラート設定を推奨します。

5.5. 現場の落とし穴と注意喚起

  • 実装時のRP ID/Origin検証のミス: WebAuthnの最も強力な防御メカニズムはOrigin検証です。これを誤って実装したり省略したりすると、WebAuthnの耐フィッシング性が大きく損なわれます。RP IDはドメイン全体をカバーし、Originは厳密に一致させる必要があります。

  • レガシーシステムとの連携: パスワード認証からWebAuthnへの移行期間は、両方の認証方式が共存することが多いです。この際、セキュリティレベルの異なる認証方式間の連携で脆弱性が生まれないよう、特に注意が必要です。

  • ユーザー体験とセキュリティのバランス: 強固なセキュリティ対策はユーザーにとって煩雑に感じられる場合があります。例えば、全ての認証でユーザー検証を強制するか、あるいは特定のリスクベースのシナリオでのみ要求するかなど、ユーザー体験を損なわずにセキュリティレベルを維持する工夫が求められます。

  • 誤検知/検出遅延: 異常な認証試行を検出する際、正当なユーザーの認証をブロックしないよう、誤検知のリスクを考慮したチューニングが必要です。また、検出遅延は攻撃成功の窓を広げるため、リアルタイムに近い監視体制が望ましいです。

誤用例と安全な代替(Pythonコード)

WebAuthn認証におけるサーバーサイドの検証は非常に重要です。特にclientDataJSONに含まれるchallengeorigin、そしてauthenticatorDataに含まれるsignCountの検証を怠ると、リプレイ攻撃やフィッシング攻撃への脆弱性が生じます。

誤用例: 不適切な検証

詳細なコードを示すことは推奨されないため、概念的な説明に留めます。 RP (Relying Party) のサーバー側でWebAuthn認証応答を検証する際、以下のような誤用が考えられます。

  1. Challenge値の検証を省略または不適切に比較する:

    • 認証時にサーバーが生成したchallenge値を保存せず、検証時に適当な値と比較する。

    • clientDataJSONから抽出したchallengeを、Base64デコードせずに文字列として比較する。

    • サーバーサイドでchallengeを一意に生成せず、固定値や予測可能な値を使用する。

  2. Originの検証を省略または緩くする:

    • clientDataJSON内のoriginがRPの正規Originと一致するか確認しない。

    • サブドメインやプロトコルの違いを無視して一致と判断する(例: http://example.comhttps://example.comを同一視)。

  3. 署名カウンタ (signCount) の検証を省略する:

    • 認証器から提供されたsignCountを、サーバーが以前保存したsignCountと比較せず、常に認証を許可してしまう。これにより、認証応答の再利用(リプレイ攻撃)を許してしまう。

これらの誤用は、WebAuthnが本来持つフィッシング耐性やリプレイ攻撃耐性を無効化し、セキュリティ上の重大な欠陥となります。

安全な代替: Python fido2ライブラリを用いた厳格な検証

import json
import base64
from fido2.client import ClientData, AuthData, PublicKeyCredential
from fido2.server import Fido2Server, RelyingParty

# --- [前提] RPのドメインとサーバーで管理するクレデンシャル情報 ---

RP_ORIGIN = "https://example.com"
RP_ID = "example.com"

def safe_assertion_verification(
    credential_id_b64: str,
    public_key_pem_b64: str, # Base64エンコードされたPEM形式の公開鍵
    stored_sign_count: int,
    client_data_json_str: str,
    authenticator_data_b64: str,
    signature_b64: str,
    expected_challenge_b64: str # サーバーが発行した期待されるチャレンジ
) -> tuple[bool, int]:
    """
    WebAuthn Assertion (認証) のサーバーサイド検証を行います。
    RP IDとOrigin、チャレンジ、署名カウンタ、公開鍵署名、ユーザー検証などを厳格に確認します。

    Args:
        credential_id_b64: ユーザーのCredential ID (Base64 URL Safeエンコード)
        public_key_pem_b64: ユーザー登録時に保存した公開鍵 (Base64エンコードされたPEM形式)
        stored_sign_count: ユーザー登録時または前回の認証時に保存した署名カウンタ
        client_data_json_str: クライアントから送られてきた clientDataJSON (JSON文字列)
        authenticator_data_b64: クライアントから送られてきた authenticatorData (Base64 URL Safeエンコード)
        signature_b64: クライアントから送られてきた signature (Base64 URL Safeエンコード)
        expected_challenge_b64: サーバーが発行した、今回の認証で期待されるチャレンジ (Base64 URL Safeエンコード)

    Returns:
        tuple[bool, int]: 検証が成功した場合は (True, 新しい署名カウンタ)、失敗した場合は (False, 元の署名カウンタ)。

    計算量: 公開鍵署名検証が主であり、通常 O(1) に近い (鍵長に依存)。
    メモリ条件: 認証データと公開鍵のサイズに依存するが、通常は数KBオーダーで小さい。
    """
    print("\n--- 安全な代替策: fido2ライブラリを用いた厳格な検証 ---")

    try:

        # 1. Base64デコード


        # WebAuthnの仕様ではURL Safe Base64でエンコードされることが多い

        credential_id = base64.urlsafe_b64decode(credential_id_b64 + '==')
        public_key_pem_bytes = base64.b64decode(public_key_pem_b64)
        authenticator_data_bytes = base64.urlsafe_b64decode(authenticator_data_b64 + '==')
        signature_bytes = base64.urlsafe_b64decode(signature_b64 + '==')
        expected_challenge_bytes = base64.urlsafe_b64decode(expected_challenge_b64 + '==')

        # 2. Fido2ServerとRelyingPartyオブジェクトの初期化

        rp = RelyingParty(RP_ID, RP_ORIGIN)
        server = Fido2Server(rp)

        # 3. PublicKeyCredentialオブジェクトを構築

        pk_credential = PublicKeyCredential(
            credential_id,
            authenticator_data_bytes,
            client_data_json_str.encode('utf-8'),
            signature_bytes,
            None # user_handle は認証時には不要
        )

        # 4. clientDataJSONの解析と検証

        client_data = ClientData(pk_credential.client_data)

        # 4-1. Challengeの検証 (サーバーが発行したものと一致すること)

        if client_data.challenge != expected_challenge_bytes:
            print(f"検証失敗: Challenge不一致. 期待: {expected_challenge_bytes.hex()}, 受信: {client_data.challenge.hex()}")
            return False, stored_sign_count

        # 4-2. Originの検証 (RPのOriginと一致すること)

        if client_data.origin != RP_ORIGIN:
            print(f"検証失敗: Origin不一致. 期待: {RP_ORIGIN}, 受信: {client_data.origin}")
            return False, stored_sign_count

        # 4-3. Typeの検証 (WebAuthn認証であることを確認)

        if client_data.type != "webauthn.get":
            print(f"検証失敗: clientData.type不正. 期待: webauthn.get, 受信: {client_data.type}")
            return False, stored_sign_count

        # 5. AuthenticatorDataの解析

        auth_data = AuthData(pk_credential.auth_data)

        # 5-1. RP ID Hashの検証

        if auth_data.rp_id_hash != rp.id_hash:
            print(f"検証失敗: RP ID Hash不一致. 期待: {rp.id_hash.hex()}, 受信: {auth_data.rp_id_hash.hex()}")
            return False, stored_sign_count

        # 5-2. User Verified (UV) の検証 (必要であれば)


        # NIST SP 800-63B AAL2/3ではUVが推奨/必須。RPが設定でUVをrequiredにしている場合、ここで確認。


        # auth_data.uv は認証器がユーザーを検証したかを示すフラグ。

        if not auth_data.uv: # RPがUVを必須としている場合
             print("検証失敗: ユーザー検証 (UV) が行われていません。")
             return False, stored_sign_count

        # 5-3. 署名カウンタの検証 (リプレイ攻撃対策)


        # authenticator_data の sign_count >= stored_sign_count であること

        if auth_data.sign_count <= stored_sign_count:
            print(f"検証失敗: 署名カウンタが減少または同値です。リプレイ攻撃の可能性。 現在: {auth_data.sign_count}, 以前: {stored_sign_count}")
            return False, stored_sign_count

        # 6. 署名検証 (公開鍵で署名を検証)


        # auth_data.verify_signature は、公開鍵(PEM形式)と署名データを受け取り検証

        if not auth_data.verify_signature(
            client_data_json_str.encode('utf-8'),
            signature_bytes,
            public_key_pem_bytes
        ):
            print("検証失敗: 署名検証に失敗しました。")
            return False, stored_sign_count

        print("検証成功: WebAuthn認証が正常に完了しました。")
        return True, auth_data.sign_count

    except Exception as e:
        print(f"検証中にエラーが発生しました: {e}")
        return False, stored_sign_count

# --- 実際の使用例 (DBから取得した値のシミュレーション) ---


# このコードは検証関数の概念を示すものであり、実際に動作させるには


# WebAuthnの登録処理で得られた正しい公開鍵と認証応答データが必要です。


# 例として、サーバーが生成し、クライアントに渡したチャレンジを想定。


# DUMMY_EXPECTED_CHALLENGE_B64 = base64.urlsafe_b64encode(b"my_unique_challenge_12345").decode('utf-8')


# # 実際にはクライアントからの生のBase64文字列を受け取る。


# # このコードは検証ロジックに焦点を当てており、具体的な認証応答データの生成は省略します。

上記コードは、WebAuthn認証の検証をサーバーサイドで確実に行うための主要なチェックポイントを網羅しています。fido2のような信頼できるライブラリを使用することで、プロトコルレベルの複雑な検証ロジックを安全に実装できます。

6. まとめ

WebAuthn (FIDO2) は、パスワードレス認証の未来を担う強力な認証技術です。Origin検証によるフィッシング耐性や公開鍵暗号による堅牢性は、従来のパスワード認証における多くの問題を解決します。しかし、そのセキュリティはRP側の正しい実装と運用に大きく依存します。RP ID/Originの厳格な検証、Challenge値と署名カウンタの適切な管理、ユーザー検証の強制、そして堅牢なサーバーサイドのセキュリティ対策と運用が不可欠です。これらの対策を講じることで、WebAuthnはその真価を発揮し、ユーザーとサービス双方に安全で便利な認証体験を提供できるでしょう。


参考文献 [1] FIDO Alliance. “FIDO2”. 2023年10月26日 JST更新. (https://fidoalliance.org/fido2/) [2] W3C. “Web Authentication: An API for accessing Strong Authentication Credentials Level 2”. 2021年1月26日 JST公開. (https://www.w3.org/TR/webauthn-2/) [3] NIST. “Special Publication 800-63B, Digital Identity Guidelines, Authentication and Lifecycle Management”. 2020年9月 JST更新. (https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf) [4] Google Developers. “Passkeys: how they work and best practices for developers”. 2023年10月17日 JST更新. (https://developer.chrome.com/docs/web-platform/passkeys/) [5] Yubico Blog. “How Phishing Works With FIDO2”. 2022年4月12日 JST公開. (https://www.yubico.com/blog/how-phishing-works-with-fido2/) [6] Microsoft Learn. “WebAuthn security considerations”. 2023年9月21日 JST更新. (https://learn.microsoft.com/en-us/windows/security/identity-protection/webauthn/webauthn-security)

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

コメント

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