Passkeys 是当前信息安全领域最重要的发展,因为它们是应对网络钓鱼攻击压倒性有效性的唯一原则性解决方案。就像内存安全是应对内存损坏攻击的唯一原则性解决方案一样。
不幸的是,在服务器端实现它们可能看起来比使用密码哈希更复杂。部分原因是不可避免的,因为 passkeys 需要与浏览器交互才能获得其防钓鱼属性。然而,其中一部分可以通过定义可互操作的 passkey 记录编码来更有效地抽象。
WebAuthn 规范定义了一个凭证记录作为具有多个组件的抽象概念,例如 type、id、publicKey、backupState、transports 和其他标志。Google 推荐使用以凭证 ID 为主键的数据库表,以及 public_key、backed_up 和 transports 列。Adam Langley 出色的 WebAuthn 之旅同样推荐使用 cred_id 为主键,以及单独的 public_key_spki 和 backed_up 列。
所有指导都建议使用库来处理 WebAuthn 身份验证,但这仍然会让应用程序面临可能不兼容的数据库架构问题。将整个身份验证流程和数据库交互都委托给库或框架可能也不切实际。
应用程序可以像处理密码哈希一样将可互操作、规范良好的 passkey 记录作为不透明字符串处理,这可以作为中间抽象层。
c2sp.org/passkey-record 是一个规范提案,它借鉴了 密码哈希竞赛 (PHC) 字符串 的语法,并重用了现有的身份验证器数据编码来完成大部分工作。一个记录看起来像这样:
$webauthn$v=1$transports=hybrid+internal$<base64 authenticator data>
有效负载是身份验证器数据,这是一种 CTAP2 CBOR 编码,包含凭证记录的大部分字段,已由 WebAuthn 指定,并包含在 AuthenticatorAttestationResponse 的 JSON 编码中(即使未使用 attestation,也是 navigator.credentials.create() 的返回类型)。Transports 是唯一缺失的字段,它们作为 PHC 参数存储。
然后,应用程序只需负责跟踪与用户账户关联的 passkey 记录,这对 Web 开发者来说是熟悉的任务,因为它与实现密码身份验证并无不同(除了每个账户有多个 passkeys)。这些不透明字符串可以传递给库来验证登录断言(或使用适当的 excludedCredentials 生成注册请求)。
通过规范良好的互操作存储格式,希望能够切换 passkey 库(甚至后端语言)同时保留凭证数据库。
您可能还想存储的其他字段
除了 passkey 记录,应用程序可能仍希望存储元数据字段,如用户选择的昵称以及创建和最后使用时间戳,以提供美观的 passkey 管理 UI。这些都不需要 WebAuthn 库的特殊处理。
唯一的例外是备份状态标志。Passkeys 会向服务器报告它们是否已备份(例如到 iCloud Keychain 或 Google 账户),服务器可以使用该信号建议从账户中删除密码。此标志可能在登录期间发生变化,而 passkey 记录是不可变的,因此必须单独存储并在每次登录时更新。我认为对于普通网站来说,这个逻辑被高估了,因为它们会继续支持电子邮件密码重置。
一个潜在的 crypto/passkey API
基于这些 passkey 记录,我起草了 一个潜在的 crypto/passkey 无状态 Go 包 API。
注册流程是
- 使用已登录(或以其他方式标识)的用户详细信息和任何现有的 passkey 记录调用
RelyingParty.NewRegistration - 将返回的 JSON 传递给
parseCreationOptionsFromJSON(),然后传递给navigator.credentials.create() - 将返回的 JSON 编码的 PublicKeyCredential 传递给
RelyingParty.Register - 将返回的 passkey 记录存储在数据库中
登录流程是
- 在生成登录页面时调用
RelyingParty.NewLogin - 将返回的请求存储在具有短 TTL 的键值缓存中,使用
RequestID(request),并将返回的 JSON 传递给parseRequestOptionsFromJSON(),然后传递给navigator.credentials.get() - 将返回的 JSON 编码的 PublicKeyCredential 传递给
Inspect,使用返回的 requestID 从键值缓存中检索请求,并使用返回的 userID 从数据库中检索 passkey 记录 - 将 JSON PublicKeyCredential、请求和 passkey 记录传递给
RelyingParty.Login
应用程序负责
- 将不透明、永久、隐私保护的用户 ID 与每个用户关联;
- 存储与用户关联的 passkey 记录;以及
- 缓存由
RelyingParty.NewLogin产生的请求挑战。
该库提供可以直接传递给 parseCreationOptionsFromJSON() 和 parseRequestOptionsFromJSON() 的 JSON 值,并接受通过对 PublicKeyCredential 调用 调用 JSON.stringify() 返回的 JSON 值。
这针对可发现凭证流程(又称 passkeys,身份验证器存储并向服务器提供用户 ID)进行了优化,但 RelyingParty.NewLoginForUser 方法也可用于第二因素流程或重新身份验证提示。该 API 适用于模态和条件 UI(自动填充)流程。
有一些辅助函数可以从 passkey 记录中提取信息(AAGUID、BackedUp)以及从 JSON 编码的 PublicKeyCredential 中提取信息(ResponseBackedUp)。
目前还没有实现;我想在可能为 Go 1.28 提出提案之前,获得对 passkey 记录格式 和 Go API 的反馈。
关于重复的凭证 ID
使用这种存储模型,您无法确保不同账户不共享具有相同凭证 ID 的 passkeys,而规范建议您应该这样做。
进行此检查的原因是避免攻击,即通过凭证 ID 查找凭证并因攻击者通过自己的账户故意注入冲突的凭证 ID 而导致错误的公钥或用户 ID。
如果您根本没有凭证 ID 索引,这种攻击根本不可能发生!索引仅用于缓解因索引存在而引入的攻击。
登录尝试携带用户 ID,如果您使用它来查找用户的 passkey 记录以验证登录,则即使其他用户具有相同凭证 ID 的 passkey 也无关紧要,就像两个用户共享密码一样。
不要让攻击者决定您的 PRIMARY KEY,您就不会遇到 PRIMARY KEY 冲突攻击。
有关更多 Go API 预览,请在 Bluesky 上关注我 @filippo.abyssdomain.expert 或在 Mastodon 上关注 @[email protected]。
图片
更多来自今年 CENTOPASSI(一项涉及精心规划、100 个坐标和三天半 1700 公里次要道路的 GPS 追踪摩托车比赛)的图片。以下是 Castel del Monte (AQ) 的掠影,从荒芜且仍有积雪的 Campo Imperatore 下山后。

我的工作得到了 Geomys 的支持,这是一个专业 Go 维护者组织,由 Ava Labs、Teleport、Datadog、Tailscale 和 Sentry 资助。通过我们的保留合同,他们确保了我们开源维护工作的可持续性和可靠性,并直接获得我和 Geomys 其他维护者的专业知识。(更多信息请参阅 Geomys 公告。) 以下是他们中的一些人的几句话!
Teleport — 在过去五年中,攻击和入侵已从传统恶意软件和安全漏洞转向通过社会工程、凭证盗窃或网络钓鱼来识别和入侵有效用户账户和凭证。Teleport Identity 旨在通过访问监控消除弱访问模式,通过访问请求最小化攻击面,并通过强制访问审查清除未使用的权限。
Ava Labs — 我们 Ava Labs,AvalancheGo(与 Avalanche Network 交互使用最广泛的客户端)的维护者,相信开源密码协议的可持续维护和开发对于区块链技术的广泛采用至关重要。我们很自豪能通过对 Filippo 及其团队的持续赞助来支持这项必要且有影响力的工作。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.