午前2時のアラート:なぜパスワードは負債なのか
数年前の午前2時、私のスマートフォンがアラートの通知で埋め尽くされました。ボットネットが私のサーバーに対して大規模なSSHブルートフォース攻撃を仕掛けていたのです。なんとか食い止めることはできましたが、その一件で従来のログインシステムに対する信頼は完全に打ち砕かれました。専用サーバーが1分間に数千回ものログイン試行を受けるのであれば、パスワードを使い回している一般ユーザーがいかに脆弱であるかは想像に難くありません。
調査によると、データ漏洩の約81%は、脆弱な、あるいは盗まれた認証情報に起因しています。私たちは何十年もの間「共有シークレット」に依存してきました。問題は単純です。ユーザーとサーバーの両方がそのシークレットを知っていなければならないという点です。ユーザーがフィッシングに遭えばシークレットは盗まれます。データベースが流出すれば、全ユーザーが危険にさらされます。SMSベースの多要素認証(MFA)でさえ、SIMスワップ攻撃がハッカーの常套手段となった今、もはや安全とは言えません。
根本的な欠陥:シークレットを通信させてはいけない理由
従来の認証が失敗するのは、機密データをネットワーク上に流す必要があるからです。暗号化されたHTTPS接続であっても、パスワードはブラウザのメモリやサーバーのRAMに一瞬だけ存在します。これにより、主に3つの大きな脆弱性が生まれます。
- 認証情報の詰め合わせ攻撃(パスワードリスト攻撃): 65%の人が複数のサイトでパスワードを使い回しているため、小さな掲示板での漏洩が企業の銀行口座の侵害につながる可能性があります。
- フィッシング: 攻撃者は偽のUI構築のプロです。ユーザーを騙して認証情報を入力させるのに、1分もかかりません。
- オフライン解析: ハッカーがユーザーテーブルを盗み出せば、ハッシュ値に対して1秒間に数十億回もの推測を実行できます。
シークレットがユーザーのハードウェアから決して出ないシステムが必要です。認証は特定のドメインに紐付けられ、誤ったサイトに認証情報を「入力」すること自体を不可能にすべきです。
比較:パスワード vs MFA vs パスキー
すべての認証方法が同じように作られているわけではありません。現実世界での比較は以下の通りです。
1. 従来のパスワード
開発者は実装しやすいですが、現代の脅威に対しては無力です。IT史上、最大の攻撃ベクトルであり続けています。
2. レガシーなMFA(SMS、メール、TOTP)
セキュリティ層は追加されますが、大きな摩擦(手間)が生じます。ユーザーはコードを待つのを嫌います。さらに、ユーザーが「承認」をクリックするまで通知を送り続ける「MFA疲労」攻撃も、現在では一般的な回避手法となっています。
3. パスキー(WebAuthn)
パスキーは公開鍵暗号方式に基づいています。デバイス(スマートフォンやノートPC)がサイトごとに固有の鍵ペアを作成します。**秘密鍵**はハードウェア内の安全な領域(セキュア・エンクレーブ)に留まり、FaceIDなどの生体認証で保護されます。サーバーに送られるのは**公開鍵**のみです。ブラウザが鍵を動作させる前にドメイン名をチェックするため、フィッシングは数学的に不可能になります。
実装:SimpleWebAuthnによるWebAuthnの構築
生のWeb Authentication APIは強力ですが、非常に冗長です。開発の効率を上げるために、@simplewebauthn の使用をお勧めします。これは、WebAuthnの実装を困難にしているバイナリのエンコード・デコード処理をスマートに処理してくれます。
ステップ1:登録フロー
パスキーを登録するには、サーバーが「チャレンジ」を発行する必要があります。これは、ハッカーが過去のログインセッションを記録して再利用できないようにするためのランダムな文字列です。
サーバー側(Node.js):
import { generateRegistrationOptions } from '@simplewebauthn/server';
const options = await generateRegistrationOptions({
rpName: 'SecureApp Inc',
rpID: 'example.com',
userID: user.id,
userName: user.email,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
},
});
// 後で検証するためにチャレンジをセッションに保存する
request.session.currentChallenge = options.challenge;
return options;
クライアント側(JavaScript):
import { startRegistration } from '@simplewebauthn/browser';
// APIからチャレンジを取得する
const resp = await fetch('/generate-registration-options');
const options = await resp.json();
// ネイティブのFaceID/TouchIDプロンプトを起動する
const registrationResponse = await startRegistration(options);
// 生体認証の署名をサーバーに送り返す
await fetch('/verify-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(registrationResponse),
});
ステップ2:検証と保存
サーバーがレスポンスを受け取ったら、署名を検証する必要があります。検証に成功したら、公開鍵を保存します。このユーザーに対して、二度とパスワードを求める必要はありません。
import { verifyRegistrationResponse } from '@simplewebauthn/server';
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge: request.session.currentChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
});
if (verification.verified) {
const { credentialID, credentialPublicKey, counter } = verification.registrationInfo;
// これらのフィールドをデータベースに保存する
await db.saveKey(user.id, credentialID, credentialPublicKey, counter);
}
本番環境への導入から学んだ教訓
パスキーを実際のユーザーに提供して分かったのは、コードの実装は戦いの半分に過ぎないということです。以下の点に注意してください。
- フォールバックをまだ廃止しない: 最新ブラウザの98%がWebAuthnをサポートしていますが、ユーザーはスマートフォンを紛失することがあります。使い捨てのリカバリーコードやメールによるマジックリンクなどのバックアップ手段を必ず用意しましょう。
- rpIDは永続的な選択である:
rpIDはメインドメインにする必要があります。test.comで登録した鍵は、realapp.comに移行するとすべて使えなくなります。ドメインは早期に決定し、変更しないようにしましょう。 - 複数デバイスでの登録を促す: スマートフォンとノートPCの両方を登録するようユーザーに勧めましょう。これにより、片方のデバイスが故障してもロックアウトされないハードウェアベースのバックアップ体制が整います。
- 速さは機能である: パスキーによるログインは、通常、パスワードとMFAコードを入力するよりも50%高速です。このスピードをユーザーにアピールして、導入を促進しましょう。
パスワード時代の終焉
パスワードレス・アーキテクチャへの移行は、実施可能なセキュリティ対策の中で最も効果的なアップグレードです。ハッカーがシステムに侵入する最も一般的な経路を断ち切ると同時に、ユーザーの利便性も向上させます。「パスワードを忘れた」というループや、煩わしいSMSの遅延に悩まされることはもうありません。素早い生体認証スキャンだけで、すぐにログインできます。
私は今でもサーバーログを監視していますが、ブルートフォース攻撃のアラートに動じることはなくなりました。盗むべきパスワードがなければ、ハッカーはもっと簡単な標的を探しに行くしかないのです。

