Passkeys:什么鬼?为什么?

Avatar of Neal Fennimore
Neal Fennimore

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 价值200美元的免费积分!

这些名为 passkeys 的东西最近很火。它们是 W3C TPAC 2022 的主要亮点,获得了 Safari 16 的支持,正在进入 macOS 和 iOS,并将成为 像 1Password 这样的密码管理器的未来。它们 已得到 Android 的支持,并将在未来版本中进入 Chrome OS 和 Windows。

极客式的操作系统安全增强功能并没有在前端社区引起轰动,但 passkeys 将成为“事物”是合乎逻辑的。考虑到密码和密码应用程序如何影响身份验证和表单处理等方面的用户体验,我们至少应该了解它们,这样我们就知道将要发生什么。

这就是本文的目的。我一直在研究和尝试 passkeys——以及它们基于的 WebAuthn API——已经有一段时间了。让我分享一下我所学到的知识。

目录

术语

这是我们深入了解时需要了解的术语的必备部分。像大多数科技一样,passkeys 充斥着深奥的词汇和缩略词,这些词汇和缩略词往往是理解的障碍。我将在这里尝试为您解释一些内容。

  • 依赖方: 您将要验证的服务器。在本文中,我们将使用“服务器”来隐含依赖方。
  • 客户端: 在我们的例子中,就是 web 浏览器或操作系统。
  • 验证器: 允许生成和存储公钥对的软件和/或硬件设备。
  • FIDO:一个开放标准机构,也创建关于 FIDO 凭据的规范。
  • WebAuthn:passkeys 的底层协议,也称为 FIDO2 凭据或单设备 FIDO 凭据。
  • Passkeys:WebAuthn,但带有云同步(也称为多设备 FIDO 凭据、可发现凭据或驻留凭据)。
  • 公钥密码学: 一个生成的密钥对,包括一个私钥和一个公钥。根据算法,它应该用于签名和验证,或者用于加密和解密。这也被称为 非对称密码学
  • RSA: 三位创作者 Rivest、Shamir 和 Adel 的首字母缩写。RSA 是一个较老但仍然有用的公钥密码学家族,它基于素数分解。
  • 椭圆曲线密码学 (ECC): 一个较新的密码学家族, 基于椭圆曲线.
  • ES256: 一个椭圆曲线公钥,使用 ECDSA 签名算法(PDF)和 SHA256 进行哈希运算。
  • RS256: 与 ES256 相似,但它使用 RSA,使用 RSASSA-PKCS1-v1.5 和 SHA256。

什么是 passkeys?

在具体谈论 passkeys 之前,我们需要谈论另一个名为 WebAuthn (也称为 FIDO2)的协议。Passkeys 是建立在 WebAuthn 之上的一个规范。WebAuthn 允许使用公钥密码学来取代密码。我们使用某种安全设备,例如硬件密钥或 可信平台模块 (TPM),来创建私钥和公钥。

公钥可以被任何人使用。但是,私钥不能从生成它的设备中删除。这是 WebAuthn 的问题之一;如果您丢失了设备,您将失去访问权限。

Passkeys 通过提供您的凭据的云同步解决了这个问题。换句话说,您在计算机上生成的内容现在也可以在您的手机上使用(尽管令人困惑的是,也存在单设备凭据)。

目前,在撰写本文时,只有 iOS、macOS 和 Android 提供对云同步 passkeys 的完全支持,即使那样,它们也受到所用浏览器的限制。Google 和 Apple 分别通过其 Google 密码管理器 和 Apple iCloud 钥匙串 服务提供同步界面。

passkeys 如何取代密码?

在公钥密码学中,您可以执行称为 签名 的操作。签名会获取一段数据,然后使用私钥将其运行到签名算法中,然后可以使用公钥对其进行验证。

任何人都可以生成公钥对,它不属于任何个人,因为任何人都可能首先生成了它。使其有用的原因是,只有使用私钥签名的数据才能使用公钥进行验证。这部分取代了密码——服务器存储公钥,我们通过验证我们是否拥有另一半(例如私钥)来登录,方法是签署一个随机挑战。

作为一项额外的好处,由于我们将用户的公钥存储在数据库中,因此不再担心密码泄露会影响数百万用户。这减少了网络钓鱼、泄露以及我们当前依赖密码的世界所面临的一系列其他安全问题。如果数据库被泄露,所有存储在用户公钥中的内容都将变得对攻击者毫无用处。

也不再需要忘记电子邮件及其关联的密码!浏览器将记住您为哪个网站使用了哪些凭据——您只需要点击几下,就可以登录。您可以提供一种辅助验证方式来使用 passkey,例如生物识别或 PIN,但这些方式仍然比过去几年的密码快得多。

更多关于密码学的知识

公钥密码学涉及拥有私钥和公钥(称为密钥对)。密钥一起生成并具有不同的用途。例如,私钥应保密,而公钥应提供给您希望与之交换消息的任何人。

在加密和解密消息时,使用接收者的公钥加密消息,以便只有接收者的私钥才能解密消息。在安全术语中,这被称为“提供机密性”。但是,这并不能证明发送者就是他们自称的人,因为任何人都可以使用公钥向某人发送加密消息。

在某些情况下,我们需要验证消息确实来自发送者。在这些情况下,我们使用签名和签名验证来确保发送者就是他们自称的人(也称为 真实性)。在公钥(也称为 非对称)密码学中,这通常通过对消息的哈希值进行签名来完成,以便只有公钥才能正确验证它。哈希值和发送者的私钥在通过算法运行后会生成签名,然后任何人都可以使用发送者的公钥验证消息来自发送者。

我们如何访问 passkeys?

要访问 passkeys,我们首先需要生成它们并将其存储在某处。一些功能可以通过验证器提供。 验证器 是任何提供加密密钥生成能力的硬件或软件支持的设备。想想您从 Google Authenticator、 1Password 或 LastPass 等获得的一次性密码。

例如,软件身份验证器可以使用设备的受信任平台模块 (TPM) 或安全区域来创建凭据。然后可以将凭据存储在远程位置并在设备之间同步,例如密码键。硬件身份验证器类似于 YubiKey,它可以在设备本身生成和存储密钥。

要访问身份验证器,浏览器需要访问硬件,为此,我们需要一个接口。我们在这里使用的接口是客户端到身份验证器协议 (CTAP)。它允许通过不同的机制访问不同的身份验证器。例如,我们可以通过利用 CTAP 通过 NFC、USB 和蓝牙访问身份验证器。

使用密码键的一个更有趣的方法是通过蓝牙将您的手机连接到可能不支持密码键的另一台设备。当设备通过蓝牙配对时,我可以使用我的手机作为中介登录到我计算机上的浏览器!

密码键与 WebAuthn 之间的区别

密码键和 WebAuthn 密钥在几个方面有所不同。首先,密码键被认为是多设备凭据,可以在设备之间同步。相比之下,WebAuthn 密钥是单设备凭据——一种花哨的说法,即您绑定到一个设备进行验证。

其次,要对服务器进行身份验证,WebAuthn 密钥需要提供用户的登录句柄,之后服务器会将一个 allowCredentials 列表返回给客户端,告知可以使用哪些凭据登录。密码键跳过此步骤,并使用服务器的域名来显示哪些密钥已绑定到该站点。 您能够选择与该服务器关联的密码键,因为它已经被您的系统识别。

否则,密钥在密码学上是相同的;它们只是在存储方式以及用于启动登录过程的信息方面有所不同。

过程… 简而言之

生成 WebAuthn 或密码键的过程非常相似:从服务器获取一个挑战,然后使用 navigator.credentials.create Web API 生成一个公钥对。然后,将挑战和公钥发送回服务器以进行存储。

服务器在收到公钥和挑战后,会验证挑战和创建它的会话。如果验证通过,则会将公钥存储起来,以及数据库中的任何其他相关信息,例如用户标识符或证明数据。

用户还有一个步骤——从服务器检索另一个挑战,并使用 navigator.credentials.get API 对挑战进行签名。我们将签名的挑战发送回服务器,服务器会验证挑战,然后如果签名通过,则登录我们。

当然,每个步骤还有更多内容。但这通常是我们使用 WebAuthn 或密码键登录网站的方式。

重点

密码键用于两个不同的阶段:证明断言阶段。

证明阶段也可以看作是注册阶段。您将在新网站上使用电子邮件和密码进行注册,但是,在这种情况下,我们将使用我们的密码键。

断言阶段类似于您在注册后登录网站的方式。

证明

查看全尺寸

navigator.credentials.create API 是我们证明阶段的重点。我们已在系统中注册为新用户,需要生成一个新的公钥对。但是,我们需要指定要生成哪种密钥对。这意味着我们需要向 navigator.credentials.create 提供选项。

// The `challenge` is random and has to come from the server
const publicKey: PublicKeyCredentialCreationOptions = {
  challenge: safeEncode(challenge),
  rp: {
    id: window.location.host,
    name: document.title,
  },
  user: {
    id: new TextEncoder().encode(crypto.randomUUID()), // Why not make it random?
    name: 'Your username',
    displayName: 'Display name in browser',
  },
  pubKeyCredParams: [
    {
      type: 'public-key',
      alg: -7, // ES256
    },
    {
      type: 'public-key',
      alg: -256, // RS256
    },
  ],
  authenticatorSelection: {
    userVerification: 'preferred', // Do you want to use biometrics or a pin?
    residentKey: 'required', // Create a resident key e.g. passkey
  },
  attestation: 'indirect', // indirect, direct, or none
  timeout: 60_000,
};
const pubKeyCredential: PublicKeyCredential = await navigator.credentials.create({
  publicKey
});
const {
  id // the key id a.k.a. kid
} = pubKeyCredential;
const pubKey = pubKeyCredential.response.getPublicKey();
const { clientDataJSON, attestationObject } = pubKeyCredential.response;
const { type, challenge, origin } = JSON.parse(new TextDecoder().decode(clientDataJSON));
// Send data off to the server for registration

我们将获得 PublicKeyCredential,其中包含在创建后返回的 AuthenticatorAttestationResponse。凭据包含生成的密钥对的 ID。

响应提供了几个有用的信息。首先,我们在该响应中拥有我们的公钥,需要将其发送到服务器以进行存储。其次,我们还获得 clientDataJSON 属性,我们可以对其进行解码,并从那里获得密码键的 typechallengeorigin

对于证明,我们想要在服务器上验证 typechallengeorigin,以及存储公钥及其标识符,例如 kid。我们也可以选择存储 attestationObject,如果我们愿意。另一个有用的存储属性是 COSE 算法,它在我们的 PublicKeyCredentialCreationOptions 中定义,alg: -7alg: -256,以便在断言阶段轻松验证任何签名的挑战。

断言

查看全尺寸

navigator.credentials.get API 将是我们断言阶段的重点。从概念上讲,这将是用户在注册后登录 Web 应用程序的地方。

// The `challenge` is random and has to come from the server
const publicKey: PublicKeyCredentialRequestOptions = {
  challenge: new TextEncoder().encode(challenge),
  rpId: window.location.host,
  timeout: 60_000,
};
const publicKeyCredential: PublicKeyCredential = await navigator.credentials.get({
  publicKey,
  mediation: 'optional',
});
const {
  id // the key id, aka kid
} = pubKeyCredential;
const { clientDataJSON, attestationObject, signature, userHandle } = pubKeyCredential.response;
const { type, challenge, origin } = JSON.parse(new TextDecoder().decode(clientDataJSON));
// Send data off to the server for verification

我们又将获得一个带有 AuthenticatorAssertionResponsePublicKeyCredential。凭据再次包含密钥标识符。

我们还从 clientDataJSON 中再次获得 typechallengeoriginsignature 现在包含在响应中,以及 authenticatorData。我们需要它们以及 clientDataJSON 来验证这是使用私钥签名的。

authenticatorData 包含一些值得跟踪的属性。第一个是您正在使用的源的 SHA256 哈希,位于前 32 个字节内,这对于验证请求是否来自相同的源服务器很有用。第二个是 signCount,它来自第 33 到 37 个字节。这是从身份验证器生成的,应该与其以前的值进行比较,以确保密钥没有发生任何问题。当它是多设备密码键时,该值应始终为 0,并且当它是单设备密码键时,该值应始终大于以前的值。

一旦您断言了您的登录,您就应该登录了——恭喜!密码键是一个非常棒的协议,但它确实有一些注意事项。

一些缺点

密码键有很多优点,但是,在撰写本文时,它也存在一些问题。首先,密码键在某种程度上仍然处于早期支持阶段,Windows 上只允许使用单设备凭据,而 Linux 系统几乎没有支持。 Passkeys.dev 提供了一个 类似于该协议的 Caniuse 的好表格

此外,Google 和 Apple 的密码键平台无法相互通信。如果您想将您的凭据从 Android 手机转移到 iPhone 上……好吧,目前您只能祝你好运。这并不是说没有互操作性!您可以通过使用手机作为身份验证器来登录到计算机。但是,将其直接内置到操作系统中并同步会更简洁,而不会被锁定在供应商级别。

未来将会怎样?

未来的密码键协议会是什么样子?看起来很棒!一旦它获得更多操作系统的支持,使用量应该会增加,您会开始看到它在野外被越来越多地使用。一些 密码管理器 甚至会直接支持它们。

密码键绝不仅限于 Web 支持。AndroidiOS 都将支持原生密码键作为一等公民。我们仍处于这一切的早期阶段,但预计会看到它被越来越多地提及。

毕竟,我们消除了对密码的需要,从而使世界变得更加安全!

资源

如果您想了解更多关于 Passkeys 的信息,这里还有一些额外的资源。 这篇文章还附带了一个我整理的代码库和演示。