深入解析Passkeys技术:从WebAuthn到Bitwarden的实现细节

本文深入探讨Passkeys技术的底层实现,包括WebAuthn规范、密钥生成与存储机制,以及Bitwarden密码管理器中的具体实现方式,对比硬件安全密钥的安全模型差异。

Passkeys – 技术内幕

去年,Passkeys技术引起了广泛关注,有时甚至被称为"密码杀手技术"。这主要源于苹果和谷歌宣布支持该技术,随后许多其他服务也纷纷跟进。与传统密码相比,Passkeys的主要优势在于其抗钓鱼攻击和抗服务器泄露的能力。某些厂商还推动了一项功能:即使尚未在所有地方实现,也能将Passkeys同步到多个设备。这将解决硬件安全密钥的一个主要缺点:用户凭证备份。

然而,“Passkey"这个术语容易让人困惑。许多文章从概念上解释了Passkeys的工作原理,但很少有文章详细说明实际工作方式和具体实现。在本博客中,我们希望深入探讨,看看一些现有解决方案的实际工作方式,并将它们与硬件安全密钥进行比较。

首先,Passkey是一种FIDO凭证,由浏览器根据WebAuthn规范创建。如之前的博客文章所述,WebAuthn指定了一个API,允许网站使用浏览器对用户进行身份验证。简而言之,服务或网站(在WebAuthn中称为依赖方)通过要求客户端使用与服务已知公钥匹配的客户端私钥对随机生成的挑战和其他信息进行签名来验证客户端。设计上,服务只存储公钥,因此即使在某些时候被攻破,也不会泄露用户私钥的任何信息。与传统密码认证相比,这是一个巨大优势。此外,浏览器在签名中包含服务地址,因此可以阻止钓鱼攻击。

例如,网站webauthn.io允许测试Passkey创建。

如果我们点击"Register"按钮,浏览器将开始创建凭证。实际上,调用navigator.credentials.create()函数为服务生成非对称密钥对。在兼容的Microsoft Windows操作系统和Firefox等浏览器中,会出现以下弹出窗口:

它要求我们输入指纹或PIN码,以确认我们要为此服务创建凭证。为了了解实际发生的情况,我们可以打开浏览器控制台(F12键)并检查消息:

注册选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "rp": {
    "name": "webauthn.io",
    "id": "webauthn.io"
  },
  "user": {
    "id": "c3lsdmFpbg",
    "name": "sylvain",
    "displayName": "sylvain"
  },
  "challenge": "5MvoufqYlltIT9JaQFMGG83ej7yeHqxOYmzE0vFkzVs2bIJEesg7zGoYiGhnrDBoj4ui9Uqa1wgfagbzlHluLQ",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 60000,
  "excludeCredentials": [],
  "authenticatorSelection": {
    "residentKey": "preferred",
    "requireResidentKey": false,
    "userVerification": "preferred"
  },
  "attestation": "none",
  "hints": [],
  "extensions": {
    "credProps": true
  }
}

服务(或依赖方)webauthn.io要求使用算法-7和-257生成公钥,即使用SHA-256的ECDSA或使用SHA-256的RSASSA-PKCS1-v1_5。一旦我们扫描指纹或输入PIN码,控制台中就会显示新生成的公钥:

注册响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "id": "j5MX4uBITwi0zQBMyu5CaQ",
  "rawId": "j5MX4uBITwi0zQBMyu5CaQ",
  "response": {
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAANVIgm55tNtAo9gREW9-g0kAEI-TF-LgSE8ItM0ATMruQmmlAQIDJiABIVggc9C6bLjbr1myHSzFFrU60bsXemfXoeHNHRkpvu6EPvMiWCBX0h4x51kN_kA0UY_iIM9ZCcCO9vJv87YYvNRZi5ZDvQ",
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiNU12b3VmcVlsbHRJVDlKYVFGTUdHODNlajd5ZUhxeE9ZbXpFMHZGa3pWczJiSUpFZXNnN3pHb1lpR2huckRCb2o0dWk5VXFhMXdnZmFnYnpsSGx1TFEiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
    "transports": [
      "internal"
    ],
    "publicKeyAlgorithm": -7,
    "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc9C6bLjbr1myHSzFFrU60bsXemfXoeHNHRkpvu6EPvNX0h4x51kN_kA0UY_iIM9ZCcCO9vJv87YYvNRZi5ZDvQ",
    "authenticatorData": "dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAANVIgm55tNtAo9gREW9-g0kAEI-TF-LgSE8ItM0ATMruQmmlAQIDJiABIVggc9C6bLjbr1myHSzFFrU60bsXemfXoeHNHRkpvu6EPvMiWCBX0h4x51kN_kA0UY_iIM9ZCcCO9vJv87YYvNRZi5ZDvQ"
  },
  "type": "public-key",
  "clientExtensionResults": {},
  "authenticatorAttachment": "cross-platform"
}

浏览器还返回一个随机ID,允许为同一登录注册多个Passkey。私钥仅存储在用户端,因此不会被服务器泄露。之后,当用户在同一服务上进行身份验证时,调用函数navigator.credentials.get。同样,Windows会出现以下弹出窗口:

在控制台中,同时出现以下消息:

认证选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "challenge": "tGrdV4e5c2Ysb2ESzSOoje9nZk0ExA-RkG7j-rejmryRdPM02Mtr-f_gEAUQB4OEBeD_0TzeGkhKWfB5Xh9QBQ",
  "timeout": 60000,
  "rpId": "webauthn.io",
  "allowCredentials": [
    {
      "id": "j5MX4uBITwi0zQBMyu5CaQ",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "userVerification": "preferred"
}

本质上,服务要求我们对挑战以及服务地址和其他信息进行签名。服务还显示允许登录的凭证ID及其类型。对于Passkey,它标记为"internal”,而对于硬件安全密钥,则标记为"usb"。再次输入PIN码后,服务对我们进行身份验证。在控制台中,有以下消息:

认证响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "id": "j5MX4uBITwi0zQBMyu5CaQ",
  "rawId": "j5MX4uBITwi0zQBMyu5CaQ",
  "response": {
    "authenticatorData": "dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAQ",
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoidEdyZFY0ZTVjMllzYjJFU3pTT29qZTluWmswRXhBLVJrRzdqLXJlam1yeVJkUE0wMk10ci1mX2dFQVVRQjRPRUJlRF8wVHplR2toS1dmQjVYaDlRQlEiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
    "signature": "MEUCIHbydreK68UUV7fEcFPDn3vEbmHL4AIyA6xYIWClv5GdAiEAidtPntmfvy4X5kGK1LWYl76OEqqCwYD5aFkiBIMU1O4",
    "userHandle": "c3lsdmFpbg"
  },
  "type": "public-key",
  "clientExtensionResults": {},
  "authenticatorAttachment": "cross-platform"
}

我们注意到,作为签名消息一部分的"clientDataJSON"字段在其内容中包含"origin"字段:

1
2
3
>>> from base64 import urlsafe_b64decode
>>> urlsafe_b64decode("eyJjaGFsbGVuZ2UiOiJyUkYtSUxGNGN6dklObGpnbnhfUXVFd1dRc2JUbmt5Y2RxcTJjVVZUUjJTT3NaZmtsaU9ZZ3VxMkJqQVBEdmJIa3VWZTd2V3Z2TF9EdE1YSkRpTTg3ZyIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uaW8iLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0==")
b'{"challenge":"rRF-ILF4czvINljgnx_QuEwWQsbTnkycdqq2cUVTR2SOsZfkliOYguq2BjAPDvbHkuVe7vWvvL_DtMXJDiM87g","origin":"https://webauthn.io","type":"webauthn.get"}'

该字段由浏览器直接读取,而不是由服务提供。它可以检测任何钓鱼尝试,因为签名对其他服务无效。

最后,在Windows中,Passkeys由Microsoft Hello使用系统TPM(如果可用)进行保护。我们可以在Passkey设置菜单中管理保存的Passkeys:

到目前为止,唯一的问题是我们对Passkeys的生成、存储和保护方式了解不多。我们也不能将它们导出到其他设备,例如Linux机器。我们可以使用命令行中的certutil工具获取一些额外信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> certutil -csp "Microsoft Passport Key Storage Provider" -key -v
Microsoft Passport Key Storage Provider:
S-1-12-1-3939627729-1327541301-18900911-3508007247/a946056c-151d-469b-8fb1-f2efad69b10a/FIDO_AUTHENTICATOR//74a6ea9213c99c2f74b22492b320cf40262a94c1a950a0397f29250b60841ef0_63336c73646d46706267

ECDSA_P256
RSA
Key Id Hash(rfc-sha1): 32384a43c96daa0f4a46652d479a9b075227b0f8
Key Id Hash(sha1): ae7d6c5050694a46dbfcb94d8104d07e59252c11
Key Id Hash(bcrypt-sha1): d62022dbd4eef93cb7268d6abd59da6cf931aace
Key Id Hash(bcrypt-sha256): 0b0d814e0bcba1bbd31f1d4b9f362b621790694f3220c6a76313b26d3cb04b92
Container Public Key:
0000  04 be a2 6b 2f 32 96 ab  75 b8 b7 c6 7e 5d 1b 93
0010  29 f8 79 4b 48 e4 85 22  06 2d 99 58 bc 1e d1 f3
0020  65 dc 11 98 85 17 5b 4a  6b c0 83 dc 3d 24 b3 3b
0030  0c dc ec fe 47 62 3c 53  75 7d 6f b4 31 82 54 a3
0040  ad

它显示公钥值和其他一些信息,但关于如何存储或加密的信息不多。此外,Microsoft不允许将Passkeys与其他设备同步。在Apple或Android设备上,此功能已启用。它解决了先前安全密钥的一个主要问题:用户备份。然而,这可能会将用户锁定到特定供应商,因为Passkeys不会在不同生态系统(如Apple和Google)的设备之间同步。例如,使用Apple笔记本电脑的用户将无法在Android手机上检索其Passkeys。对于硬件安全密钥,由于私钥无法随时访问,需要为每个服务注册第二个硬件安全密钥,以防第一个损坏或丢失。这对这类设备来说是一个很大的缺点。

另一方面,安全模型已经改变。对于安全密钥,私钥存储在安全元件内部,具有物理访问安全密钥的攻击者无法恢复私钥值。对于Passkeys,私钥在某个时刻被解密并存储在内存中,因此可能被具有机器访问权限的攻击者访问。这种威胁模型的改变需要根据用户的需求进行了解和选择。

Bitwarden

为了更深入地研究,我们可以检查最近实现Passkey支持的Bitwarden密码管理器。主要优势是Bitwarden是开源的,因此我们可以检查其实现。浏览器扩展可以从其网站下载,但为了能够调试扩展,我们使用了GitHub存储库中的源代码。

让我们看看Bitwarden浏览器扩展的工作原理。一旦在浏览器中安装了扩展,当我们浏览使用Passkeys的服务时,会看到扩展拦截Webauthn调用并显示自己的弹出窗口,允许将Passkey保存在Bitwarden中。

实际上,在代码中我们注意到Webauthn调用被重写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const browserCredentials = {
  create: navigator.credentials.create.bind(
    navigator.credentials,
  ) as typeof navigator.credentials.create,
  get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get,
};

const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window));

navigator.credentials.create = createWebAuthnCredential;
navigator.credentials.get = getWebAuthnCredential;

现在,每次浏览器调用navigator.credentials.create时,最终都会调用函数createWebAuthnCredential,该函数本身调用makeCredential。之前的浏览器函数指针保存在browserCredentials中,以防用户选择"hardware key"选项。在这种情况下,将使用先前的操作系统Passkey机制(如Microsoft Hello)或硬件安全密钥。

如果我们在makeCredential函数末尾设置断点,可以检查创建的FIDO2凭证:

有趣的是,与硬件安全密钥(其中私钥等信息永远无法访问)相比,可以看到在Bitwarden的情况下一切是如何生成的。最后,当Passkey创建后,它以与Bitwarden密码相同的方式加密存储。Passkeys还通过端到端加密同步到Bitwarden服务器,并且可以在安装Bitwarden的任何品牌的其他设备上访问。这稍微缓解了先前描述的供应商锁定问题。另一个有趣的功能是私钥可以从保险库以JSON格式导出。这可能允许在另一个密码管理器中使用Passkeys:

我们恢复了与之前断点相同的信息。我们可以验证存储在"keyValue"中的私钥确实是有效的ECDSA密钥:

1
2
3
4
5
6
>>> from base64 import urlsafe_b64decode
>>> from Crypto.PublicKey import ECC
>>> key = urlsafe_b64decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg_dPKJYzILFODdIoqCMNFSf8lW2eshE1svRoSDTI5fW-hRANCAATZ_O0udqtAzQgVlpvSJR-W_ATFwfJe5zZQZZR8jZIBbZBHkTFMXknfbPYAnkcBiaZ2I65_ekaFZka7w5SF7dj7")
>>> mykey = ECC.import_key(key)
>>> mykey
EccKey(curve='NIST P-256', point_x=98598770569431995048367531420607085473572368805074580755539128000146379506029, point_y=65259498419889818768139648241655501916327374218050147091927245960562103671035, d=114809350587340781021412553336309859741152322992353337399001953495223948115311)

类似地,当浏览器在Bitwarden代码中调用navigator.credentials.get函数时,调用函数getAssertion。当返回签名时,我们也可以在Python中使用公钥验证其有效性:

1
2
3
4
>>> from Crypto.PublicKey import ECC
>>> from Crypto.Hash import SHA256
>>> from Crypto.Signature import DSS
>>> signature = urlsafe_b64decode("MEYCIQCwDTCys2jgUyfnArlYrVeByRuasP8sjM73iYJzk14UrAIhALp2BBronN3ds0wLxI13B7YKDn1jdRCtGyseBwzq
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计