その認証、まだ「穴」だらけだ。FIDO2/WebAuthnでフィッシングを過去の遺物にする方法
エンジニア諸君、日々お疲れ様。今日もコードを書き、サーバーを守り、頭を悩ませていることだろう。
さて、読者の中には「うちはTOTP(6桁の数字を入力するやつ)を導入しているから安全だ」と胸を張っている者がいるかもしれない。だが、断言しよう。TOTPやSMS認証は、現代のフィッシング攻撃の前では無力に等しい。 攻撃者がプロキシサーバー(Evilginx等)を立ててリアルタイムでトークンを中継すれば、いとも簡単に突破される。
今日は、そんな「古臭いセキュリティ」に終止符を打ち、公開鍵暗号方式による最強の防壁、FIDO2/WebAuthnの実装について解説する。
—
なぜ「フィッシング耐性」が必要なのか(PoCのリスク)
攻撃者は、巧妙に作り込まれた偽サイトへユーザーを誘導し、inputタグに打ち込まれたワンタイムパスワードを背後でリアルタイムに拾い上げ、本物のサイトへ瞬時に転送する。これが「中間者攻撃(AiTM)」だ。
TOTPは単なる「パスワードの代わり」であって、「誰に対して認証しているか」というOrigin(ドメイン)の検証機能を持っていない。 だから、偽サイトに情報を渡しても、システムは「正しい数字が来た」と判断してしまう。
対してFIDO2は、ブラウザがOriginを検証し、ハードウェアキーがそのドメインと結びついた署名を作成する。偽サイトでは署名が一致せず、認証が成立しない。これが「フィッシング耐性」の正体だ。
—
WebAuthnの実装:バックエンドのロジック
WebAuthnは複雑そうに見えるが、要は「チャレンジ」を生成し、ブラウザ経由で鍵に署名させ、その結果を検証するだけだ。今回はPython(Flask)を例に、サーバーサイドの要所を解説する。
サーバーサイド:登録時のチャレンジ生成
# サーバー側で生成するチャレンジ。必ずセッションに保持し、使い回しを防ぐ
import os
import base64
def generate_registration_options(user_id, username):
# チャレンジは暗号論的に安全な乱数であること
challenge = os.urandom(32)
# クライアント(ブラウザ)に送るオプション
options = {
"challenge": base64.b64encode(challenge).decode('utf-8'),
"rp": {"name": "My Secure Service", "id": "example.com"},
"user": {
"id": base64.b64encode(user_id).decode('utf-8'),
"name": username,
"displayName": username
},
"pubKeyCredParams": [{"alg": -7, "type": "public-key"}] # ES256を使用
}
return options
クライアントサイド:ブラウザでの認証実行
フロントエンドでは navigator.credentials.create を使用する。ここでは PublicKeyCredential を呼び出すための最短のJSコードを示す。
// サーバーから受け取ったoptionsを引数に実行
async function registerCredential(options) {
// サーバーから来たbase64文字列をArrayBufferに変換する関数が別途必要
options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));
try {
const credential = await navigator.credentials.create({
publicKey: options
});
console.log("登録成功:", credential);
// このcredentialをサーバーへ送信し、検証を行う
} catch (err) {
console.error("認証失敗:", err);
}
}
—
運用上の「泥臭い」鉄則
コードが書けただけでは、セキュリティは完成しない。現場でインシデントを起こさないために、以下の3点を徹底してくれ。
1. Originの厳格な検証
rpId(Relying Party ID)は、デプロイするドメインと完全に一致させること。サブドメイン間で共有する場合も、スコープを最小限に絞る。ここが甘いと、ドメイン検証というFIDO2の最大のメリットが死ぬ。
2. バックアップ認証の罠
ハードウェアキーを紛失した際、多くのシステムが「メールによるリセット」を許可する。ここが最大の脆弱性だ。
- リカバリコードを物理的に印刷させる。
- 信頼できるデバイスを複数登録させる。
- 紛失時のリカバリプロセスそのものを、最も高い認証レベル(例:管理者による対面確認)に設定する。
3. WAFでの保護(Nginx設定例)
FIDO2のエンドポイントは、ボットによる総当たり攻撃の対象になりやすい。WAF(ModSecurityやNginxのRate Limit)で保護をかけるのは鉄則だ。
# Nginxで認証エンドポイントへのリクエストを制限
location /api/auth/webauthn {
limit_req zone=auth_limit burst=5 nodelay;
# 信頼できるOrigin以外からのアクセスを拒否
if ($http_origin !~* "^https://(www\.)?example\.com$") {
return 403;
}
proxy_pass http://backend_upstream;
}
—
最後に:セキュリティは「諦めない」ことの積み重ねだ
「実装が面倒だ」「ユーザーにハードウェアキーを買わせる予算がない」……そんな言い訳を耳にすることもある。だが、一度でも大規模なフィッシング被害に見舞われれば、その代償はハードウェアキーの調達コストなど比較にならないほど大きい。
インシデントハンドリングの現場でいつも思うのは、「攻撃者は最も楽な道を通り、守る側は最も面倒な道を通らなければならない」という非対称性の厳しさだ。しかし、WebAuthnはその非対称性を、強力な暗号学でこちら側に有利に傾けてくれる。
まずは開発環境で WebAuthn を触ってみてほしい。そして、自分たちのサービスを、攻撃者が「狙う気すら失せる」ほど堅牢なものに変えていこう。質問があれば、いつでも現場の知見を共有する。次もまた、泥臭く、かつスマートな防御技術の話をしよう。
コメント