我从构建 passkeybot 中学到的关于密码密钥的一些知识。
Things I learnt about passkeys when building passkeybot

原始链接: https://enzom.dev/b/passkeys/

## Passkeybot.com & Passkey Insights Passkeybot.com 简化了网站使用服务器端 HTTP 处理程序集成 passkey 的过程。构建它揭示了 passkey 技术的关键方面。Passkey 依赖于**安全飞 enclave 处理器 (SEP)**——设备内隔离的安全芯片——来存储私钥,需要生物识别/密码验证才能进行签名。类似的安全元件存在于手机 SIM 卡中。 至关重要的是,**用户验证 (UV)**——生物识别/密码确认——比**用户存在 (UP)**——简单的按钮按下——更安全。Passkey 使用**认证器**(如 SEP 或 YubiKey),通过标准化的 JavaScript API 访问,抽象了底层的操作系统交互。 **证明 (Attestation)** 验证用于*创建* passkey 的硬件,从而实现信任策略,但也引发了指纹识别问题。它通常默认禁用(例如在 Apple 设备上),并且如果密钥在设备之间同步,则会失效。Passkey 仅用于身份验证,不用于通用签名。 安全性依赖于安全的 JavaScript 代码,尽管浏览器提供了诸如子资源完整性之类的保护措施,但仍可能受到损害。即将推出的功能,如**即时中介 (immediate mediation)**,旨在实现更快的登录。**相关来源请求 (Related Origin Requests)** 允许对 passkey 创建进行域名授权。 最后,诸如用于附近设备的蓝牙配对、用于删除提示的信号 API 以及数字凭证 API(用于访问操作系统钱包)之类的功能扩展了 passkey 的功能。最初用于 OAuth 的 PKCE 被 Passkeybot 用于在无需管理密钥的情况下保护 API 访问。

## Passkeys:早期经验与挑战 最近在Hacker News上的一场讨论,源于一位开发者构建“passkeybot”的经验,突显了passkey采用的潜力与当前障碍。虽然passkey比密码提供更强的安全性——尤其是在防范网络钓鱼方面——但易用性和实施复杂性仍然存在。 主要问题包括:**备用机制**(或缺乏)在passkey失效时的情况,**供应商锁定**,即苹果、谷歌和微软优先考虑自己的passkey管理器,以及**网站之间不一致的用户界面/用户体验**。一些网站限制多次passkey注册,并且可能出现系统冲突(例如Bitwarden与iCloud)。 开发者还指出了passkey规范本身的问题,包括网站可能禁止客户端以及缺乏针对第三方应用程序的标准化API集成。讨论还涉及了对生物识别技术的依赖以及对强大的备份/恢复选项的需求。 最终,共识是,虽然passkey代表了重大的安全改进,但更广泛的采用需要解决这些易用性和互操作性挑战,并避免用户被锁定在特定生态系统中的未来。
相关文章

原文

I recently released passkeybot.com, a hosted sign in page that allows you to add passkey auth to your site with just a few server side HTTP handlers.

Here are the things I learnt in the process.

What Secure Enclave Processors (SEP) are

Apple devices have secure enclaves which are like a separate tiny computer living inside the main CPU that has its own isolated encrypted memory and OS. It can create secrets that never leave the secure enclave. The main OS can only prove it has possession of that secret by asking the secure enclave to sign some data and getting the signature as a response (it can only use this message protocol with the SEP).

When the user signs in with their passkey with User Verification = true, the SEP requires a biometric/passcode auth first before signing the data with the private key.

Other devices have something similar to the SEP, but are branded with different names.

Phone SIM cards are actually a form of secure element. SIM cards are CPUs that run a stripped down version of Java, and use the same principle of “secrets can never leave the SIM” and “prove possession with message signing”.

User Presence (UP) vs User Verification (UV)

Presence means “the user tapped a button and was there”, verification means “the user entered their biometric or passcode”.

You can request which one you require with the JS passkey API.

The difference is presence can be faked by anyone with the unlocked device by pressing a button, but verification always requires the re-auth of the user with biometrics or a passcode.

What an authenticator is

An authenticator is the hardware and software that holds the private/public key pairs and signs the passkey challenge to prove it has the private key. On Apple devices that is the SEP.

The browser asks the user which authenticator they want to use, then uses OS level APIs to interact with the chosen authenticator.

For example:

  • User chooses on-device Apple SEP → site calls JS API → browser uses Swift API for passkey operations.
  • User chooses Yubikey → site calls JS API → browser uses Yubikey API over USB for passkey operations

The interesting thing here is that the JS API normalises all these different possible authenticator APIs. Under the hood the browser implements all the possible API protocols for different authenticators.

The Chrome Dev Tools also has a virtual authenticator to bypass reptetive OS password entry for testing.

What attestation is

Signing proves possession: Being able to sign with the private key proves you have possession of it.

Attestation proves device hardware used: Attestation proves what hardware and software combination created the passkey pair. It allows enforcing policies for what set of hardware devices are trusted, and which are blocked.

The issue is that attestation data also allows fingerprinting as it reveals exactly what hardware the user is using.

Hardware attestation only occurs for the creation of the passkey pair (not on every auth). This creates an issue: if keys are synced to another device, the attestation is no longer valid. So if you require strict attestation that specific hardware is used, you create a new keypair for every device instead of allowing use of a passkey pair that has moved to a non-attested device.

Without attestation the secure enclave is still used, it is just not proven to the webauthn client API.

Apple hardware has attestation disabled by default unless you have enterprise device management enabled. This lets enterprises define an allow-list of trusted authenticator hardware.

Passkeys are just for authentication, not for general signing of intent

When the user authenticates with a passkey, they sign a challenge that is a hash unique to that particular sign in flow. The challenge hash needs to have 16 bytes or more of random data to avoid replays, but it can also include a hash over other metadata.

The authenticator GUI only shows “sign in to your_domain.com”. It never allows a more general “sign this content for your_domain.com”. E.g. “sign this transaction request to move £50 to Bob's account”.

The JS code must be secure, but it cannot be verified

If the JS code of a site is compromised, the attacker can read all personal data. They could also trick the user into signing something with their authenticator (as the authenticator does not show the user what they are actually signing) - this leads to a real private key signing over faked challenge data.

The browser has Subresource Integrity (SRI) which only allows executing JS scripts with a given hash. But the root document HTML is not checked in that case, which means the attacker can change the SRI hashes to match their own JS. Chrome Extensions also allow injecting JS.

It would be interesting if authenticators could also attest as to what HTML/JS was loaded on the page to rule out that they have been compromised.

Immediate mediation - an upcoming “fast sign in” API

This Chrome origin trial will add an option the passkey API:

navigator.credentials.get({mediation: "immediate"})  

This allows you to sign in a user who already has a passkey quickly.

If they do not have a passkey, you can decide what to do from JS (instead of having the browser show them a UI to find passkeys on other devices).

There is no way to get a list of the user's passkeys from JS - lists are always shown from the browser UI.

The immediate mediation option allows your JS to get an immediate “the user has 0 local keys” message without any user interaction:

  • 0 keys: Immediate JS response with NotAllowedError, JS decides next step.
  • 1 key: Immediately ask the user to sign in with that one.
  • >1 key: Ask the user to choose.

Related Origin Requests

Related Origin Requests allow you as a domain owner to define a list of other domains that can create passkeys for your domain. They work by having you serve the list from /.well-known/webauthn.

This is what passkeybot.com uses to allow domain owners to grant permissions to passkeybot.

But RORs do not work over HTTP, only HTTPS. The reason is the authenticator requests the well-known file over HTTPS only, so localhost will not work.

They are also not supported in iOS 18 or Firefox.

If an authenticator creates a passkey for the root domain, that passkey will work for all subdomains. But if it creates it for a subdomain, it only ever works on that specific subdomain.

The counter is just a “heuristic”

Authenticators store a counter which increments for each passkey usage. In theory this can detect a cloned authenticator. But much of the recommendations say to use the counter as a heuristic rather than evidence of a cloned authenticator because there are many legitimate reasons the counter can be wrong. I think in practice this counter is often ignored.

Use passkeys stored on nearby devices using Bluetooth

You can sign into a public computer that does not have your passkeys by having your own device's authenticator communicate with it over Bluetooth Low Energy (BLE). Bluetooth is used to assert your close proximity with the device you are signing into. Your keys never leave your device, the signing protocol travels between the two devices.

Deleting passkeys with the Signal API

The JS API cannot list or modify the list of passkeys, only the browser GUI or Apple Passwords can do that.

But you can asynchronously signal that you want to delete a passkey. It is only a hint, and you will not receive back any confirmation as that may leak user data.

The Signal API methods currently are:

PublicKeyCredential.signalUnknownCredential({ rpId, credentialId })  
PublicKeyCredential.signalAllAcceptedCredentials({ rpId, userId, allAcceptedCredentialIds })  
PublicKeyCredential.signalCurrentUserDetails({ rpId, userId, name, displayName })  

user.id and userHandle represent “one account”

The user.id and userHandle are the same value, but with different names in different JS API calls.

They are used to map many passkeys to a single logical account. It should be set and stored as passkey APIs require the user.id (like the signal APIs above).

You can generate one unique user.id per new passkey and just store the user => passkey relation in your database, but this may prevent “per account” passkey grouping and management in the browser UI.

crypto.subtle.generateKey can create non-extractable keys

generateKey is a JS API that allows you to create new key pairs, where the private key cannot be extracted similar to passkeys.

crypto.subtle.generateKey(algorithm, extractable, keyUsages)  

You can perform general operations like signing with this key pair, but in the case of JS being compromised, the private key cannot be read and moved to a different device. But compromised JS can still sign using that unextractable private key.

PKCE = "Proof Key for Code Exchange” was retrofitted into OAuth

PKCE is a protocol that works like a one time password: at the start of every sign in flow an actor creates a code_verifier and code_challenge.

The code_verifier is a secret random 32 bytes held on the flow initiating actor. The code_challenge is the sha256 hash of those bytes, and is shared with the user going through the sign in flow.

The code_challenge is also sent to the auth service that verifies the user and creates a (token, code_challenge).

PKCE protects this token by only allowing the holder of the code_verifier secret to redeem it by sending (token, code_verifier).

This means even if the token is stolen, only the actor that started the flow can redeem it with the auth API.

PKCE was originally designed for environments that cannot hold static secrets because the source code can be read - like JS or desktop apps. Instead of embedding static secrets, these actors dynamically create secrets at runtime at the start of each sign in flow (the code_verifier and code_challenge pair).

Passkeybot uses PKCE to avoid having to manage API bearer token secrets for each API client. Note: Passkeybot uses the general principle behind PKCE, but naming and interaction differs from the OAuth standard.

It is interesting how they managed to retrofit PKCE into the OAuth standard to solve the “token interception” problem. It only requires a sha256 hash function so is very easy to implement.

Digital Credentials API is a browser bridge to the native OS wallet

This is a passkey adjacent JS API. The Digital Credentials API will allow you to request things from the user's native OS wallet like IDs, tickets, badges, membership cards. It allows you to do things like prove your age or ability to drive without having to share your actual identity cards.

联系我们 contact @ memedata.com