Passkey 和 WebAuthn

最近如果有关注一些新闻的话,应该会注意到 Apple,Google,Microsoft 这三家又在搞一个叫 Passkey 的东西。尤其是苹果用户感知应该比较强,毕竟也就苹果在宣传这个,其他用户就算宣传了也不一定会关注吧,就好比苹果发布会天天热度爆满一堆人熬夜都要看,Google 和 Microsoft 发布会国内压根没人看一样。

Passkey 算是一个比较抽象的概念,是一个比较新的标准。但是 Passkey 其实又不算是新的东西,因为它是建立在已有的东西上的标准,本身没有增加新的功能或者修改某些东西,算是拓展一类的,丰富了一下之前的标准。

首先简单的来说,Passkey 就是一个用来做无密码登录的东西,而且目标是多设备之间的无密码登录。无密码登录就是字面意思的那样,登录的时候不需要输入用户名和密码,他和你用密码管理器自动填充是有那么一点点类似的,但是并不使用密码,而是建立在公钥加密体系之上的。

就像之前说的,这是一个比较抽象的概念,要想了解他,就必须先了解之前的技术,对比他们之间的区别。

注意

由于 Passkey 的标准处于初期的演化阶段,因此本文后部分关于 Passkey 的描述将会有很强的时效性,阅读时注意发表日期,同时也会尽力跟踪最新的动态即时更新本文。

U2F

首先需要了解 U2F,它的全称是 Universal 2nd Factor,即通用第二因子验证,也就是常说的 2FA 这一类的。

U2F 标准提出的时间很早,大概在2014年就提出了 1.0 版本,但是 U2F 其实并没有发展起来,这个待会再说。

U2F 的设想很简单,基于公钥体系的 2FA。既然是 2FA,那么就会有两个操作,首先当然是注册一个 2FA,就像你绑定一个 OTP 验证器,或者绑定手机号码一样的。

U2F 的注册就是生成一对密钥,然后服务端生成一段随机字符作为挑战(challenge),使用这对密钥的公钥对挑战签名,将签名结果、对应的公钥、ID返回服务端。

服务端收到上述信息之后验证签名合法性,然后将公钥和用户绑定。在之后的验证阶段,也是一样的逻辑,生成挑战,发给客户端或者前端要求签名,服务端用已注册的公钥验证签名合法性。

这一逻辑总体上和你输入短信验证码、TOTP之类的并没有区别,只不过是改成了公钥体系而已。

那么生成密钥的这一部分怎么做呢,谁来生成,谁来保存私钥。常见的就是硬件密钥,

就是图上的这种东西,它是 Yubikey 的产品,一个硬件密钥。生成的私钥是不可导出的,只会储存在这个设备中,签名操作也是发送给硬件密钥让它来签名。

然而事实上来说,U2F 的硬件密钥基本上是不具备储存密钥的功能的,这听起来似乎很矛盾,但是又很合理。

那么如果一个硬件密钥不具备储存功能,又怎么保存私钥呢。答案很简单,在 U2F 当中,当完成注册操作的时候,是会返回一个 ID 的,这个ID的作用就是用来区分密钥的,为了方便区分“密钥”这一概念和密钥扮演的角色,我们现在给他称呼为“凭证”。

那么这个 ID 就是凭证ID,凭证 ID 的作用就是,当你需要进行验证过程的时候,把 ID 随着挑战发送出去,硬件密钥可以根据 ID 判断我有没有储存这个凭证,如果没有的话就直接不签名,告诉用户换一个密钥再试试。

既然如此,硬件密钥只需要自己储存一份主密钥,以后生成的每一对密钥,将私钥使用主密钥进行加密,再把加密后的结果编码到 ID 当中,即可实现伪储存功能,将密钥的实际储存交给外部来做。后续验证的时候通过 ID 解码出私钥即可。

U2F 的实现

前面说到,U2F 并没有发展起来,有两方面的没发展起来。

第一方面是浏览器接口上,U2F 毕竟是解决互联网问题的,那么它的使用场景基本可以认为就是在浏览器上。然而 U2F 的 API 在浏览器上的实现并没有做的很好,Chrome 浏览器上它的实现是用了浏览器拓展,在使用的时候需要连接拓展,非常的不方便

/**
 * The U2F extension id
 * @const {string}
 */
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
 u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';

/**
 * Connects directly to the extension via chrome.runtime.connect.
 * @param {function(u2f.WrappedChromeRuntimePort_)} callback
 * @private
 */
u2f.getChromeRuntimePort_ = function(callback) {
  var port = chrome.runtime.connect(u2f.EXTENSION_ID,
      {'includeTlsChannelId': true});
  setTimeout(function() {
    callback(new u2f.WrappedChromeRuntimePort_(port));
  }, 0);
};

而且如果在其他浏览器,例如 Firefox 上要使用的话,就又是另一种 API,这对于开发者来说简直是一个灾难性的问题,要考虑非常多的兼容问题,甚至遇到版本低的浏览器,IE、Safari 之类的甚至可能完全不支持 U2F。如果你作为一个开发者来说,会主动给自己找不痛快吗。

第二个问题就是,实际上支持 U2F 的网站也非常的少,国内是0,国外也依然很少,基本没有人实现了 U2F 作为一个 2FA 的选项。也没有人去推动这件事情。

而且 U2F 并没有解决密码管理的问题,用户仍然需要在登录的时候输入用户名和密码。

于是后来就出现了 FIDO2。

FIDO2

FIDO2 可以说是在 U2F 上的一次升级,并且统一了 API。在 FIDO2 之后,原先的 U2F 就叫做 FIDO U2F,而新的就是 FIDO2。

FIDO2 在注册和验证上和 U2F 的一样的,都是基于公钥体系的验证,没有破坏性升级,甚至可以说是兼容的。

FIDO2 相比 U2F 的一大升级就是,FIDO2 它真的支持储存密钥了!而且不光可以储存密钥,他甚至还可以储存用户名。

不同于 U2F 使用 ID 编码私钥的方案,FIDO2 设备允许真正意义上的把密钥储存在硬件密钥当中,返回的 ID 不包含任何私钥信息,纯粹的作为一个识别的用途。

同时 FIDO2 的宣传也不再是 2FA,而是 passwordless ,也就是无密码登录,将 FIDO2 的验证作为登陆选项,从以前的输入用户名和密码,登录之后再输入 TOTP 或者插入密钥进行 2FA 验证,升级成只需要输入用户名,再插入密钥,完成认证就等于登录完成。

总体上来说更方便,而且公钥体系总比密码要更安全,除非你的密码远超32位,包含数字大小写字母各种字符。

额外储存的用户名信息甚至可以更进一步的,登录的时候连用户名都不用输入!取而代之的是,由浏览器等客户端列出硬件密钥中储存的凭证信息,供用户根据用户名选择一个凭证,自动完成填充用户名和验证的步骤。不觉得很酷吗?作为一个程序员,我觉得这太酷了,很符合我对未来互联网的想象,科技并带着趣味。

我们说 FIDO2 是兼容 U2F 的,因为硬件密钥不可以无限制的储存凭证,它有储存个数的上限,例如 Yubikey 5系列来说,它能储存的带用户名的凭证最多25个。

FIDO2 中将这种带有用户名的凭证称为Resident Key,或者是 Discoverable Credentials,因为如果凭证带有用户名,那他一定是真正意义上的储存在设备上的,否则我们的浏览器就没有办法让用户选择凭证。

FIDO2 同时也支持 U2F 中将密钥编码到 ID 中的方案,那么这个一般来说就没有一个统一的称呼了。由于密钥编码在 ID 中,理论上一个硬件密钥可以“储存”无上限和这种凭证,即使已经储存了25个 resident key。

好吧这个图片和上面的看起来除了颜色之外没区别,实际上也确实没什么区别,Yubikey 4代只支持 U2F 的产品和 5 代支持 FIDO2 的产品外观上确实没什么太大区别。或许我们找一个别的产品,比如 Google 的 Titan Key

WebAuthn

上面讲了一大堆“凭证”、“硬件密钥”,“挑战”,“签名”等,那么作为一个开发者来说,我到底应该怎么写代码呢?这就是 WebAuthn 标准规定的。

WebAuthn 是一个 W3C 的标准,它定义了关于凭证相关的 API 接口,并且不光限于公钥体系的 FIDO U2F 和 FIDO2。可以说它是一个高层接口,屏蔽了底层交互信息,开发者只需要按照规定调用 API,就可以完成注册和登录。

在这个标准中,主要就是两个接口,当然了,说的是在浏览器中的 JavaScript 接口。

// 创建凭证
navigator.credentials.create();

// 使用凭证进行验证
navigator.credentials.get();

举个例子,如果我们要让用户创建一个 FIDO 凭证(此处的 FIDO 既指 U2F,也指 FIDO2),类似这样

const publicKeyCredentialCreationOptions = {
    challenge: [0, 1, 2, 3, 4, 5],
    rp: {
        name: "SnowStar",
        id: "snowstar.org",
    },
    user: {
        id: "UZSL85T9AFC",
        name: "[email protected]",
        displayName: "User1",
    },
    pubKeyCredParams: [{alg: -7, type: "public-key"}],
    authenticatorSelection: {
        authenticatorAttachment: "cross-platform",
    },
    timeout: 60000,
    attestation: "direct"
};

const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
});

在这上面的代码片段中,可以看到需要提供一些信息,这是当然的。

challenge 挑战就不多说了,服务端生成的随机字符串。

rp 全称是 Relying Party,指的就是服务的 ID。rp 有两个作用,第一个作用就是区分不同服务的,因为 FIDO2 可以储存用户名,当登录一个网站的时候,我们不可能把所有储存的凭证全都列出来让用户选。

比如我要登录 google.com,浏览器却列出一个 microsoft.com 上的用户名,这很明显的不合理的,而且 Yubikey 5 虽然现在只能储存25个凭证,但将来可能会支持上百甚至上千个凭证,一次性列出几百个凭证让用户选择吗?

这时候 rp 就起作用了,我们将 rp 信息传递给硬件密钥,后者只需要返回指定 rp 相关联的凭证即可。一般来说用户可能在几十个网站上拥有账户,而只会在同一个网站中拥有一到两个账户,通过 rp 就可以很好的进行筛选。

rp 的第二个作用就是和 CORS 跨域策略一样的作用,一定程度上防止钓鱼。

怎么防止呢,举个例子,假如现在用户访问了一个 banana.com,这是一个钓鱼网站,网站中的 JS 调用 navigator.credentials.get,并且将 rp 设置成 id: "apple.com"

这个时候如果我们真的把这个凭证请求传递给了密钥,那么攻击者就可以顺利的使用在 apple.com 上注册的凭证进行签名,从而拿到用户账户。

为了防止这种情况,浏览器就会对这个 rp 信息进行第一道校验,使用真正的域名 banana.com 进行判断,发现这两个信息不符合,就会阻断这次 API 调用,不会真正的把请求发送给后面的密钥。

那么第二道校验就是我们用户自己来做

一般来说真正和硬件密钥进行通信的这一层,可以理解为驱动之类的,可以把 rp 信息展示给用户,让用户明白这个请求具体要使用什么网站的账户。假如你现在正在登录 apple.com 网站,但是弹出的提示框说“请登录到 google.com”,那么我们就会很明显发现不对,进而手动取消这次验证。

FIDO 不光可以用在浏览器上,即使是原生应用,也是可以使用 fido 库来直接创建凭证和调用的,那么在这种情况下, 由于没有浏览器介入,因此没有第一道验证,此时的防钓鱼就只能由用户自己来判断。你打开的是 office 365,却要求登录到 google.com,那可以合理怀疑运行的 office 365 是修改过后的恶意软件了。

WebAuthn的验证器

这个就是上面代码片段中 authenticatorAttachment 参数的作用,在之前的例子中是 cross-platform,那么它还有一个可选值,就是 platform,是什么意思呢。

我们知道 WebAuthn 不光是给 FIDO 标准的硬件密钥使用的,他还可以使用其他的方案来实现公钥体系,只要它是一个具有密钥管理的服务就行,他甚至可以不需要是一个硬件,软件模拟也可以。

比如说,TPM 就是一种硬件密钥管理方案。TPM 使用 Windows 的用户应该不陌生,在升级 Windows 11 的时候有一项需求就是要求支持 TPM。TPM 从物理上来说,是一个附着在主板上,或者通过接口插在主板上的芯片,用途就是管理密钥,和安全验证。如果要详细的说明什么是 TPM,可能要单独写一篇长文,现在只需要明白它是和主板严格绑定的,你从这个主板上拆下来 TPM 芯片移植到另一个主板上,那他里面的密钥就无法再使用了。

再比如说,苹果的产品也都有加密芯片,起到的作用和 TPM 是类似的。Android 手机如果是最近几年比较新的,那么也是会有类似的功能,例如指纹模块,配合锁屏它同样的可以作为 WebAuthn 中储存密钥的验证器。

讲了这么多就会发现,platform 实际上就是指的这种和平台绑定的验证器,例如 TPM,Windows Hello,Android 指纹这些。因为它们都是不能离开这个设备的。而 cross-platform 就是指不和平台绑定的那些硬件密钥,例如上面图片中的 Yubikey 系列和谷歌 Titan Key。这些设备的特点就是可以移动,你可以在一台电脑上设置好凭证,在另一台电脑中插入硬件密钥来完成验证。

但是不论是 platform 还是 cross-platform,都会有缺点。

对于前者来说,缺点很明显,就是更换设备或者设备丢失之后,之前的凭证就无法使用了,这对于一些经常需要更换环境的用户来说是不可接受的,例如你有十台电脑,或许有点多,那就需要在十台电脑上分别设置十个凭证。

更要命的是,在这种情况下它不能完全取代密码,假设你在一台电脑上注册了账号,只设置了使用 platform 的验证器,那你无论如何都不能在第二台电脑上登录,因此还需要有另外的方法能让你在全新的设备上登录,那就是密码了。

而对于后者来说,并不会比 platform 的方便很多。首先他需要用户额外购买一个设备,意味着你需要付出额外的金钱,设备还需要随身携带,这增加了丢失的风险。并且如果你忘带了设备,也同样没办法登录。

Passkeys

于是乎,passkeys 他来了,我们在最一开始提到,passkey 并没有新增功能或者修改功能,那么他到底做了什么呢?

passkey 只做了一件微小的事情,那就是,允许 platform 类型的凭证漫游,并且将这种漫游的凭证称呼为 “passkey”。除此以外没有别的功能改动了。

对于 cross-platform 类型来说,密钥是无法离开硬件密钥存在的,无论是编码进 ID,还是储存在内部,没有设备就没有访问权限,凭证是无法进行漫游的,因为如果允许硬件内部的凭证漫游,那么这个硬件就毫无意义。

因此能够漫游的只能是 platform 类型的凭证,通过更改 platform 验证器的行为,我们将使用 platform 生成的凭证,通过一些方案,同步到其他设备中,从而能够在其他电脑或者手机上进行登录。

这个同步方案现在仍然处于一个前期方案。举个例子,当你在 Android 手机上创建凭证的时候,这个凭证会使用指纹和锁屏进行保护,并且会储存在谷歌密码管理当中,也就是 chrome 用的那个密码管理器,随后凭证就会跟随谷歌账户,同步到其他的手机中。

而对于苹果的产品来说,凭证则是储存在 iCloud keychain中的,同步也是 keychain 来做。微软同样有他的方案,凭证使用 Windows Hello 进行储存和同步。

在目前的情况下,这三家的同步方案并不能做到真正意义上的跨平台,例如你在 Android 上创建的凭证,他并不会同步到你在 Windows 上使用的 Chrome 浏览器中。反之你在 Windows 的 Chrome 中创建的凭证也不会同步到 Android 手机中。

那么既然不能真正跨平台,那 passkey 又有什么用呢。凭证不跨平台漫游,并不代表不能跨平台验证。这就是 passkey 的另一个亮点,那就是可以跨平台调用凭证。

如图中所示,当我们在桌面平台上需要进行验证,而当前系统或者设备中并没有储存凭证的时候,怎么办呢,passkey 给我们提供了多种方案。

例如图中第二个选项,就是我本人使用的 Android 手机,如果选择这一选项,就会发送一个通知给手机,用户打开手机,在手机上完成选择凭证,并且验证。随后验证结果会通过蓝牙协议传输回电脑,此时电脑上的 Chrome 就可以使用接收到的验证结果来完成登录或者其他请求。

为什么要使用蓝牙呢,因为蓝牙是近距离通信,虽然距离比 NFC 长,但对比互联网来说仍然属于近距离,这也是为了安全角度,确保当使用凭证的时候,手机肯定在我们手上,并且我们就在目标电脑附近,从而在一定程度上防止远程钓鱼和窃取凭证。

那么另一种就是扫描二维码,本质上和上面的方案是一样的,同样也要求蓝牙的支持。由于是测试网站,即便读者扫了二维码也没有任何用。

而在完成跨平台的验证之后,对于厂商来说,为了进一步提升便利性,可以供用户选择,是否在新登录的设备中创建新的凭证,以便后续登录时可以不需要再借助外部其他设备进行验证。当然这也是可选项而已。

Passkeys 中称呼的改变

由于 passkey 增加了漫游这一功能,因此对于 FIDO2 时代的一些称呼就要随着改变。

首先 FIDO2 中不管的 platform 还是 cross-platform 来说,凭证都是不可复制的,从来不会离开设备。而 passkey 版本来说,可漫游的密钥目前的一个叫法是 copyable passkeys,也就是可复制的 passkey,也会叫做 “multi-device,” “syncable,” “backup enabled,” “shareable,” 等这些说法。

这些说法可能会和 FIDO 中的 platformcross-platform 产生一些混淆,因此 Yubico 那边会更偏向于叫它们 copyable。与此相对的,不可复制的,也就是真正储存在 platform 或者 cross-platform 中的凭证,就叫做 single-device passkeys。同时 Yubico 更倾向于叫做 hardware bound 也就是硬件绑定的

硬件绑定的这种说法它准确的描述了凭证实际储存的位置,同时也避免了在 single-device 的叫法时,让用户误以为这个凭证只能在一个固定设备中使用的麻烦。

Passkeys 的代价与优势

由于 passkey 中,凭证是可以离开设备进行漫游的,因此相比于硬件绑定的凭证来说,的确会牺牲一些安全性,然而与此同时带来的便利性提升却是巨大的。

这种略微降低安全性却能大幅提升便利性的改动,对于普通用户和普通产品来说,是利大于弊的,这些情况是不需要如此高的安全性。而对于真正需要高安全性的产品和用户来说,硬件绑定的凭证仍然是一个最佳选择,因为它有一个简单的道理,没有设备就没有权限(no device,no access)

passkey 的出现将会比 WebAuthn 和 FIDO2 的出现更能推动互联网向更安全更简单的 passwordless 甚至是 usernameless 发展,因为它给用户带来的侵入性更小,迁移成本更小,无需额外付出金钱,但却可以带来巨大的便利。

在 FIDO2 时代,没有能够大范围推广,我相信需要一个单独的硬件密钥这一条件是占非常大的比重,从而导致很少有用户会去购买一个硬件密钥,进而厂商不愿意投入资源为了这些少数用户进行 FIDO2 支持,反过来又造成用户不会为了少数厂商支持的 FIDO2 而去使用的恶心循环。

而现在 passkey 则可以一定程度打破这个循环,用户只需要一部近几年来比较新的手机,就可以几乎无成本的享受 FIDO2 带来的便利性。同时它与目前国内广泛流行的强制扫码登录不同,即便用户手机不在身边,也依然可以使用其他的凭证进行登录。这对于用户来说几乎是没有副作用的。

passkey 白皮书

总结

实际上本文并没有从开发者角度来过多的讲述 API 的使用,而是从 U2F 开始介绍 FIDO 联盟的发展,最终引出 passkey 是什么。

关于 WebAuthn,已经有大量的资料可以参考。

例如在线的 demo网站:https://webauthn.io,在这个网站上可以随意输入用户名,测试注册和登录,并且还支持一些设置。

以及开发指导 https://webauthn.guide,这个网站给出了一个简单明了的 API 使用案例。

还有一个专门给 passkey 做的 demo 网站, https://www.passkeys.io,和第一个 demo 类似,但是不支持设置。

关于 “Passkey 和 WebAuthn” 的 2 个意见

  1. 感謝分享優質文章
    不過有一個地方還是不大明白
    所以passkey並不是註冊時生成的私鑰
    只是註冊時產生的憑證囉?
    可是這樣同步到其他裝置驗證的時候,驗證不就無法通過了嗎?
    還是漫遊的passkey就是註冊時的私鑰
    那這樣私鑰如果同步過程中被竊取
    不就有資安的風險?
    抱歉一直想不通,再請不吝指教,謝謝

    1. passkey 其实也是私钥,简单的理解就是一个支持漫游的 webauthn/fido2 版本,如果要漫游的时候也是同步私钥,所以他的确是会有这个风险的。因此对于特别需要安全性的场景来说,是可以要求用户使用不能漫游的设备来注册,来避免私钥在同步的时候泄露,比如 yubikey 这种。另外就是 passkey 不一定是一定要漫游的,比如文章后面 chrome 上面的截图,它可以有一个扫码的选项,等于是把“挑战”或者叫“验证码”发到储存私钥的实际设备上,再把签名结果传回来,这一过程中私钥是不会传输和同步,也就不会泄露。说到底 passkey 就是解决一下 fido2 的入门门槛问题,毕竟你以前想享受 passwordless 这一功能,你需要先花钱买个设备,现在通过牺牲一定的安全性,换取了一些便利性。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注