文章

Passkey原理浅析:从共享密码到私钥签名

Passkey原理浅析:从共享密码到私钥签名

背景

这几年 Apple、Google、Microsoft 都在推 Passkey,也就是中文里经常说的【通行密钥】。

如果只看产品宣传,Passkey 很容易被理解成“用指纹替代密码”。这个说法不能算错,但它只说到了用户体感,没有说到技术本质。
指纹、人脸、PIN 只是本地解锁动作,真正替代密码的,是背后的【非对称密钥】和【挑战签名】。

本文不打算把 WebAuthn 规范从头到尾翻译一遍,那样大概率会变成另一篇难读的标准文档。笔者更想从工程视角把这件事捋清楚:

  1. 传统密码到底差在哪里?
  2. Passkey 换掉了认证链路里的哪一环?
  3. 它为什么天然比密码和 OTP 更抗钓鱼?
  4. 普通用户和业务系统分别应该怎么理解这件事?

先抛一个结论:

Passkey 的关键不是“免输入”,而是把认证模型从【共享秘密】升级成了【私钥签名】。

密码的问题不是不够复杂

我们平时说密码不安全,很容易把问题归因到用户:密码太短、复用密码、不定期修改、不用密码管理器。

这些当然都是问题,但不是根因。

传统密码认证的根因在于:服务器和用户之间共享同一个秘密

flowchart LR
    U[用户持有密码] -->|登录时提交同一个秘密| S[业务服务器]
    S -->|保存可验证材料| DB[(密码哈希库)]
    DB -.->|用于证明用户知道该秘密| S

这个模型有三个天然缺陷。

  1. 密码一定会被提交出去。
    无论链路上有没有 HTTPS,用户最终都要把“能证明自己身份的东西”交给服务端。只要用户把同一串字符填进了钓鱼网站,攻击者就拿到了可以复用的凭证。

  2. 服务器一定要保存某种可验证材料。
    正经系统不会存明文密码,而是存加盐哈希。但哈希仍然是高价值资产。数据库一旦泄露,攻击者就可以离线爆破、撞库,或者利用用户复用密码的问题横向扩散。

  3. 人类不适合管理高熵秘密。
    所以现实世界里就会出现 123456、生日、手机号、多个网站共用一套密码。这个锅不能全甩给用户,因为让人脑长期记住几十个高熵随机串,本来就不符合人类的使用方式。

于是业界又加了一层 2FA,比如短信验证码、邮箱验证码、TOTP 动态码。

这一步确实提升了安全性,但它更像是补丁,不是换模型。因为 OTP 仍然是一段会被用户读出来、输出来、提交出去的短期秘密。
只要攻击者能搭一个实时转发的钓鱼站,就可以让用户把验证码填进去,然后立刻拿去真站登录。

换句话说,只要认证链路里还存在【用户提交秘密】这个动作,钓鱼攻击就一直有操作空间。

Passkey换掉了哪一环

Passkey 的思路很朴素:既然“提交秘密”这件事天然危险,那就不要提交秘密了。

注册时,用户设备为某个网站生成一对密钥:

  • 【公钥】交给服务器保存
  • 【私钥】留在用户设备或密码管理器里

登录时,服务器不再问“你的密码是什么”,而是给用户一个随机挑战值,让用户设备用私钥签名。服务器拿之前保存的公钥验证签名,验过了就说明:这个用户确实持有那把私钥。

注册阶段只做一件事:服务器记录“这个账号以后可以用哪把公钥来验签”。

flowchart LR
    A[认证器生成密钥对] --> B[公钥]
    A --> C[私钥]
    B -->|交给服务器保存| S[(账号凭证库)]
    C -->|留在用户设备或密码管理器| D[本地安全存储]

登录阶段才是真正的身份验证。这里的【挑战值】可以理解成服务器临时出的一道题,而且每次登录都不一样。

为什么要有这个东西?
因为服务器不能只让客户端签一个固定文本。固定文本一旦被截获,攻击者下次还可以原样重放。挑战值的作用就是把每次登录都变成一次新的问答:服务器现场出题,认证器现场签名,签名结果只能用于这一次。

用 HTTP 请求来理解,大概是这样的。下面字段做了简化,主要用于说明认证交互,不是完整的 WebAuthn 报文定义。

第一步,浏览器先向服务器要一道本次登录专用的题:

1
2
3
4
5
6
POST /passkeys/login/options HTTP/1.1
Content-Type: application/json

{
  "username": "alice"
}

服务器返回一个一次性的 challenge,以及这个用户允许使用哪些 Passkey 凭证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 200 OK
Content-Type: application/json

{
  "challenge": "q9xL8f0pWmQ3...",
  "rpId": "example.com",
  "allowCredentials": [
    {
      "id": "cred_7f3a...",
      "type": "public-key"
    }
  ],
  "timeout": 60000
}

这里先补两个小概念:

  1. rpId 是 Relying Party ID。Relying Party 是 WebAuthn 对“网站 / 业务方”的叫法。
    所以 rpId 可以先理解成【站点标识】。大多数场景下,它就是网站域名,比如 example.comgithub.com
  2. allowCredentials 是服务器允许本次登录使用的凭证列表。一个账号可以绑定多个 Passkey,所以服务器要告诉浏览器“这次可以用哪几把钥匙”。

第二步,浏览器把这些参数交给 navigator.credentials.get()。认证器会检查当前页面是否属于 rpId 对应的站点。
确认站点没问题后,再让用户做本地指纹、人脸或 PIN 验证。验证通过后,认证器用本地私钥对这次 challenge 签名。

最后,浏览器把签名结果提交给服务器:

1
2
3
4
5
6
7
8
9
POST /passkeys/login/verify HTTP/1.1
Content-Type: application/json

{
  "credentialId": "cred_7f3a...",
  "authenticatorData": "SZYN5YgOjGh0NBcP...",
  "clientDataJSON": "eyJ0eXBlIjoi...",
  "signature": "MEUCIQDa..."
}

这里的 credentialId 可以理解成【这把 Passkey 的编号】。服务器拿它找到注册时保存的公钥,然后验证三件事:

  1. challenge 是不是自己刚发出去的。
  2. 当前页面来源是否属于这个站点。
  3. signature 能不能被公钥验过。

其中 authenticatorDataclientDataJSON 可以先理解成浏览器和认证器打包的上下文信息,里面包含本次操作类型、挑战值、页面来源、认证器状态等内容。

flowchart LR
    S[服务器生成一次性挑战值] -->|发给客户端| A[认证器]
    A -->|用私钥签名挑战值| R[签名结果]
    R -->|提交给服务器| V[服务器用公钥验签]
    V -->|签名正确且挑战值未过期| OK[登录成功]

类比 Git 会更好理解。

我们用 SSH key 操作 Git 仓库时,不会把私钥上传给 GitHub。GitHub 保存的是你的公钥。真正登录时,客户端拿私钥完成一次签名,GitHub 用公钥验签。
Passkey 本质上也是类似思路,只是把密钥生成、存储、调用、跨设备同步这些麻烦事交给了浏览器、操作系统和密码管理器。

所以 Passkey 不是“更长的密码”,而是“不再使用密码”。

FIDO2、WebAuthn和CTAP2是什么关系

Passkey 不是某一家公司的私有能力,它建立在 FIDO2 这套开放标准上。

里面最常见的三个词是 FIDO2、WebAuthn、CTAP2。它们的关系可以酱紫理解:

flowchart LR
    Site[网站 / App] -->|调用 WebAuthn API| Browser[浏览器 / 操作系统]
    Browser -->|通过 CTAP2 通信| Authenticator[认证器<br/>手机 / TPM / Secure Enclave / YubiKey]
    Authenticator -->|生成或使用密钥| Key[(Passkey)]
  • FIDO2:整体标准集合,可以理解成“这套无密码认证体系”的总称。
  • WebAuthn:W3C 定义的 Web API,网站通过 navigator.credentials.create() 注册凭证,通过 navigator.credentials.get() 发起登录。
  • CTAP2:浏览器或操作系统和认证器之间的通信协议,比如调用手机安全芯片、电脑 TPM、USB 安全密钥。

这也是 Passkey 能跨浏览器、跨平台发展的原因。网站侧接的是 WebAuthn 标准,不是某个厂商的私有 SDK。

注册流程

注册 Passkey 的过程,本质上是“给当前账号登记一个公钥”。

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as 服务器
    participant A as 认证器

    U->>B: 点击创建 Passkey
    B->>S: 请求注册参数
    S-->>B: 返回挑战值、站点标识、用户信息
    B->>A: 调用 navigator.credentials.create()
    A->>A: 本地验证指纹 / 人脸 / PIN
    A->>A: 为该站点生成密钥对
    A-->>B: 返回凭证 ID、公钥、设备证明等数据
    B->>S: 提交注册结果
    S->>S: 校验挑战值和页面来源,保存公钥
    S-->>B: 注册成功

这里有三个关键点。

  1. 服务器给的是一次性 challenge,用于防重放。
    攻击者不能拿一份旧注册结果重复提交,因为服务器会检查挑战值是否匹配、是否过期、是否已经用过。

  2. 密钥和站点绑定。
    这里的站点就是前面说的 rpId。认证器不是生成一把到处都能用的万能钥匙,而是为某个站点生成特定凭证。

  3. 服务器保存的是公钥,不是私钥。
    公钥泄露当然也不是什么好事,但它不能反推出私钥,也不能直接拿来登录。这一点和密码哈希完全不同。

登录流程

登录时,服务器要验证的不是“用户知道什么秘密”,而是“用户是否持有对应私钥”。

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as 服务器
    participant A as 认证器

    U->>B: 点击使用 Passkey 登录
    B->>S: 请求登录参数
    S-->>B: 返回挑战值和允许的凭证列表
    B->>A: 调用 navigator.credentials.get()
    A->>A: 检查当前页面是否属于绑定站点
    A->>A: 本地验证指纹 / 人脸 / PIN
    A->>A: 使用私钥签名挑战值
    A-->>B: 返回凭证 ID 和签名结果
    B->>S: 提交签名结果
    S->>S: 查公钥并验签
    S-->>B: 登录成功

注意,指纹和人脸不是发给服务器做识别的。
它们只是本地解锁私钥的手段,类似“打开保险箱之前先确认开箱人”。服务器最终看到的是签名结果,而不是你的生物特征。

这一点很重要。很多人一听 Passkey 绑定指纹,就担心“网站是不是拿到了我的指纹”。从 WebAuthn 的模型看,网站拿不到,也不应该拿到。

为什么更抗钓鱼

Passkey 最有价值的地方,不是少输入几个字符,而是它把【域名校验】放进了浏览器和认证器的协议链路里。

github.com 为例。你在 GitHub 注册 Passkey 时,认证器会把凭证绑定到 github.com 这个站点。后续只有当前页面确实来自 GitHub,浏览器才会允许调用这个凭证完成签名。

如果用户打开的是 githvb.com 这种钓鱼站,情况就变了:

flowchart TD
    A[用户打开钓鱼站 githvb.com] --> B[钓鱼站请求使用 Passkey]
    B --> C[浏览器检查当前页面来源]
    C --> D{是否属于 github.com?}
    D -->|否| E[不返回 GitHub 对应凭证]
    E --> F[认证器无法为钓鱼站签 GitHub 的挑战值]

    G[用户打开 github.com] --> H[浏览器检查当前页面来源]
    H --> I{属于 github.com?}
    I -->|是| J[允许认证器使用对应私钥签名]

这和密码/OTP 的区别非常大。

密码是用户自己填的。钓鱼站只要页面做得像,用户就可能填进去。
OTP 也是用户自己填的。钓鱼站可以实时转发到真站,趁验证码没过期完成登录。

Passkey 则不一样。用户并不知道私钥是什么,也不能把私钥复制出来填到某个页面里。
能不能签名,不由用户肉眼判断页面像不像,而由浏览器、操作系统和认证器共同检查当前来源。

换句话说,Passkey 把“识别真假网站”这件事,从用户脑子里搬到了协议和客户端实现里。

当然,这不代表 Passkey 绝对无敌。
如果设备本身被恶意软件完全控制,或者用户被诱导在真实站点里完成了某些高危授权,Passkey 也救不了所有业务逻辑问题。它解决的是认证凭证被窃取、被复用、被钓鱼转发这类问题,不是整个安全体系的终点。

同步密钥和设备绑定密钥

Passkey 落地时,经常会遇到两个概念:【同步密钥】和【设备绑定密钥】。

维度同步密钥设备绑定密钥
存储位置iCloud 钥匙串、Google 密码管理器、1Password、Bitwarden 等TPM、Secure Enclave、YubiKey 等具体硬件
跨设备可以跟随账号或密码管理器同步通常不能离开原设备或硬件 Key
可恢复性设备丢了还有机会恢复硬件丢了可能需要备用凭证
安全边界信任系统账号和密码管理器的同步安全信任本地硬件和物理持有
适合场景普通账号、日常办公、个人服务管理员账号、生产系统、金融交易等高敏场景

对普通用户来说,同步密钥的体验更接近“密码管理器里的密码”,换手机或换电脑以后还能恢复。
对高敏账号来说,设备绑定密钥更接近“物理钥匙”,安全边界更硬,但丢失恢复也更麻烦。

这不是谁完全替代谁的问题,而是取舍问题。

笔者个人建议:

  1. 普通互联网账号优先使用系统或密码管理器提供的同步 Passkey。
  2. GitHub、Cloudflare、云厂商控制台这类开发者高价值账号,至少准备一个硬件安全密钥作为备用。
  3. 企业系统不要只设计“添加 Passkey”,还要认真设计账号恢复、设备丢失、离职交接、管理员兜底这些流程。

扫码登录是怎么回事

Passkey 还有一个很容易被误解的体验:在电脑上登录时,页面弹出二维码,然后你用手机扫码完成认证。

这不是“把私钥通过二维码传给电脑”。

更准确地说,这是 FIDO2 的跨设备认证能力。二维码通常只承载临时连接信息,手机作为认证器完成本地验身和私钥签名,再把签名结果通过安全通道交回给电脑侧浏览器。

sequenceDiagram
    participant PC as 电脑浏览器
    participant Phone as 手机认证器
    participant S as 服务器

    PC->>S: 请求登录挑战值
    S-->>PC: 返回挑战值
    PC-->>Phone: 二维码传递临时连接信息
    Phone->>PC: 建立加密通道并做邻近性检查
    Phone->>Phone: 本地指纹 / 人脸 / PIN 解锁私钥
    Phone->>Phone: 使用私钥签名挑战值
    Phone-->>PC: 回传签名结果
    PC->>S: 提交签名
    S->>S: 用公钥验签
    S-->>PC: 登录成功

这里的重点仍然是:私钥不离开手机。
电脑只是拿到了可以交给服务器验签的结果,而不是拿到了你的 Passkey 本身。

对业务系统意味着什么

如果只是普通用户,理解到这里基本就够用了:能开 Passkey 的重要账号,尽量开。

但如果你是业务系统开发者,事情会更复杂一点。Passkey 不是往登录页上加一个按钮就结束了,它会影响整套账号体系。

账号标识要和认证凭证解耦

密码时代,很多系统天然把“账号 + 密码”绑在一起。
Passkey 时代,一个用户可能有多个凭证:手机一个、电脑一个、硬件 Key 一个、密码管理器里一个。

所以数据模型里至少要区分:

  • 用户账号:表示业务身份
  • 凭证 ID:表示某个 Passkey
  • 公钥:用于验签
  • 凭证状态:是否启用、是否撤销、最后使用时间

简单理解就是:一个用户可以挂多把钥匙,业务系统要能管理这些钥匙。

恢复流程比登录流程更重要

登录成功很容易做得漂亮,恢复流程才是真正考验工程质量的地方。

比如:

  1. 用户手机丢了怎么办?
  2. 用户换了密码管理器怎么办?
  3. 用户只有一个 Passkey,且无法访问原设备怎么办?
  4. 企业员工离职后,他注册过的设备凭证如何撤销?
  5. 管理员帮用户恢复账号时,如何防止社工攻击?

这些问题如果不设计清楚,Passkey 反而会变成客服和安全团队的新负担。

不要急着删除所有 fallback

Passkey 的方向是对的,但现实系统需要兼容旧设备、旧浏览器、特殊网络环境和用户迁移成本。

更稳妥的策略通常是:

flowchart LR
    A[第一阶段<br/>密码 + 2FA] --> B[第二阶段<br/>鼓励添加 Passkey]
    B --> C[第三阶段<br/>Passkey 优先登录]
    C --> D[第四阶段<br/>高风险操作要求 Passkey]
    D --> E[第五阶段<br/>按用户群逐步弱化密码]

也就是说,Passkey 适合渐进式接入。
先让高价值用户、高风险操作、内部管理员账号用起来,再逐步扩大范围。不要一上来就把所有密码登录砍掉,除非你已经把恢复、迁移、客服、安全审计都准备好了。

国内为什么感知不强

Passkey 在海外大厂账号里已经比较常见,比如 Google、Apple、Microsoft、GitHub、Amazon 等服务都已经支持或推广过相关能力。
但在国内,普通用户的感知确实不强。

原因倒也不神秘。

国内互联网账号体系过去十几年主要围绕手机号、短信验证码、App 扫码、微信/支付宝生态登录展开。
这些方案未必在安全模型上更优,但它们已经深度嵌入业务增长、风控、营销触达、实名认证和客服体系里。

Passkey 要真正普及,不只是浏览器支持就行,还要业务方愿意改账号体系、客服流程和风控策略。
从 ROI 看,很多国内业务短期内没有那么强的动力。

所以笔者对它的判断比较保守:

Passkey 是认证模型上的明确进步,但国内大规模普及还需要时间。短期内,它更可能先出现在开发者工具、跨境服务、金融/企业安全场景里。

总结

一路走下来,Passkey 其实没有那么玄乎。

它做的事情可以概括成三句话:

  1. 密码认证依赖【共享秘密】,秘密需要被用户提交,也需要被服务端保存可验证材料,所以天然怕钓鱼、怕拖库、怕复用。
  2. Passkey 使用【非对称密钥】,服务端保存公钥,用户侧保存私钥,登录时只提交签名结果,不提交可复用秘密。
  3. WebAuthn 把站点校验、一次性挑战值和用户本地验证串成一条协议链路,让钓鱼站很难骗到可用凭证。

对普通用户来说,建议先从高价值账号开始启用 Passkey,比如邮箱、GitHub、Apple、Google、云厂商控制台。
对业务系统来说,重点不是“页面上支持 Passkey”,而是把凭证管理、恢复流程、设备丢失、审计撤销这些工程问题一起设计好。

太阳底下没有新鲜事。
Passkey 本质上不是魔法,而是把我们在 SSH key、TLS 客户端证书里早就熟悉的公私钥认证,包装成了普通用户终于能用明白的一套产品体验。

以上是笔者对 Passkey 的一点浅析,如有错误,欢迎指正。

参考

  1. FIDO Alliance:Passkeys
  2. W3C:Web Authentication Level 3
  3. Google for Developers:Passkeys
  4. web.dev:Sign in with a passkey through form autofill
  5. Microsoft Learn:Passwordless security key sign-in
  6. 1Password:What are passkeys?
本文由作者按照 CC BY 4.0 进行授权