本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
FIDO2によるパスワードレス認証実装におけるセキュリティリスクと対策
FIDO2は、公開鍵暗号に基づく強力なパスワードレス認証標準であり、従来のパスワード認証が抱えるフィッシング、クレデンシャルスタッフィング、総当たり攻撃などの脅威に対する堅牢な防御を提供します。WebAuthn(Web Authentication API)とCTAP2(Client to Authenticator Protocol 2)から構成され、W3CによってWebAuthn Level 3 Candidate Recommendationが2024年5月28日(JST)に公開されるなど、標準化が進んでいます[1]。しかし、その実装には特有のセキュリティリスクが存在し、適切な対策を講じなければFIDO2の真価を発揮できません。本記事では、セキュリティエンジニアの視点から、FIDO2実装における脅威モデル、攻撃シナリオ、検出・緩和策、および運用対策を具体的に解説します。
脅威モデル
FIDO2の導入により、フィッシング攻撃への耐性が大幅に向上しますが、万能ではありません。以下に主な脅威モデルを挙げます。
フィッシング攻撃への耐性: FIDO2はAuthenticator(認証器)が認証時にRelying Party(RP、サービス提供者)のオリジン(ドメイン)を確認するため、正規のオリジンと異なる偽サイトでは認証情報が生成・送信されません。これにより、従来のパスワード認証に比べてフィッシング攻撃のリスクを格段に低減します[3]。
リプレイ攻撃: 認証済みのアサーション(認証応答)が傍受され、RPサーバーへ再送信されることで不正アクセスを試みる攻撃です。FIDO2は
signCountというカウンタ値を用いてこれを防ぐ仕組みがありますが、RPサーバー側でこの検証を怠ると脆弱性となります。認証器の紛失・盗難: ユーザーのセキュリティキーや生体認証デバイスが物理的に失われ、PINや生体認証情報が突破された場合、不正に利用されるリスクがあります。
RPサーバーの侵害: FIDO2の秘密鍵はAuthenticator内に安全に保存されますが、RPサーバーが管理する公開鍵、Credential ID、User Handleなどの情報が漏洩した場合、認証システム全体の信頼性が損なわれる可能性があります。また、RPサーバー側のWebAuthnプロトコル実装に不備があれば、不正なAuthenticatorの登録や認証を許してしまう恐れがあります。
攻撃シナリオ
上述の脅威モデルに基づき、具体的な攻撃シナリオを可視化します。
graph TD
A["攻撃者"] -->|フィッシングサイトへ誘導| B["ユーザー"];
B -->|偽サイトへアクセス| C{"偽RPサーバーからWebAuthn認証要求"};
C -->|Authenticatorへ認証器要求| D["FIDO2 Authenticator"];
D -->|オリジン不一致を検知し認証を拒否| E["認証拒否"];
E -->|FIDO2のフィッシング耐性| F["認証情報窃取を防止"];
subgraph FIDO2導入後も残る脅威
G["RPサーバー侵害"] -->|盗まれたCredential IDとUser Handleの悪用| H["不正ログイン試行"];
H -->|SignCountの検証不備を悪用| I["リプレイ攻撃(検出困難)"];
G -->|Attestation/Assertion検証の不備| J["不正なAuthenticator登録を許容"];
D -->|Authenticatorの物理的盗難| K["PIN/生体認証の突破試行"];
K -->|サイドチャネル攻撃やブルートフォース| L["認証突破"];
B -->|マルウェア感染によるクライアント侵害| M["認証済みセッションのハイジャック"];
M -->|セッションクッキーの窃取| N["アカウント乗っ取り"];
end
F --> P["FIDO2導入の利点"];
P --> G;
P --> D;
P --> B;
P_Mitigation["必要な緩和策"] --> GM["RPサーバーの堅牢化と厳格な検証"];
P_Mitigation --> DM["AuthenticatorのPIN/生体認証の利用強制"];
P_Mitigation --> KM["紛失/盗難時の迅速なAuthenticator失効"];
P_Mitigation --> MM["エンドポイントセキュリティ強化とユーザー教育"];
フィッシングサイトへの誘導とFIDO2認証情報の窃取(困難性): 攻撃者は偽のログインページを作成し、ユーザーを誘導します。しかし、ユーザーが偽サイトでFIDO2認証を試みると、AuthenticatorはRPのオリジンが正規のものではないことを検知し、認証を拒否します。これにより、秘密鍵がAuthenticatorから漏洩することはありません。これがFIDO2の主要な強みです。
RPサーバーの脆弱性を悪用したCredential IDの不正利用: RPサーバーがSQLインジェクションやXSSなどの脆弱性を持つ場合、攻撃者はRPサーバーに保存されているCredential IDとUser Handleを窃取する可能性があります。これらの情報自体では秘密鍵は得られませんが、RPサーバー側の検証に不備がある場合、攻撃者は不正な認証リクエストを構築し、リプレイ攻撃や不正な認証器の登録を試みる可能性があります。
マルウェアによるセッションハイジャック: ユーザーの端末がマルウェアに感染した場合、認証が成功した後のセッション情報(セッションクッキーなど)が窃取される可能性があります。FIDO2は認証フェーズを保護しますが、その後のセッション管理は別途堅牢に行う必要があります。
物理的な認証器の盗難とPIN/生体認証の突破: ユーザーの物理的なセキュリティキーが盗まれたり、生体認証デバイスが不正に利用されたりするリスクです。多くのAuthenticatorはPINや生体認証によるユーザー検証(User Verification)機能を持ちますが、これらの保護が弱い場合、サイドチャネル攻撃やブルートフォース攻撃によって突破される可能性があります。
検出/緩和策
FIDO2のセキュリティを最大化するためには、RPサーバー側での厳格な検証と、認証器を含めたライフサイクル管理が不可欠です。
RPサーバーの実装におけるベストプラクティス
RPサーバーは、クライアントから送信されるWebAuthnのAttestation(登録時)およびAssertion(認証時)オブジェクトを厳格に検証する必要があります。
署名検証: 認証(Assertion)要求時には、Authenticatorが秘密鍵で署名した結果が送信されます。RPサーバーは、登録時に保存した公開鍵を用いて、この署名が正当なものであるかを検証しなければなりません。
signCount検証:signCountはAuthenticatorが認証時に署名を行うたびに増加させるカウンタ値です。RPサーバーは、認証器ごとにこのカウンタ値を保存し、認証のたびに送られてくるsignCountが保存されている値よりも厳密に増加していることを確認する必要があります。これがリプレイ攻撃に対する主要な防御メカニズムです。# WebAuthn Assertion (認証) 検証の擬似コード import json from base64 import urlsafe_b64decode def verify_assertion(client_data_json_b64, authenticator_data_b64, signature_b64, credential_public_key, stored_sign_count, rp_origin): """ FIDO2 WebAuthnの認証(Assertion)を検証する関数(簡略化)。 RPサーバーで実行されるべき重要な検証ロジック。 計算量: O(1) (主に署名検証による) メモリ条件: 認証データと公開鍵のサイズに依存 """ # 1. clientDataJSONのパースと検証 client_data = json.loads(urlsafe_b64decode(client_data_json_b64).decode('utf-8')) if client_data.get('type') != 'webauthn.get': raise ValueError("Invalid clientData type.") if client_data.get('origin') != rp_origin: # オリジン検証は必須 raise ValueError("Origin mismatch. Potential phishing attempt.") # 2. authenticatorDataのパースと検証 authenticator_data = urlsafe_b64decode(authenticator_data_b64) flags = authenticator_data[25] # A: Attested Credential Data Included, UP: User Present, UV: User Verified if not (flags & 0x01): # User Present (UP) フラグの確認 raise ValueError("User not present.") # 必要に応じてUser Verified (UV) フラグも確認(例: if (flags & 0x04) == 0: raise ValueError("User not verified.")) current_sign_count = int.from_bytes(authenticator_data[26:30], 'big') # 3. リプレイ攻撃対策: signCountの検証 (非常に重要) # 誤用例: この検証を怠る、または 'current_sign_count <= stored_sign_count' を許可する # -> 以前の有効な署名済みアサーションが再利用され、不正ログインを許す可能性がある。 # 安全な代替: 常に厳密に増加していることを確認 if current_sign_count <= stored_sign_count: raise ValueError("Sign count has not strictly increased. Potential replay attack.") if current_sign_count > 0xFFFFFFFF: # signCountの最大値を超えた場合の対処 raise ValueError("Sign count overflow.") # 4. 署名の検証 (擬似コードのため実際の実装はより複雑) # ハッシュ化されたclientDataJSONとauthenticatorDataを結合し、公開鍵で署名を検証する # 例えば、cryptographyライブラリなどを使用: # from cryptography.hazmat.primitives import hashes # from cryptography.hazmat.primitives.asymmetric import ec # from cryptography.hazmat.primitives.asymmetric import utils # from cryptography.hazmat.backends import default_backend # # message_hash = hashes.Hash(hashes.SHA256(), backend=default_backend()) # message_hash.update(authenticator_data) # message_hash.update(urlsafe_b64decode(client_data_json_b64)) # final_hash = message_hash.finalize() # # public_key_object = load_public_key_from_pem(credential_public_key) # PEM形式から公開鍵オブジェクトに変換 # if not public_key_object.verify(urlsafe_b64decode(signature_b64), final_hash, ec.ECDSA(utils.Prehashed(hashes.SHA256()))): # raise ValueError("Invalid signature.") # 検証成功 print(f"Assertion verified successfully. New signCount: {current_sign_count}") return current_sign_count # --- RPサーバー側での利用例(簡略化) --- # ユーザー登録時に保存される情報 # 実際はデータベースに永続化される user_credentials_db = { "user_id_123": { "credential_id": "...", # FIDO2 Credential ID (バイト列のBase64エンコードなど) "public_key": "...", # PEM形式の公開鍵文字列 "sign_count": 0, # 初期値は0 "rp_origin": "https://example.com" } } # 認証リクエストが来た場合(実際の値はクライアントから送られてくる) try: client_data_json = "eyJjaGFsbGVuZ2UiOiJ..." # 実際のBase64URLエンコードされたJSON authenticator_data = "oYA_AATgI..." # 実際のBase64URLエンコードされたバイナリ signature = "MEUCIQD..." # 実際のBase64URLエンコードされたバイナリ user_id = "user_id_123" # セッション情報や事前に提供されたCredential IDからユーザーを特定 credential_info = user_credentials_db.get(user_id) if credential_info: new_sign_count = verify_assertion( client_data_json, authenticator_data, signature, credential_info["public_key"], credential_info["sign_count"], credential_info["rp_origin"] ) # 認証成功。データベースの新しいsignCountを更新 credential_info["sign_count"] = new_sign_count print("ユーザー認証成功!") else: print("認証情報が見つかりません。") except ValueError as e: print(f"ユーザー認証失敗: {e}")Attestationの利用(オプション): Authenticatorの登録時(Attestation)に、そのAuthenticatorが正規のものであるかを確認することができます。これにより、不正なハードウェアやソフトウェアによる認証器の登録を検知し、認証システム全体のセキュリティレベルを向上させることが可能です。ただし、プライバシーへの影響や複雑さが増すため、導入の是非は慎重に検討する必要があります。
Credential IDとUser Handleの安全な管理: FIDO2の秘密鍵はAuthenticatorに保存されるため、RPサーバーは公開鍵、Credential ID、User Handleをデータベースに安全に保存する必要があります。これらの情報は暗号化して保存し、データベースへのアクセスは最小権限の原則に従って厳しく制限します。Credential IDやUser Handleは、ユーザー識別子として利用されることが多いため、個人情報保護の観点からも取り扱いに注意が必要です。
多要素認証としての利用とバックアップ認証: FIDO2はNIST SP 800-63BにおいてAAL3(Authenticator Assurance Level 3)相当と評価される強力な認証メカニズムです[2]。FIDO2を第一要素認証とし、SMS OTPやTOTPなどの追加の認証要素と組み合わせることで、セキュリティをさらに強化できます。また、認証器の紛失・盗難に備え、別のFIDO2認証器の登録や、リカバリーコードなどのバックアップ認証手段を用意することも重要です。
現場の落とし穴
FIDO2の導入には、以下のような現場で陥りやすい落とし穴があります。
signCount検証の軽視: 上述の通り、signCountの検証はリプレイ攻撃防止に不可欠です。しかし、実装の複雑さからこの検証を省略したり、不適切に実装したりするケースがあります。これはFIDO2の最大の強みの一つを失うことにつながります。Attestation検証の過度な期待と複雑性: Attestation検証はAuthenticatorの真正性を保証しますが、全てのAuthenticatorを網羅的に検証することは困難であり、RPサーバー側の実装を複雑にします。プライバシーへの配慮も必要であり、必要以上の厳格なAttestation検証は可用性を損なう可能性もあります。
ユーザー認証器の紛失・盗難時の回復プロセス: ユーザーが認証器を紛失・盗難した場合の回復プロセスが不適切だと、アカウントロックアウトによる可用性の低下や、不正な回復によるセキュリティリスクが生じます。バックアップ認証器の提供、安全なリカバリーフローの設計が重要です。
ユーザー教育の不足: FIDO2は従来のパスワード認証と異なるため、ユーザーにその仕組みと利点、そして適切な利用方法(例: 認証器の保護)を教育する必要があります。
運用対策
FIDO2認証システムを安全に運用するためには、技術的な実装だけでなく、組織的な運用対策も重要です。
認証器のライフサイクル管理:
登録: ユーザーが認証器を登録する際には、その真正性を確認するAttestation検証の適用を検討します。
紛失/盗難時の失効: 認証器が紛失または盗難された場合は、速やかに当該認証器のCredential IDをRPサーバーから失効させるメカニズムを確立します。これにより、不正利用のリスクを最小限に抑えます。
再登録: ユーザーが新しい認証器を登録する際のプロセスを明確化し、安全性を確保します。
最小権限の原則: RPサーバーがFIDO2関連情報を格納するデータベースへのアクセスは、最小限の権限を持つサービスアカウントのみに許可します。また、開発者や運用担当者も必要最小限のアクセス権限に制限します。
監査ログの収集と監視: FIDO2認証の成功・失敗、Credential IDの登録・失効、
signCountのリセットなどのイベントを詳細なログとして記録します。これらのログはSIEM(Security Information and Event Management)システムなどを用いてリアルタイムに監視し、異常なアクセスパターンや不審な行動を検出できるようにします。特に、同一Credential IDからの認証失敗が連続する場合や、signCountの不適切な変化があった場合は、アラートを発するべきです。定期的なセキュリティレビューと脆弱性診断: RPサーバーのWebAuthn実装や関連するAPIエンドポイントに対して、定期的にセキュリティレビューや脆弱性診断(ペネトレーションテストを含む)を実施します。特に、FIDO2プロトコルのアップデートや新しい攻撃手法の登場に合わせて、対策を見直す必要があります。
ユーザー教育: ユーザーに対して、FIDO2の安全性、認証器の適切な保管方法、PINや生体認証の重要性、紛失時の対処法などを継続的に教育します。不明点が生じた場合に相談できる窓口も設置し、ユーザーが安心して利用できる環境を整備します。
まとめ
FIDO2によるパスワードレス認証は、従来のパスワード認証の脆弱性を克服し、ユーザー体験とセキュリティの両面で大きなメリットをもたらします。しかし、その導入と運用には、RPサーバー側での厳格なプロトコル検証、安全な鍵・秘匿情報管理、認証器のライフサイクル管理、そして継続的な運用対策が不可欠です。特に、signCountの適切な検証やオリジンチェックといったRPサーバーの責務を果たすことで、フィッシング耐性やリプレイ攻撃耐性といったFIDO2の主要な利点を最大限に引き出すことができます。セキュリティエンジニアは、FIDO2の技術的詳細を深く理解し、これらのリスクと対策を考慮した上で、堅牢なパスワードレス認証システムを構築・運用することが求められます。
参考文献: [1] W3C. “Web Authentication: An API for accessing Public Key Credentials Level 3”. Candidate Recommendation, 28 May 2024 (JST). https://www.w3.org/TR/webauthn-3/ [2] NIST. “NIST Special Publication 800-63B, Digital Identity Guidelines, Authenticator Assurance Level”. March 2020 (JST). https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf [3] FIDO Alliance. “WebAuthn and FIDO2: What’s the Difference?”. https://fidoalliance.org/webauthn-and-fido2-whats-the-difference/

コメント