学习 WebAuthn,先把“网站、浏览器、认证器、账户、凭据”区分开。它们承担不同职责;错误地把某一方返回的数据直接当成可信事实,往往比加密算法选错更容易造成漏洞。
英文原文 §4 Terminology · §6 WebAuthn Authenticator Model
参与方:谁信任谁#
| 英文术语 | 本书译法 | 具体职责 |
|---|---|---|
| Relying Party / RP | 依赖方,通常就是你的网站 | 产生请求,保存公钥,验证响应,管理账户和会话 |
| WebAuthn Client / User Agent | WebAuthn 客户端 / 用户代理 | 通常由浏览器与系统协作;检查调用来源、协调认证器、组织客户端数据 |
| Authenticator | 认证器 | 创建或使用凭据密钥,执行用户在场 / 验证操作,产生响应 |
| User | 用户 | 选择账户、授权创建或使用凭据 |
| Credential | 凭据 | 面向某个 RP 的认证材料;不能简单等同于一个用户账户 |
| Ceremony | 交互流程 | 由多方共同完成的注册或认证过程 |
网页脚本不能读取凭据私钥。网站收到的是凭据标识、公钥相关材料、签名及上下文数据。生物识别通常在认证器本地执行;网站只获得 UP/UV 等结果标志,不获得指纹图像、人脸模型或设备 PIN。英文原文 §14.3 本地生物识别
“不把私钥发给网站”并不等于“私钥永远只存在于一块硬件里”。现代同步型通行密钥可能由提供者在受保护的同步体系中同步;设备绑定型凭据有不同的保护与恢复模型。网站需要依据实际政策和验证结果判断保证程度,不能从一个指纹弹窗推断密钥一定不可导出。
origin 与 RP ID:一个精确来源,一个凭据作用域#
origin 是来源,通常包含协议、主机与端口;例如 https://login.example.com:8443。它不包含路径。
RP ID 是凭据的作用域标识,采用域名形式,没有协议、端口或路径。对于 Level 2 的常规 Web 场景,RP ID 必须是当前有效域名本身,或该域名的可注册域后缀;不能随意指定别人的域名,也不能使用公共后缀。英文原文:RP ID 定义
| 页面 origin | 请求的 RP ID | 是否符合常规域名关系 |
|---|---|---|
https://login.example.com:8443 |
login.example.com |
是,精确主机 |
https://login.example.com:8443 |
example.com |
是,可注册父域 |
https://login.example.com:8443 |
m.login.example.com |
否,是子域而非父域 |
https://login.example.com:8443 |
com |
否,是公共后缀 |
https://evil.example.net |
example.com |
否,域名不匹配 |
浏览器的 RP ID 检查和服务器的 origin 检查解决不同问题。即便 login.example.com 和 admin.example.com 都能在域名规则上使用 example.com,服务器仍必须按本系统允许的 origin 精确验证。不要用字符串包含、模糊后缀匹配或接受所有 *.example.com 来代替明确的来源策略。
**域名选择是长期设计决策。**以 example.com 注册的凭据,不会因为你把新网站放到 example.net 就自动迁移。测试站点、Pages 预览 URL、生产域名应有明确边界;正式接入前应先确定稳定的生产 RP ID。英文原文 §7.1 注册验证
三种 ID,不要混用#
| 字段 | 谁生成 | 用途 | 常见错误 |
|---|---|---|---|
user.id / user handle |
RP 服务器 | 用不透明字节稳定标识本网站账户 | 使用邮箱、昵称或每次都变化的随机值 |
credential ID / rawId |
认证器 | 找到某个具体凭据及其公钥 | 把它当成秘密,或当成整个账户的唯一凭据 |
| challenge | RP 服务器 | 绑定并区分本次操作,防止重放 | 写成固定值、由客户端决定、使用后不失效 |
用户句柄在创建时必须是 1~64 字节;建议给每个账户分配稳定的随机值,不包含用户名或邮箱。一个账户的多个凭据通常使用同一个 user handle。user.name 和 user.displayName 是展示字段,可改变,也可能重复;认证与授权不能依据显示名称决定。英文原文 §5.4.3 用户参数
credential ID 是不透明字节。网站应原样保存,不解析内部格式,不根据长度猜设备厂商。一个账户允许多个 credential ID,例如电脑内置认证器和备用安全密钥;一个 credential ID 对应的账户关系应由服务器保存并强制校验。
challenge:公开,但必须不可预测且只用一次#
challenge 不是密码,不需要向客户端保密;它的价值在于攻击者无法事先预测并重复使用。服务器使用密码学安全随机数生成器产生,记录预期值、操作类型、关联账户或会话和过期时间;完成验证时比对并消费。
原规范建议挑战至少 16 字节;本书示例采用 32 字节随机值。长度只是基本条件,Math.random() 拼成的字符串、时间戳和递增序号都不能替代安全随机数。timeout 只影响客户端交互,不会替服务器使挑战失效。英文原文 §13.4.3 Cryptographic Challenges
// 服务器思路示意;不是可接受客户端自报 challenge 的接口。
const bytes = crypto.getRandomValues(new Uint8Array(32));
// 将 bytes 对应的挑战与操作、会话、截止时间保存在服务器。
// 验证响应时读取服务器记录,而非从提交内容中建立“预期值”。UP 与 UV:在场和验证不是同一个保证#
| 标志 | 含义 | 典型操作 | 网站能否据此推断身份细节 |
|---|---|---|---|
| UP:User Presence | 检测到用户在场并参与 | 触碰安全密钥按钮等 | 不能证明触碰者是哪位用户 |
| UV:User Verification | 认证器执行了本地用户验证 | 设备 PIN、指纹、面容等 | 不能由此知道使用了哪种具体生物特征 |
userVerification: "required" 表示本次操作必须执行 UV,服务器也必须检查响应的 UV 位。"preferred" 表示希望执行,但无法执行时不一定失败;"discouraged" 表示不要求 UV,不是强制禁止认证器执行。不要把 preferred 当成“总是要求指纹”。英文原文 §5.8.6 UV 需求
要求无密码、单步骤登录时,本书示例选择 required 作为学习策略。更广的兼容性策略需要结合风险、是否还有其他因素及用户群体确定。无论选什么值,服务器必须按自己的预期验证,不能信任客户端声称“已经验证”。
认证器类型与凭据类型是两条轴#
平台认证器(platform)通常集成在当前设备中;漫游认证器(roaming / cross-platform)可以用于不同设备,例如外接安全密钥。这描述的是认证器与客户端的附着关系。
可发现凭据(discoverable credential)允许客户端在服务器未提前列出凭据 ID 时找到可用于该 RP 的账户;不可发现凭据需要服务器在 allowCredentials 中提供 ID。旧名称 resident / non-resident 仍留在字段中,如 residentKey。这描述的是凭据发现能力。英文原文 §6.2 认证器分类
因此,“平台 = 可发现”“安全密钥 = 不可发现”“可发现 = 已同步到云端”都不是成立的等式。现代通行密钥学习中应分别检查:能否发现、是否执行 UV、是否允许备份、是否已经备份、认证器采用何种保护方式。
attestation 与 assertion#
attestation 译为“证明”,主要用于注册,可能为认证器来源或属性提供可验证信息。assertion 译为“认证断言”,主要用于登录,证明持有该凭据私钥的一方为本次挑战进行了操作。
普通消费类应用通常不需要识别硬件厂商,可采用 attestation: "none" 作为起点。这不代表跳过注册响应验证,也不代表公钥不需要解析;服务器仍然要验证挑战、来源、RP ID、标志及凭据结构。需要企业设备合规或特定硬件保证时,再设计证明信任策略和元数据维护。英文原文 §6.5 Attestation
学习检查#
- 指纹验证成功,网站究竟获得了什么信息?
user.name从“小林”改成邮箱后,为什么不应该影响已有凭据与账户的绑定?- RP ID 正确时,为什么仍必须验证完整 origin?
allowCredentials为空,会允许攻击者不提供签名直接登录吗?
最后一题的答案是否定的。它改变的是“如何选择凭据”,服务器仍然必须找到已登记的公钥并完成全部认证验证;下一章会把这条信任链串起来。