一个密码,是你的设备自己生成的,从不给你看,也拒绝交给任何人——包括你自己。正是这个特性让通行密钥(passkey)能挡住钓鱼,也正是它让一篇题为《我不喜欢通行密钥》的帖子在Hacker News首页挂了整整一天,收获616分和605条评论。
微软实测了一组数字:通行密钥的登录成功率是98%,密码只有32%。换算一下,大约每50次登录失败1次,而不是每3次失败2次。FIDO联盟统计,目前全球在用的通行密钥已达50亿个。
但"无法被钓鱼"和"无法被搬走"是同一个词的两面。这既是它最硬的安全属性,也是它最招人烦的地方。
一对密钥,只认一个网站
通行密钥本质上是一对公钥/私钥,由你的设备(认证器)为某一个特定网站(依赖方)生成。按FIDO/W3C文档站passkeys.dev的说法,它是一种"可发现凭证":设备不用等网站先发用户名,自己就能找到它。规范里更早的叫法是"可发现常驻凭证",2022年苹果、谷歌、微软把它改了名。
整个设计靠两条事实撑起来:
- 一个网站一对密钥。你在github.com的通行密钥和在google.com的通行密钥毫无关系。数据库泄露,攻击者拿到的只有公钥。
- 私钥永远不以可用形式离开认证器。你看不到它,也打不出它,所以没有东西可以被复用,也没有东西会被输进错误的页面。
Hacker News用户kenrick95一句话点破了问题所在:"通行密钥有个营销难题,没人能用简单的话说清它是什么。"
注册和登录,各叫一场"仪式"
W3C的Web认证规范把注册或登录称为一场"仪式":浏览器发起调用,服务器验证签名。
注册时,服务器生成随机字节(challenge),页面请求浏览器创建凭证。认证器为这个网站生成一对新密钥,把公钥和凭证id返回给服务器存起来。参数里的alg: -7指的是ES256,即P-256曲线上的ECDSA;residentKey: "required"要求的是可发现凭证——这正是它区别于老式第二因素的地方。
登录时,服务器发来一个全新的challenge,页面调用navigator.credentials.get(),用户碰一下传感器或输入PIN,认证器完成签名。签名覆盖认证器数据,外加一个叫clientDataJSON的小型JSON对象的SHA-256哈希。服务器拿存着的公钥去验签,验过了,就说明只有私钥持有者能生成它。
服务器上既没有密码哈希,也没有共享密钥——没有东西可泄露。
浏览器替你写下origin,页面碰不到
防钓鱼的关键就藏在一个字段里。clientDataJSON长这样:
- "type": "webauthn.get"
- "challenge": "Zm9vYmFy…"
- "origin": "https://accounts.google.com"
这个origin不是页面填的,是浏览器根据实际加载的地址写进去的,并且在认证器签名之前先和依赖方id核对过。页面上的JavaScript没法在这件事上撒谎。所以当你落到accounts.g00gle-login.com这类仿冒域名上,只会发生两种情况:那个域名下压根没有对应的通行密钥;或者返回的签名绑定了错误的origin,被真正的服务器拒绝。
用户可以被骗,数学不会。
服务器端要做的检查,简化后大致是:类型必须是webauthn.get;challenge必须和会话里那个一次性随机数完全一致;origin必须精确匹配;然后用存着的公钥验证签名。跳过origin检查,等于自己把钓鱼的口子重新捅开。
两个例外,让"防钓鱼"这句话保持诚实
第一,跨设备路径出过bug。CVE-2024-9956曾允许攻击者通过混合(蓝牙)传输在移动浏览器上接管通行密钥账户,目前Chrome、Safari和Firefox均已修复。
第二,攻击者会绕开这场仪式。用户@T3chFalcon本月发帖警告:"黑客正把通行密钥当诱饵,用来窃取微软365账户。"签名本身钓不走,但那个还留着密码和短信找回方式的账户可以。
私钥住在哪:同步型与设备绑定型
故事在这里分成两条线。同步意味着密钥材料确实离开了安全芯片,住进服务商的加密保险库。这是一次刻意的取舍,它把风险从"设备"挪到了"账户"。
网站可以通过AAGUID判断拿到的是哪一种——那是认证器数据里一个16字节的型号标识。大多数网站直接忽略它。
私钥读不出来,也就复制不出去。管理器之间的导出(FIDO的凭证交换格式)已在iOS 26和Android上开始落地,紧挨着它的协议目前还是工作草案。硬件密钥则从不导出,这是设计使然。
回到那组数字。谷歌报告称,通行密钥在不到一年内于4亿个账户上被使用超过10亿次,"比密码快50%"。微软每秒拦截7000次密码攻击。两边的账本都指向同一个方向,只是用户还得先接受一件事:这把钥匙,你永远拿不到手里。
热门跟贴