パスキー/FIDO2認証の攻撃耐性と安全な実装

Tech

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

パスキー/FIDO2認証の攻撃耐性と安全な実装

パスキーおよびそれを支えるFIDO2(WebAuthn)認証は、従来のパスワード認証が抱える多くの課題を解決するために設計された、次世代の認証技術です。フィッシング耐性やリプレイ耐性など、その堅牢性が注目される一方で、実装や運用を誤ると脆弱性につながる可能性もあります。本稿では、実務家のセキュリティエンジニアの視点から、FIDO2/パスキーの脅威モデル、主要な攻撃シナリオ、そして安全な実装と運用対策について具体的に解説します。

脅威モデル

パスキー/FIDO2の主要な目的は、パスワード認証で頻繁に発生する以下の脅威を緩和することです。

  • フィッシング攻撃: 偽サイトに誘導し、認証情報を詐取する攻撃。

  • リプレイ攻撃: 盗聴した認証応答を再利用して不正アクセスを試みる攻撃。

  • クレデンシャルスタッフィング/ブルートフォース攻撃: 流出した認証情報や推測に基づき、多数のアカウントにログインを試みる攻撃。

  • 認証情報漏洩: サーバー側での認証情報(パスワードハッシュなど)の漏洩。

FIDO2/パスキーは、Relying Party ID(RP ID)の検証やチャレンジ-レスポンスメカニズムにより、上記脅威に対して高い耐性を有します。しかし、以下の脅威に対しては、認証器(Authenticator)またはRelying Party(RP)側の実装、あるいは運用で対処する必要があります。

  • 認証器(デバイス)の物理的奪取とバイパス: 生体認証の偽装やPINの推測による認証器の不正利用。

  • マルウェアによる認証フローの妨害: ユーザーデバイス上のマルウェアが認証プロンプトを改ざんしたり、セッションをハイジャックしたりする。

  • Relying Party (RP) サーバー側の脆弱性: FIDO2プロトコルの実装不備(例: 署名検証の不備、チャレンジの再利用)や、RPサーバー自体の他の脆弱性。

  • ソーシャルエンジニアリング: ユーザーを騙して正規の認証を承認させる。

攻撃シナリオ

FIDO2/パスキーはパスワードに比べて堅牢ですが、誤った実装や新たな攻撃手法によって脅威にさらされる可能性があります。

攻撃チェーン(kill chain/ATT&CK風)

以下のMermaid図は、パスキー/FIDO2環境における潜在的な攻撃シナリオを示しています。

graph TD
    A["攻撃者"] -->|標的選定| B("ユーザー/Relying Party")
    B --> C{"初期侵入ベクトル"}

    C --> |フィッシング誘導 (パスキー耐性あり)| D1("偽RPサイト")
    D1 --> E1("偽サイトへの認証要求")
    E1 --x F1("FIDO認証器がRP ID不一致を検知")
    F1 --> G1["認証失敗 - パスキーは耐性あり"]

    C --> |デバイスの物理的奪取| D2("ユーザーデバイス")
    D2 --> |生体認証/PINバイパス試行| E2("認証器ロック解除")
    E2 --x F2("生体認証/PIN失敗")
    E2 --> F3("生体認証/PIN成功")
    F3 --> G2["デバイス上のパスキーを悪用して正規サイトへ認証"]

    C --> |マルウェア感染 (デバイス)| D3("ユーザーデバイス")
    D3 --> |認証セッションハイジャック/承認操作の欺罔| E3("ブラウザ/OSレベルでの操作")
    E3 --> G3["正規のFIDO認証フローを悪用"]

    C --> |RPサーバーの脆弱性| D4("Relying Partyサーバー")
    D4 --> |FIDOプロトコル実装不備| E4("チャレンジ再利用/署名検証不備")
    E4 --> G4["不正な認証成功"]

    C --> |ソーシャルエンジニアリング| D5("ユーザー")
    D5 --> |正規の認証要求を誤って承認させる| E5("ユーザーによる認証承認")
    E5 --> G5["正規の認証フローを悪用"]

    G1 --> H["パスキーのフィッシング耐性"]
    G2 --> H
    G3 --> H
    G4 --> H
    G5 --> H

    style G1 fill:#e6e6e6,stroke:#333,stroke-width:2px
    style H fill:#d9f7be,stroke:#2a9d8f,stroke-width:2px

具体的な攻撃シナリオ

  1. 物理デバイスの奪取と認証器バイパス

    • シナリオ: 攻撃者がユーザーのスマートフォンやPCを物理的に奪取し、生体認証(指紋、顔認証)やデバイスのPINロックを突破して、デバイス内に保存されているパスキーを悪用し正規サービスにログインします。

    • パスキーの防御: パスキー自体は強力ですが、デバイスのロック解除が突破されれば攻撃のリスクは高まります。FIDO認証器は通常、ユーザープレゼンスの確認(指紋認証、顔認証、PIN入力)を求めますが、これらが突破されると意味がありません。

    • 対策: 強固なデバイスロック設定、リモートワイプ機能、短時間のアイドルロック。

  2. マルウェアによる認証セッションハイジャック

    • シナリオ: ユーザーデバイスにインストールされた高度なマルウェアが、ブラウザのプロセスをフックし、FIDO認証要求が生成された際に、そのセッションを傍受して不正な操作(例: 異なるRPへの認証、認証応答の改ざん)を行います。

    • パスキーの防御: FIDOプロトコルは、RP IDの厳格な検証やチャレンジの使い捨てによって中間者攻撃やリプレイ攻撃に耐性がありますが、デバイスが完全に侵害された場合、ユーザーの意図しない認証を騙し取られる可能性は残ります。

    • 対策: デバイスのセキュリティパッチ適用、アンチマルウェア導入、認証時の情報(認証先URLなど)をユーザーに明示し確認を促すUI。

  3. Relying Party (RP) サーバー側の実装不備

    • シナリオ: RPサーバーがFIDO2のプロトコル仕様を完全に理解せず、不適切な実装を行った場合、脆弱性が生まれます。例えば、認証フローで提供されるチャレンジの再利用、RP IDの不厳格な検証、Authenticator Attestationの無視などです。

    • パスキーの防御: プロトコル自体は安全ですが、RP側の実装が脆弱であればその恩恵を受けられません。

    • 対策: FIDOアライアンスのガイドライン[1]やW3C WebAuthn仕様[2]に基づいた厳格な実装、セキュリティレビュー、ペネトレーションテスト。

検出/緩和

プロトコルレベルの耐性

  • フィッシング耐性: FIDO2(WebAuthn)は、認証器がセッションのOrigin(ドメイン)とRelying Party ID(RP ID)を厳密に照合するため、偽サイトでは認証が成功しません[2]。攻撃者が偽サイト(例: evil.com)で認証を試みても、認証器は正規のRP ID(例: example.com)との不一致を検知し、署名生成を拒否します。

  • リプレイ耐性: RPは認証要求ごとに一意で十分なエントロピーを持つチャレンジを生成し、認証応答に含まれるチャレンジがRPが発行したものと一致するかを検証します[2]。これにより、過去の認証応答を再利用するリプレイ攻撃を防ぎます。

安全な実装

RP側でFIDO2/パスキーを安全に実装するための重要なポイントを挙げます。

  1. RP IDの厳格な検証: 認証応答に含まれるclientDataJSON.originrpIdが、自サイトのドメインと完全に一致することを確認します。

  2. チャレンジのランダム性、使い捨て性、十分なエントロピー:

    • 各認証要求に対し、暗号学的に安全な乱数ジェネレーター(CSPRNG)で生成された一意のチャレンジを使用します。

    • チャレンジは認証ごとに一度だけ有効とし、使用後は即座に無効化します。

    • チャレンジの長さは少なくとも16バイト(128ビット)とし、十分なエントロピーを確保します。

  3. 署名検証の徹底: 認証応答に含まれるAuthenticator署名を、登録時に保存した公開鍵と、提供されたChallenge、RP ID、Credential IDなどを用いて検証します。署名が有効でない場合は認証を拒否します。

  4. Authenticator Attestationの検証(オプション): 新規登録時、Authenticator Attestationを検証することで、認証器が正規のハードウェアであるか、特定のセキュリティ特性を持つかを確認できます。これは追加のセキュリティ層を提供しますが、匿名性とのトレードオフや運用負荷も考慮が必要です[3]。

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

FIDO2認証の実装においては、特にチャレンジ管理とRP ID検証の不備が脆弱性につながりやすい点です。

誤用例:チャレンジの再利用

import hashlib
import os

# --- 誤用例 ---


# 毎回同じチャレンジを使用したり、セッション全体で使い回すのはNG


# challenge = b"fixed_static_challenge_for_all_users" # 固定チャレンジ


# challenge = session.get("last_challenge") # 過去のチャレンジを再利用

# 安全でないチャレンジ検証

def unsafe_verify_auth(response_challenge_hash, expected_challenge_hash):

    # 実際にはハッシュではなく元のチャレンジを検証すべき


    # ここでは、固定値との比較でチャレンジの再利用を表現

    if response_challenge_hash == hashlib.sha256(b"fixed_static_challenge").hexdigest():
        print("WARN: チャレンジが固定値であるか、再利用されている可能性があります。")
        return True # 不正に認証成功

# --- 安全な代替 ---


# 認証要求ごとに暗号学的に安全な乱数で一意なチャレンジを生成し、RPサーバー側でセッションに紐付けて保存

def generate_safe_challenge():
    """
    暗号学的に安全な乱数で一意なチャレンジを生成

    - 入力: なし

    - 出力: bytes (32バイト)

    - 前提: os.urandomが利用可能であること

    - 計算量: O(1)

    - メモリ: O(1)
    """
    return os.urandom(32) # 32バイト = 256ビットのエントロピー

# チャレンジの検証(RPサーバー側)

def safe_verify_challenge(received_challenge, session_stored_challenge):
    """
    受信したチャレンジがセッションに保存されたものと一致するか検証し、使用後は無効化

    - 入力: received_challenge (bytes), session_stored_challenge (bytes)

    - 出力: bool

    - 前提: チャレンジがbytes形式であること

    - 計算量: O(N) (バイト列比較)

    - メモリ: O(1)
    """
    if received_challenge == session_stored_challenge:

        # 認証成功後、チャレンジをセッションから削除または無効化


        # 例: session.pop("fido_challenge")

        print("INFO: チャレンジは有効で、一度だけ使用されました。")
        return True
    else:
        print("ERROR: チャレンジが一致しないか、再利用されました。")
        return False

# 実行例

session_challenge = generate_safe_challenge()
print(f"生成されたチャレンジ (RP): {session_challenge.hex()}")

# ユーザーから送られてきたチャレンジ (想定)

user_received_challenge = session_challenge # 正しいケース

# user_received_challenge = os.urandom(32) # 不正なケース (異なるチャレンジ)


# user_received_challenge = generate_safe_challenge() # 不正なケース (新しいチャレンジ)

safe_verify_challenge(user_received_challenge, session_challenge)

# チャレンジの再利用を試みる (不正)

safe_verify_challenge(user_received_challenge, session_challenge) # 実際にはここでセッションから削除されているべき

誤用例:RP IDの不厳格な検証

# --- 誤用例 ---


# RP IDのチェックが緩い、あるいは全く行わない

def unsafe_rp_id_check(client_data_origin, rp_id):

    # 'example.com' が含まれていればOKとする (サブドメイン spoofingの可能性)

    if "example.com" in client_data_origin:
        print("WARN: RP IDチェックが不厳格です。")
        return True
    return False

# --- 安全な代替 ---


# クライアントデータに含まれるoriginとrpIdが、RPサーバーの厳密なドメインと一致することを検証

def safe_rp_id_check(client_data_origin, received_rp_id, expected_rp_id):
    """
    受信したoriginとRP IDが期待されるものと厳密に一致するか検証

    - 入力: client_data_origin (str), received_rp_id (str), expected_rp_id (str)

    - 出力: bool

    - 前提: ドメイン名がFQDN形式であること

    - 計算量: O(N) (文字列比較)

    - メモリ: O(1)
    """
    if client_data_origin == f"https://{expected_rp_id}" and received_rp_id == expected_rp_id:
        print(f"INFO: RP ID '{received_rp_id}' は有効です。")
        return True
    else:
        print(f"ERROR: RP ID不一致 (Expected: {expected_rp_id}, Received Origin: {client_data_origin}, Received RP ID: {received_rp_id})")
        return False

# 実行例

expected_domain = "example.com"
safe_rp_id_check("https://example.com", "example.com", expected_domain) # 正常
safe_rp_id_check("https://evil.com", "evil.com", expected_domain)     # 異常 (フィッシングサイトからの認証)
safe_rp_id_check("https://sub.example.com", "example.com", expected_domain) # 正常 (WebAuthn仕様のrpIdルールに従う)

運用対策

鍵/秘匿情報の取り扱い

FIDO2/パスキーでは、秘密鍵はユーザーの認証器(デバイス)内にセキュアに保管され、RPサーバーには公開鍵のみが登録されます。これにより、サーバーからパスワードハッシュが漏洩するリスクを排除します。

  • RPサーバー側:

    • 公開鍵: ユーザーごとにCredential IDと公開鍵、および署名カウンターをDBに安全に保管します。公開鍵は秘匿情報ではありませんが、改ざん防止のため整合性を確保します。

    • Credential ID: これはユーザーを一意に識別するためのIDであり、攻撃者による列挙攻撃を防ぐために、推測困難なランダムな値を使用します。

    • 署名カウンター (Signature Counter): リプレイ攻撃検知のために、Authenticatorからの認証応答に含まれるカウンター値がRPサーバーに記録された値よりも大きいことを常に検証します。

  • バックアップとリカバリ: パスキーはデバイス依存のため、デバイス紛失・破損時のリカバリ戦略が不可欠です。クラウド同期(例: iCloud Keychain, Google Password Manager)は利便性と可用性を提供しますが、クラウドアカウントの堅牢なセキュリティ(強力なパスワードと多要素認証)が前提となります。

ローテーション

パスキー自体のローテーションは必須ではありませんが、セキュリティ上の理由やデバイス紛失時に必要となる場合があります。

  • ユーザーによる再登録: ユーザーが古いパスキーを削除し、新しいデバイスでパスキーを再登録するプロセスを提供します。

  • RP側からの強制ローテーション: 稀なケースですが、セキュリティ侵害の兆候がある場合など、RPが特定のCredential IDを無効化し、ユーザーに再登録を促すメカニズムも考慮できます。

最小権限

FIDO2認証システムを構成するコンポーネントに対し、厳格な最小権限の原則を適用します。

  • RPサーバー: 認証処理に必要な公開鍵の読み取り、署名カウンターの更新、チャレンジの生成・検証に必要な権限のみを与えます。

  • Authenticator Attestation検証サービス: Attestation Root証明書へのアクセスや、FIDO Metadata Service (MDS)へのアクセス権限を必要最低限に絞ります。

監査

認証システムの健全性を維持するためには、詳細な監査ログの収集と分析が不可欠です。

  • 認証ログ:

    • FIDO2認証の成功/失敗。

    • Credential ID、ユーザーID、RP ID。

    • 使用されたAuthenticator(Attestation情報から推測可能)。

    • チャレンジ、署名カウンター。

    • 日時、SourceIP。

  • 異常検知:

    • 不審な地理的位置からのログイン試行。

    • 署名カウンターの不一致(リプレイ攻撃の兆候)。

    • 短時間に多数の認証失敗。

    • 未知のAuthenticatorからの登録試行。

現場の落とし穴

  • 誤検知/検出遅延: 不正な認証試行の検知は重要ですが、あまりに厳しすぎるルールは正規ユーザーの利便性を損ないます。検出遅延は攻撃の機会を与えるため、リアルタイムに近い監視が必要です。デバイス紛失時のパスキー失効処理が遅れると、その間は不正利用のリスクが残ります。

  • 可用性とのトレードオフ: セキュリティを優先しすぎると、ユーザーが認証できなくなる事態(例: パスキーを保存したデバイスが使えない、リカバリ手段がない)を招き、システムの可用性を低下させます。複数デバイスでのパスキー登録や、セカンドファクタとしての利用など、柔軟な設計が求められます。

  • クロスプラットフォーム互換性: パスキーはプラットフォーム間の互換性を目指しますが、一部のOSバージョンやブラウザで挙動が異なる場合があります。RPは主要な環境でのテストを徹底し、ユーザーに利用可能な環境を明確に伝える必要があります。

まとめ

パスキー/FIDO2認証は、従来のパスワード認証における多くの根本的なセキュリティ問題を解決する強力な技術です。特にフィッシング攻撃に対する高い耐性は、現代の脅威環境において大きな利点となります。しかし、その恩恵を最大限に享受するためには、Relying Party側でのプロトコル仕様に基づいた厳格な実装と、デバイスの物理的セキュリティ、マルウェア対策、そして堅牢な運用対策が不可欠です。

チャレンジの適切な管理、RP IDの厳格な検証、署名検証の徹底といった技術的実装に加え、デバイス紛失時のリカバリ戦略や、包括的な監査と異常検知メカニズムの構築は、パスキーを安全に運用する上で見過ごすことのできない要素です。セキュリティと利便性のバランスを取りながら、継続的な監視と改善を通じて、パスキー/FIDO2の潜在能力を最大限に引き出すことが、実務家のセキュリティエンジニアに求められます。


参考文献:

  1. FIDO Alliance. “FIDO2: WebAuthn.” 2024年5月15日. https://fidoalliance.org/fido2/

  2. W3C. “Web Authentication: An API for accessing Strong Authentication Credentials Level 3.” 2023年11月28日. https://www.w3.org/TR/webauthn-3/

  3. Google Developers. “Passkeys developer guide.” 2024年5月15日. https://developers.google.com/identity/passkeys?hl=ja

  4. Apple Developer. “Passkeys.” 2024年3月7日. https://developer.apple.com/jp/passkeys/

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

コメント

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