<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。
<!--META
{
"title": "WebAuthn (FIDO2) パスワードレス認証のセキュリティベストプラクティス",
"primary_category": "セキュリティ>Web認証",
"secondary_categories": ["パスワードレス認証","FIDO2"],
"tags": ["WebAuthn","FIDO2","パスワードレス","セキュリティキー","多要素認証"],
"summary": "WebAuthn(FIDO2)パスワードレス認証の脅威モデル、攻撃シナリオ、検出・緩和策、運用対策をセキュリティエンジニアの視点で解説します。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"WebAuthnパスワードレス認証のセキュリティベストプラクティスを解説。フィッシング耐性が高い一方、実装ミスによるリスクも。脅威モデルから運用対策まで網羅。 #WebAuthn #FIDO2 #セキュリティ","hashtags":["#WebAuthn","#FIDO2","#セキュリティ"]},
"link_hints": ["https://www.w3.org/TR/webauthn-3/","https://fidoalliance.org/fido2/","https://developers.google.com/identity/passkeys"]
}
--></p>
<h1 class="wp-block-heading">WebAuthn (FIDO2) パスワードレス認証のセキュリティベストプラクティス</h1>
<p>WebAuthn(Web Authentication)は、W3CとFIDO Allianceによって標準化されたWebベースの公開鍵認証プロトコルであり、FIDO2の一部を構成します。従来のパスワード認証に比べてフィッシング耐性が高く、強力なセキュリティを提供しますが、その堅牢性は実装の詳細と運用方法に大きく依存します。本記事では、セキュリティエンジニアの視点から、WebAuthnの実装における脅威モデル、具体的な攻撃シナリオ、検出・緩和策、そして運用上のベストプラクティスについて解説します。</p>
<h2 class="wp-block-heading">脅威モデル</h2>
<p>WebAuthnはパスワード認証と比較して多くの脅威に対して強固ですが、それでも考慮すべき固有の脅威モデルが存在します。</p>
<ul class="wp-block-list">
<li><p><strong>フィッシング(RP偽装)</strong>:
従来のパスワード認証に対するWebAuthnの最大の利点はフィッシング耐性です。しかし、Relying Party (RP) サーバが<code>origin</code>検証を怠った場合、ユーザーが偽サイトで認証を行った際に、その認証リクエストが正規のRPに中継されてしまう可能性があります。これはWebAuthnの<code>origin</code>検証メカニズムが適切に機能しない場合に発生し得ます。</p></li>
<li><p><strong>中間者攻撃 (Man-in-the-Middle, MitM)</strong>:
TLS(Transport Layer Security)が適切に設定されていない環境では、クライアントとRPサーバ間の通信が傍受・改ざんされる可能性があります。WebAuthnはTLSの利用を前提としており、プロトコル自体はMitMに強いものの、基盤となる通信路の保護は不可欠です。</p></li>
<li><p><strong>認証器の盗難/紛失</strong>:
物理的なセキュリティキー(ハードウェア認証器)やデバイス内の生体認証器(指紋、顔認証など)が盗難または紛失した場合、攻撃者が認証器を不正に利用しようとする可能性があります。ただし、多くの認証器はPINや生体認証によるユーザー検証(User Verification, UV)を要求するため、単なる盗難では認証は困難です。</p></li>
<li><p><strong>登録フェーズでの不正なクレデンシャル登録</strong>:
RPサーバの登録フローに不備がある場合、攻撃者が意図しない認証器(自身の認証器など)をユーザーアカウントに登録してしまう可能性があります。これにより、後からその認証器を使ってユーザーアカウントにアクセスできる状態になります。</p></li>
<li><p><strong>リプレイ攻撃</strong>:
認証プロセス中にRPサーバが発行する<code>challenge</code>値が使い捨てではない、または適切に検証されない場合、攻撃者が過去の認証応答(署名済みアサーション)を傍受し、それを再送することで不正に認証を突破しようとする可能性があります。</p></li>
<li><p><strong>RP (Relying Party) サーバ側の脆弱性</strong>:
認証器から受け取った公開鍵などのクレデンシャル情報が保存されるRPサーバのデータベースが漏洩した場合、攻撃者がクレデンシャルIDなどの情報を取得し、他の攻撃の足がかりにする可能性があります。WebAuthnの公開鍵はパスワードハッシュと異なり、そのままでは認証には使えませんが、クレデンシャルIDの悪用やユーザー追跡の可能性は残ります。</p></li>
<li><p><strong>サイドチャネル攻撃</strong>:
認証器の実装によっては、生体認証センサーに対するサイドチャネル攻撃(例えば、電力消費パターンからの情報抽出)など、より高度な物理的攻撃のリスクが存在する可能性があります。これは認証器自体のハードウェアやファームウェアのセキュリティに依存します。</p></li>
</ul>
<h2 class="wp-block-heading">攻撃シナリオ</h2>
<p>ここでは、WebAuthn実装における具体的な攻撃シナリオとして「不適切な<code>challenge</code>検証を悪用したリプレイ攻撃」と「フィッシング攻撃(RP偽装)」を取り上げ、後者をMermaid図で可視化します。</p>
<h3 class="wp-block-heading">1. 不適切な<code>challenge</code>検証を悪用したリプレイ攻撃</h3>
<p>攻撃者は、正規の認証プロセス中にRPサーバとクライアント間で交換される<code>challenge</code>値と署名済みアサーションを傍受します。RPサーバが<code>challenge</code>値の使い捨てを強制せず、かつ認証器から提供される<code>signature counter</code>を適切に検証しない場合、攻撃者は傍受した認証応答を再送することで、ユーザーの介入なしに認証を突破できる可能性があります。</p>
<p><strong>具体的な流れ:</strong></p>
<ol class="wp-block-list">
<li><p>ユーザーが正規RPで認証を試みる。</p></li>
<li><p>RPサーバが<code>challenge_A</code>を発行し、クライアントに送信。</p></li>
<li><p>クライアントが認証器で<code>challenge_A</code>を署名し、アサーション<code>assertion_A</code>をRPに送信。</p></li>
<li><p>RPサーバは<code>challenge_A</code>と<code>assertion_A</code>を検証し、ユーザーを認証。<strong>この際、RPサーバは<code>challenge_A</code>をセッションから削除せず、<code>signature counter</code>も検証しない。</strong></p></li>
<li><p>攻撃者は<code>challenge_A</code>と<code>assertion_A</code>を傍受済み。</p></li>
<li><p>後日、攻撃者は傍受した<code>challenge_A</code>と<code>assertion_A</code>をRPサーバに再送。</p></li>
<li><p>RPサーバは、古い<code>challenge_A</code>がまだ有効であると判断し、<code>assertion_A</code>も検証を通過させてしまい、攻撃者をユーザーとして認証してしまう。</p></li>
</ol>
<h3 class="wp-block-heading">2. フィッシング攻撃(RP偽装)</h3>
<p>WebAuthnは<code>origin</code>検証によってフィッシング耐性を高めていますが、ユーザーが認証情報を提供するサイトが本当に正規のものであるかを確認する基本的なユーザー教育と、RPサーバ側の厳密な<code>origin</code>検証が重要です。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["攻撃者 (Phisher)"] -->|1. 偽装サイトへ誘導 (例: 悪意あるリンク)| B("ユーザー")
B -->|2. 偽装サイトで認証開始| C{"偽装RPサイト"}
C -->|3. WebAuthn AuthenticationRequestを正規RPへ中継| D["正規RPサーバ"]
D -->|4. challenge発行 (正規のoriginでのみ有効)| C
C -->|5. challengeをユーザーに提示| B
B -->|6. 認証器でchallengeを署名し、CredentialAssertionを偽装サイトへ送信| C
C --X|7. (攻撃失敗) challengeのoriginが偽装サイトのため正規RPが拒否| D
style C fill:#f9f,stroke:#333,stroke-width:2px;
style D fill:#afa,stroke:#333,stroke-width:2px;
note right of C: 攻撃者Cは、正規RP Dから発行されたchallengeを自身(偽装サイト)のoriginで署名しようとする。
note right of D: 正規RP Dは、challengeの署名に使用されたoriginが自身のサイトと一致しないため、認証を拒否する。
</pre></div>
<p><strong>上記図の説明:</strong></p>
<ol class="wp-block-list">
<li><p>攻撃者(Phisher)は、ユーザーを正規サイトそっくりの偽装サイト(C)に誘導します。</p></li>
<li><p>ユーザーは偽装サイト(C)でWebAuthn認証を開始します。</p></li>
<li><p>偽装サイト(C)は、ユーザーのブラウザから受け取った認証リクエストを正規のRPサーバ(D)に中継します。</p></li>
<li><p>正規RPサーバ(D)は認証プロセスを開始し、ユーザーに<code>challenge</code>値を発行し、偽装サイト(C)に送り返します。この<code>challenge</code>値には、正規RPの<code>origin</code>情報が紐付けられています。</p></li>
<li><p>偽装サイト(C)は<code>challenge</code>値をユーザーに提示します。</p></li>
<li><p>ユーザーは自身の認証器(セキュリティキーや生体認証デバイスなど)で<code>challenge</code>値を署名し、CredentialAssertionを偽装サイト(C)に送り返します。<strong>ここで重要なのは、認証器は<code>challenge</code>だけでなく、認証が発生しているWebサイトの<code>origin</code>(URLのスキーム、ホスト、ポート)も署名に含める点です。</strong></p></li>
<li><p>偽装サイト(C)は、署名されたCredentialAssertionを正規RPサーバ(D)に中継しようとします。しかし、正規RPサーバ(D)は、CredentialAssertionに含まれる<code>origin</code>情報(偽装サイトの<code>origin</code>)と、自身が期待する<code>origin</code>(正規RPの<code>origin</code>)が一致しないことを検知し、認証を拒否します。</p></li>
</ol>
<p>このメカニズムにより、WebAuthnはフィッシングに対して非常に強固ですが、RPサーバが<code>origin</code>検証を厳密に行うこと、そしてユーザーがブラウザのアドレスバーでURLを確認する習慣が重要です。</p>
<h2 class="wp-block-heading">検出と緩和策</h2>
<p>WebAuthnのセキュリティを確保するためには、RPサーバ側での厳格な検証と適切な設定が不可欠です。</p>
<h3 class="wp-block-heading">RPサーバ側の検証強化</h3>
<ul class="wp-block-list">
<li><p><strong><code>challenge</code>値の適切な生成と検証</strong>:
認証リクエストごとに<strong>一意で予測不可能な</strong><code>challenge</code>値を生成し、セッションと紐付けてサーバーサイドに保存します。認証応答を受け取った際には、保存された<code>challenge</code>値と、認証応答に含まれる<code>challenge</code>値が一致するかを確認し、<strong>検証後には速やかに<code>challenge</code>値を破棄(使い捨て)</strong>します。これにより、リプレイ攻撃を防ぎます。</p></li>
<li><p><strong><code>origin</code>の検証</strong>:
認証応答に含まれる<code>clientDataJSON.origin</code>と、RPサーバ自身(または許可されたRPの<code>origin</code>リスト)の<code>origin</code>が完全に一致するかを厳格に検証します。これにより、フィッシングサイトからの認証試行を効果的にブロックします。</p></li>
<li><p><strong><code>signature counter</code>の検証</strong>:
認証器は、認証を行うたびに<code>signature counter</code>をインクリメントします。RPサーバは、前回の認証時に保存した<code>signature counter</code>より、今回受け取った<code>signature counter</code>が厳密に増加しているかを確認します。これにより、認証器のクローンやリプレイ攻撃(<code>challenge</code>が使い捨てられていたとしても)を検出できます。増加していない場合は、認証を拒否し、警告を発するべきです。</p></li>
<li><p><strong><code>authenticator data</code>の検証</strong>:
<code>authenticator data</code>に含まれるフラグ(<code>UP</code> <code>UV</code> <code>AT</code> <code>ED</code>など)や<code>AAGUID</code>(Authenticator Attestation Globally Unique Identifier)を検証し、期待される認証器の特性(ユーザー検証が実施されたか、Attestation情報が含まれているかなど)に合致するかを確認します。</p></li>
<li><p><strong>登録時のクレデンシャルIDの重複チェック</strong>:
新しいクレデンシャル(公開鍵)を登録する際、そのクレデンシャルIDが既に他のユーザーに紐付けられていないかを確認します。これにより、攻撃者が既存のクレデンシャルを自身のものとして再登録しようとする試みを防ぎます。</p></li>
</ul>
<h3 class="wp-block-heading">WebAuthn APIの安全な利用</h3>
<ul class="wp-block-list">
<li><p><strong><code>authenticatorSelection</code>パラメータの適切設定</strong>:</p>
<ul>
<li><p><code>userVerification</code>: <code>required</code>に設定することで、認証器がPINや生体認証などによるユーザー検証を強制するようにします。これにより、認証器が盗難されても不正利用されにくくなります。</p></li>
<li><p><code>authenticatorAttachment</code>: <code>platform</code> (デバイス内蔵) か<code>cross-platform</code> (外部セキュリティキー) か、あるいは両方を許可するかを設定します。セキュリティ要件に応じて制限を検討します。</p></li>
<li><p><code>residentKey</code> (<code>discoverable credential</code>): <code>required</code>に設定することで、パスキーのようなユーザー名なしで認証可能なクレデンシャルを要求します。これはセキュリティと利便性を両立する強力な機能です。</p></li>
</ul></li>
<li><p><strong>Attestationの検証(オプション)</strong>:
クレデンシャル登録時に認証器から提供されるAttestation情報を検証することで、認証器が特定の信頼されたモデルであるか、または本物であるかを確認できます。これにより、悪意のあるソフトウェア認証器や偽造された認証器の登録を防ぐことができます。ただし、Attestation検証は認証器の互換性を損なう可能性があり、運用上のトレードオフを慎重に検討する必要があります。</p></li>
</ul>
<h3 class="wp-block-heading">MFAとしてのWebAuthn</h3>
<p>WebAuthnは強力な認証手段ですが、単体で利用するだけでなく、多要素認証(MFA)の一部として導入することも有効です。例えば、WebAuthn認証に加えて、さらに高い機密性の操作には追加のパスワード入力やOAUTH認証を組み合わせるなど、リスクベースの認証戦略を検討します。</p>
<h2 class="wp-block-heading">運用対策</h2>
<p>WebAuthn認証システムを安全に運用するためには、技術的な実装だけでなく、鍵管理、アクセスポリシー、監査といった運用上の対策が不可欠です。</p>
<h3 class="wp-block-heading">鍵/秘匿情報の取り扱い</h3>
<ul class="wp-block-list">
<li><p><strong><code>challenge</code>値のライフサイクル管理</strong>: RPサーバで生成される<code>challenge</code>値は、セッションと厳密に紐付けられ、Redisや安全なデータベースのキャッシュに短時間保存されるべきです。検証が完了した直後、または一定時間経過後に自動的に破棄されるメカニズムを実装し、再利用を防ぎます。</p></li>
<li><p><strong>公開鍵の安全な保管</strong>: 認証器から受け取った公開鍵(<code>credentialPublicKey</code>)は、RPサーバのデータベースに安全に保管される必要があります。これはユーザーIDとクレデンシャルIDに紐付けて保存されます。データベースへのアクセスは最小限の権限を持つサービスアカウントのみに限定し、保存データは常に暗号化(AES-256など)すべきです。</p></li>
<li><p><strong>秘密鍵の管理</strong>: WebAuthnの秘密鍵は認証器内にのみ存在し、決してRPサーバに送信されることはありません。このプロトコル上の特性がWebAuthnのセキュリティを支える根幹であるため、この原則が破られることがないように、設計とコードレビューで徹底します。</p></li>
</ul>
<h3 class="wp-block-heading">ローテーション</h3>
<p>WebAuthnの公開鍵/秘密鍵ペアは、従来のパスワードのように定期的なローテーションが必須ではありません。秘密鍵が認証器内に保護されているため、鍵が漏洩するリスクが低いからです。しかし、以下の状況では適切な対応が必要です。</p>
<ul class="wp-block-list">
<li><p><strong>認証器の紛失/盗難</strong>: ユーザーが認証器を紛失または盗難された場合、速やかにそのクレデンシャル(公開鍵)をRPサーバから削除できる機能を提供する必要があります。また、ユーザーが代替の認証器を登録できるように、アカウントリカバリプロセスを安全に設計することも重要です。</p></li>
<li><p><strong>複数クレデンシャルの推奨</strong>: ユーザーには複数の認証器(クレデンシャル)を登録することを推奨し、一つが使えなくなってもアカウントにアクセスできる冗長性を提供します。</p></li>
</ul>
<h3 class="wp-block-heading">最小権限の原則</h3>
<ul class="wp-block-list">
<li><p><strong>RPサーバのサービスアカウント</strong>: WebAuthn関連の処理(<code>challenge</code>の生成、クレデンシャルの保存、認証検証など)を行うRPサーバのバックエンドサービスは、最小限のデータベースアクセス権限とAPIキーのみを持つサービスアカウントで実行されるべきです。例えば、ユーザーの個人情報全体にアクセスできる権限ではなく、クレデンシャル情報テーブルのみにアクセスできる権限に限定するなどです。</p></li>
<li><p><strong>APIキー/トークンの管理</strong>: WebAuthnに関連する外部サービス(例: Attestation検証サービス)を利用する場合、そのためのAPIキーやトークンは環境変数やセキュアな鍵管理システム(AWS Secrets Manager, Azure Key Vaultなど)で管理し、コードにハードコードしないようにします。</p></li>
</ul>
<h3 class="wp-block-heading">監査とロギング</h3>
<ul class="wp-block-list">
<li><p><strong>詳細なログの取得</strong>: 以下のイベントについて、タイムスタンプ、IPアドレス、ユーザーID、クレデンシャルID(可能であれば匿名化して)を含む詳細なログを記録します。</p>
<ul>
<li><p>クレデンシャル登録(成功/失敗)</p></li>
<li><p>認証試行(成功/失敗、ユーザー検証の有無)</p></li>
<li><p>クレデンシャル削除</p></li>
<li><p>アカウントリカバリ試行</p></li>
</ul></li>
<li><p><strong>異常検知システムの導入</strong>: ログデータをリアルタイムで監視し、以下のような異常な挙動を検出するシステムを導入します。</p>
<ul>
<li><p>特定のユーザーからの認証失敗が短期間に連続して発生</p></li>
<li><p>地理的に離れた場所からの同時認証試行</p></li>
<li><p>登録されていない、または不正な<code>AAGUID</code>を持つ認証器からの登録試行</p></li>
<li><p><code>signature counter</code>の異常な値(減少、増加なし)</p></li>
</ul></li>
</ul>
<h3 class="wp-block-heading">誤検知/検出遅延/可用性トレードオフ</h3>
<p>現場の運用では、セキュリティ強化とユーザーエクスペリエンス、可用性との間でトレードオフが生じることがあります。</p>
<ul class="wp-block-list">
<li><p><strong>厳格なAttestation検証のトレードオフ</strong>: Attestation検証を<code>required</code>に設定すると、特定の認証器(特に新規またはニッチな製品)が利用できなくなる可能性があります。これはユーザーの選択肢を狭め、可用性を低下させる誤検知となり得ます。セキュリティ要件に応じて、Attestation検証は<code>preferred</code>(可能であれば検証するが必須ではない)にするか、または信頼できる認証器のリストを限定的にホワイトリストとして運用するなどのバランスを検討します。</p></li>
<li><p><strong>検出遅延とリアルタイム性</strong>: 攻撃検知が遅れると、被害が拡大する可能性があります。ログ収集から異常検知、アラート発報までの一連のプロセスを可能な限りリアルタイム化し、検出遅延を最小限に抑える必要があります。ストリーミングログ処理やSIEM(Security Information and Event Management)ツールの活用が有効です。</p></li>
<li><p><strong>アカウントロックアウトのリスク</strong>: 認証失敗回数によるアカウントロックアウトは、ブルートフォース攻撃対策として有効ですが、攻撃者が意図的に多数の認証失敗を引き起こし、正規ユーザーをロックアウトさせるサービス妨害(DoS)攻撃に利用される可能性があります。閾値の設定や、CAPTCHA、別のMFA要素による解除など、巧妙な対策が必要です。</p></li>
</ul>
<h2 class="wp-block-heading">コード例</h2>
<p>WebAuthn実装において、<code>challenge</code>値の取り扱いはセキュリティの鍵となります。ここでは、不適切な<code>challenge</code>の再利用を防ぐためのPython(Flask/FastAPIなどのバックエンドを想定)での安全な実装例を示します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">import os
import secrets
import base64
from datetime import datetime, timedelta
# 運用上の前提条件:
# - challengeはセッションやDBに紐付けて安全に保存される。
# - WebAuthnライブラリ (例: python-fido2) を使用して、より高レベルのプロトコル処理を行うのが一般的。
# 以下のコードはchallengeの生成と検証の概念を示すもので、完全なWebAuthn実装ではない。
# - 計算量: O(1) for generation and verification, assuming hash map for session_store.
# - メモリ条件: 各challengeとセッションIDのペアを保存するのに必要なメモリ。
# 例示のためのダミーセッションストア。本番ではRedis、Memcached、または永続DBを利用
# {
# "session_id_abc": {
# "challenge": "base64_encoded_challenge_value",
# "timestamp": "ISO_8601_datetime_string"
# }
# }
session_store = {}
# --- 誤用例:challengeの再利用または固定値の利用 ---
# production環境では絶対に行わない
_global_fixed_challenge = "a_fixed_challenge_for_all_users_and_requests" # 固定値
_global_reusable_challenge = None # グローバル変数や一度生成したら使い回す値
def generate_challenge_bad_fixed():
"""固定のchallengeを返す (絶対にNG)"""
print("WARNING: Using a fixed challenge. DO NOT USE IN PRODUCTION!")
return _global_fixed_challenge
def generate_challenge_bad_reusable():
"""一度生成したら使い回すchallengeを返す (NG)"""
global _global_reusable_challenge
if _global_reusable_challenge is None:
_global_reusable_challenge = secrets.token_urlsafe(32) # 生成はランダムだが、再利用される
print("WARNING: Reusing a challenge. DO NOT USE IN PRODUCTION!")
return _global_reusable_challenge
# --- 安全な代替:セッションごとの使い捨てchallenge ---
def generate_webauthn_challenge(session_id: str, expiry_minutes: int = 5) -> str:
"""
セッションごとに一意で使い捨てのWebAuthn challengeを生成し、サーバーサイドに保存します。
challengeはBase64 URL-safeエンコードされます。
Args:
session_id (str): 現在のユーザーセッションID。
expiry_minutes (int): challengeの有効期限(分)。
Returns:
str: Base64 URL-safeエンコードされたchallenge文字列。
"""
# 1. 十分なエントロピーを持つランダムなバイト列を生成 (32バイト = 256ビットを推奨)
challenge_bytes = secrets.token_bytes(32)
# 2. Base64 URL-safeエンコードして、WebAuthn APIで安全に扱える文字列形式にする
challenge_str = base64.urlsafe_b64encode(challenge_bytes).rstrip(b'=').decode('ascii')
# 3. challengeと有効期限をセッションストアに保存
# この例ではグローバル辞書だが、本番ではRedis, secure cookie, または永続DBを利用
expiry_time = datetime.utcnow() + timedelta(minutes=expiry_minutes)
session_store[session_id] = {
"challenge": challenge_str,
"timestamp": expiry_time.isoformat() # ISO 8601形式で保存
}
print(f"[{datetime.utcnow().isoformat()}] Generated challenge for session '{session_id}': {challenge_str[:10]}... (Expiry: {expiry_time.isoformat()})")
return challenge_str
def verify_webauthn_challenge(session_id: str, received_challenge: str) -> bool:
"""
受信したWebAuthn challengeがサーバーサイドに保存されたものと一致し、有効期限内であるか検証します。
検証後、challengeはセッションストアから破棄されます。
Args:
session_id (str): 現在のユーザーセッションID。
received_challenge (str): クライアントから受け取ったchallenge文字列。
Returns:
bool: challengeが有効であればTrue、そうでなければFalse。
"""
if session_id not in session_store:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: No challenge found for session '{session_id}'.")
return False
stored_data = session_store[session_id]
stored_challenge = stored_data.get("challenge")
stored_timestamp_str = stored_data.get("timestamp")
# 1. challengeをセッションストアから削除(使い捨ての原則)
# これにより、同じchallengeを複数回使用することを防ぐ
del session_store[session_id]
if not stored_challenge:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Stored challenge is empty for session '{session_id}'.")
return False
# 2. 有効期限の確認
try:
expiry_time = datetime.fromisoformat(stored_timestamp_str)
if datetime.utcnow() > expiry_time:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Challenge for session '{session_id}' has expired.")
return False
except ValueError:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Invalid timestamp format for session '{session_id}'.")
return False
# 3. challenge値の一致確認
if stored_challenge != received_challenge:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Mismatch for session '{session_id}'. Received '{received_challenge[:10]}...', Expected '{stored_challenge[:10]}...'.")
return False
print(f"[{datetime.utcnow().isoformat()}] Challenge verification successful for session '{session_id}'.")
return True
# --- 使用例 ---
if __name__ == "__main__":
user_session_id = "user_abc_123"
print("\n--- 誤用例のテスト ---")
fixed_challenge = generate_challenge_bad_fixed()
reused_challenge_1 = generate_challenge_bad_reusable()
reused_challenge_2 = generate_challenge_bad_reusable() # 同じchallengeが返される
print(f"Fixed Challenge: {fixed_challenge}")
print(f"Reused Challenge 1: {reused_challenge_1}")
print(f"Reused Challenge 2 (same as 1): {reused_challenge_2}")
print("\n--- 安全な代替のテスト ---")
# 1. ユーザーがWebAuthn認証を開始 -> RPがchallengeを生成し、クライアントに送信
print("Scenario 1: Successful Challenge Verification")
first_challenge = generate_webauthn_challenge(user_session_id, expiry_minutes=1)
# クライアントはfirst_challengeを使って認証器で署名し、first_challengeを含む認証応答をRPに送り返す
# RPが認証応答を受信し、challengeを検証
is_valid_1 = verify_webauthn_challenge(user_session_id, first_challenge)
print(f"Verification result 1: {is_valid_1}") # Trueになるはず
# 2. 同じchallengeを再度検証しようとする (リプレイ攻撃のシミュレーション)
print("\nScenario 2: Attempting to reuse challenge (replay attack)")
is_valid_2 = verify_webauthn_challenge(user_session_id, first_challenge)
print(f"Verification result 2: {is_valid_2}") # Falseになるはず (challengeが破棄されているため)
# 3. challengeの有効期限切れのテスト
print("\nScenario 3: Expired Challenge")
expired_session_id = "expired_user_xyz"
# 短い有効期限でchallengeを生成
expired_challenge = generate_webauthn_challenge(expired_session_id, expiry_minutes=0.01) # ほぼ即座に期限切れ
import time
time.sleep(0.02) # 意図的に期限切れを待つ
is_valid_expired = verify_webauthn_challenge(expired_session_id, expired_challenge)
print(f"Verification result (expired): {is_valid_expired}") # Falseになるはず
# 4. challengeの不一致テスト (改ざんまたは異なるchallenge)
print("\nScenario 4: Mismatched Challenge")
mismatch_session_id = "mismatch_user_123"
correct_challenge = generate_webauthn_challenge(mismatch_session_id)
incorrect_challenge = secrets.token_urlsafe(32) # クライアントから別のchallengeが送られてきた場合
is_valid_mismatch = verify_webauthn_challenge(mismatch_session_id, incorrect_challenge)
print(f"Verification result (mismatch): {is_valid_mismatch}") # Falseになるはず
</pre>
</div>
<p>このコード例では、<code>secrets</code>モジュールを用いて暗号学的に安全なランダムな<code>challenge</code>を生成し、<code>session_store</code>にセッションIDと紐付けて保存しています。検証時には、保存された<code>challenge</code>と受け取った<code>challenge</code>を比較し、さらに<strong>検証が完了した時点で<code>session_store</code>から<code>challenge</code>を削除する</strong>ことで、再利用を防いでいます。また、有効期限を設定することで、長時間の<code>challenge</code>待機によるリスクも低減しています。</p>
<h2 class="wp-block-heading">まとめ</h2>
<p>WebAuthn (FIDO2) は、パスワードレス認証の未来を担う強力な標準であり、特にフィッシング耐性において従来のパスワード認証を大きく凌駕します。しかし、そのセキュリティはRPサーバ側の厳格な実装と運用に依存します。<code>challenge</code>、<code>origin</code>、<code>signature counter</code>の適切な検証、鍵/秘匿情報のライフサイクル管理、最小権限の原則、そして詳細な監査ロギングは、WebAuthnシステムを安全に運用するための不可欠な要素です。</p>
<p>セキュリティエンジニアは、これらのベストプラクティスを理解し、誤用例と安全な代替を明確に区別することで、潜在的な攻撃シナリオからユーザーとシステムを保護することができます。実装の簡素化を図るWebAuthnライブラリを利用する際も、その内部挙動や設定オプションがこれらのセキュリティ要件を満たしているかを確認することが重要です。WebAuthnを適切に導入・運用することで、より安全で利便性の高い認証体験をユーザーに提供することが可能になります。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
WebAuthn (FIDO2) パスワードレス認証のセキュリティベストプラクティス
WebAuthn(Web Authentication)は、W3CとFIDO Allianceによって標準化されたWebベースの公開鍵認証プロトコルであり、FIDO2の一部を構成します。従来のパスワード認証に比べてフィッシング耐性が高く、強力なセキュリティを提供しますが、その堅牢性は実装の詳細と運用方法に大きく依存します。本記事では、セキュリティエンジニアの視点から、WebAuthnの実装における脅威モデル、具体的な攻撃シナリオ、検出・緩和策、そして運用上のベストプラクティスについて解説します。
脅威モデル
WebAuthnはパスワード認証と比較して多くの脅威に対して強固ですが、それでも考慮すべき固有の脅威モデルが存在します。
フィッシング(RP偽装):
従来のパスワード認証に対するWebAuthnの最大の利点はフィッシング耐性です。しかし、Relying Party (RP) サーバがorigin検証を怠った場合、ユーザーが偽サイトで認証を行った際に、その認証リクエストが正規のRPに中継されてしまう可能性があります。これはWebAuthnのorigin検証メカニズムが適切に機能しない場合に発生し得ます。
中間者攻撃 (Man-in-the-Middle, MitM):
TLS(Transport Layer Security)が適切に設定されていない環境では、クライアントとRPサーバ間の通信が傍受・改ざんされる可能性があります。WebAuthnはTLSの利用を前提としており、プロトコル自体はMitMに強いものの、基盤となる通信路の保護は不可欠です。
認証器の盗難/紛失:
物理的なセキュリティキー(ハードウェア認証器)やデバイス内の生体認証器(指紋、顔認証など)が盗難または紛失した場合、攻撃者が認証器を不正に利用しようとする可能性があります。ただし、多くの認証器はPINや生体認証によるユーザー検証(User Verification, UV)を要求するため、単なる盗難では認証は困難です。
登録フェーズでの不正なクレデンシャル登録:
RPサーバの登録フローに不備がある場合、攻撃者が意図しない認証器(自身の認証器など)をユーザーアカウントに登録してしまう可能性があります。これにより、後からその認証器を使ってユーザーアカウントにアクセスできる状態になります。
リプレイ攻撃:
認証プロセス中にRPサーバが発行するchallenge値が使い捨てではない、または適切に検証されない場合、攻撃者が過去の認証応答(署名済みアサーション)を傍受し、それを再送することで不正に認証を突破しようとする可能性があります。
RP (Relying Party) サーバ側の脆弱性:
認証器から受け取った公開鍵などのクレデンシャル情報が保存されるRPサーバのデータベースが漏洩した場合、攻撃者がクレデンシャルIDなどの情報を取得し、他の攻撃の足がかりにする可能性があります。WebAuthnの公開鍵はパスワードハッシュと異なり、そのままでは認証には使えませんが、クレデンシャルIDの悪用やユーザー追跡の可能性は残ります。
サイドチャネル攻撃:
認証器の実装によっては、生体認証センサーに対するサイドチャネル攻撃(例えば、電力消費パターンからの情報抽出)など、より高度な物理的攻撃のリスクが存在する可能性があります。これは認証器自体のハードウェアやファームウェアのセキュリティに依存します。
攻撃シナリオ
ここでは、WebAuthn実装における具体的な攻撃シナリオとして「不適切なchallenge検証を悪用したリプレイ攻撃」と「フィッシング攻撃(RP偽装)」を取り上げ、後者をMermaid図で可視化します。
1. 不適切なchallenge検証を悪用したリプレイ攻撃
攻撃者は、正規の認証プロセス中にRPサーバとクライアント間で交換されるchallenge値と署名済みアサーションを傍受します。RPサーバがchallenge値の使い捨てを強制せず、かつ認証器から提供されるsignature counterを適切に検証しない場合、攻撃者は傍受した認証応答を再送することで、ユーザーの介入なしに認証を突破できる可能性があります。
具体的な流れ:
ユーザーが正規RPで認証を試みる。
RPサーバがchallenge_Aを発行し、クライアントに送信。
クライアントが認証器でchallenge_Aを署名し、アサーションassertion_AをRPに送信。
RPサーバはchallenge_Aとassertion_Aを検証し、ユーザーを認証。この際、RPサーバはchallenge_Aをセッションから削除せず、signature counterも検証しない。
攻撃者はchallenge_Aとassertion_Aを傍受済み。
後日、攻撃者は傍受したchallenge_Aとassertion_AをRPサーバに再送。
RPサーバは、古いchallenge_Aがまだ有効であると判断し、assertion_Aも検証を通過させてしまい、攻撃者をユーザーとして認証してしまう。
2. フィッシング攻撃(RP偽装)
WebAuthnはorigin検証によってフィッシング耐性を高めていますが、ユーザーが認証情報を提供するサイトが本当に正規のものであるかを確認する基本的なユーザー教育と、RPサーバ側の厳密なorigin検証が重要です。
graph TD
A["攻撃者 (Phisher)"] -->|1. 偽装サイトへ誘導 (例: 悪意あるリンク)| B("ユーザー")
B -->|2. 偽装サイトで認証開始| C{"偽装RPサイト"}
C -->|3. WebAuthn AuthenticationRequestを正規RPへ中継| D["正規RPサーバ"]
D -->|4. challenge発行 (正規のoriginでのみ有効)| C
C -->|5. challengeをユーザーに提示| B
B -->|6. 認証器でchallengeを署名し、CredentialAssertionを偽装サイトへ送信| C
C --X|7. (攻撃失敗) challengeのoriginが偽装サイトのため正規RPが拒否| D
style C fill:#f9f,stroke:#333,stroke-width:2px;
style D fill:#afa,stroke:#333,stroke-width:2px;
note right of C: 攻撃者Cは、正規RP Dから発行されたchallengeを自身(偽装サイト)のoriginで署名しようとする。
note right of D: 正規RP Dは、challengeの署名に使用されたoriginが自身のサイトと一致しないため、認証を拒否する。
上記図の説明:
攻撃者(Phisher)は、ユーザーを正規サイトそっくりの偽装サイト(C)に誘導します。
ユーザーは偽装サイト(C)でWebAuthn認証を開始します。
偽装サイト(C)は、ユーザーのブラウザから受け取った認証リクエストを正規のRPサーバ(D)に中継します。
正規RPサーバ(D)は認証プロセスを開始し、ユーザーにchallenge値を発行し、偽装サイト(C)に送り返します。このchallenge値には、正規RPのorigin情報が紐付けられています。
偽装サイト(C)はchallenge値をユーザーに提示します。
ユーザーは自身の認証器(セキュリティキーや生体認証デバイスなど)でchallenge値を署名し、CredentialAssertionを偽装サイト(C)に送り返します。ここで重要なのは、認証器はchallengeだけでなく、認証が発生しているWebサイトのorigin(URLのスキーム、ホスト、ポート)も署名に含める点です。
偽装サイト(C)は、署名されたCredentialAssertionを正規RPサーバ(D)に中継しようとします。しかし、正規RPサーバ(D)は、CredentialAssertionに含まれるorigin情報(偽装サイトのorigin)と、自身が期待するorigin(正規RPのorigin)が一致しないことを検知し、認証を拒否します。
このメカニズムにより、WebAuthnはフィッシングに対して非常に強固ですが、RPサーバがorigin検証を厳密に行うこと、そしてユーザーがブラウザのアドレスバーでURLを確認する習慣が重要です。
検出と緩和策
WebAuthnのセキュリティを確保するためには、RPサーバ側での厳格な検証と適切な設定が不可欠です。
RPサーバ側の検証強化
challenge値の適切な生成と検証:
認証リクエストごとに一意で予測不可能なchallenge値を生成し、セッションと紐付けてサーバーサイドに保存します。認証応答を受け取った際には、保存されたchallenge値と、認証応答に含まれるchallenge値が一致するかを確認し、検証後には速やかにchallenge値を破棄(使い捨て)します。これにより、リプレイ攻撃を防ぎます。
originの検証:
認証応答に含まれるclientDataJSON.originと、RPサーバ自身(または許可されたRPのoriginリスト)のoriginが完全に一致するかを厳格に検証します。これにより、フィッシングサイトからの認証試行を効果的にブロックします。
signature counterの検証:
認証器は、認証を行うたびにsignature counterをインクリメントします。RPサーバは、前回の認証時に保存したsignature counterより、今回受け取ったsignature counterが厳密に増加しているかを確認します。これにより、認証器のクローンやリプレイ攻撃(challengeが使い捨てられていたとしても)を検出できます。増加していない場合は、認証を拒否し、警告を発するべきです。
authenticator dataの検証:
authenticator dataに含まれるフラグ(UP UV AT EDなど)やAAGUID(Authenticator Attestation Globally Unique Identifier)を検証し、期待される認証器の特性(ユーザー検証が実施されたか、Attestation情報が含まれているかなど)に合致するかを確認します。
登録時のクレデンシャルIDの重複チェック:
新しいクレデンシャル(公開鍵)を登録する際、そのクレデンシャルIDが既に他のユーザーに紐付けられていないかを確認します。これにより、攻撃者が既存のクレデンシャルを自身のものとして再登録しようとする試みを防ぎます。
WebAuthn APIの安全な利用
authenticatorSelectionパラメータの適切設定:
userVerification: requiredに設定することで、認証器がPINや生体認証などによるユーザー検証を強制するようにします。これにより、認証器が盗難されても不正利用されにくくなります。
authenticatorAttachment: platform (デバイス内蔵) かcross-platform (外部セキュリティキー) か、あるいは両方を許可するかを設定します。セキュリティ要件に応じて制限を検討します。
residentKey (discoverable credential): requiredに設定することで、パスキーのようなユーザー名なしで認証可能なクレデンシャルを要求します。これはセキュリティと利便性を両立する強力な機能です。
Attestationの検証(オプション):
クレデンシャル登録時に認証器から提供されるAttestation情報を検証することで、認証器が特定の信頼されたモデルであるか、または本物であるかを確認できます。これにより、悪意のあるソフトウェア認証器や偽造された認証器の登録を防ぐことができます。ただし、Attestation検証は認証器の互換性を損なう可能性があり、運用上のトレードオフを慎重に検討する必要があります。
MFAとしてのWebAuthn
WebAuthnは強力な認証手段ですが、単体で利用するだけでなく、多要素認証(MFA)の一部として導入することも有効です。例えば、WebAuthn認証に加えて、さらに高い機密性の操作には追加のパスワード入力やOAUTH認証を組み合わせるなど、リスクベースの認証戦略を検討します。
運用対策
WebAuthn認証システムを安全に運用するためには、技術的な実装だけでなく、鍵管理、アクセスポリシー、監査といった運用上の対策が不可欠です。
鍵/秘匿情報の取り扱い
challenge値のライフサイクル管理: RPサーバで生成されるchallenge値は、セッションと厳密に紐付けられ、Redisや安全なデータベースのキャッシュに短時間保存されるべきです。検証が完了した直後、または一定時間経過後に自動的に破棄されるメカニズムを実装し、再利用を防ぎます。
公開鍵の安全な保管: 認証器から受け取った公開鍵(credentialPublicKey)は、RPサーバのデータベースに安全に保管される必要があります。これはユーザーIDとクレデンシャルIDに紐付けて保存されます。データベースへのアクセスは最小限の権限を持つサービスアカウントのみに限定し、保存データは常に暗号化(AES-256など)すべきです。
秘密鍵の管理: WebAuthnの秘密鍵は認証器内にのみ存在し、決してRPサーバに送信されることはありません。このプロトコル上の特性がWebAuthnのセキュリティを支える根幹であるため、この原則が破られることがないように、設計とコードレビューで徹底します。
ローテーション
WebAuthnの公開鍵/秘密鍵ペアは、従来のパスワードのように定期的なローテーションが必須ではありません。秘密鍵が認証器内に保護されているため、鍵が漏洩するリスクが低いからです。しかし、以下の状況では適切な対応が必要です。
認証器の紛失/盗難: ユーザーが認証器を紛失または盗難された場合、速やかにそのクレデンシャル(公開鍵)をRPサーバから削除できる機能を提供する必要があります。また、ユーザーが代替の認証器を登録できるように、アカウントリカバリプロセスを安全に設計することも重要です。
複数クレデンシャルの推奨: ユーザーには複数の認証器(クレデンシャル)を登録することを推奨し、一つが使えなくなってもアカウントにアクセスできる冗長性を提供します。
最小権限の原則
RPサーバのサービスアカウント: WebAuthn関連の処理(challengeの生成、クレデンシャルの保存、認証検証など)を行うRPサーバのバックエンドサービスは、最小限のデータベースアクセス権限とAPIキーのみを持つサービスアカウントで実行されるべきです。例えば、ユーザーの個人情報全体にアクセスできる権限ではなく、クレデンシャル情報テーブルのみにアクセスできる権限に限定するなどです。
APIキー/トークンの管理: WebAuthnに関連する外部サービス(例: Attestation検証サービス)を利用する場合、そのためのAPIキーやトークンは環境変数やセキュアな鍵管理システム(AWS Secrets Manager, Azure Key Vaultなど)で管理し、コードにハードコードしないようにします。
監査とロギング
誤検知/検出遅延/可用性トレードオフ
現場の運用では、セキュリティ強化とユーザーエクスペリエンス、可用性との間でトレードオフが生じることがあります。
厳格なAttestation検証のトレードオフ: Attestation検証をrequiredに設定すると、特定の認証器(特に新規またはニッチな製品)が利用できなくなる可能性があります。これはユーザーの選択肢を狭め、可用性を低下させる誤検知となり得ます。セキュリティ要件に応じて、Attestation検証はpreferred(可能であれば検証するが必須ではない)にするか、または信頼できる認証器のリストを限定的にホワイトリストとして運用するなどのバランスを検討します。
検出遅延とリアルタイム性: 攻撃検知が遅れると、被害が拡大する可能性があります。ログ収集から異常検知、アラート発報までの一連のプロセスを可能な限りリアルタイム化し、検出遅延を最小限に抑える必要があります。ストリーミングログ処理やSIEM(Security Information and Event Management)ツールの活用が有効です。
アカウントロックアウトのリスク: 認証失敗回数によるアカウントロックアウトは、ブルートフォース攻撃対策として有効ですが、攻撃者が意図的に多数の認証失敗を引き起こし、正規ユーザーをロックアウトさせるサービス妨害(DoS)攻撃に利用される可能性があります。閾値の設定や、CAPTCHA、別のMFA要素による解除など、巧妙な対策が必要です。
コード例
WebAuthn実装において、challenge値の取り扱いはセキュリティの鍵となります。ここでは、不適切なchallengeの再利用を防ぐためのPython(Flask/FastAPIなどのバックエンドを想定)での安全な実装例を示します。
import os
import secrets
import base64
from datetime import datetime, timedelta
# 運用上の前提条件:
# - challengeはセッションやDBに紐付けて安全に保存される。
# - WebAuthnライブラリ (例: python-fido2) を使用して、より高レベルのプロトコル処理を行うのが一般的。
# 以下のコードはchallengeの生成と検証の概念を示すもので、完全なWebAuthn実装ではない。
# - 計算量: O(1) for generation and verification, assuming hash map for session_store.
# - メモリ条件: 各challengeとセッションIDのペアを保存するのに必要なメモリ。
# 例示のためのダミーセッションストア。本番ではRedis、Memcached、または永続DBを利用
# {
# "session_id_abc": {
# "challenge": "base64_encoded_challenge_value",
# "timestamp": "ISO_8601_datetime_string"
# }
# }
session_store = {}
# --- 誤用例:challengeの再利用または固定値の利用 ---
# production環境では絶対に行わない
_global_fixed_challenge = "a_fixed_challenge_for_all_users_and_requests" # 固定値
_global_reusable_challenge = None # グローバル変数や一度生成したら使い回す値
def generate_challenge_bad_fixed():
"""固定のchallengeを返す (絶対にNG)"""
print("WARNING: Using a fixed challenge. DO NOT USE IN PRODUCTION!")
return _global_fixed_challenge
def generate_challenge_bad_reusable():
"""一度生成したら使い回すchallengeを返す (NG)"""
global _global_reusable_challenge
if _global_reusable_challenge is None:
_global_reusable_challenge = secrets.token_urlsafe(32) # 生成はランダムだが、再利用される
print("WARNING: Reusing a challenge. DO NOT USE IN PRODUCTION!")
return _global_reusable_challenge
# --- 安全な代替:セッションごとの使い捨てchallenge ---
def generate_webauthn_challenge(session_id: str, expiry_minutes: int = 5) -> str:
"""
セッションごとに一意で使い捨てのWebAuthn challengeを生成し、サーバーサイドに保存します。
challengeはBase64 URL-safeエンコードされます。
Args:
session_id (str): 現在のユーザーセッションID。
expiry_minutes (int): challengeの有効期限(分)。
Returns:
str: Base64 URL-safeエンコードされたchallenge文字列。
"""
# 1. 十分なエントロピーを持つランダムなバイト列を生成 (32バイト = 256ビットを推奨)
challenge_bytes = secrets.token_bytes(32)
# 2. Base64 URL-safeエンコードして、WebAuthn APIで安全に扱える文字列形式にする
challenge_str = base64.urlsafe_b64encode(challenge_bytes).rstrip(b'=').decode('ascii')
# 3. challengeと有効期限をセッションストアに保存
# この例ではグローバル辞書だが、本番ではRedis, secure cookie, または永続DBを利用
expiry_time = datetime.utcnow() + timedelta(minutes=expiry_minutes)
session_store[session_id] = {
"challenge": challenge_str,
"timestamp": expiry_time.isoformat() # ISO 8601形式で保存
}
print(f"[{datetime.utcnow().isoformat()}] Generated challenge for session '{session_id}': {challenge_str[:10]}... (Expiry: {expiry_time.isoformat()})")
return challenge_str
def verify_webauthn_challenge(session_id: str, received_challenge: str) -> bool:
"""
受信したWebAuthn challengeがサーバーサイドに保存されたものと一致し、有効期限内であるか検証します。
検証後、challengeはセッションストアから破棄されます。
Args:
session_id (str): 現在のユーザーセッションID。
received_challenge (str): クライアントから受け取ったchallenge文字列。
Returns:
bool: challengeが有効であればTrue、そうでなければFalse。
"""
if session_id not in session_store:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: No challenge found for session '{session_id}'.")
return False
stored_data = session_store[session_id]
stored_challenge = stored_data.get("challenge")
stored_timestamp_str = stored_data.get("timestamp")
# 1. challengeをセッションストアから削除(使い捨ての原則)
# これにより、同じchallengeを複数回使用することを防ぐ
del session_store[session_id]
if not stored_challenge:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Stored challenge is empty for session '{session_id}'.")
return False
# 2. 有効期限の確認
try:
expiry_time = datetime.fromisoformat(stored_timestamp_str)
if datetime.utcnow() > expiry_time:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Challenge for session '{session_id}' has expired.")
return False
except ValueError:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Invalid timestamp format for session '{session_id}'.")
return False
# 3. challenge値の一致確認
if stored_challenge != received_challenge:
print(f"[{datetime.utcnow().isoformat()}] Challenge verification failed: Mismatch for session '{session_id}'. Received '{received_challenge[:10]}...', Expected '{stored_challenge[:10]}...'.")
return False
print(f"[{datetime.utcnow().isoformat()}] Challenge verification successful for session '{session_id}'.")
return True
# --- 使用例 ---
if __name__ == "__main__":
user_session_id = "user_abc_123"
print("\n--- 誤用例のテスト ---")
fixed_challenge = generate_challenge_bad_fixed()
reused_challenge_1 = generate_challenge_bad_reusable()
reused_challenge_2 = generate_challenge_bad_reusable() # 同じchallengeが返される
print(f"Fixed Challenge: {fixed_challenge}")
print(f"Reused Challenge 1: {reused_challenge_1}")
print(f"Reused Challenge 2 (same as 1): {reused_challenge_2}")
print("\n--- 安全な代替のテスト ---")
# 1. ユーザーがWebAuthn認証を開始 -> RPがchallengeを生成し、クライアントに送信
print("Scenario 1: Successful Challenge Verification")
first_challenge = generate_webauthn_challenge(user_session_id, expiry_minutes=1)
# クライアントはfirst_challengeを使って認証器で署名し、first_challengeを含む認証応答をRPに送り返す
# RPが認証応答を受信し、challengeを検証
is_valid_1 = verify_webauthn_challenge(user_session_id, first_challenge)
print(f"Verification result 1: {is_valid_1}") # Trueになるはず
# 2. 同じchallengeを再度検証しようとする (リプレイ攻撃のシミュレーション)
print("\nScenario 2: Attempting to reuse challenge (replay attack)")
is_valid_2 = verify_webauthn_challenge(user_session_id, first_challenge)
print(f"Verification result 2: {is_valid_2}") # Falseになるはず (challengeが破棄されているため)
# 3. challengeの有効期限切れのテスト
print("\nScenario 3: Expired Challenge")
expired_session_id = "expired_user_xyz"
# 短い有効期限でchallengeを生成
expired_challenge = generate_webauthn_challenge(expired_session_id, expiry_minutes=0.01) # ほぼ即座に期限切れ
import time
time.sleep(0.02) # 意図的に期限切れを待つ
is_valid_expired = verify_webauthn_challenge(expired_session_id, expired_challenge)
print(f"Verification result (expired): {is_valid_expired}") # Falseになるはず
# 4. challengeの不一致テスト (改ざんまたは異なるchallenge)
print("\nScenario 4: Mismatched Challenge")
mismatch_session_id = "mismatch_user_123"
correct_challenge = generate_webauthn_challenge(mismatch_session_id)
incorrect_challenge = secrets.token_urlsafe(32) # クライアントから別のchallengeが送られてきた場合
is_valid_mismatch = verify_webauthn_challenge(mismatch_session_id, incorrect_challenge)
print(f"Verification result (mismatch): {is_valid_mismatch}") # Falseになるはず
このコード例では、secretsモジュールを用いて暗号学的に安全なランダムなchallengeを生成し、session_storeにセッションIDと紐付けて保存しています。検証時には、保存されたchallengeと受け取ったchallengeを比較し、さらに検証が完了した時点でsession_storeからchallengeを削除することで、再利用を防いでいます。また、有効期限を設定することで、長時間のchallenge待機によるリスクも低減しています。
まとめ
WebAuthn (FIDO2) は、パスワードレス認証の未来を担う強力な標準であり、特にフィッシング耐性において従来のパスワード認証を大きく凌駕します。しかし、そのセキュリティはRPサーバ側の厳格な実装と運用に依存します。challenge、origin、signature counterの適切な検証、鍵/秘匿情報のライフサイクル管理、最小権限の原則、そして詳細な監査ロギングは、WebAuthnシステムを安全に運用するための不可欠な要素です。
セキュリティエンジニアは、これらのベストプラクティスを理解し、誤用例と安全な代替を明確に区別することで、潜在的な攻撃シナリオからユーザーとシステムを保護することができます。実装の簡素化を図るWebAuthnライブラリを利用する際も、その内部挙動や設定オプションがこれらのセキュリティ要件を満たしているかを確認することが重要です。WebAuthnを適切に導入・運用することで、より安全で利便性の高い認証体験をユーザーに提供することが可能になります。
コメント