比较通行密钥(passkey)与密码,就是比较共享秘密与公钥身份验证。密码由用户掌握,服务器也以哈希形式掌握它;它在任何索要它的页面上都能使用,包括伪造的页面。通行密钥是一种 WebAuthn 凭证:它是为单个网站创建的密钥对,在用户的设备上用指纹、面容或 PIN 解锁,用来对一个绑定了网站身份的新挑战值签名。
概览
| 密码 | 通行密钥 | |
|---|---|---|
| 用户证明的是什么 | 知道一个共享秘密 | 持有私钥,私钥通过生物识别或 PIN 在本地解锁 |
| 服务器存储什么 | 盐和慢哈希(Argon2id、scrypt、PBKDF2) | 凭证 ID 和公钥(在下面的演示中,P-256 公钥为 91 字节) |
| 仿冒的钓鱼网站 | 用户一旦输入,网站就拿到了密码 | 拿不到任何可用的东西:凭证绑定到网站的 RP ID 和源 |
| 跨网站复用 | 很常见,这让撞库攻击成为可能 | 不可能:每个网站、每个账号各有一个密钥对 |
| 数据库泄露 | 攻击者可以针对哈希进行离线猜测 | 泄露的只有公钥,没有可以重放的东西 |
| NIST SP 800-63B-4(2025年7月) | 单因素:至少 15 个字符,AAL1 | 多因素密码学认证器:AAL2;绑定设备的硬件密钥可以达到 AAL3,同步通行密钥不能 |
| 设备丢失 | 不受影响 | 同步通行密钥可以随提供商账号恢复;绑定设备的密钥需要一个备用凭证 |
| 其他设备 | 在哪里都能输入 | 在提供商的各台设备之间同步;其他设备通过 QR 码和蓝牙近距离检测(混合传输)使用 |
| 标准 | 除 HTML 表单和 HTTP 身份验证外没有其他标准 | W3C WebAuthn Level 3(2026年8月25日起成为推荐标准)、FIDO CTAP 2.2 |
登录时发生了什么
注册时,认证器(手机、笔记本电脑、密码管理器或安全密钥)为网站的依赖方 ID(例如 example.com)创建密钥对,网站保存公钥。登录时,网站发送一个随机挑战值;浏览器把页面的源写入要签名的客户端数据,认证器在完成用户验证后,对这些数据连同 RP ID 的哈希一起签名。服务器检查挑战值、源、RP ID 哈希和签名。
下面的脚本(Node.js 26.10,Apple M4 Max)展示了两种方式各自在服务器上留下什么,以及哪些检查能拦住仿冒网站。
import { argon2Sync, createHash, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
const sha256 = (b) => createHash("sha256").update(b).digest();
// What a password database stores: an Argon2id hash (OWASP minimum: 19 MiB, t=2, p=1)
const params = { nonce: randomBytes(16), memory: 19456, passes: 2, parallelism: 1, tagLength: 32 };
const t0 = performance.now();
const tag = argon2Sync("argon2id", { message: "correct horse battery staple", ...params });
const ms = performance.now() - t0;
console.log(`password: stores salt + ${tag.length}-byte Argon2id hash; one guess costs ${ms.toFixed(0)} ms`);
// What a passkey database stores: the credential's public key
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const spki = publicKey.export({ type: "spki", format: "der" });
console.log(`passkey: stores a ${spki.length}-byte public key; a leak lets nobody sign in`);
// Authenticator: signs authenticatorData || SHA-256(clientDataJSON)
function getAssertion(rpId, origin, challenge) {
const clientDataJSON = Buffer.from(JSON.stringify({ type: "webauthn.get", challenge, origin }));
const authData = Buffer.concat([sha256(rpId), Buffer.from([0x05]), Buffer.alloc(4)]);
const signature = sign("sha256", Buffer.concat([authData, sha256(clientDataJSON)]), privateKey);
return { clientDataJSON, authData, signature };
}
// Relying party: the checks from WebAuthn §7.2
function check(a, challenge) {
const c = JSON.parse(a.clientDataJSON);
return {
challenge: c.challenge === challenge,
origin: c.origin === "https://example.com",
rpIdHash: a.authData.subarray(0, 32).equals(sha256("example.com")),
signature: verify("sha256", Buffer.concat([a.authData, sha256(a.clientDataJSON)]), publicKey, a.signature),
};
}
const challenge = randomBytes(32).toString("base64url");
console.log("real site:", check(getAssertion("example.com", "https://example.com", challenge), challenge));
console.log("look-alike:", check(getAssertion("examp1e.com", "https://examp1e.com", challenge), challenge));
password: stores salt + 32-byte Argon2id hash; one guess costs 25 ms
passkey: stores a 91-byte public key; a leak lets nobody sign in
real site: { challenge: true, origin: true, rpIdHash: true, signature: true }
look-alike: { challenge: true, origin: false, rpIdHash: false, signature: true }
仿冒网站这个例子刻意对攻击者放宽了条件:实际上,浏览器根本不会在其他网站上使用 example.com 的凭证,所以不会产生任何签名。即使攻击者通过中继会话拿到了签名,签名中的源和 RP ID 哈希也是错的。正因如此,NIST 认为 WebAuthn 通过验证者名称绑定实现了抗钓鱼,而密码、一次性验证码和推送确认都做不到这一点。Argon2id 让每猜一次密码都要花费几十毫秒,但选用常见密码或重复使用密码的用户仍然很快会被猜中;公钥则没有给攻击者留下任何可猜的东西。
两者各自仍会在哪里失守
密码会因重复使用(攻击者用泄露的列表撞库)、钓鱼页面、连一次性验证码也一并截获的实时钓鱼代理以及弱密码而失守。因此,NIST SP 800-63B-4 要求单因素密码至少 15 个字符(在多因素认证中至少 8 个),禁止字符组合规则和定期更换,要求将新密码与已泄露的密码和常见密码进行比对,并建议允许使用至少 64 个字符的密码。带有按域名匹配自动填充功能的密码管理器可以解决重复使用和部分钓鱼问题,但并不是每个用户都在用。
通行密钥把风险转移到了两个地方。第一个是提供商账号:同步通行密钥的安全程度,取决于存放它们的 Apple、Google、Microsoft 或密码管理器账号,这也是 NIST 不允许在 AAL3 使用可同步、可导出密钥的原因。第二个是你网站上的账号恢复:如果丢失的通行密钥可以通过电子邮件链接或短信验证码换掉,攻击者就会转而走这条路。用户还需要第二个凭证或可同步的提供商,才能避免被锁在账号之外。
如何选择
- 面向消费者的网站和应用:以通行密钥为主要方式,在用户下一次用密码登录后立即创建;密码可选。
- 员工登录:通过你的身份提供商使用通行密钥;管理员和其他 AAL3 角色使用绑定设备的安全密钥。
- 高价值的消费者账号(银行、电子邮件、云控制台):通行密钥加上安全的恢复流程,最好再注册第二个通行密钥。
- 共享设备或自助终端:让用户用自己的手机通过混合方式登录,而不是把凭证存放在共享设备上。
- 不支持 WebAuthn 的协议(IMAP、旧式 VPN、SSH):使用应用专用密码,或者在可以使用通行密钥的浏览器中发起的 OAuth 流程。
迁移说明
- 在密码之外添加通行密钥:用户用密码成功登录后,提示用户创建通行密钥;如果浏览器的密码管理器支持,也可以用 WebAuthn Level 3 的条件式创建自动添加一个。
- 使用自动填充界面:在登录页面上调用
navigator.credentials.get并传入mediation: "conditional",让通行密钥出现在用户名字段的建议列表中。 - 保存需要的数据:凭证 ID、公钥、签名计数器、传输方式、备份标志,以及标明提供商的 AAGUID;AAGUID 查询可以把它对应到具体产品。
- 先完善恢复流程,再移除密码:要求使用已有的通行密钥、已注册的第二个通行密钥或身份核验,而不只是一个电子邮件链接。
- 与提供商保持同步:用户删除凭证或更改名称时,用 Level 3 的信号方法通知提供商;如果你在多个域名之间共用登录,还要声明相关源。
- 在通行密钥沙盒中测试,并用 WebAuthn 查看器检查真实的响应,然后再上线。
参考资料
- W3C Web 身份验证规范 Level 3(§7.1 注册,§7.2 身份验证流程)
- NIST SP 800-63B-4:身份验证与认证器管理(§3.1.1.2 密码,§3.2.5 抗钓鱼,附录 B 可同步认证器)
- FIDO 联盟:客户端到认证器协议(CTAP)2.2
- OWASP 密码存储速查表
- RFC 9106:Argon2