






















本文永久链接 – https://tonybai.com/2026/07/22/go-passkey-record-crypto-passkey-api
大家好,我是Tony Bai。
【导读】 Go 官方密码学库维护者、age 加密工具作者 Filippo Valsorda 昨天丢出一枚"重磅提案":他认为 Passkey(无密码登录凭证)完全可以像密码哈希一样,被压缩成一行不透明字符串存进数据库。围绕这个新格式,他还顺手起草了一份可能进入 Go 1.28 标准库的 crypto/passkey API。这篇文章不仅给出了具体的字符串格式,还有一个反直觉的安全洞见:数据库里的 Credential ID,压根不需要跨账号做唯一性检查。
【文章要点】
c2sp.org/passkey-record 规范草案,借用密码哈希字符串(PHC Strings)的语法,把 Passkey 凭证压缩成一行不透明字符串存储;crypto/passkey Go 标准库 API 草案,明确了注册和登录两条完整流程;
如果要在“当下信息安全领域最重要的一件事”里选一个答案,Filippo Valsorda 的答案是:Passkey。
理由很直接——网络钓鱼(phishing)几乎无法被“教育”或“警惕性”彻底解决,而 Passkey 是目前唯一一个从协议层面就能杜绝钓鱼的方案。这和内存安全语言解决内存破坏攻击是一个道理:不是让人更小心,而是让这类攻击从根上失效。
但问题也很现实:相比密码哈希,Passkey(也就是 WebAuthn)在服务端的实现体验要复杂得多。
一部分复杂度是无法避免的,因为 Passkey 的防钓鱼特性天然要求和浏览器交互;但另一部分复杂度,其实是可以通过一套标准化、可互操作的存储格式抹平的。
WebAuthn 规范里定义了一个抽象概念叫 credential record(凭证记录),里面包含 type、id、publicKey、backupState、transports 等一堆字段。
问题是,具体怎么落到数据库表结构上,各家给出的建议五花八门:
public_key、backed_up、transports 几个字段;cred_id 为主键,但拆成 public_key_spki 和 backed_up 两个独立字段。所有指南都会告诉你“用现成的库来处理 WebAuthn 认证流程”,但这仍然留下一个问题:不同应用、不同语言、不同库之间的数据库 schema 互不兼容。而且很多时候,把整个认证流程和数据库交互都甩给一个框架,也未必现实。
Filippo 给出的答案是:搞一套可互操作、规范明确的 Passkey 记录格式,让应用层像对待密码哈希一样,把它当成一个不透明字符串来处理——这正是密码哈希字符串(如 bcrypt、argon2 的存储格式)几十年来证明有效的思路。
passkey-record:借密码哈希字符串的“壳”,装 WebAuthn 的“核”Filippo 提出的规范草案叫 c2sp.org/passkey-record,语法上直接借用了 PHC 字符串格式(Password Hashing Competition Strings,也就是常见密码哈希的那种 $算法$参数$哈希值 写法)。
一条记录长这样:
$webauthn$v=1$transports=hybrid+internal$<base64 authenticator data>
关键的巧思在于:这里的 payload 并不是重新设计的私有格式,而是复用了 WebAuthn 规范里本就存在的 authenticator data 编码——一段 CTAP2 CBOR 编码,本身就包含了凭证记录里绝大多数字段,并且已经被包含在 AuthenticatorAttestationResponse(也就是 navigator.credentials.create() 的返回值,即使不启用 attestation 也一样)的 JSON 编码里。
唯一缺失的字段是 transports(凭证支持的传输方式,如 hybrid、internal),这个字段被作为 PHC 参数附加在字符串里。
这样一来,应用层唯一要做的事,就是记录并维护“某个用户账号关联了哪些 passkey record”——这件事其实和实现密码认证没什么本质区别(无非是一个账号可能对应多个 passkey)。这些不透明字符串可以直接传给库去验证登录断言,或者在生成注册请求时用来填充 excludedCredentials。
有了这套统一、可互操作的存储格式,未来更换 Passkey 库(甚至更换后端语言)时,理论上可以完整保留原有的凭证数据库,不需要做数据迁移。
除了 passkey record 本身,应用通常还会想存一些元数据,比如用户自定义的昵称、创建时间、最后使用时间,方便做出一个体验友好的 Passkey 管理界面。这些字段都不需要 WebAuthn 库做任何特殊处理。
唯一的例外,是“是否已备份”(backed up)这个状态标志。
Passkey 会向服务端报告自己是否已经被备份(比如同步到了 iCloud 钥匙串或 Google 账号),服务端可以利用这个信号,提示用户可以考虑移除密码、彻底转向无密码登录。
但这个标志有个特殊之处:它在每次登录时都可能变化,而 passkey record 本身是不可变的。所以它必须被单独存储,并且在每次登录时更新。
Filippo 在这里也表达了一个偏“实用主义”的态度:他认为这套逻辑对于普通网站来说其实被高估了——反正大部分网站还是会保留邮箱密码重置这条后路,过度设计这个信号的实际收益有限。
crypto/passkey API在 passkey record 这套存储格式的基础上,Filippo 起草了一份 crypto/passkey 无状态 Go 包 API 草案。
注册流程:
RelyingParty.NewRegistration,传入已登录(或已识别)用户的信息,以及该用户已有的 passkey record;parseCreationOptionsFromJSON(),再传给 navigator.credentials.create();RelyingParty.Register;登录流程:
RelyingParty.NewLogin;RequestID(request) 为 key,存进一个短 TTL 的键值缓存里,同时把返回的 JSON 传给 parseRequestOptionsFromJSON(),再传给 navigator.credentials.get();Inspect,用返回的 requestID 从缓存里取回 request,用返回的 userID 从数据库里取回该用户的 passkey record;RelyingParty.Login。从这两个流程可以看出,应用层真正需要负责的只有三件事:
RelyingParty.NewLogin 产生的 request 挑战值。这个库返回的 JSON 值可以直接传给浏览器原生的 parseCreationOptionsFromJSON() 和 parseRequestOptionsFromJSON(),也能直接接收调用 PublicKeyCredential.toJSON() 得到的返回值——中间不需要任何手动格式转换。
这套设计主要针对可发现凭证流程(discoverable credential,也就是我们常说的“Passkey”体验:认证器自己存好并向服务端提供用户 ID)。
不过 RelyingParty.NewLoginForUser 方法也支持用在二次验证或重新身份确认这类场景。同时,模态(modal)和条件 UI(autofill,也就是输入框自动弹出 Passkey 选项)两种交互方式,这套 API 都能覆盖。
此外还提供了几个辅助函数:从 passkey record 里提取信息的 AAGUID、BackedUp,以及从 JSON 编码的 PublicKeyCredential 里提取信息的 ResponseBackedUp。
需要说明的是,这份 API 目前还没有实现,Filippo 表示希望先收集社区对 passkey record 格式和这份 Go API 设计的反馈,之后再考虑正式提案,争取进入 Go 1.28。
文章最后一节,抛出了一个很多人可能没细想过的问题:这种存储模型下,没办法保证不同账号之间不会共享同一个 Credential ID——而 WebAuthn 规范里明确写着,服务端 SHOULD(应当)做这个检查。
先看这个检查存在的初衷:如果服务端用 Credential ID 做索引查找凭证,攻击者理论上可以故意在自己的账号下注册一个和别人 Credential ID 相同的凭证,制造 ID 碰撞,从而让查找逻辑“走错账号”,查到错误的公钥或用户 ID。
但 Filippo 指出了一个更根本的解法:这种攻击的前提,是你的系统里存在一个 Credential ID 索引。如果压根不建这个索引,攻击自然也就无从谈起——防御一个“因索引而生的攻击”最彻底的方式,就是不建索引。
在他设计的模型里,登录请求本身携带用户 ID,服务端用这个用户 ID 去查找该用户名下的 passkey record 进行校验。这种情况下,其他用户是否恰好有一个 Credential ID 相同的 Passkey,根本不重要——就像两个用户使用了相同的密码,也不影响系统安全性一样。
他把这个原则总结成一句很好记的话:
不要让攻击者决定你的 PRIMARY KEY,你就不会遇到 PRIMARY KEY 碰撞攻击。
这篇文章没有发布任何正式的库或工具,而是一份“设计提案”:一套借鉴密码哈希字符串思路的 Passkey 存储格式,加上一份尚未实现、但已经把接口想清楚的 Go API 草案。
对于正在用 Go 做 WebAuthn 服务端集成的开发者来说,这份草案至少提供了一个值得参考的方向——把 Passkey 当成密码哈希一样简单地存起来,把索引设计的坑提前避开。
Filippo Valsorda 表示,他会在 Bluesky(@filippo.abyssdomain.expert)和 Mastodon(@filippo@abyssdomain.expert)持续更新 Go API 的后续进展,感兴趣的读者可以关注。
参考链接:
还在为写 Agent 框架频频死循环、上下文爆炸而束手无策?我的新专栏 《从0 开始构建 Agent Harness》 将带你:
扫描下方二维码,开启从 0 开始构建Agent Harness 的实战之旅。

还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。