パスワード暗記が不要になる「パスキー(FIDO2)」安心ガイド:フィッシング詐欺を仕組みで防ぐ玄関の生体鍵
監修:加藤 明生(登録情報処理安全確保支援士(第007617号))
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 身近なたとえで見る仕組み:「合言葉の紙メモ」と「ドアと対になるスマートキー」 | 指紋や顔のデータはお手元のスマホやPCの安全チップ(Secure Enclave / TPM)内だけで照合され、外部サーバーへは一切送信されません。 |
| 2. 現場での進め方と対策 | 図解で見る仕組み:パスワードとパスキーのちがい | AppleやGoogleアカウント等で安全に同期されるため機種変更時も引き継げます。紛失時も生体認証が守るため悪用されず、別端末からログイン可能です。 |
| 3. 関連する制度とルール | 実践のポイント:パスキーが守る3つの安心 | はい。Windows HelloやMacのTouch IDを使って、業務パソコン自体にパスキーを登録できます。 |
| 4. 詳しい技術仕様としくみ | パスキーを使うと、指紋や顔のデータが運営会社に送られませんか? | 「パスキー(FIDO2)」は、文字列を暗記する代わりにスマホやPCの指紋・顔認証でログインできる新しい生体鍵です。本物のサイトと端末が1対1で結びつくため、偽サイトを開いても仕組み上「鍵が原則として開かない」安心が手に入ります。 |
この記事の要点
「パスキー(FIDO2)」は、文字列を暗記する代わりにスマホやPCの指紋・顔認証でログインできる新しい生体鍵です。本物のサイトと端末が1対1で結びつくため、偽サイトを開いても仕組み上「鍵が原則として開かない」安心が手に入ります。
01. 身近なたとえで見る仕組み:「合言葉の紙メモ」と「ドアと対になるスマートキー」
従来の方式は「合言葉の紙メモ(パスワード)」でした。泥棒が偽窓口を作って尋ねれば、人間はうっかり教えてしまいます。一方パスキーは「本人とドアが対になるスマートキー(公開鍵暗号)」です。偽ドアの前ではドメインが一致せず反応しないため、鍵を盗まれません。
年間171万件超
フィッシング報告件数は年間171万件超。目視だけで偽サイトを見抜くのは困難です。
原因の80%以上
不正ログイン原因の80%以上がパスワード使い回し・窃取に起因しています。
フィッシング突破率 0%
偽サイトではドメイン不一致で鍵が作動しないため、フィッシング突破率0%を実現します。
02. 図解で見る仕組み:パスワードとパスキーのちがい
| 比較項目 | 従来のやり方・よくある課題 | パスキー(FIDO2)による解決策 |
|---|---|---|
| ログイン操作 | 長い文字列を暗記・手入力しSMSコードも打ち直す | 指紋・顔認証に1秒触れるだけ(暗記・手入力ゼロ) |
| 偽サイト対策 | 偽画面にうっかり入力するとその場で突破される | ブラウザがURLを自動照合し偽サイトでは鍵が開かない |
| 定期変更負担 | 定期変更を強いられ付箋メモや使い回しが発生 | 定期変更不要。一度設定すればずっと安全・快適 |
03. 実践のポイント:パスキーが守る3つの安心
1. なぜSMS確認コードだけでは防げなくなったのか
近年のフィッシング詐欺(AiTM:Adversary-in-the-Middle攻撃)は、偽画面に入力されたパスワードとSMSコードを0.1秒で本物サイトへ中継します。人間の目視だけに頼らず、文字を入力しない認証への移行が重要です。
2. 指紋や顔のデータは端末の外へ一切出ない
パスキーではサービス側に「鍵穴(公開鍵)」だけを預け、「鍵(秘密鍵)」は端末内の安全チップに保管されます。生体情報は端末内で鍵を動かすためだけに使われ外部には送られません。
3. 中小企業は「メール・会計・通販」の3つから開始
Google、Microsoft 365、freee会計、Amazon等は無料でパスキーに対応済みです。重要窓口から有効化するだけで、経産省「SCS評価制度(★3)」の認証強化要件にも直結します。
今日からできる3項目チェックリスト
W3C WebAuthn Level 3 / FIDO2 における authenticatorData バイト構造と COSE ES256 (P-256) / Ed25519 署名検証
本稿では、フィッシング詐欺やクレデンシャル・スタッフィング攻撃を暗号学的に無効化する『パスキー(Passkey / FIDO2 WebAuthn)』のプロトコル内部構造をバイト単位で詳細解説し、(1) ブラウザ層が clientDataJSON.origin をハードウェア署名対象へ束縛するフィッシング耐性の数理証明、(2) 37バイト以上の authenticatorData(rpIdHash + flags + signCount + COSE公開鍵)のバイナリレイアウト、および (3) Rust によるゼロアロケーション認証データ検証実装を提示する。
1. 【なぜ従来のSMS二要素認証(OTP)はリアルタイム・リバースプロキシ型フィッシングに無力なのか】 Evilginx 等の Man-in-the-Middle(MitM)フィッシングキットは、被害者が偽サイト(例:login-bank-secure.example)に入力したID・パスワード・6桁のワンタイムパスコード(TOTP/SMS)をミリ秒単位で正規サイトへ中継し、発行されたセッショントークン(Cookie)を奪取する。人間が文字列を読み取って手入力する方式である限り、どれほど桁数を増やしてもオリジン(ドメイン名)の真正性を暗号学的に束縛できないためである。
2. 【WebAuthn における Origin 束縛の暗号学的証明】 WebAuthn(navigator.credentials.get)では、ブラウザ(User Agent)が TLS チャネルの接続先オリジンを検証した上で clientDataJSON({"type":"webauthn.get","challenge":"...","origin":"https://ok.st"})を生成し、またスマートフォンの Secure Enclave / TPM 内の認証器(Authenticator)は要求元の Relying Party ID の SHA-256 ハッシュ rpIdHash = SHA256(rpId)(32バイト)に紐づく秘密鍵 sk_RP のみを検索する。仮に攻撃者がフィッシングサイト上で WebAuthn API を呼び出しても、ブラウザが origin に攻撃者のドメインを刻印し、認証器は正規サイトの秘密鍵を一切解放しないため、フィッシングはプロトコル層で 100% 失敗する。
3. 【authenticatorData のバイナリ構造(37+ bytes)と署名対象データの結合】 認証器が生成する authenticatorData は、先頭 32バイトが rpIdHash、オフセット 32(1バイト)が flags ビットフィールド(Bit 0 UP: User Present、Bit 2 UV: User Verified[生体認証またはPIN完了]、Bit 3 BE: Backup Eligible、Bit 4 BS: Backup State、Bit 6 AT: Attested Credential Data 付属)、オフセット 33〜36(4バイト Big-Endian u32)が署名カウンタ signCount である。最終的な ECDSA P-256(COSE alg = -7)または Ed25519(COSE alg = -8)の署名は、authData || SHA256(clientDataJSON) の連結バイト列に対して計算される。
| Byte Offset | Field Name | Length (Bytes) | Bitwise / Cryptographic Verification Rule |
|---|---|---|---|
| 0x00 .. 0x1F | rpIdHash | 32 Bytes | Must equal SHA-256(relying_party_domain_ascii) in constant time |
| 0x20 | flags | 1 Byte (u8) | Bit 0 (UP=0x01) & Bit 2 (UV=0x04) must be 1 for passwordless MFA login |
| 0x21 .. 0x24 | signCount | 4 Bytes (u32 BE) | If > 0, must be strictly greater than stored_sign_count (clone detection) |
| 0x25 .. End | attestedCredentialData | Variable (Registration) | 16B AAGUID + 2B credIdLen + credId + CBOR COSE_Key (kty, alg, crv, x, y) |
#![forbid(unsafe_code)]
pub const FLAG_USER_PRESENT: u8 = 1 << 0; — 0x01 (UP)
pub const FLAG_USER_VERIFIED: u8 = 1 << 2; — 0x04 (UV)
pub const FLAG_BACKUP_ELIGIBLE: u8 = 1 << 3; — 0x08 (BE - Synced Passkey capability)
pub const FLAG_BACKUP_STATE: u8 = 1 << 4; — 0x10 (BS - Currently backed up)
pub const FLAG_ATTESTED_CRED: u8 = 1 << 6; — 0x40 (AT)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedAuthData<'a> {
pub rp_id_hash: &'a [u8; 32],
pub flags: u8,
pub sign_count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebAuthnVerifyError {
BufferTooShort,
RpIdHashMismatch,
UserPresenceRequired,
UserVerificationRequired,
ClonedAuthenticatorSuspected { stored: u32, received: u32 },
}
/// Parses and validates the 37-byte header of a WebAuthn `authenticatorData` payload
/// in constant time without heap allocations.
pub fn verify_authenticator_data<'a>(
raw: &'a [u8],
expected_rp_id_hash: &[u8; 32],
stored_sign_count: u32,
require_user_verification: bool,
) -> Result<ParsedAuthData<'a>, WebAuthnVerifyError> {
if raw.len() < 37 {
return Err(WebAuthnVerifyError::BufferTooShort);
}
let rp_id_hash: &[u8; 32] = raw[0..32].try_into().unwrap();
// Constant-time 32-byte comparison to prevent timing side-channels
let mut diff: u8 = 0;
for i in 0..32 {
diff |= rp_id_hash[i] ^ expected_rp_id_hash[i];
}
if diff != 0 {
return Err(WebAuthnVerifyError::RpIdHashMismatch);
}
let flags = raw[32];
if (flags & FLAG_USER_PRESENT) == 0 {
return Err(WebAuthnVerifyError::UserPresenceRequired);
}
if require_user_verification && (flags & FLAG_USER_VERIFIED) == 0 {
return Err(WebAuthnVerifyError::UserVerificationRequired);
}
let sign_count = u32::from_be_bytes([raw[33], raw[34], raw[35], raw[36]]);
if (sign_count > 0 || stored_sign_count > 0) && sign_count <= stored_sign_count {
return Err(WebAuthnVerifyError::ClonedAuthenticatorSuspected {
stored: stored_sign_count,
received: sign_count,
});
}
Ok(ParsedAuthData {
rp_id_hash,
flags,
sign_count,
})
}
FIDO2 / WebAuthn Level 3 と CTAP2 公開鍵暗号によるフィッシング無効化プロトコル実装
プロトコル仕様・数理モデル・Rust 2024 実装コードの詳細は、専門技術モノグラフにて公開しています。