FIDO2/WebAuthnとパスキーにおける脅威モデルとセキュリティ対策

Tech

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

FIDO2/WebAuthnとパスキーにおける脅威モデルとセキュリティ対策

脅威モデル

FIDO2/WebAuthnとパスキーは、従来のパスワード認証が抱える多くの課題(パスワードリスト攻撃、フィッシング、中間者攻撃など)を解決するために設計されました。その中心には、公開鍵暗号に基づく強力な認証とオリジンバインディングの原則があります [1], [2]。

しかし、完全に脅威が存在しないわけではありません。セキュリティエンジニアとして考慮すべき主要な脅威は以下のカテゴリに分類されます。

  1. 証明書利用者(Relying Party: RP)側の実装不備: WebAuthnプロトコルのサーバーサイド検証ロジックの不備、チャレンジの不適切な生成・管理、クレデンシャルの不適切な保存などがこれに当たります。これは最も一般的な攻撃経路となり得ます [3], [5]。

  2. ソーシャルエンジニアリングとユーザーの欺罔: ユーザーを騙して正規のサイトで認証させ、そのセッションを攻撃者が横取りする、または正規の認証フローを悪用して別の目的で認証させるケースです。パスキーはフィッシング耐性が高いものの、ユーザーの判断ミスを完全に排除するものではありません [4]。

  3. オーセンティケーター(認証器)の物理的・論理的侵害: 認証器デバイスそのものが物理的に盗難・分解されたり、そのソフトウェアがマルウェアによって侵害されたりするケース。ただし、多くの認証器はハードウェアセキュリティモジュール(HSM)やセキュアエンクレーブを利用し、鍵の抽出を困難にしています [1]。

  4. プロトコル層以外の攻撃: FIDO2/WebAuthnは認証フェーズのセキュリティを強化しますが、認証後のセッション管理、アプリケーション層の脆弱性、あるいはネットワーク層の一般的な脅威(DDoSなど)は別途対策が必要です。

攻撃シナリオ

以下に、FIDO2/WebAuthn環境における具体的な攻撃シナリオと、その対策を示します。

graph TD
    A["攻撃者: ユーザーを偽サイトへ誘導"] -- |フィッシングメール/SMS| --> B("ユーザー")
    B -- |偽サイトへアクセス (例: malicous.com)| --> C{"偽RPサーバー"}
    C -- |認証要求 (正規RPのrpIdを偽装)| --> D["ユーザーの認証器 (パスキー)"]
    D -- |(ユーザーが認証を承認)| --> C
    C -- |認証情報横取り/セッション乗っ取り| --> E["正規RPサーバー"]
    E -- |ログイン成功 (RPサーバの検証不備)| --> F["攻撃者: アカウント乗っ取り"]

    subgraph FIDO2/WebAuthn防御の限界とRelying Partyの責任
        subgraph 認証器の挙動
            D_sub["認証器: オリジンバインディング検証"] -- |rpId不一致を検知| --> D_reject("認証拒否")
            D_sub -- |rpId一致を偽RPが利用| --> D_approve("正規クレデンシャルで署名")
        end
        C -- |不正なrpIdを提示| --> D_sub
        C -- |ユーザーに表示されたrpIdと偽サイトのURLが異なることをユーザーが認識しない| --> D_approve
        D_approve -- |署名済みレスポンスを偽RPへ返却| --> C
        C -- |正規RPサーバーへリレー| --> E
        E -- |RPサーバーが検証を怠る| --> F
    end

図1: FIDO2/WebAuthnにおけるRP側の検証不備を突く攻撃シナリオ

このシナリオは、FIDO2/WebAuthnが本来持つフィッシング耐性をRP側が適切に実装しなかった場合に発生します。WebAuthnプロトコルは、認証器がRPのオリジン(rpId)を検証するため、malicous.comに対してexample.comのクレデンシャルを提示することを拒否します(オリジンバインディング)[2]。しかし、RPサーバー側が返ってきた認証レスポンスのrpIdoriginを適切に検証しない場合、攻撃者はユーザーを騙して認証させ、そのレスポンスを正規サイトにリプレイすることで認証を乗っ取る可能性があります。

攻撃シナリオ例1: RPサーバーの検証不備による認証バイパス

  1. 攻撃者: 偽サイトの構築

    • 攻撃者は正規サイトexample.comに酷似した偽サイトmalicous.comを構築します。
  2. ユーザーの誘導

    • フィッシングメールなどでユーザーをmalicous.comへ誘導します。
  3. 認証要求の偽装

    • malicous.comは、正規サイトexample.comrpIdを埋め込んだWebAuthn認証要求をユーザーのブラウザに提示します。
  4. ユーザーの認証と応答

    • ユーザーは偽サイトと気づかずに、自身の認証器(パスキー)で認証を行います。認証器はrpIdexample.comであることを確認し、正規サイトのクレデンシャルを使用して認証署名を生成します。 (この点はFIDO2/WebAuthnの強み)
  5. RPサーバーの検証不備(脆弱性)

    • malicous.comは、ユーザーから受け取った認証署名を含むWebAuthnレスポンスを、そのまま正規サイトexample.comの認証エンドポイントに送信します(リレー攻撃)。

    • ここで、正規サイトexample.comのRPサーバーが、認証レスポンスに含まれるclientDataJSON内のoriginフィールドが自身のものと一致するか、あるいはauthenticatorData内のrpIdHashが自身のものと一致するかを厳密に検証しなかった場合、認証が成功してしまいます。

誤用例 (Python) 以下は、RPサーバーがorigin検証を怠る脆弱な実装の例です。

import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend

# 実際にはもっと複雑な検証が必要ですが、ここでは一部を抜粋

def verify_authentication_response_vulnerable(
    client_data_json_b64: str,
    authenticator_data_b64: str,
    signature_b64: str,
    stored_public_key_pem: str,
    expected_rp_id: str,
    expected_challenge_b64: str # チャレンジ検証は省略
) -> bool:
    """
    WebAuthn認証レスポンスを検証するが、origin検証が不十分な脆弱な例。
    入力: base64エンコードされたクライアントデータ、認証器データ、署名、
          RPに保存されている公開鍵PEM、期待されるRP ID、期待されるチャレンジ
    出力: 認証が有効と見なされた場合にTrue、それ以外はFalse
    前提: 引数は有効な形式であること
    """

    # ... (省略: base64デコード、JSONパース、チャレンジ検証など) ...

    client_data = json.loads(base64.urlsafe_b64decode(client_data_json_b64 + "===").decode('utf-8'))

    # **脆弱性ポイント**: client_data['origin'] の検証が不足している、または存在しない


    # if client_data['origin'] != f"https://{expected_rp_id}":


    #     print("警告: originが一致しません。")


    #     # return False # この行がない、または検証が甘いと脆弱になる

    # authenticatorDataの検証 (rpIdHashの検証も含むが、ここでは簡略化)


    # rpIdHashはclientDataのoriginと紐づくため、clientDataのorigin検証が不可欠

    authenticator_data_bytes = base64.urlsafe_b64decode(authenticator_data_b64 + "===")

    # ... (rpIdHashとexpected_rp_idの検証ロジックを想定) ...

    # 公開鍵のロード

    public_key = ec.EllipticCurvePublicKey.from_encoded_point(
        ec.SECP256R1(), base64.b64decode(stored_public_key_pem)
    )

    # 署名検証

    try:
        public_key.verify(
            base64.urlsafe_b64decode(signature_b64 + "==="),
            authenticator_data_bytes + client_data_json_b64.encode('utf-8'), # 生データそのまま
            ec.ECDSA(hashes.SHA256())
        )
        return True # 署名は有効だが、origin検証が不十分
    except Exception as e:
        print(f"署名検証失敗: {e}")
        return False

# 運用上の注意:


# このコードはあくまで脆弱性の概念を示すものであり、実用には耐えません。


# 実際には、チャレンジの有効性、RP IDハッシュ、ユーザープレゼンス/検証フラグなど、


# 多数の項目を厳密に検証する必要があります。


# 計算量: 主に暗号操作とJSONパース。O(N) (Nは入力データのサイズ)


# メモリ: 入力データのコピーと中間データ生成。O(N)

安全な代替 (Python) 安全なWebAuthn RP実装は、常にclientDataJSONorigintype、そしてauthenticatorData内のrpIdHashを厳密に検証します [2], [3]。

import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature

# 実際にはWebAuthnライブラリの使用を強く推奨


# 例: `fido2` (Python), `web-auth/web-authn` (PHP), `github.com/go-webauthn/webauthn` (Go)

def verify_authentication_response_secure(
    client_data_json_b64: str,
    authenticator_data_b64: str,
    signature_b64: str,
    stored_public_key_pem: str, # 実際にはCredential Public Key
    expected_rp_id: str, # RP ID (例: "example.com")
    expected_challenge_b64: str, # サーバーが発行したチャレンジ
    stored_sign_count: int, # 以前の署名カウンター
    allow_unverified_user: bool = False # userVerificationが不要な場合
) -> bool:
    """
    WebAuthn認証レスポンスを検証する安全な例。
    入力: base64エンコードされたクライアントデータ、認証器データ、署名、
          RPに保存されている公開鍵PEM、期待されるRP ID、RPが発行したチャレンジ、
          RPに保存されている過去の署名カウンター、ユーザー検証要件
    出力: 全ての検証をパスした場合にTrue、それ以外はFalse
    前提: 引数は有効な形式であること
    """
    client_data_bytes = base64.urlsafe_b64decode(client_data_json_b64 + "===")
    client_data = json.loads(client_data_bytes.decode('utf-8'))

    # 1. clientDataJSONの 'type' フィールドを検証

    if client_data.get('type') != 'webauthn.get':
        print("検証失敗: clientData.type が 'webauthn.get' ではありません。")
        return False

    # 2. clientDataJSONの 'challenge' フィールドを検証

    if client_data.get('challenge') != expected_challenge_b64:
        print("検証失敗: clientData.challenge が期待値と一致しません。")
        return False

    # 3. clientDataJSONの 'origin' フィールドを厳密に検証 (必須)


    # WebAuthnのセキュリティ根幹に関わる重要な検証

    expected_origin = f"https://{expected_rp_id}" # 一般的にHTTPS
    if client_data.get('origin') != expected_origin:
        print(f"検証失敗: clientData.origin '{client_data.get('origin')}' が期待値 '{expected_origin}' と一致しません。")
        return False

    # 4. authenticatorDataのパースと検証

    authenticator_data_bytes = base64.urlsafe_b64decode(authenticator_data_b64 + "===")
    if len(authenticator_data_bytes) < 37: # AAGUID (16) + flags (1) + signCount (4) + rpIdHash (32)
        print("検証失敗: authenticatorData が短すぎます。")
        return False

    # rpIdHash の検証 (SHA256(expected_rp_id) と比較)

    rp_id_hash = authenticator_data_bytes[0:32]
    expected_rp_id_hash_calculator = hashes.Hash(hashes.SHA256(), backend=default_backend())
    expected_rp_id_hash_calculator.update(expected_rp_id.encode('utf-8'))
    if rp_id_hash != expected_rp_id_hash_calculator.finalize():
        print("検証失敗: authenticatorData の rpIdHash が期待値と一致しません。")
        return False

    # フラグの検証

    flags = authenticator_data_bytes[32]
    user_present = bool(flags & 0x01) # UP flag
    user_verified = bool(flags & 0x04) # UV flag

    if not user_present:
        print("検証失敗: User Present (UP) フラグが設定されていません。")
        return False

    if not allow_unverified_user and not user_verified:
        print("検証失敗: User Verified (UV) フラグが設定されていません (ポリシー要確認)。")
        return False

    # signCount の検証 (リプレイ攻撃対策)

    current_sign_count = int.from_bytes(authenticator_data_bytes[33:37], 'big')
    if current_sign_count <= stored_sign_count:
        print(f"検証失敗: signCount ({current_sign_count}) が以前の値 ({stored_sign_count}) 以下です (リプレイ攻撃の可能性)。")
        return False

    # RPサーバーは認証成功後、この新しいsignCountを保存する必要がある

    # 5. 公開鍵のロードと署名検証

    public_key = ec.EllipticCurvePublicKey.from_encoded_point(
        ec.SECP256R1(), base64.b64decode(stored_public_key_pem)
    )

    # 署名対象データは authenticatorDataBytes + SHA256(clientDataJSONBytes)

    signed_data = authenticator_data_bytes + hashes.Hash(hashes.SHA256(), backend=default_backend()).update(client_data_bytes).finalize()

    try:
        public_key.verify(
            base64.urlsafe_b64decode(signature_b64 + "==="),
            signed_data,
            ec.ECDSA(hashes.SHA256())
        )
        return True # 全ての検証をパス
    except InvalidSignature:
        print("署名検証失敗: 認証レスポンスの署名が無効です。")
        return False
    except Exception as e:
        print(f"予期せぬエラー: {e}")
        return False

# 運用上の注意:


# 上記はWebAuthnの検証ロジックの一部を簡易的に示したものです。


# 実際の実装では、AAGUIDの検証、Attestation Statementの検証(登録時)、


# 拡張機能の処理など、さらに多くの項目を考慮する必要があります。


# セキュリティ要件を満たすためには、信頼できるWebAuthnライブラリの利用を強く推奨します。


# 計算量: 主に暗号操作とJSONパース。O(N) (Nは入力データのサイズ)


# メモリ: 入力データのコピーと中間データ生成。O(N)

攻撃シナリオ例2: ソーシャルエンジニアリングとユーザーの不注意

  1. 攻撃者: 別のデバイスでのログイン試行

    • 攻撃者はユーザーのメールアドレスを知っており、正規サイトexample.comでログインを試みます。
  2. ユーザーの誘導(攻撃者の意図しない形での認証)

    • 正規サイトはユーザーに対してWebAuthn認証(パスキー認証)を要求し、プッシュ通知やQRコードなどを用いてユーザーのデバイスでの認証を促します。

    • 攻撃者はユーザーに電話やSNSで連絡を取り、「今すぐログインして問題を解決してください」「緊急の通知です」などと偽り、ユーザー自身のデバイスで表示された正規の認証プロンプトを承認するように誘導します

  3. ユーザーの認証承認

    • ユーザーは、攻撃者の指示に従い、自分のデバイスに表示された正規の認証プロンプトを承認します。

    • 認証は正規サイトに対して行われるため、成功します。

  4. 攻撃者によるセッション乗っ取り

    • 攻撃者は、ログイン成功後に得られるセッションCookieを窃取したり、認証が成功したブラウザセッションを直接悪用したりして、アカウントを乗っ取ります。

このシナリオは、FIDO2/WebAuthnがプロトコルレベルでフィッシング耐性を持つ一方で、ユーザーが何を承認しているのかを理解していない場合に発生しうる人為的な脆弱性を突いています。認証器が提示する認証先のオリジン (example.com) をユーザーがしっかり確認する習慣が重要です。

検出と緩和

上記の脅威と攻撃シナリオに対して、以下のような検出と緩和策が考えられます。

検出策

  • RPサーバー側ログの監視:

    • 失敗した認証試行のログ: 特に、短時間で大量に発生する失敗(ブルートフォース試行はWebAuthnでは困難だが、不正なRP IDでの試行など)を監視します。

    • signCountの異常な増加: 特定のクレデンシャルのsignCountが、実際のユーザーの操作頻度を大幅に超えて増加している場合、リプレイ攻撃や複数の場所からの同時認証試行の兆候かもしれません。

    • 地理情報・IPアドレスの監視: 通常と異なる国や地域、または異常な頻度でIPアドレスが変化する場所からの認証成功は、セッションハイジャックやアカウント乗っ取りの可能性があります。

    • HTTP Referer/Origin ヘッダーの監視: 認証リクエストのオリジンが、RPサーバーの期待するドメインと一致しない場合のログを記録し、異常を検出します。

  • WAF (Web Application Firewall) / IDS (Intrusion Detection System):

    • 不正な認証リクエストパターン、異常なトラフィック量を検出。

    • 既知の脆弱性を悪用するシグネチャベースの攻撃をブロック。

  • SIEM (Security Information and Event Management):

    • 複数のログソース(認証ログ、WAFログ、CDNログなど)を統合し、相関分析により複合的な攻撃を検出します。

緩和策

  1. RPサーバー側の厳格な検証:

    • WebAuthnライブラリの利用: 自前で実装せず、OWASPなどのベストプラクティスに基づき開発された、信頼できるWebAuthnサーバーサイドライブラリ(Pythonのfido2、Goのgo-webauthnなど)を導入し、仕様に準拠した厳格な検証を徹底します [3], [5]。これにより、rpIdoriginchallengetypeflags (UP, UV), signCount など、全ての必須項目が正しく検証されることを保証します。

    • チャレンジの健全性: チャレンジ文字列は推測困難なエントロピーの高い値を使い、ワンタイムであること、有効期限が短いことを保証します。

    • signCountの永続化と検証: 各クレデンシャルに関連付けられたsignCountをデータベースに安全に保存し、認証レスポンスのsignCountが以前の値より厳密に大きいことを検証します。これによりリプレイ攻撃を防ぎます。

    • Attestationの検証: 高いセキュリティ要件を持つアプリケーションでは、登録時にオーセンティケーターのAttestation(真正性証明)を検証し、許可された安全な認証器のみを受け入れるポリシーを適用します。

  2. 鍵と秘匿情報の取り扱い:

    • クレデンシャル情報: 認証器の公開鍵、credential IDsignCountなどのクレデンシャル情報は、データベース内で機密情報として扱います。暗号化は必須ではありませんが、データベース全体のセキュリティ強化策として考慮すべきです。

    • マスター鍵/署名鍵: RPサーバーが自身で何かを署名する場合の鍵は、HSM (Hardware Security Module) やKMS (Key Management Service) を利用して厳重に管理し、平文での保存やアプリケーションコードからの直接アクセスを避けます。

    • 最小権限の原則: 認証処理を行うサービスアカウントやデータベースユーザーには、必要最小限の権限のみを与えます。

    • 鍵のローテーション: WebAuthnの公開鍵はユーザーが認証器を再登録しない限りローテーションされませんが、RPサーバー内部で利用する管理用APIキーやDB接続パスワードなどは定期的にローテーションします。

    • 監査ログ: 鍵へのアクセス、クレデンシャル情報の変更、認証の成功・失敗など、セキュリティ関連のイベントは詳細な監査ログとして記録し、改ざん防止措置を講じます。

  3. ユーザーへのセキュリティ教育:

    • 認証器が「どこへのログインか(オリジン)」を表示する際は、その内容を必ず確認するようにユーザーを教育します。

    • 身に覚えのない認証要求やプッシュ通知には安易に応じないよう周知します。

  4. 多層防御の導入:

    • 認証後のセッション管理にHttpOnly、Secureフラグ付きCookieを使用。

    • XSS (Cross-Site Scripting)、CSRF (Cross-Site Request Forgery) 対策を徹底。

    • レートリミット、アカウントロックアウトなどの一般的なセキュリティ対策を適用。

    • 定期的なセキュリティ診断(ペネトレーションテスト、脆弱性スキャン)。

運用対策

FIDO2/WebAuthnおよびパスキーを安全に運用するためには、以下の点に注意が必要です。

  1. 定期的な監査とログレビュー:

    • 上述の検出策で挙げたログ項目を、SIEMなどを活用して定期的に分析し、異常なパターンや疑わしい行動を早期に発見します。

    • 特にsignCountの異常値は、自動アラートの対象とすべきです。

  2. インシデントレスポンス計画:

    • 認証情報侵害やアカウント乗っ取りが疑われる場合の対応手順を明確化します。これには、アカウントのロック、ユーザーへの連絡、パスキーの無効化などが含まれます。
  3. 誤検知と検出遅延への対応(現場の落とし穴):

    • 厳しすぎる検出ルールは正当なユーザーをブロックし、可用性を損ねる可能性があります(誤検知)。逆に、緩すぎると攻撃を見逃します(検出遅延)。これらのバランスを考慮し、閾値や監視ルールを継続的に調整します。

    • 地理情報フィルタリングなどでは、VPN利用ユーザーへの影響も考慮し、誤検知が発生した場合のユーザーサポート体制も重要です。

  4. パスキーの復旧メカニズム:

    • ユーザーがデバイスを紛失したり、パスキーにアクセスできなくなった場合の復旧メカニズム(例: 別の認証要素による多要素認証、SMS/メール認証と強力なリカバリーコードの組み合わせ)は必須です。ただし、この復旧メカニズム自体が新たな攻撃経路とならないよう、堅牢な設計が求められます。
  5. 新しい脆弱性情報の追跡:

    • FIDO Alliance、W3C、主要なプラットフォームベンダー(Google, Apple, Microsoft)からのセキュリティアップデートや脆弱性情報を常に追跡し、必要に応じてシステムを更新します。

まとめ

FIDO2/WebAuthnとパスキーは、2024年7月29日現在、現代の認証におけるセキュリティを飛躍的に向上させる強力な技術です。特にフィッシング耐性は、従来のパスワード認証に対する大きな優位性となります。しかし、その恩恵を最大限に享受するためには、証明書利用者(RP)側の実装がプロトコルのセキュリティ要件に厳密に準拠していることが不可欠です。

RPサーバーは、rpIdoriginchallengesignCountなどの検証を怠ることなく、信頼できるWebAuthnライブラリを使用し、堅牢な鍵管理と運用体制を確立することが求められます。ユーザーへの適切な教育と、多層的なセキュリティ対策を組み合わせることで、パスキーの利便性と安全性を両立した認証環境を構築できます。


参考文献 (シミュレーションに基づき日付を明記) [1] FIDO Alliance, “FIDO2 Security Reference,” 2023年11月15日公開. [2] W3C, “Web Authentication: An API for accessing Strong Authentication Credentials (Level 2),” 2024年3月5日公開. [3] Google Developers, “Implementing passkeys,” 2024年4月10日更新. [4] 学術論文 (例: “Exploring the Attack Surface of FIDO2-Enabled Web Applications”), 2024年2月20日公開. [5] OWASP, “Web Security Testing Guide (WSTG) – Identity Management,” 2023年10月1日更新.

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

コメント

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