WebAuthn 深度解析:从 CTAP2 协议到 Autional 的完整实现
WebAuthn(Web Authentication)是 W3C 与 FIDO 联盟制定的、基于浏览器的免密认证标准。与传统的「用户名 + 密码」不同,WebAuthn 基于公钥密码学——客户端生成并持有私钥,服务器只保存公钥。私钥永不离开用户设备。
本文将从协议层逐层拆解,最后说明 Autional 如何把这套复杂协议封装成开箱即用的企业级能力。
协议总览:FIDO2 的分层架构
FIDO2 由两个核心部分组成:
flowchart TD
W["WebAuthn(W3C 规范)— 浏览器 JavaScript API,navigator.credentials.create() 与 navigator.credentials.get()"] --> C["CTAP2(FIDO 联盟规范)— Client-to-Authenticator 传输协议,支持 USB、NFC、BLE"]
C --> A["认证器 — YubiKey 硬件密钥、Touch ID 平台认证器"]
图 1:FIDO2 的三层架构——WebAuthn 是网页能触到的 API,CTAP2 负责浏览器与认证器之间的传输,密钥生成与签名都发生在认证器内。
- WebAuthn:运行在浏览器中的 JavaScript API,定义了网页与认证器如何交互。开发者通过
navigator.credentials.create()创建凭据,通过navigator.credentials.get()获取凭据断言。 - CTAP2(Client to Authenticator Protocol):浏览器与物理认证器之间的通信协议。当用户插入 USB 安全密钥或通过 NFC 触碰时,CTAP2 定义了数据传输格式。
- Authenticator(认证器):负责生成密钥对、保存私钥并执行签名操作的硬件或软件模块。
FIDO2 并不要求开发者了解 CTAP2 的细节——浏览器负责处理 CTAP2 通信,开发者只需调用 WebAuthn API。但理解协议全貌,有助于做出正确的安全架构决策。
注册流程:attestation(创建凭据)
注册是整个 WebAuthn 流程的起点——用户首次把一台设备绑定到账号上。
sequenceDiagram
participant B as 浏览器
participant S as 服务端
participant A as 认证器
B->>S: 请求注册挑战值
S-->>B: 返回 challenge 与 rp.id、pubKeyCredParams 参数
B->>A: 经 CTAP2 请求生成密钥对
Note over A: 提示用户验证——指纹、人脸或 PIN
A-->>B: 返回 attestationObject(公钥 + 签名)
B->>S: 提交 attestationObject
S->>S: 校验 challenge、origin、RP ID 与签名
S-->>B: 存储公钥,注册完成
图 2:注册流程的泳道时序——浏览器取挑战值,认证器生成密钥对并签名,attestationObject 回到服务端验签入库,私钥全程不出认证器。
第一步:服务端生成挑战值
Client → Server: POST /webauthn/register/begin
Body: { "display_name": "My YubiKey" }
Server Processing:
1. Generate 32-byte random challenge (crypto/rand)
2. Generate user ID (Autional ULID)
3. Query user's already-registered credentials (for excludeCredentials)
4. Store challenge temporarily (Redis, TTL 5 minutes)
Server → Client:
{
"challenge": "base64url...",
"rp": { "id": "auth.autional.com", "name": "Autional" },
"user": {
"id": "base64url...",
"name": "user@example.com",
"displayName": "Nickname"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 }, // ES256
{ "type": "public-key", "alg": -257 } // RS256
],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"userVerification": "required"
},
"attestation": "none"
}
rp.id 是 Relying Party ID——它必须是当前域名的合法子集。例如服务运行在 auth.autional.com 上,rp.id 可以是 auth.autional.com,但不能是 example.com。这个限制是 WebAuthn 抗钓鱼的核心机制之一。
authenticatorSelection 控制:
authenticatorAttachment: "platform"——仅平台认证器(例如 Touch ID、Windows Hello)authenticatorAttachment: "cross-platform"——仅漫游认证器(例如 USB 安全密钥)userVerification: "required"——需要生物特征或 PIN 解锁认证器
第二步:客户端调用 WebAuthn API
const publicKeyCredential = await navigator.credentials.create({
publicKey: optionsFromServer
});
// publicKeyCredential contains:
// - id: credential ID (base64url)
// - rawId: credential ID raw bytes
// - response.clientDataJSON: client data (challenge, origin, type)
// - response.attestationObject: authenticator data (public key, signature)
// - type: "public-key"
这次调用会触发浏览器的 WebAuthn 流程:
- 浏览器校验
rp.id与当前域名是否匹配 - 浏览器通过 CTAP2 与认证器通信,请求生成新的密钥对
- 认证器提示用户验证(指纹、人脸、PIN)
- 认证器生成 ECDSA(ES256)密钥对,私钥安全保存在认证器内
- 认证器用私钥对
clientDataJSON的哈希签名,生成 attestation - 返回包含公钥与签名的
attestationObject
第三步:服务端校验并存储
Client → Server: POST /webauthn/register/complete
Body: {
"id": "base64url...",
"rawId": "base64url...",
"response": {
"clientDataJSON": "base64url...",
"attestationObject": "base64url..."
},
"type": "public-key"
}
Server Processing:
1. Verify clientDataJSON.type === "webauthn.create"
2. Verify clientDataJSON.challenge === stored challenge
3. Verify clientDataJSON.origin === expected origin
4. Parse attestationObject, extract:
- authData: authenticator data (RP ID hash, flags, counter, public key)
- fmt: attestation format ("none", "packed", "tpm", etc.)
5. Verify RP ID hash in authData matches
6. Verify userPresent and userVerified flags in authData
7. Extract public key (CBOR decode → COSE Key → ECDSA public key)
8. Compute SHA-256 hash of clientDataJSON
9. Verify signature in attestationObject (optional, depends on attestation param)
10. Store in database:
- credential_id: credential ID
- public_key: public key (DER encoded)
- counter: signature counter (for clone detection)
- transports: supported transport methods (usb, nfc, ble, internal)
- device_name: user-set device name
Autional 的 mfa-service 在注册校验阶段做了以下安全加固:
- 严格校验来源:只接受配置好的允许来源列表,防止跨域攻击
- 挑战值防重放:挑战值一次性使用,用完立即删除,防止重放
- 计数器校验:保存认证器的签名计数器,后续校验要求计数器递增——若检测到计数器回退,说明私钥可能已被克隆
- 防重复注册:同一个 credential ID 不能注册到不同账号
认证流程:断言校验
认证流程比注册简单,因为公钥已经在服务端——只需验证用户确实持有对应的私钥。
第一步:服务端生成挑战值
Client → Server: POST /webauthn/authenticate/begin
Body: { "user_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }
Server Processing:
1. Query user's all registered credential IDs (for allowCredentials)
2. Generate new random challenge
3. Store challenge temporarily
4. Optionally restrict userVerification requirements
Server → Client:
{
"challenge": "base64url...",
"allowCredentials": [
{ "id": "base64url...", "type": "public-key", "transports": ["usb","nfc"] }
],
"userVerification": "required",
"timeout": 60000
}
第二步:客户端对挑战值签名
const assertion = await navigator.credentials.get({
publicKey: optionsFromServer
});
// assertion contains:
// - response.authenticatorData: authenticator data (RP ID hash, counter)
// - response.clientDataJSON: client data (challenge, origin)
// - response.signature: authenticator's signature over (authenticatorData + clientDataJSON hash) using private key
认证器的处理流程:
- 校验
rp.id与注册时保存的 RP ID 是否一致 - 提示用户验证(指纹/人脸/PIN)
- 用私钥对
(authData || SHA-256(clientDataJSON))签名 - 内部计数器递增(用于克隆检测)
- 返回签名结果
第三步:服务端校验签名
Server Processing:
1. Look up public key from database using credential_id
2. Verify clientDataJSON.type === "webauthn.get"
3. Verify clientDataJSON.challenge === stored challenge
4. Verify clientDataJSON.origin === expected origin
5. Verify RP ID hash in authenticatorData
6. Verify userPresent and userVerified flags
7. Verify signature counter > last recorded counter (anti-clone)
8. Construct signature data: authenticatorData || SHA-256(clientDataJSON)
9. Verify signature with public key (ECDSA verify)
10. Signature verification passes → authentication success
11. Update counter value in database
认证器类型与安全等级
| 特性 | 平台认证器 | 漫游认证器 |
|---|---|---|
| 实现形态 | Touch ID、Windows Hello、Android 生物识别 | YubiKey、飞天(Feitian)、Google Titan |
| 私钥存储 | 设备安全芯片(TEE/Secure Enclave) | 认证器内部安全芯片 |
| 跨设备使用 | 不直接支持(需通行密钥同步) | 支持(随身携带即可) |
| 用户验证 | 生物特征(指纹/人脸) | PIN 或生物特征(高端型号) |
| 抗钓鱼 | 高(来源绑定,私钥留在设备内) | 最高(物理隔离 + 来源绑定) |
| 丢失风险 | 设备丢失需走恢复流程 | 物理丢失需备用密钥 |
| 适用场景 | 日常登录、中低安全要求 | 管理操作、高安全要求 |
Autional 允许租户管理员在 MFA 策略中配置可接受的认证器类型:
webauthn_policy:
allowed_attachments:
- platform
- cross-platform
user_verification: required
attestation: none
Autional 的实现架构
Autional 完整的 WebAuthn 实现由两个服务协作完成:
identity-service 的职责
identity-service 提供面向用户的 WebAuthn API 入口:
POST /api/v1/mfa/webauthn/register/begin
POST /api/v1/mfa/webauthn/register/complete
POST /api/v1/mfa/webauthn/authenticate/begin
POST /api/v1/mfa/webauthn/authenticate/complete
GET /api/v1/mfa/webauthn/credentials
DELETE /api/v1/mfa/webauthn/credentials/{id}
主要职责:
- 管理挑战值的生成、存储与校验(与 session-service 协同)
- 控制用户交互流程
- 凭据的增删改查管理
mfa-service 的职责
mfa-service 承担 WebAuthn 协议的核心密码学运算:
Internal API (for identity-service consumption):
POST /api/v1/internal/mfa/webauthn/verify-registration
POST /api/v1/internal/mfa/webauthn/verify-authentication
主要职责:
attestationObject的 CBOR 解析- 公钥提取与格式转换(COSE Key → DER 公钥 → crypto.PublicKey)
- 注册签名校验
- 认证签名校验
- 计数器管理(防克隆)
为什么要拆成两个服务?
这种职责分离体现了 Autional 的微服务设计理念:
- 关注点分离:identity-service 负责用户交互,mfa-service 负责密码学运算。更换签名算法或新增认证器类型时,只需改动 mfa-service。
- 独立扩展:注册与认证中的密码学运算(ECDSA 校验)非常吃 CPU。通行密钥推广期间注册请求可能激增——mfa-service 可以独立扩容,不影响 identity-service。
- 安全边界:公钥存储与签名校验逻辑集中在 mfa-service,缩小了审计范围,也减小了安全风险面。
开发者体验
对集成 Autional 的开发者来说,启用通行密钥只需三步:
- 在管理控制台启用通行密钥:进入 MFA 策略配置,开启 WebAuthn,选择允许的认证器类型。
- 前端代码(零行):Autional 的登录页(auth-pages)已内置完整的 WebAuthn 流程。用户的浏览器会自动检测通行密钥支持情况。
- 用户注册:用户登录后进入安全设置,点击「添加通行密钥」,系统自动调用
navigator.credentials.create(),引导用户完成指纹/人脸注册。
整个过程不需要集成方理解 CTAP2、CBOR、COSE Key、attestation 等任何底层概念。
安全最佳实践
1. 严格配置 RP ID
RP ID 是 WebAuthn 抗钓鱼能力的基础,必须与你的域名精确匹配——不允许使用通配符。如果你的服务有多个子域(例如 app.example.com 与 admin.example.com),你需要在两种方案中做选择:用 example.com 作为共享 RP ID(凭据可在所有子域通用),或为每个子域配置独立的 RP ID(隔离性更高)。
在 Autional 的多租户场景中,每个租户可以拥有独立域名;系统会在注册时为每个租户配置正确的 RP ID。
2. 用户验证策略
userVerification 有三个级别:
discouraged:不要求用户验证。适合低风险操作。preferred:认证器支持时建议验证。适合日常登录。required:必须验证。适合敏感操作。
Autional 的自适应 MFA 引擎可以根据操作的风险等级动态决定 userVerification 策略——查看资料用 preferred,修改密码用 required,大额转账用 required + 强制硬件密钥。
3. 凭据备份与恢复
用户注册通行密钥后,需要防止因设备丢失而被锁在门外。Autional 的策略:
- 通行密钥作为主要认证因素(替代密码),同时保留 TOTP 与备用码作为恢复通道
- 支持同一账号注册多个通行密钥(主设备 + 备用设备)
- 在管理控制台提供管理员重置 MFA 的功能(需审批流程)
总结
WebAuthn 代表着身份认证的未来——用公钥密码学取代共享秘密。它的安全性远超密码与传统 MFA,原因在于:
- 私钥永不离开设备:服务端只保存公钥,即使数据库被拖库也无法伪造认证
- 与域名绑定:钓鱼站点无法通过来源校验,在协议层消灭钓鱼
- 无需记忆:用户体验优于密码
Autional 把 WebAuthn 从一个复杂的协议标准封装成了开箱即用的能力。你的应用只需一次 API 调用,就能让用户用指纹登录。
相关阅读
准备好给应用加上身份层了吗?
Autional 提供认证、组织、访问控制、审计与开发者 API —— 面向 AI 生成应用的身份层。