跳到主要内容
ecdsa.com

FIDO2 vs WebAuthn vs CTAP:通行密钥标准如何相互配合

三者并非互相替代。WebAuthn 是网站调用的 W3C API,CTAP 是设备与安全密钥或手机之间的 FIDO 协议,FIDO2 则是两者合在一起的名称。

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 凭证。

概览

WebAuthnCTAPFIDO2
是什么浏览器 API 和数据格式与漫游认证器之间的通信协议两者的总称
发布方W3CFIDO 联盟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)不适用

登录过程中各方分别做什么

  1. 网站携带挑战值和自己的依赖方 ID 调用 navigator.credentials.get()。这一步属于 WebAuthn。
  2. 浏览器构造 clientDataJSON(类型、挑战值、源、跨源标志),对它计算哈希,然后请求认证器签名。对于内置的平台认证器(Touch ID、Windows Hello、Android),这一步通过操作系统 API 完成;对于安全密钥或手机,则以 CBOR 编码的 authenticatorGetAssertion 命令经由 CTAP 发送。
  3. 认证器检查用户在场,如有要求还会检查用户验证,然后对自己的认证器数据与客户端数据哈希拼接后的内容签名。响应经由 CTAP 或平台 API 返回。
  4. 浏览器把这些字节返回给页面,服务器按 WebAuthn §7.2 的规定验证它们。

下面的脚本解析官方的 WebAuthn Level 3 测试向量 §16.2(其中包含一个 ES256 凭证的一次注册和一次身份验证),并验证签名(Node.js 26,cbor-x 1.6.0)。authData 和签名由认证器生成;clientDataJSON 由浏览器生成。

解析并验证 WebAuthn Level 3 测试向量 §16.2
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”。

迁移说明

  1. 替换 U2F 注册:登录时继续通过 appid 扩展发送旧的 App ID,并在用户下次登录时注册新的 WebAuthn 凭证。
  2. 采用通行密钥:请求 residentKey: "required",以及 userVerification: "preferred" 或 "required",并保存备份标志。
  3. 逐项升级到 Level 3 功能:条件式创建、getClientCapabilities()、信号方法和相关源在不受支持时都能平稳降级。
  4. 测试:上线前在通行密钥沙盒中试用平台认证器、USB 或 NFC 安全密钥,以及从手机发起的混合流程。

参考资料

全部对比