FIDO2 与 WebAuthn 的关系是层次问题,而不是二选一。WebAuthn(W3C Web Authentication,即 Web 身份验证规范)定义了依赖方调用的 JavaScript API,以及依赖方要验证的已签名数据格式。CTAP(FIDO Client to Authenticator Protocol,即客户端到认证器协议)定义了浏览器或操作系统如何通过 USB、NFC、蓝牙或基于 QR 码的混合传输与外部认证器通信。FIDO2 是 FIDO 联盟于2018年提出的总称,指 WebAuthn 加上 CTAP 2。通行密钥(passkey)是通过这些标准创建的 FIDO 凭证。
概览
| WebAuthn | CTAP | FIDO2 | |
|---|---|---|---|
| 是什么 | 浏览器 API 和数据格式 | 与漫游认证器之间的通信协议 | 两者的总称 |
| 发布方 | W3C | FIDO 联盟 | FIDO 联盟 |
| 当前版本 | Level 3,W3C 推荐标准,2026年8月25日 | CTAP 2.3,拟议标准,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)、依赖方(服务端检查) | 浏览器、操作系统、安全密钥、手机 | 链条中的每一方 |
| 作用范围 | 网页与浏览器之间;服务器与认证器数据之间 | 客户端平台与认证器之间 | 依赖方到认证器 |
| 编码 | JavaScript 对象、clientDataJSON、CBOR 证明对象 | CTAP2 规范化 CBOR 命令,例如 authenticatorGetAssertion(0x02) | 两者兼有 |
| 传输方式 | 不适用 | USB HID、NFC、低功耗蓝牙、混合传输(QR 码、近距离检查、隧道) | 两者兼有 |
| 主要入口 | navigator.credentials.create() 和 .get() | authenticatorMakeCredential(0x01)、authenticatorGetInfo(0x04) | 不适用 |
登录过程中各方分别做什么
- 网站携带挑战值和自己的依赖方 ID 调用
navigator.credentials.get()。这一步属于 WebAuthn。 - 浏览器构造
clientDataJSON(类型、挑战值、源、跨源标志),对它计算哈希,然后请求认证器签名。对于内置的平台认证器(Touch ID、Windows Hello、Android),这一步通过操作系统 API 完成;对于安全密钥或手机,则以 CBOR 编码的authenticatorGetAssertion命令经由 CTAP 发送。 - 认证器检查用户在场,如有要求还会检查用户验证,然后对自己的认证器数据与客户端数据哈希拼接后的内容签名。响应经由 CTAP 或平台 API 返回。
- 浏览器把这些字节返回给页面,服务器按 WebAuthn §7.2 的规定验证它们。
下面的脚本解析官方的 WebAuthn Level 3 测试向量 §16.2(其中包含一个 ES256 凭证的一次注册和一次身份验证),并验证签名(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;通过它注册的凭证仍可借助 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 联盟为认证器提供认证,并在 FIDO 元数据服务中发布认证器的元数据,包括证明根证书和认证级别。大多数面向消费者的网站不应请求证明,并应接受任何认证器;必须只允许经过认证的硬件密钥的企业,则请求 direct 或 enterprise 证明,并依据元数据进行检查,YubiKey 证明验证工具和 AAGUID 查询演示了这一做法。
该实现什么
- 网站或 Web 应用:在浏览器中使用 WebAuthn,并使用实现了 §7 验证步骤的服务端库。
- 原生 iOS 或 Android 应用:平台的通行密钥 API(AuthenticationServices、Credential Manager),它们返回 WebAuthn 格式的数据,并通过关联域(associated domains)或 Digital Asset Links 绑定到你的 Web 域名。
- 只允许硬件密钥的企业:带证明的 WebAuthn,并依据 FIDO 元数据服务的条目检查证明。
- 安全密钥、手机或操作系统厂商:为漫游认证器实现 CTAP,并为产品申请 FIDO 认证。
- 采购:询问 FIDO 认证级别、支持的 CTAP 版本和传输方式,以及你所依赖的 WebAuthn 功能,而不是只问是否“支持 FIDO2”。
迁移说明
- 替换 U2F 注册:登录时继续通过
appid扩展发送旧的 App ID,并在用户下次登录时注册新的 WebAuthn 凭证。 - 采用通行密钥:请求
residentKey: "required",以及userVerification: "preferred"或"required",并保存备份标志。 - 逐项升级到 Level 3 功能:条件式创建、
getClientCapabilities()、信号方法和相关源在不受支持时都能平稳降级。 - 测试:上线前在通行密钥沙盒中试用平台认证器、USB 或 NFC 安全密钥,以及从手机发起的混合流程。
参考资料
- W3C Web 身份验证规范 Level 3(§6.1 认证器数据,§7 依赖方操作,§16 测试向量)
- FIDO 客户端到认证器协议 2.3
- FIDO 客户端到认证器协议 2.2(§6 命令,§11 传输绑定)
- FIDO 元数据服务
- RFC 9052:COSE 结构与处理和 RFC 9053:COSE 算法