FIDO2 と WebAuthn の違いは、どちらかを選ぶ問題ではなく、層の問題です。WebAuthn(W3C Web Authentication)は、リライングパーティが呼び出す JavaScript API と、リライングパーティが検証する署名付きデータの形式を定めています。CTAP(FIDO Client to Authenticator Protocol)は、ブラウザーやオペレーティングシステムが USB、NFC、Bluetooth、または QR コードを使うハイブリッドトランスポートを介して外部の認証器と通信する方法を定めています。FIDO2 は、FIDO Alliance が 2018年に導入した、WebAuthn と CTAP 2 を合わせた総称です。パスキーは、これらの標準によって作成される FIDO の認証情報です。
比較表
| WebAuthn | CTAP | FIDO2 | |
|---|---|---|---|
| 概要 | ブラウザーの API とデータ形式 | ローミング認証器との通信プロトコル | 両者の総称 |
| 策定団体 | W3C | FIDO Alliance | FIDO Alliance |
| 現行バージョン | Level 3、W3C 勧告(2026年8月25日) | CTAP 2.3、Proposed Standard(2026年2月26日)。2.2 は 2025年7月 | WebAuthn Level 3 と CTAP 2.3 |
| 以前のバージョン | Level 1(2019年3月)、Level 2(2021年4月) | CTAP 2.0、2.1(2021年)。CTAP1 は U2F | U2F(2014年)が前身 |
| 実装する側 | ブラウザーとプラットフォーム(API)、リライングパーティ(サーバーでの検証) | ブラウザー、オペレーティングシステム、セキュリティキー、スマートフォン | 関係するすべての当事者 |
| 対象となる区間 | Web ページとブラウザー、サーバーと認証器データ | クライアントプラットフォームと認証器 | リライングパーティから認証器まで |
| エンコーディング | JavaScript オブジェクト、clientDataJSON、CBOR のアテステーションオブジェクト | authenticatorGetAssertion(0x02)などの CTAP2 正準 CBOR コマンド | 両方 |
| トランスポート | 該当なし | USB HID、NFC、Bluetooth Low Energy、ハイブリッド(QR コード、近接確認、トンネル) | 両方 |
| 主なエントリーポイント | navigator.credentials.create() と .get() | authenticatorMakeCredential(0x01)、authenticatorGetInfo(0x04) | 該当なし |
ログイン時の役割分担
- Web サイトが、チャレンジと自身のリライングパーティ ID を指定して
navigator.credentials.get()を呼び出します。ここが WebAuthn です。 - ブラウザーは
clientDataJSON(タイプ、チャレンジ、オリジン、クロスオリジンフラグ)を組み立ててハッシュし、認証器に署名を求めます。内蔵のプラットフォーム認証器(Touch ID、Windows Hello、Android)の場合、この要求はオペレーティングシステムの API を経由します。セキュリティキーやスマートフォンの場合は、CBOR のauthenticatorGetAssertionコマンドとして CTAP で送られます。 - 認証器はユーザー存在確認と、要求された場合はユーザー検証を行い、認証器データにクライアントデータのハッシュを連結したものに署名します。レスポンスは CTAP またはプラットフォームの API を通じて返されます。
- ブラウザーはそのバイト列をページに返し、サーバーは WebAuthn §7.2 の規定どおりにそれを検証します。
以下のスクリプトは、ES256 の認証情報の登録と認証を含む公式の WebAuthn Level 3 テストベクトル §16.2 を解析し、署名を検証します(Node.js 26、cbor-x 1.6.0)。authData と署名は認証器が生成したもので、clientDataJSON はブラウザーが生成したものです。
import { createHash, createPublicKey, verify } from "node:crypto";
import { decode } from "cbor-x";
// WebAuthn Level 3 §16.2 test vector: ES256 credential, "none" attestation
const hex = (s) => Buffer.from(s, "hex");
const attestationObject = hex("a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220");
const authenticatorData = hex("bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000");
const clientDataJSON = hex("7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d");
const signature = hex("3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87");
const sha256 = (b) => createHash("sha256").update(b).digest();
const flagNames = { 0x01: "UP", 0x04: "UV", 0x08: "BE", 0x10: "BS", 0x40: "AT", 0x80: "ED" };
const flags = (b) => Object.entries(flagNames).filter(([bit]) => b & bit).map(([, n]) => n).join(" ");
// Registration: authenticator data produced by the authenticator (CTAP authenticatorMakeCredential)
const { fmt, authData } = decode(attestationObject);
const credIdLen = authData.readUInt16BE(53);
const coseKey = decode(authData.subarray(55 + credIdLen));
console.log("attestation format:", fmt);
console.log("rpIdHash == SHA-256('example.org'):", authData.subarray(0, 32).equals(sha256("example.org")));
console.log("flags:", flags(authData[32]), "| signCount:", authData.readUInt32BE(33));
console.log("AAGUID:", authData.subarray(37, 53).toString("hex"), "| credential ID bytes:", credIdLen);
console.log("COSE key: kty", coseKey[1], "alg", coseKey[3], "crv", coseKey[-1]);
// Authentication: verify the assertion with the registered public key
const publicKey = createPublicKey({ key: { kty: "EC", crv: "P-256",
x: Buffer.from(coseKey[-2]).toString("base64url"), y: Buffer.from(coseKey[-3]).toString("base64url") }, format: "jwk" });
console.log("\nclientDataJSON (built by the browser):", clientDataJSON.toString());
console.log("assertion flags:", flags(authenticatorData[32]));
const ok = verify("sha256", Buffer.concat([authenticatorData, sha256(clientDataJSON)]), publicKey, signature);
console.log("signature over authenticatorData || SHA-256(clientDataJSON):", ok ? "valid" : "invalid");
attestation format: none
rpIdHash == SHA-256('example.org'): true
flags: UP BE BS AT | signCount: 0
AAGUID: 8446ccb9ab1db374750b2367ff6f3a1f | credential ID bytes: 32
COSE key: kty 2 alg -7 crv 1
clientDataJSON (built by the browser): {"type":"webauthn.get","challenge":"OcDnUhQXulTUPo3JUXT0I97pvzzYBP9tZchXyav01Ag","origin":"https://example.org","crossOrigin":false}
assertion flags: UP BE BS
signature over authenticatorData || SHA-256(clientDataJSON): valid
登録時のフラグは、ユーザー存在確認(UP)、バックアップ可能でかつバックアップ済みの認証情報(BE、BS:同期パスキー)、アテステーション済みの認証情報データ(AT)を示しています。COSE 鍵(鍵タイプ 2 = EC2、アルゴリズム −7 = ES256、曲線 1 = P-256)は両方の仕様に共通の形式です。WebAuthn インスペクターは、実際のブラウザーのレスポンスから同じ構造をデコードします。
U2F からパスキーへ
FIDO U2F(2014年)は、現在 CTAP1 と呼ばれるシンプルな二要素認証用のプロトコルと、ブラウザーの API をセキュリティキーにもたらしました。WebAuthn は 2018〜2019年にその API を置き換え(Chrome 67、Firefox 60、Safari 13)、CTAP 2 は PIN、内蔵のユーザー検証、検出可能な認証情報を追加しました。これにより、セキュリティキーはパスワードを補うのではなく、パスワードに取って代われるようになりました。Chrome はバージョン 104(2022年)で古い U2F JavaScript API を削除しましたが、その API で登録された認証情報は、appid 拡張機能を通じて WebAuthn で引き続き使えます。
2022年に Apple、Google、Microsoft が同期パスキーを発表したとき、新しいプロトコルは必要ありませんでした。パスキーは、バックアップフラグが設定された検出可能な認証情報であり、同じ WebAuthn API で作成・使用されるからです。CTAP 2.2(2025年7月)は、QR コードを読み取った後にスマートフォンが近くのコンピューターの認証器として機能できるハイブリッドトランスポートを、PIN の複雑さに関するポリシーとともに標準化しました。CTAP 2.3(2026年2月)は小規模な改訂で、たとえば認証器が対応している設定コマンドを報告できるようになりました。
アテステーションと認定
登録時に認証器は、同じバッチのデバイス間で共有される鍵で署名されたアテステーションステートメントと、モデルを識別する AAGUID を含めることができます。WebAuthn はステートメントの形式(packed、tpm、android-key、apple、fido-u2f、none)を定めています。FIDO Alliance は認証器を認定し、アテステーションのルートや認定レベルを含むメタデータを FIDO メタデータサービスで公開しています。一般消費者向けのサイトのほとんどは、アテステーションを要求せず、どの認証器も受け入れるべきです。認定済みのハードウェアキーだけを許可しなければならない企業は、direct または enterprise のアテステーションを要求してメタデータと照合します。その例は YubiKey アテステーション検証ツールと AAGUID 検索ツールで確認できます。
実装すべきもの
- Web サイトまたは Web アプリ:ブラウザーでの WebAuthn と、§7 の検証手順を実装したサーバーライブラリ
- iOS または Android のネイティブアプリ:関連ドメイン(Associated Domains)または Digital Asset Links によってアプリの Web ドメインに結び付けられた、WebAuthn 形式のデータを返すプラットフォームのパスキー API(AuthenticationServices、Credential Manager)
- ハードウェアキーだけを許可する企業:FIDO メタデータサービスのエントリーと照合するアテステーション付きの WebAuthn
- セキュリティキー、スマートフォン、OS のベンダー:ローミング認証器向けの CTAP と、製品の FIDO 認定
- 調達:「FIDO2 対応」の一言ではなく、FIDO 認定レベル、対応する CTAP のバージョンとトランスポート、利用する WebAuthn の機能を要件として明示
移行時の注意点
- U2F の登録を置き換える:ログイン時には
appid拡張機能で古い App ID を引き続き送信し、ユーザーが次回ログインしたときに新しい WebAuthn の認証情報を登録します。 - パスキーを導入する:
residentKey: "required"と、userVerification: "preferred"または"required"を要求し、バックアップフラグを保存します。 - Level 3 へのアップグレードは機能ごとに 1 つずつ行います。条件付き作成、
getClientCapabilities()、シグナルメソッド、関連オリジンは、いずれも未対応の環境では問題なく縮退動作します。 - テスト:リリース前に、パスキーサンドボックスでプラットフォーム認証器、USB または NFC のキー、スマートフォンからのハイブリッドフローを試します。
参考資料
- W3C Web Authentication Level 3(§6.1 認証器データ、§7 リライングパーティの処理、§16 テストベクトル)
- FIDO Client to Authenticator Protocol 2.3
- FIDO Client to Authenticator Protocol 2.2(§6 コマンド、§11 トランスポートバインディング)
- FIDO メタデータサービス
- RFC 9052:COSE の構造と処理と RFC 9053:COSE のアルゴリズム