Auth Series 4: Passkeys

by cthos
3235 words


Auth Series 4: Passkeys
I'm apparently obliged to tell you: The passkey icon is a trademark of FIDO Alliance, Inc

Here we are again! This time we're going to talk about one of my favorite subsets of this topic: Passkeys. You should mentally prepare yourself for this, it's going to be a lot of information and a fair amount of it is dense. I'll be using information from my other post on Passkeys so you don't need to read it first, but you can if you want.

😈 Guess what! "Passkey" doesn't actually have a real definition, it's a marketing term that a bunch of folks vied for control of. I think we've settled on a common definition, but keep that in mind.

That's as good a place to start as any, so let's gooooooo.

Okay, so, "passkeys" are rooted in the idea that passwords are bad, and we should securely eliminate them. As a reminder, passwords are "bad" because:

  • You have to remember them
  • People tend to reuse them across multiple websites so that they can remember them
  • Humans also tend to create insecure passwords (hunter2 comes to mind) that are easy to guess
  • Which means if they leak, the blast radius can be real big for a given individual
  • They can be phished relatively easily

The best advice that I can give to an individual in the absence of passkey technology is to get a password manager and have that autogenerate and fill secure passwords for you. Apple and Google both have password managers built in, but my preference would be Bitwarden or 1Password. This is going to be good advice until passkeys become reliably ubiquitous, if they ever do.

😈 I think they will, but if you want a more pessimistic view read this article.

Viewed with that lens, you want something that can be used in place of passwords that doesn't have the same security issues as passwords, but can accurately attest the identity of the user signing in to the service. That's what passkeys are. Technically, passkeys are a cryptographic private/public key pair tied to a particular service (they cannot be reused across services) which are stored somewhere that hopefully only the unique user in question has access to. This can be a bunch of things, which we'll talk about in a minute, but they're often also behind a biometric authentication mechanism as well. If you remember the second post in this series, this will be a combination of "something you have" and "something you are".

😈 BTW I'm not actually using the prevailing definition of passkeys here, I'll correct that later in the post. Stick with it.

Passkeys use asymmetric (public-key) cryptography to do their magic, so if you're unfamiliar, the Wikipedia page is a great resource to learn more. But, basically, when enrolling a passkey your device will generate a private key and a public key. The public key is given to the service and stored there. Because of how public key cryptography works, the public key and private key can each generate messages (signatures, or encryption) that you need the other side to verify. The private key stays with the user, the public key goes to the service.

So, when you as an end user start using passkeys to log into things, you'll begin to collect a pile of key pairs stored ... somewhere. Right, okay. So now we need to talk about the various things that can generate/store passkeys. I'll start from the end-user perspective and then move over to the developer perspective after that.

There's going to be a lot of "it depends" here. Also, a lot of "it's being worked on". I'll go ahead and say it now, the UX around passkeys still needs a lot of work (and folks are working on it).

This is probably the most confusing thing to the average user, I think, because the answer is "wide" but also the major platforms are trying to make it seamless to people who don't think about authentication all day every day (... I don't have a problem). So, the options you have right now are:

  • Apple Password Manager with Apple biometrics (Face ID or Fingerprints)
  • Google Password Manager with whatever biometrics your device supports
  • Windows Hello on certain devices (you need a secure element on the device, most modern machines will have this) with a pin or a biometric
  • Physical security keys, YubiKey being the most prominent, with a pin or a biometric (YubiKey sells a fingerprint reader fob)
  • Certain Password Managers like Bitwarden, KeePassX, 1Password
    • This requires an app or a browser plugin to be installed

Note that I listed the platform-specific ones up front, because that's where the "seamless migration" stuff is happening. Apple, Google, and Microsoft are all positioned to make the downsides of passkeys a bit less stabby to the end user. The other options require more effort and understanding to function...and there are some other sharp edges that I'll talk about shortly.

Anyhow, regardless of which option you use, there's a key pair associated with each service being generated on that device. What the prompt looks like when you generate a passkey depends a lot on the platform. Here's what my iPhone looks like when trying to register a new passkey on passkeys.io:

iPhone showing both Bitwarden and Passwords App
😈 This is actually a lot better than it used to be, because it now tells you where the passkeys get synced.

Because I have a saved passkey in my iCloud passwords, I can also use that same passkey on the mac. Here's how that UX looks:
Showing the same passkey on the Mac

So how did that work? Well, for Apple Password Manager with iCloud (and Google Passwords, and the password managers), when you generate a passkey it's synced to every device that's signed in to your iCloud account via the intertubes along with the rest of your password vault. All of that is encrypted with your device code so if someone manages to steal the backups they can't decrypt the keys (hopefully). If they didn't sync the passkeys across devices you wouldn't be able to access your account unless you had the specific phone or computer you used to create the passkey on. It'd also be a major problem if you lose your phone, because that passkey would be unrecoverable.

This is especially true of physical security keys. The private key, by design, cannot leave that device. If you lose the device, you lose the passkey. You might remember that constraint from the Multifactor post.

But wait, you might be wondering: What happens if I generate a bunch of passkeys in Apple's password manager, and then I decide to leave the Apple ecosystem? "We're working on it" is the answer to that question. The FIDO Alliance has released draft specifications for Secure Credential Exchange so you can migrate from one provider to another. At the time of this writing, though, the answer is "you don't". You'd have to go to each service and change out your passkey.

Passkey support is still a bit spotty in some browsers. Librewolf, for example, only reports partial passkey support so when browsers try to detect what UX it can show you, it fails and falls back to just showing a "please touch your device" popup with no further explanation. You could conceivably plug in a USB key after the prompt happens, but the messaging you get around that is very bad since you're now in the "edge case".

Right, fine, but how are Passkeys more secure?

Permalink to “Right, fine, but how are Passkeys more secure?”

Well, the first thing is you don't need to remember anything about them other than you used one to sign in to a service (and they can be autodiscovered, sometimes, we'll talk about that in the dev section). The second thing is they can't be phished. How?

Remember earlier I mentioned that passkeys are bound to a particular website / service? There are a couple of ways this is accomplished (which has a lot of overlap with the multifactor post on WebAuthn) is with the declaration of the Relying Party, the way browsers have implemented the communication channel, and the fact that those attestations are part of the authentication ceremony. While it's theoretically possible the FIDO alliance missed something critical, there are strong assurances built into the spec that the service prompting you for the passkey is the same service you registered with.

The final bit is stealing private keys is also difficult, depending on the storage mechanism. For password managers, it's as secure as your password service, typically encrypted with a thing you, the human, know. For Physical keys, the key cannot be extracted barring exotic attacks and physical access to the key.

😈 There was a fun Yubikey side channel discovered earlier this year, but it also requires knowing your pin or a valid service login.

So, in summary, passkeys are better than passwords because:

  • You can't tell someone a passkey, or have it stolen from a service's database.
  • It can't be phished, since it doesn't ever leave the device in question.
  • If you're using a syncing service, you also can't really lose them since they're backed up to an account.
    • But you could drop your phone and break it, or lose a YubiKey or the like.
  • Assuming the place storing the private key portion of the passkey is secure, it's quite difficult to steal them.

The Technical bits, how do you implement Passkey Authentication?

Permalink to “The Technical bits, how do you implement Passkey Authentication?”

Right. So here's where that "definition" of passkeys gets in the way of explaining this. There are two things that could conceivably be called passkeys: Resident and non-resident keys (also known as discoverable and non-discoverable).

Resident keys are generated when you register with a service and are stored on the device in question. Your browser (or other integrated software) can query the device for these keys to figure out if you've registered with a service. This allows you to use only the passkey to authenticate, no need for a service identifier like a username. In short, Resident keys can replace both usernames and passwords.

Non-resident keys use an identifier held by the service to deterministically generate the public / private key pair repeatably. This means the key in question does not need to store any information about the service its registered with, because it can just regenerate the key from that service identifier each time. This means that you still need a username or other account identifier to figure out what you need to send the key for it to generate the proper attestations. You cannot query the device for what services this has been done for, as the device does not store that information. Non-resident keys replace passwords, but not usernames.

😈 Remember how I said I think we've "settled" on a definition? As far as I can tell the common parlance for "passkeys" is now "Resident" or "Discoverable" keys (indeed, passkeys.dev uses this definition). This is a problem, for a few reasons, a key one being how they interact with hardware tokens like Yubikeys. I'll talk about that shortly.

Okay, so now that is out of the way, let's talk about how a Relying Party (service provider / website / etc.) can implement passkey authentication. We'll start with non-resident keys.

The steps to do this are basically as follows (and this will look very familiar to the 2FA post because it's exactly the same process). For code examples see Part 2 of the series.

For Registration:

  1. The user goes to a website and clicks on "register", providing a Username in the process.
  2. The site responds by requesting the public key from the device using navigator.credentials.create (providing a few values from the server).
  3. The authenticator generates a seed, which is sent back in the PublicKeyCredential.id field of the response. The site must store that ID alongside the username and will need to send it back when logging in. It also generates a public key which will be used for signature verification later. Store that as well.
    1. Make this a one-to-many relationship on your user record please.
    2. If you're curious how YubiKeys any program can query every key on the device. You do not need to know the relying party. There's an operation to just get it to dump the service list. Generally this requires a pin or a biometric to unlock do this Duo wrote up a blog post
  4. The server must verify the credential using the 19 point process outlined in the spec (just use a library).
  5. You now have a non-resident key stored for the user.

In snarky diagram format:

sequenceDiagram
    Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Register.
	Browser->>+User: Username please!
	User->>+Browser: It's cthos
	Browser->>+Server: New user coming in, please give me an id and a challenge key
	Server->>+Browser: Here you go. "e98fowfi" and "bacon".
	Browser->>+Passkey Device: Please give me a new key for user ID "e98fowfi" and sign this challenge: "bacon".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with the key and signature.
	Browser->>+Server: Here's the public key, Id "1d889" and stuff
	Server->>+Browser: It all checks out, let them in.

To Log the user in:

  1. The user goes to the website, clicks "login" and then provides their username.
  2. The server retrieves the key IDs that have been registered with the account (this could be multiple keys). Then, it uses navigator.credentials.get passing those key IDs along in the allowCredentials stanza.
  3. Using the credentialId list, the authenticator will loop through and attempt to re-create the private key from the input. If it can do so, it will sign the request with the private key and send back a PublicKeyCredential like in the registration step.
  4. The server must validate that response and that the signature matches what it was expecting.
sequenceDiagram
	Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Log in.
	Browser->>+User: Username please!
	User->>+Browser: It's cthos
	Browser->>+Server: Please give me the keys and challenge phrase for "cthos"
	Server->>+Browser: Here you go. Key ID "1d889" and "justicier".
	Browser->>+Passkey Device: Please sign this challenge: "justicier" using key "1d889".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with that signature.
	Browser->>Server: Here's the response, is it valid?
	Server->>Browser: It all checks out, let them in.

And that's it! For a given user ID, something like a YubiKey will only generate a single key pair, but you can use the same key for different users with a different generated key. Password managers and Apple/Google can operate in this mode in much the same way (though I believe they generate and store a key).

So what's an advantage of using key derivation over storing keys each time? Well, something like a YubiKey only has limited storage space. In fact, most resident-key-capable YubiKeys can only store 25 of them. Likewise, by virtue of not storing keys on the device, someone who steals your YubiKey can't figure out what sites you use by probing the key.

Right, so how are resident (discoverable) keys different?

These follow a very similar process, but instead of needing to provide a username, you can instead query the device for valid identities.

To Register:

  1. The user goes to a website and clicks on "register".
  2. The site responds by requesting the public key from the device using navigator.credentials.create. This time you pass requireResidentKey as true (if you want to force resident key creation).
  3. The authenticator generates a key pair, which is stored on the device associated with the Relying Party ID you sent in the request and the user.id you provided. Like before, you get a PublicKeyCredential response that includes an id and the public key.
    1. Store this along with your user record and whatever user.id you send along with the request, you'll need it later.
  4. The server must verify the credential using the 19 point process outlined in the spec (just use a library).
sequenceDiagram
Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Register.
	Browser->>+Server: New user coming in, please generate a new id and a challenge key
	Server->>+Browser: Here you go. "99209ed" and "bacon".
	Browser->>+Passkey Device: Please give me a new key for user ID "99209ed" and sign this challenge: "bacon".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with the key and signature. I've stored it under `99209ed` and your domain name.
	Browser->>Server: Here's the public key for user `99209ed`
	Server->>Browser: It all checks out, let them in.

To Authenticate:

  1. The user goes to a website and clicks on "Authenticate with a Passkey or something" (there's also an autofill API you can use).
  2. The server creates its secret, like before, but this time when calling navigator.credentials.get leave allowCredentials as an empty array, or leave it out entirely.
  3. The device will query itself based on the relying party ID and provide a list of credentials and the browser will prompt the user which one to use (if there are multiple).
  4. The device will send back a PublicKeyCredential which will include the key ID, an attestation, and the user.id it was provided during registration.
  5. On the server, query for that id, pull the public key associated with it, and validate the signature using the 19 point process.
  6. At this point you now know the user and their info! You're here. Hooray.
sequenceDiagram
Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Log in.
	Browser->>+Server: User login coming in hot, please give me a challenge.
	Server->>+Browser: Here you go: "lemonaide".
	Browser->>+Passkey Device: Please sign this challenge: "lemonaide" using any key you possess for my website.
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with that signature, and the user.id `99209ed`.
	Browser->>Server: Here's the response, is it valid for user.id `99209ed`
	Server->>Browser: It all checks out, let them in.

So, you've gained the ability to authenticate the user without them needing to remember anything besides their device.

This is good, generally, but it's also bad when you consider security keys. Because of their limited memory if every service were to move to the resident key model you'd either need to carry an entire key ring of YubiKeys...or we're going to need a bigger storage YubiKey.

Likewise, because the keys are stored you can list them. Fortunately this usually requires you to enter a pin or use your biometrics to do so.

😈 Here's the point where I rant a little bit. I prefer non-resident keys because I prefer physical security keys. If we get a physical key with unlimited storage that doesn't cost a small fortune, I might change my tune.

Like the MFA post, I want to recommend a Google Developers post, which will walk you through how to do this in depth, from both the server and client side.

There are a lot of sharp edges, but I'm confident you too can implement passkeys.

Looks like this did manage to be a fair amount shorter than the previous posts in the series. Nice.

For the final post in the series, Part 5, I'm going to cover some oddball scenarios and other things that don't fit neatly into the rest of the posts like machine authentication and non-web identity systems.

See you all next time!

Comments