Passkeys and WebAuthn
23.0 What this chapter gives you#
- You will be able to explain to somebody with no technical background why a passkey cannot be stolen from a website’s database, and why that is a different claim from “it is encrypted”.
- You will be able to state exactly what is created during a passkey registration, what is sent to the website, what stays behind, and what the website is now able to check.
- You will be able to read a real clientDataJSON object field by field and say which single field is doing the anti-phishing work and why the user cannot override it.
- You will be able to lay out the 37 bytes of authenticator data from memory, name all six defined flag bits with their bit positions, and say which of them a relying party must check and which it may only use as a hint.
- You will be able to explain the relying party identifier scoping rule, say when a related origin request is needed, name the file that has to be served and the exact limit on how many labels it may cover.
- You will be able to describe what CTAP2 is, name the four transports it defines, and explain how the hybrid QR flow proves that the phone and the computer are in the same room.
- You will be able to name the attestation statement formats defined in the specification and give the argument, with figures, for why most relying parties should ask for none of them.
- You will be able to say precisely what the backup eligibility and backup state flags mean, what a synced credential gives up in exchange for surviving a lost phone, and what NIST says the ceiling on its assurance is.
- You will be able to explain what makes a credential discoverable, what a usernameless sign-in actually costs in storage on the authenticator, and why discoverability is the feature that makes lost-credential handling hard.
- You will be able to walk through a complete authentication ceremony with real bytes, verify the signature, and then show exactly where the same ceremony fails when it is run from an attacker’s website.
Every authenticator in this volume so far has had the same defect at its centre. A password is a secret that you and the website both hold. A one-time code from an app comes from a seed that you and the website both hold. A code sent by text message is a secret that you, the website, and the mobile network all hold. In each case there is something that can be copied, and there is therefore a place from which it can be stolen and a conversation in which you can be tricked into handing it over. Two decades of security engineering have gone into making that conversation harder to fake, and the attackers have kept up, because the underlying arrangement always leaves something worth stealing.
The passkey is the first widely deployed authenticator that removes the thing worth stealing. There is no shared secret. Your device holds a private key that has never been transmitted anywhere and, in the hardware case, has no command that would export it. The website holds the matching public key, which is not secret and is worth nothing to a thief. There is no database of password hashes to breach, because there are no passwords. There is no seed to phish, because there is no seed. What the website stores is a number that could be printed in a newspaper without harming anybody.
That alone would be a large improvement. It is not the reason this chapter exists. Removing the shared secret stops the theft of the credential, but it does not by itself stop a person from being tricked into using their credential at the wrong door, and being tricked at the wrong door is how almost every real account takeover begins. What makes a passkey different is a second property, and it is a small and unglamorous one: the browser, not the user, tells the authenticator which website is asking, and the authenticator stamps that answer into everything it signs. A user can be fooled by a website that looks identical to their bank. A browser cannot be fooled about the address bar it is currently displaying. The trick is to make the machine that cannot be fooled the one that answers the question.
This chapter takes that apart completely. We build the model in plain words, we say carefully where the plain words stop being true, and then we do the whole thing again with real field names, real byte offsets, real specification section numbers, and a worked authentication that you can verify with a hand calculator and a hash function. The chapter before this one, “Challenge and Response”, established why the answer has to be different every time; we take that as given here and concentrate on what the answer is bound to. The chapter after this one, “Recovery”, deals with what happens when the device is gone, which is the single largest unsolved problem in this design and gets a chapter of its own for that reason.
The plain version#
The shop that keeps a copy of your key#
Start with what a password is, described physically.
You want to be able to get into a shop’s back room whenever you visit. So you have a key cut, and you post a copy of it to the shop. The shop keeps your copy in a drawer behind the counter. When you turn up and say who you are, the shopkeeper takes your copy out of the drawer, you produce yours, and if the two are the same you are let through.
Two things about this arrangement should bother you.
The first is the drawer. Every shop you have ever dealt with has a drawer with a copy of one of your keys in it. If a burglar gets into any one of those drawers, they have a key of yours. If you were lazy and had the same key cut for several shops, the burglar now has keys to all of them. The shop can put the drawer in a safe, and shops do exactly that, but the copy is in there and it is worth stealing, so people keep trying to steal it, and every so often somebody succeeds.
The second is more subtle and it is the one that does most of the damage. Somebody can put up a shop front that looks exactly like the real one, on a street that looks exactly like the real street, with a shopkeeper in the right uniform. You walk in, you hand over your key as you always do, and the person you handed it to walks straight round the corner to the real shop and lets themselves in. Nothing about your key was broken. You simply used it at the wrong door. This is phishing, and no amount of making the key harder to copy fixes it, because the attack does not copy the key. It borrows the owner.
The stamping machine#
Now replace the whole arrangement with a different machine.
You carry a small machine. It has a slot for a piece of paper, a button, and a window that reads the address off the door of whatever shop you are standing in. Inside, it can manufacture matched pairs of things: a lock, which is a lump of metal with a particular shape cut into it, and a turning piece that fits that lock and nothing else. The pairs are made together, and the machine makes a fresh pair for every shop.
The first time you visit a shop, the machine manufactures a new pair. It hands the lock over the counter. The shop bolts the lock to the back room door and writes your name next to it in a ledger. The turning piece stays inside the machine. It is not handed over, it is not shown to the shopkeeper, and there is no lever on the outside of the machine that would get it out. If you smashed the machine open you could find it, but nobody can ask for it politely and nobody can ask for it from a distance.
From then on, proving you are the customer works like this. The shopkeeper writes a number on a slip of paper. It is a fresh, unpredictable number every time, so an eavesdropper who copies down a slip learns nothing useful for tomorrow. You feed the slip into the machine. The machine reads the shop’s address off the door, writes it on the slip underneath the number, presses your turning piece against it to leave a mark, and hands the slip back. You pass it over the counter. The shopkeeper holds it up against the lock on the door. The mark either fits or it does not. If it fits, you are the person who registered, because only the turning piece inside your machine can make a mark that fits that lock.
Notice what has changed. The shop’s drawer is empty. There is no copy of anything of yours behind the counter. There is a lock bolted to a door, and a lock is not a secret. A burglar can photograph it, measure it, publish it in a newspaper; none of that helps them make a turning piece, and none of it helps them at any other shop, because every shop got its own pair.
The address written on the door#
The mark on the slip is the interesting part.
The machine did not ask you which shop you were in. It read the address off the door itself, and it wrote that address on the slip before making the mark. The mark covers everything on the slip, so the mark now says, in effect, “the holder of this turning piece was standing at this address when they marked this number”.
Now run the fake shop attack again. Somebody builds a perfect copy of the shop on a different street. You walk in, you are entirely fooled, you take the slip, you feed it into the machine. The machine reads the door. The door says the fake street. It writes the fake street on the slip and makes the mark.
The thief now runs round to the real shop with your slip. The real shopkeeper looks at it. The number is right; the thief got that from the real shop in the first place. The mark is genuine; it really was made by your turning piece. And the address written on the slip says the fake street. The real shopkeeper is standing in the real shop, on the real street, and this slip says somewhere else. So the slip is refused.
That is the whole trick, and it is worth being precise about why it works. It does not work because the machine is clever about detecting fakes. The machine has no idea it is in a fake shop; it will happily work there. It works because the machine reports the address it actually sees, honestly and without asking you, and because the real shop refuses anything that reports an address other than its own. The user’s judgement has been taken out of the loop entirely. You can be completely deceived and the attack still fails.
There is a second, quieter protection. The machine does not just write the address on the slip; it also uses the address to decide which lock’s turning piece to reach for. Each turning piece is filed under the address of the shop it belongs to. Standing on the fake street, the machine looks for a turning piece filed under the fake street, finds nothing, and in most cases will not produce a slip at all. So the attack usually dies before anything is signed. The address check at the shop is the backstop.
A worked example you can follow#
Let us do it with a real shop and real numbers.
The shop is kedbyte.com. You visit for the first time. Your machine manufactures a pair and hands the lock across. The shop writes down two things: the lock itself, and a short reference number so it can find the right lock next time. The reference number in our example, written the way computers write such things, is nyx0obDjSG1af8kBLeS4Og. It is not a secret either. It is a label on a drawer.
Some time later you come back to sign in. The shop writes a fresh number on a slip. Ours is this one, written in the compact form that machines use:
OnwfngSyjW9VEcoD55tI0m8MmnG9PlggSfwXq20Ok8Q
Written out in ordinary decimal digits that is a number 78 digits long. Nobody, including you, can predict what tomorrow’s will be.
You feed the slip in. The machine reads the address off the door, which is the secure web address of kedbyte.com. It writes a small note on the slip containing three things, in this order: that this is a sign-in and not a first registration, the number the shop just gave you, and the address it read. Then it makes the mark, covering all of it.
The shop gets the slip back and checks four things in turn. Is this a sign-in note and not a registration note. Is the number on it the number I just issued, and have I not seen it before. Is the address on it my address. Does the mark fit the lock I have on file for this reference number. All four must pass. Any one failing ends the visit.
Now the fake shop. The attacker’s street is kedbyte.com.secure-login.example. To a human being in a hurry that reads as “kedbyte.com” with some noise after it, and people click on it every day. The attacker’s site has fetched a genuine slip from the real kedbyte.com a second earlier and is showing it to you. You feed it in. The machine reads the door, and the door says kedbyte.com.secure-login.example, which is a different address, not a version of the first one. The machine writes that on the slip and marks it. The attacker relays the slip to kedbyte.com. The real shop reads the address on the note, sees somewhere it has never heard of, and refuses.
Nothing had to be detected. Nobody had to notice anything. The user was fooled from beginning to end and the account did not open.
What a burglar gets from the shop#
It is worth doing the inventory from the other side, because this is the part that surprises people.
Break into kedbyte.com and steal the entire user database. What have you got? For each user: a name, an email address, a reference number, and a lock. The lock is a public number. You can publish every one of them. You cannot make a mark that fits any of them, because making marks needs the turning piece, and the turning pieces are in several million separate machines in several million pockets. There is no central pile of them. There never was one to steal.
Compare that with the password case, where the drawer contains something that a patient attacker with a large computer can often turn back into the original key, and where users habitually cut the same key for thirty shops.
That is the first half of the claim in this chapter’s title: the passkey removes the shared secret entirely. The second half, that this is the first authenticator phishing cannot beat, comes from the address on the slip. Both halves are needed. A system with no shared secret but no address binding could still be phished in real time. A system with address binding but a shared secret could still be breached at the database. The passkey has both, and that combination is what is new.
Where the machine actually lives#
One last thing before we take the analogy apart.
The machine in this story is not a separate gadget you buy, in most cases. It is your phone, or your laptop, or a small metal stick that plugs into a USB port. When it is built into the device you are already using, the “reading the address off the door” is done by your web browser, which knows perfectly well what address it is displaying because it is the thing that fetched the page. When the machine is a separate stick, the browser reads the address and passes it to the stick over the cable, and the stick refuses to work with anything else.
Either way, the honest reporting of the address is done by software you did not write and the attacker does not control. That is the load-bearing assumption of the whole design, and it is the first thing we are going to question.
Where the plain version stops being true#
The turning piece does leave the machine, for most people#
The honest version: for the overwhelming majority of passkeys in use today, the private key is copied off the device that made it, encrypted, and stored on a company’s servers so that it can be restored onto your next phone.
The plain story said the turning piece never leaves the machine. For a hardware security key that is exactly true; the chip has no export command and the key is generated inside it. For a passkey held by Apple Passwords, Google Password Manager, Windows Hello, 1Password, Bitwarden or any of the other credential managers, it is not true. Those are synced credentials, called multi-device credentials in the specification. The private key is wrapped in encryption and uploaded, and it is downloaded again on your other devices.
This is not a scandal; it is a deliberate trade. A key that exists in exactly one place dies with that place. Before syncing existed, losing a phone meant losing every account whose only credential was on it, and the recovery path that had to exist to fix that was itself the weakest link. Syncing buys survival of device loss at the cost of introducing a new party, the sync provider, whose compromise would matter.
What you get in exchange for the loss of the strict claim is written into the protocol as two flag bits, which we will come to, and it is written into policy by NIST: a syncable authenticator can reach authentication assurance level 2 and cannot reach level 3, because level 3 requires that the key not be exportable at all. If you need the strict version of the claim, you need a hardware key. Most people do not, and the honest framing is that synced passkeys are enormously better than passwords and are not the same thing as a smartcard.
“Phishing cannot beat it” is a claim about one attack#
The chapter’s thesis is that phishing cannot beat a passkey, and that is true in the precise sense that a credential scoped to one site cannot be exercised at another. It is not a claim that a user with passkeys cannot be robbed.
The honest version: origin binding defeats credential relay. It does not defeat everything an attacker can do once they have your attention.
Three attacks survive. The first is downgrade: if the site still accepts a password, or a code by text message, as an alternative, then the attacker simply asks for that instead, using the relaying phishing kits chapter 18 describes, and the passkey’s protection was never engaged. As of August 2026 almost every consumer site with passkeys still has a password behind them, so this is not a theoretical worry, it is the normal case.
The second is stealing what comes after the sign-in. The passkey proves you at the door; the site then issues a session cookie, and a cookie is a bearer token that can be stolen by malware on the machine, in which case the attacker never has to sign in at all.
The third is the one the research community demonstrated most sharply. On 9 August 2025, at DEF CON 33, the researcher Marek Toth published work on what he called DOM-based extension clickjacking, and he tested eleven password manager browser extensions. All eleven were vulnerable in their default configuration. In the passkey case, which he showed working against eight of the eleven, the attacker makes the extension’s own interface invisible and overlays it with something innocuous like a cookie banner. The victim clicks once, on the attacker’s page, on the real origin’s behalf; the extension approves a passkey assertion the user never consciously agreed to, and the attacker receives it. Notice carefully what did and did not fail. The origin binding held. The signature was over the correct origin. What failed was the user interface that was supposed to obtain the user’s consent to sign at all. Phishing was not beaten; consent was.
So the accurate sentence is that a passkey removes the class of attack in which an attacker obtains something at one site and replays it at another. That class contains the great majority of real account takeovers, which is why the change is so large, but it is a class and not the universe.
The browser is inside the trusted base#
The plain version said the address is read by “software you did not write and the attacker does not control”. That is a real assumption and it deserves to be stated as one rather than assumed.
The chain of trust runs: the operating system is honest about which application is running, the browser is honest about which origin it is displaying, and the connection to that origin is genuinely that origin’s, which is a statement about certificates and is the subject of Volume III. If malware has code running inside your browser, all bets are off. If a browser extension can manipulate what you see and click, as the 2025 clickjacking work showed, then some bets are off. The passkey pushes the attack from “trick the human” to “compromise the client”, which is far harder and far noisier, but it is a relocation, not an elimination.
The stamp is not a fingerprint, and the counter is mostly a fiction#
The plain version implied that the mark is unique per turning piece in a way that would let the shop spot two turning pieces made from the same mould. That is roughly true, but the specification also carries a device intended to detect exactly that, and in practice it has stopped doing its job.
The authenticator can include a counter that goes up by at least one each time it signs. A relying party that remembers the last value it saw can notice a value that has not increased, which is a hint that two copies of the key are in use. That works for hardware keys. It cannot work for synced credentials, because the same key legitimately lives on five devices with no shared counter, and the near-universal behaviour is to report zero forever. The specification tells relying parties to treat a stalled counter as “a signal, but not proof” of cloning, and if both the stored and reported values are zero, to skip the check altogether. Anyone who reads about the anti-cloning counter and imagines it protects their synced passkeys has been misled.
The passkey proves control of a key, not who you are#
A verified assertion says one thing: the holder of the private key matching this public key authorized these exact bytes, and did so at this origin. It says nothing about the person. It does not say your name, it does not say you are over eighteen, it does not say you are the same human who registered rather than your spouse whose thumb is also enrolled on that phone, and it does not say you are not under duress.
Whether the person was checked at all is reported in a single bit, the user verified flag, and even that bit only reports that the authenticator carried out a check of the kind it does, which might be a face scan or might be a four-digit PIN. Binding a key to a real person is registration and identity proofing, which is Volume I’s subject, and the strength of the check on the day is a separate question from the strength of the cryptography.
Attestation says less than the word suggests#
Attestation is the part of registration where the authenticator can produce a signed statement about what kind of device it is. It sounds like the answer to “was this really a hardware key”. It is weaker than that in three ways.
It identifies a model, not a device, and deliberately so: the FIDO UAF specification asks that at least 100,000 devices share one attestation certificate precisely so that the certificate cannot be used to track an individual. It is absent by default, because the WebAuthn interface defines the attestation preference as “none” unless the site asks otherwise, and the client is required to strip the statement when it is set to none. And for synced passkeys it usually tells you the name of a software credential manager, which any software can claim to be if it is willing to lie, unless you are checking a signature chain back to a trusted root, which almost nobody outside enterprise deployments does.
We will make the full argument in the technical half. The short version is that attestation is an enterprise feature with a real use, that using it in a consumer deployment tends to lock out perfectly good authenticators, and that a relying party that asks for it and then does not verify it has added complexity and zero security.
The path around the front door#
Finally: everything above concerns the front door. When the phone is lost, the sync account is locked out, or the only hardware key went through the wash, something else has to happen, and whatever that something is becomes the real strength of the account. That is the subject of chapter 24, “Recovery”, and it is deliberately not discussed further here.
The technical version#
The credential model: one key pair per relying party#
A WebAuthn credential is an asymmetric key pair, generated on an authenticator, scoped to a single relying party. The specification calls the stored bundle a public key credential source: it holds the credential private key, the relying party identifier the credential is scoped to, an optional user handle, and a credential identifier.
Three consequences follow from “one pair per relying party” and they are the architectural core of the design.
The public key is not a secret. It is stored by the relying party in the clear, alongside the credential ID, in what the specification calls a credential record. A stolen credential record database yields nothing that can be used to authenticate. There is nothing to crack offline, which means the entire industry of password hashing parameters, salting and work factors simply does not apply.
The keys do not correlate across sites. Two relying parties that compare their databases cannot tell that a given user is the same person, because the key pairs are independently generated and share no material. This is a privacy property that passwords never had, since a reused password hash is a very effective join key.
The private key never appears in the protocol. There is no message in either ceremony that carries it. The only operations exposed are “make a credential” and “produce an assertion”, and both return signatures, never key material. Where the key is manufactured inside a secure element or a Trusted Platform Module, there is no command sequence that emits it at all; where it is manufactured by a software credential manager, it may be exported into a sync fabric, which is the distinction developed later in this section.
The user handle deserves a note because it is the piece most often misused. It is an opaque byte string, up to 64 bytes, chosen by the relying party to identify the account. It must not be an email address, a username or anything else meaningful, because it can be displayed on other devices and it is stored on the authenticator. It is the key by which a discoverable credential finds its account.
The registration ceremony, field by field#
Registration begins with the relying party constructing a set of options and the page calling navigator.credentials.create(). Here are the options for our worked example, in the JSON form that WebAuthn Level 3 added, with the base64url encoding of binary fields that the new parseCreationOptionsFromJSON() method understands.
{
"rp": { "id": "kedbyte.com", "name": "KedByte" },
"user": {
"id": "dXNlci0wMDAxNzQ",
"name": "shikhar",
"displayName": "Shikhar Singh"
},
"challenge": "OnwfngSyjW9VEcoD55tI0m8MmnG9PlggSfwXq20Ok8Q",
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "preferred"
},
"excludeCredentials": [],
"attestation": "none",
"timeout": 300000
}
The challenge is 32 bytes of randomness. The specification’s security consideration in section 13.4.3 requires enough entropy to make guessing infeasible and says challenges should be at least 16 bytes long; 32 is the common deployment choice and costs nothing. The relying party must store it and must reject any response carrying a different value, and it should expire it in a time comparable with the ceremony timeout, whose recommended range in section 15.1 is 300,000 to 600,000 milliseconds with a recommended default of 300,000, that is five minutes.
The alg values are COSE algorithm identifiers from the IANA registry: -7 is ES256, ECDSA over the NIST P-256 curve with SHA-256, and -257 is RS256, RSASSA-PKCS1-v1_5 with SHA-256. Those two are the pragmatic minimum list because between them they are accepted by essentially every authenticator ever shipped. As of August 2026 the interesting additions are the fully-specified identifiers and the post-quantum ones, which we return to below.
The client then does its own work before the authenticator sees anything. It determines the effective domain of the calling origin, checks the supplied rp.id against it, assembles the client data, hashes it, and issues a CTAP2 authenticatorMakeCredential command. What comes back, wrapped by the browser into an AuthenticatorAttestationResponse, is two things: the clientDataJSON exactly as it was serialized, and the attestation object.
The attestation object is a CBOR map with three members, encoded in CTAP2 canonical form, which orders map keys by length and then bytewise, giving the order fmt, attStmt, authData:
a3 map(3)
63 666d74 "fmt"
64 6e6f6e65 "none"
67 61747453746d74 "attStmt"
a0 {}
68 6175746844617461 "authData"
59 0094 bytes(148)
...148 bytes of authenticator data...
Total length in our example is 179 bytes. The relying party’s job is then the 29 numbered steps of section 7.1. The load-bearing ones, in order, are: verify that C.type is “webauthn.create” (step 7); verify the challenge (step 8); verify that C.origin is an origin expected by the relying party (step 9); verify that the rpIdHash in authData is the SHA-256 hash of the expected relying party identifier (step 14); check the user present and, if required, user verified bits (steps 15 and 16); verify that if the backup eligibility bit is clear then the backup state bit is also clear (step 17); check the credential public key algorithm is one that was requested (step 20); and finally verify the credential ID is not already registered to another user, before storing the credential record.
Step 17 is small and easy to skip and it exists because the combination of backup eligibility clear and backup state set is defined as not allowed, so its appearance means a malfunctioning or lying authenticator.
Client data JSON, and why origin is the whole trick#
The client data is the browser’s sworn statement, and the entire anti-phishing property of WebAuthn lives in it. In Level 3 the dictionary is:
dictionary CollectedClientData {
required DOMString type;
required DOMString challenge;
required DOMString origin;
boolean crossOrigin;
DOMString topOrigin;
};
Here is the real serialization from our worked authentication, 132 bytes:
{"type":"webauthn.get",
"challenge":"OnwfngSyjW9VEcoD55tI0m8MmnG9PlggSfwXq20Ok8Q",
"origin":"https://kedbyte.com",
"crossOrigin":false}
The type field is either “webauthn.create” or “webauthn.get”, and the specification is explicit that its purpose is to prevent signature confusion attacks in which an attacker substitutes one legitimate signature for another. Without it, a registration signature and an authentication signature over similar bytes could be interchanged.
The challenge is the base64url encoding of the relying party’s random value, which is what makes the response fresh; the chapter before this one dealt with why freshness is non-negotiable.
The origin is the fully qualified origin of the requester in the syntax of RFC 6454: scheme, host and, where non-default, port. Four properties make it the whole trick.
It is written by the client, not by the page. JavaScript running on the attacker’s site cannot set it, cannot influence it, and cannot see it before it is signed. It is the browser reporting what the browser knows.
It includes the scheme, so an ordinary HTTP page cannot produce a credential usable on the HTTPS site, and it includes the port, so a development server on a different port is a different origin.
It is covered by the signature, indirectly. The authenticator never sees the client data; it sees only the SHA-256 hash of it, and it signs over authenticator data concatenated with that hash. Any change to any byte of the client data changes the hash and invalidates the signature.
And it is checked by the relying party against a list it controls, in step 12 of section 7.2. The check is a comparison against expected origins, not a pattern match, which matters because the test “does this string contain kedbyte.com” is passed by the origin kedbyte.com.secure-login.example, and by any attacker origin that merely carries the name in a query string. A relying party that implements the check as a substring test has removed the entire security property of the system while appearing to have implemented it.
Level 3 adds topOrigin, present only when the call was made from a context that is not same-origin with its ancestors, that is, from a cross-origin iframe, in which case crossOrigin is true. This lets a relying party decide whether it is willing to be embedded, and by whom. The tokenBinding member from Levels 1 and 2 is now reserved and unused.
Alongside origin the authenticator contributes its own binding, which is the rpIdHash inside authenticator data. There are therefore two independent checks: the client asserts the full origin, and the authenticator asserts the domain scope of the credential it used. Neither alone is quite enough. The origin without the rpIdHash would let a compromised client use a credential scoped elsewhere; the rpIdHash without the origin would not pin the scheme or the port.
The authentication ceremony and the assertion#
Authentication is navigator.credentials.get() with a request containing, at minimum, a fresh challenge and the relying party identifier:
{
"challenge": "OnwfngSyjW9VEcoD55tI0m8MmnG9PlggSfwXq20Ok8Q",
"rpId": "kedbyte.com",
"allowCredentials": [],
"userVerification": "preferred",
"timeout": 300000
}
An empty allowCredentials is the usernameless case: the relying party is not naming a credential, and the authenticator must find a discoverable one itself. When allowCredentials is empty the authenticator is required to return the user handle, which is how the relying party works out which account just signed in.
The response is an assertion: an AuthenticatorAssertionResponse carrying clientDataJSON, authenticatorData, signature and userHandle, plus the credential ID on the outer object. The verification is the 25 steps of section 7.2, of which step 21 is the one everything else exists to make meaningful: using the stored public key, verify that the signature is a valid signature over the binary concatenation of authenticatorData and the SHA-256 hash of the client data.
That concatenation is worth stating as a formula, because implementers get it wrong:
signedBytes = authenticatorData || SHA-256(clientDataJSON)
signature = Sign(credentialPrivateKey, signedBytes)
The hash is of the exact bytes the client produced. This is why the specification insists the exact JSON serialization must be preserved and why a relying party must never re-serialize the client data before hashing it. Parse a copy if you must, but hash the original.
Authenticator data: 37 bytes and six flags#
Authenticator data is the authenticator’s own contribution, and it is a byte array of 37 bytes or more.
| Field | Bytes | Contents |
|---|---|---|
| rpIdHash | 32 | SHA-256 of the RP ID |
| flags | 1 | Six defined bits |
| signCount | 4 | Big-endian counter |
| attestedCredentialData | varies | Only if AT set |
| extensions | varies | Only if ED set |
The flags byte, with bit 0 the least significant, is where a great deal of policy is expressed in very little space.
| Bit | Name | Meaning when set |
|---|---|---|
| 0 | UP | User was present |
| 1 | RFU1 | Reserved, must be zero |
| 2 | UV | User was verified |
| 3 | BE | Backup eligible |
| 4 | BS | Currently backed up |
| 5 | RFU2 | Reserved, must be zero |
| 6 | AT | Attested data present |
| 7 | ED | Extension data present |
User present means a test of user presence was performed: somebody touched the key or dismissed a prompt. It is a liveness-of-interaction check, nothing more, and it is mandatory to verify in step 16 of the authentication ceremony. User verified means the authenticator carried out user verification, which might be a fingerprint, a face match, a device PIN or a password. The specification does not say which, and a relying party that requires user verification is requiring that some check happened, not any particular one. If the authenticator did both, both bits are set.
Backup eligibility and backup state are the pair that describe syncing, and they were the most consequential addition of Level 3’s predecessor work. BE is fixed at creation and must never change afterwards. BS may change over time. Their legal combinations are exactly:
| BE | BS | Meaning |
|---|---|---|
| 0 | 0 | Single-device credential |
| 0 | 1 | Not allowed |
| 1 | 0 | Multi-device, not backed up |
| 1 | 1 | Multi-device, backed up |
The specification recommends storing the most recent values with the user account and gives three concrete uses. If BE is 0 the credential will never be backed up and cannot survive loss of that device, so the relying party should ensure a second authenticator or a recovery path exists. If BS transitions from 0 to 1 the credential has become protected against device loss, and the relying party may take that as the moment to offer the user removal of their password. If BS transitions from 1 to 0 the credential has stopped being backed up, perhaps because the user turned off the backup service, and the relying party should guide the user through validating other factors before they are left with a single fragile credential.
Here is our worked registration authenticator data, all 148 bytes, produced with flags 0x5D, which is UP, UV, BE, BS and AT set:
6b9cf1cb00a4ab985d6a8f2890459fc630bd10e62157f18df6ecc73b
cb2ccbc1 5d 00000000 fbfc3007154e4ecc8c0b6e020557d7bd
0010 9f2c74a1b0e3486d5a7fc9012de4b83a
a5010203262001215820
403d28194989d2fb302d6b577426ee3d1308cc7495265492d6d34d11
a4a95836
225820
cc006891c2b08b98d2e8593ae8952b423f535eecf3f59e1b5657796e
204fa97d
Reading it: the first 32 bytes are SHA-256 of the ASCII string kedbyte.com; then the flags byte 0x5D; then four zero bytes of counter; then the 16-byte AAGUID fbfc3007-154e-4ecc-8c0b-6e020557d7bd, which is the published identifier for Apple Passwords; then the two-byte credential ID length 0x0010, that is 16; then the credential ID itself; then a 77-byte COSE key.
The COSE key decodes as a five-member map: 1:2 meaning key type EC2, 3:-7 meaning ES256, -1:1 meaning curve P-256, -2 the 32-byte x coordinate, -3 the 32-byte y coordinate. The credential ID length field is a 16-bit big-endian integer and the specification caps it at 1,023.
Note that the AAGUID here is a real value and not sixteen zero bytes, even though attestation was set to none. That is a Level 3 change: in Level 2 the client zeroed the AAGUID when the attestation preference was none, and Level 3 no longer requires that. It is the reason a community-maintained list of AAGUIDs is now useful for showing users the name of the credential manager holding each passkey, and it is the reason a relying party can display “Apple Passwords” next to a credential without doing any attestation verification at all.
A worked assertion, with real bytes#
Now the same credential produces an assertion. The relying party issues the challenge shown earlier. The client builds the 132-byte client data given above and hashes it:
SHA-256(clientDataJSON) =
4aaa67166fad20b852def433fbe9364b
6c17eae9b96dc4b00193039341f6b5e0
The authenticator builds 37 bytes of authenticator data with flags 0x1D, which is UP, UV, BE and BS, with AT clear because attested credential data appears only at registration:
6b9cf1cb00a4ab985d6a8f2890459fc6
30bd10e62157f18df6ecc73bcb2ccbc1
1d 00000000
It signs the 69-byte concatenation with ECDSA over P-256 and SHA-256, giving a DER-encoded signature of 71 bytes with these components:
r = 77d1578efc68357c6ec0d624b8552fca
7b3e2da430cfd71360f03d6c0f85a4b4
s = dcca9b0c7b32fdbaf68647978069810a
bb522b59585980b0b1b374c4df4f81a1
The relying party recomputes the hash of the client data it received, concatenates it after the authenticator data it received, and verifies against the stored public key. It succeeds.
Now run the phishing case with the same key and the same challenge, changing nothing but the origin the browser reports:
origin https://kedbyte.com
SHA-256 of clientData = 4aaa6716...41f6b5e0 valid
origin https://kedbyte.com.secure-login.example
SHA-256 of clientData = different signature
invalid
The signature over the first message does not verify over the second, because the hash differs in essentially every bit. The relying party would in fact never get that far, because step 12 rejects the unexpected origin before any signature is checked. But it is worth seeing that even a relying party that forgot to check the origin string would still fail at the signature, provided it took the origin from the signed client data and compared the recomputed hash. The two checks are independent and both are cheap.
There is a third barrier before either. The authenticator holds this credential filed under the relying party identifier kedbyte.com and, in the authentication ceremony, verifies that the RP ID supplied by the client exactly matches the scope of the requested credential. A client on the origin kedbyte.com.secure-login.example may only ask for credentials scoped to kedbyte.com.secure-login.example or a registrable suffix of it. There is no credential there. The ceremony ends with “no credentials available” and nothing is signed at all.
attacker page real site
evil origin kedbyte.com
| |
| 1. fetch a real challenge |
|--------------------------->|
|<---------------------------|
| |
v
+----------------+
| browser | origin it will report:
| (client) | https://kedbyte.com.secure-login.example
+----------------+
|
| 2. rpId must be a suffix of that origin
v
+----------------+
| authenticator | looks for a credential scoped to
| | kedbyte.com.secure-login.example
+----------------+
|
| 3. none found -> ceremony fails here
| if one existed, origin in clientData
| would still be the evil one, and the
| real site rejects at step 12
v
no usable assertion ever reaches the attacker
Relying party identifier, scoping and related origins#
The relying party identifier is a domain, never an origin: no scheme, no port. It defaults to the effective domain of the caller’s origin. A caller may set it explicitly, but only to a value that is a registrable domain suffix of, or equal to, its own effective domain.
The rule in practice is that a page on the origin accounts.kedbyte.com may set the RP ID to accounts.kedbyte.com or kedbyte.com, but not to kedbyte.co.uk, not to com, and not to anything ending in kedbyte.com that it does not itself sit under. “Registrable domain suffix” carries the public suffix list with it, which is why com and co.uk are excluded: they are public suffixes, and a credential scoped to a public suffix would be usable by everybody underneath it.
The scope of the resulting credential is every origin whose effective domain is that identifier or a subdomain of it. So an RP ID of kedbyte.com covers www.kedbyte.com and accounts.kedbyte.com but a credential registered with an RP ID of accounts.kedbyte.com does not cover the bare domain. The practical deployment rule follows immediately: register at the highest domain you control and are willing to share credentials across, because you can narrow the scope of new credentials but you cannot widen the scope of old ones.
This left one real-world case unserved: a business operating the same service on genuinely different registrable domains, most often per-country. The Level 3 answer is related origin requests, in section 5.11. When the requested RP ID is not a registrable domain suffix of the caller’s origin, a supporting client runs the related origins validation procedure of section 5.11.1: it fetches, over https, without credentials and without a referrer, the well-known resource at the path /.well-known/webauthn on the RP ID’s domain. That resource must return status 200 with content type application/json and a JSON object with an origins member holding an array of strings.
{
"origins": [
"https://kedbyte.co.uk",
"https://kedbyte.de",
"https://kedbyte.sg",
"https://kedbyte-rewards.com"
]
}
The client walks the list, computes for each entry the registrable origin label, which is the first label of the registrable domain, and returns true as soon as it finds an entry that is same-origin with the caller. The limit is on distinct labels, not on entries: the client has a maximum number of labels allowed by policy, and once that many distinct labels have been seen, further entries introducing new labels are skipped. The label of kedbyte.co.uk and of kedbyte.de is in both cases kedbyte; the label of kedbyte-rewards.com is kedbyte-rewards. So an unlimited number of country domains sharing one brand name costs one label, and each separate brand costs another.
The specification requires clients to support at least five labels. As of August 2026 no shipping client is known to support more than five, so five should be treated as the deployment ceiling. Chrome shipped related origin requests in 2024, announced on the Chrome developer blog in August of that year alongside the hints member and the JSON serialization methods, the latter arriving in Chrome 129.
CTAP2: the protocol underneath the API#
WebAuthn is the browser-facing half. The authenticator-facing half is the Client to Authenticator Protocol, CTAP, published by the FIDO Alliance. Together they are FIDO2. As of August 2026 the current version is CTAP 2.3, a Proposed Standard dated 26 February 2026; CTAP 2.2 was a Proposed Standard of 14 July 2025 and CTAP 2.1 of 15 June 2021. CTAP 2.3 is explicitly backwards compatible: any CTAP 2.2-conformant implementation is conformant to 2.3.
CTAP2 messages are CBOR. A command is a single byte followed by a CBOR-encoded parameter map with integer keys.
| Command | Value | Purpose |
|---|---|---|
| MakeCredential | 0x01 | Register |
| GetAssertion | 0x02 | Authenticate |
| GetInfo | 0x04 | Capabilities |
| ClientPIN | 0x06 | PIN and UV tokens |
| Reset | 0x07 | Factory reset |
| GetNextAssertion | 0x08 | Next of several |
| BioEnrollment | 0x09 | Enrol fingerprints |
| CredentialManagement | 0x0A | List and delete |
| Selection | 0x0B | Which key was touched |
| LargeBlobs | 0x0C | Blob storage |
| Config | 0x0D | Authenticator config |
The parameters of authenticatorMakeCredential are numbered rather than named, again to save bytes on a slow link: 0x01 clientDataHash, 0x02 rp, 0x03 user, 0x04 pubKeyCredParams, 0x05 excludeList, 0x06 extensions, 0x07 options, 0x08 pinUvAuthParam, 0x09 pinUvAuthProtocol, 0x0A enterpriseAttestation, 0x0B attestationFormatsPreference.
Notice 0x01. The authenticator receives the hash of the client data and never the client data itself. It cannot read the origin, cannot parse JSON, and cannot make a policy decision about the website. It signs the hash it is given. All the origin logic lives in the client, and the authenticator’s independent contribution is only the rpIdHash, which it computes from the RP ID the client supplied and compares against the scope recorded in the credential. This division is deliberate: it keeps the authenticator small, and it means a security key with a few kilobytes of memory can be as phishing-resistant as a laptop.
GetNextAssertion at 0x08 exists for the case where several discoverable credentials on one authenticator match one relying party. The authenticator returns the first with a count, and the client walks the rest.
Transports: USB, NFC, BLE and hybrid#
CTAP defines bindings for four transports, in sections 11.2 to 11.5 of the CTAP 2.2 and 2.3 specifications.
| Transport | Section | Typical use |
|---|---|---|
| USB HID | 11.2 | Security key in a port |
| NFC | 11.3 | Tap card or key |
| BLE | 11.4 | Paired Bluetooth key |
| Hybrid | 11.5 | Phone authenticates PC |
USB HID is the original U2F transport and remains the most common for security keys; it works without drivers because the key presents itself as a human interface device. NFC uses ISO 7816 framing over ISO 14443, with applet selection and fragmentation, which is why a FIDO card and a payment card can share a chip. Classic BLE uses a GATT service and requires pairing, and it is the least used of the four because pairing is unpleasant and battery matters.
Hybrid is the interesting one, and it is what people mean when they say they signed in on a laptop by scanning a code with their phone. The mechanism, in outline: the computer displays a QR code whose contents are a URI beginning with the uppercase scheme FIDO:/ followed by digit-encoded data, carrying a public key and a shared secret. The phone scans it, derives keys, and emits a Bluetooth Low Energy advertisement encrypted under a key derived from that secret. The computer, listening, recognizes the advertisement. Both then connect to a tunnel service over the internet and run ordinary CTAP2 inside that encrypted tunnel. CTAP 2.3 adds Bluetooth Low Energy itself as an alternative data-transfer channel alongside the WebSocket tunnel.
The BLE step is not a convenience; it is the security of the design. Bluetooth Low Energy has a range measured in metres, so hearing the advertisement is evidence that the phone and the computer are physically close. Without it, an attacker could display a QR code on their own phishing page, have the victim scan it, and complete an assertion for a session on the attacker’s machine anywhere in the world. With it, the attacker would need to be in the room. This is why a hybrid sign-in works at your desk and fails over a video call, which users experience as a bug and is in fact the entire point.
After the first hybrid transaction the devices may store state for one another, which the specification calls state-assisted transactions, so subsequent sign-ins can skip the QR code.
Attestation formats, and the case for ignoring them#
Section 8 of WebAuthn Level 3 defines eight attestation statement formats.
| Format | Section | Produced by |
|---|---|---|
| packed | 8.2 | Most FIDO2 keys |
| tpm | 8.3 | Windows with a TPM |
| android-key | 8.4 | Android keystore |
| android-safetynet | 8.5 | Older Android |
| fido-u2f | 8.6 | U2F-era keys |
| none | 8.7 | No attestation |
| apple | 8.8 | Apple devices |
| compound | 8.9 | Several at once |
Section 6.5.3 defines the trust models these express: Basic attestation, where a key pair is shared across a model or batch; Self attestation, where the credential private key signs its own statement and no external assurance exists; Attestation CA, where a per-device key is used to obtain per-credential certificates; Anonymization CA, where a cloud service issues per-credential certificates so that no long-lived identifier is exposed; and None.
Here is the argument for asking for none of it in a consumer deployment, in five parts.
It is off by default. The WebAuthn creation options dictionary declares attestation with a default value of “none”, and when the preference is none the client is required to replace the format with “none” and the statement with an empty CBOR map. You have to opt in.
It identifies a batch, not a device. Attestation certificates and key pairs can be used to track users, so the specification’s privacy section 14.4.1 recommends batches large enough for meaningful anonymity, and points at the FIDO UAF requirement that at least 100,000 devices share one attestation certificate. Whatever you learn is therefore a statement about a hundred thousand devices.
Verifying it properly is real work. You must parse the statement, validate a certificate chain to a trusted root, and check the AAGUID against metadata, which in practice means consuming the FIDO Metadata Service, the signed MDS3 BLOB published by the FIDO Alliance, keeping it current, and deciding what to do about authenticators that are not in it. Requesting attestation and not doing this is pure cost.
It fails closed against users you wanted. Enforce a list of approved AAGUIDs and every user with an unlisted authenticator, including a perfectly good new model shipped last month, is blocked from registering.
And it usually will not answer the question you asked. The common motivation is to distinguish hardware-bound from synced credentials, and there is a cheaper and more honest instrument for exactly that: the backup eligibility flag, which is in every assertion for free.
The legitimate uses are narrow and real: an enterprise that has issued specific hardware to specific staff and must enforce it, a regulator requiring a certified authenticator, a government relying party bound by a hardware policy. Those are the cases the enterprise attestation conveyance and the direct preference exist for. Everybody else should send “none”, read the AAGUID for display purposes only, and read the BE bit for policy.
Synced versus device-bound credentials#
The specification’s terms are multi-device credential and single-device credential; the industry says synced and device-bound. The distinction is not about where the credential is right now but about whether the generating authenticator will ever allow it to be copied, and that is fixed at creation.
| Property | Synced | Device-bound |
|---|---|---|
| BE flag | 1 | 0 |
| Survives device loss | Yes | No |
| Highest NIST AAL | AAL2 | AAL3 possible |
| Extra trusted party | Sync provider | None |
The security question a synced credential raises is what protects the key while it is in the sync fabric, and this is where policy has caught up with practice. NIST published Special Publication 800-63-4 as final on 31 July 2025, superseding the 2017 third revision from 1 August 2025; it followed a supplement on syncable authenticators issued in April 2024 that first admitted passkeys to AAL2. The normative requirements in SP 800-63B-4’s appendix on cloning authentication keys are specific. Keys cloned or exported to a sync fabric shall only be stored encrypted, using a key with at least the minimum security strength of the current revision of SP 800-131A, that is 112 bits, and encrypted by a method employing a user-controlled secret. Access to the keys in the fabric shall be protected by access control such that only the authenticated user can reach them, and that access shall be protected by multi-factor authentication equivalent to AAL2. All authentication transactions shall perform the private-key operation on the local device. And the ceiling is stated plainly: syncing violates the non-exportability requirement of AAL3.
That last sentence is the honest summary of the trade. If your threat model includes an attacker who can compromise a user’s cloud account, a synced passkey inherits that risk and a hardware key does not. For consumer accounts this is almost always the right trade, because the alternative failure, the user losing their only credential and falling back to an email reset link, is both more likely and worse. For a systems administrator’s account it is usually the wrong trade.
There is a second-order point that gets missed. The sync provider is trusted for availability as well as confidentiality. If the provider locks the account, or the user forgets the credential that unlocks the sync fabric, every passkey held there is inaccessible at once. Correlated failure is the price of correlated convenience, which is why the FIDO deployment guidance and the specification both push relying parties towards registering more than one authenticator per account.
Portability between providers, which was the standing complaint against synced passkeys, now has a specification: the FIDO Alliance’s Credential Exchange Format was approved as a Proposed Standard on 14 August 2025, with the Credential Exchange Protocol alongside it, developed by a group including 1Password, Apple, Bitwarden, Dashlane, Enpass, Google, Microsoft, NordPass, Okta, Samsung and SK Telecom. Whether an exported passkey remains phishing-resistant while in transit between two vaults is exactly the question those specifications exist to answer, and it is worth watching rather than assuming.
Discoverable credentials and usernameless sign-in#
A discoverable credential, called a resident key in earlier drafts and still called that in CTAP, is one the authenticator can find and use without being told its credential ID. The authenticator stores the credential and its associated user handle and user name locally, indexed by relying party identifier. A non-discoverable credential is the older design: the credential private key is encrypted into the credential ID itself, so the authenticator stores nothing and can reconstruct the key only when the relying party hands the credential ID back.
The difference decides what a sign-in looks like:
| Step | Non-discoverable | Discoverable |
|---|---|---|
| User types username | Yes | No |
| RP sends allowCredentials | Populated | Empty |
| Authenticator storage | None used | One slot used |
| Response includes handle | Optional | Required |
Discoverable credentials are what make the passkey user experience possible: the browser can offer accounts in the autofill dropdown before the user has typed anything, which the specification supports through conditional mediation. They are the reason a relying party can present a single button that signs the user in with no identifier at all.
They cost storage, and on a hardware key that storage is finite. A YubiKey 5 with firmware 5.7.4 or later holds up to 100 discoverable credentials, alongside 24 PIV certificates and 64 OATH seeds, according to Yubico’s technical manual; earlier firmware held considerably fewer. [UNVERIFIED: the exact discoverable-credential capacity of YubiKey firmware before 5.7] A synced credential manager has no such limit in practice.
The cost that matters more is administrative, and it leads directly into the next chapter. Because a discoverable credential is stored on the device with its user-visible name, deleting an account at the relying party leaves an orphan on the authenticator: a passkey that appears in the user’s list, offers itself in the dropdown, and fails when used. Equally, if the user deletes the passkey from their phone, the relying party has no way to know, and will keep offering passkey sign-in for a credential that no longer exists.
WebAuthn Level 3 addresses this with the signal methods in section 5.1.10, which the Web Authentication Working Group records as having an initial implementation in Chrome from version 130.0.6697.0: signalUnknownCredential, which tells the credential manager that a specific credential ID is not recognized so it can be removed; signalAllAcceptedCredentials, which supplies the full list of credential IDs still valid for a user handle so that everything else can be cleaned up, and which the specification says relying parties should prefer; and signalCurrentUserDetails, which updates the stored name and display name.
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "kedbyte.com",
userId: "dXNlci0wMDAxNzQ",
allAcceptedCredentialIds: [ "nyx0obDjSG1af8kBLeS4Og" ]
});
These are hints, not commands: a credential manager may ignore them, and none of them help when the user has lost the device entirely. That case is chapter 24’s.
What a relying party stores, and what it must check#
The credential record, in the specification’s terms, is what the relying party persists after a successful registration. Its essential members are the credential type, the credential ID, the public key, the signature counter, whether user verification was initialized, the transports the authenticator reported, and the most recent backup eligibility and backup state. Level 3 also allows storing the attestation object and its client data so that attestation can be assessed later, which is useful precisely because it lets a relying party defer the decision instead of demanding attestation on day one.
For assurance decisions, the practical checklist is short and worth committing to memory. Always verify the challenge, the type, the origin, and the rpIdHash. Always require and check the UP bit. Decide a policy on the UV bit and enforce it consistently rather than per-authenticator. Store BE and BS, use BE to decide whether the account needs a second credential, and react to BS transitions. Treat the counter as a weak hint and never fail a login on it alone. And never make the origin check a substring test.
The algorithms, as of August 2026#
The COSE algorithm identifiers a deployment should care about, with the IANA registry values:
| Algorithm | COSE value | Status in 2026 |
|---|---|---|
| ES256 | -7 | Universal default |
| RS256 | -257 | Legacy compatibility |
| EdDSA | -8 | Widely supported |
| ESP256 | -9 | Fully specified form |
| Ed25519 | -19 | Fully specified form |
| ML-DSA-44 | -48 | Post-quantum, new |
| ML-DSA-65 | -49 | Post-quantum, new |
| ML-DSA-87 | -50 | Post-quantum, new |
Two moves are visible in that table. The first is the shift to fully specified algorithm identifiers: -7 means “ECDSA with SHA-256” and leaves the curve to be inferred from the key, whereas ESP256 at -9 names both, which removes a class of implementation ambiguity. WebAuthn Level 3 adds a requirement that ESP256, ESP384 and ESP512 public keys use the uncompressed point form. The second is post-quantum. The FIDO Alliance’s Server Requirements document accompanying WebAuthn Level 3 and CTAP 2.3, published in 2026, adds ML-DSA-44, ML-DSA-65 and ML-DSA-87 as Recommended algorithms, alongside the fully specified ESP256, ESP384, ESP512 and Ed25519. ML-DSA is the signature scheme standardized as FIPS 204 on 13 August 2024. A relying party listing pubKeyCredParams today has no reason to include the post-quantum identifiers yet and every reason to know they are now nameable.
The numbers, as of August 2026#
Established fact, from published deployment data rather than vendor marketing:
| Measure | Value | Source and date |
|---|---|---|
| Passkeys in use | 5 billion | FIDO, 7 May 2026 |
| Accounts with a passkey | 36 per cent | Passkey Index, 2025 |
| Sign-ins using passkeys | 26 per cent | Passkey Index, 2025 |
| Passkey sign-in success | 93 per cent | Passkey Index, 2025 |
The FIDO Alliance’s Passkey Index of 14 October 2025 pooled data from Amazon, Google, LY Corporation, Mercari, Microsoft, NTT DOCOMO, PayPal, Target and TikTok. Beside the figures above it reported that 93 per cent of accounts were eligible for passkeys, that other authentication methods succeeded 63 per cent of the time against the passkey figure of 93, that a passkey sign-in took 8.5 seconds on average against 31.2 seconds for the alternatives, and that deploying organizations saw an 81 per cent reduction in sign-in-related help desk incidents.
On World Passkey Day, 7 May 2026, the FIDO Alliance published its State of Passkeys 2026 report, based on surveys by Sapio Research in April 2026 of 11,000 consumers across ten countries and 1,400 enterprise decision-makers at organizations with 500 or more employees, with margins of error of 0.9 and 2.6 percentage points at 95 per cent confidence. It reported 90 per cent awareness of passkeys, 75 per cent of people having enabled at least one, and 49 per cent using them whenever available or most of the time. On the enterprise side, 68 per cent of organizations were deploying or piloting passkeys for employee sign-in, 28 per cent had reached fully passwordless, and 57 per cent still relied on passwords as the primary employee sign-in method. The 5 billion figure is the Alliance’s own estimate combining public data with its members’ deployment data, which is worth knowing when quoting it.
Vendor figures should be read as vendor figures. Microsoft, on 1 May 2025, announced that brand new Microsoft accounts would be passwordless by default, and reported that users with passkeys signed in successfully about 98 per cent of the time against 32 per cent for password users, that passkey sign-ins were eight times faster than password plus multi-factor, and that nearly one million passkeys were being registered daily. Amazon reported more than 465 million customers with passkeys enrolled as of the first quarter of 2026. These are real and they are also selected.
How it got here#
The lineage is short and unusually well documented. The FIDO Alliance was founded in February 2013. Its first specifications arrived the following year: U2F 1.0 as a Proposed Standard on 9 October 2014 and UAF 1.0 on 8 December 2014, with Google shipping the first Security Key in October 2014. FIDO 2.0 was published as a Proposed Standard on 4 September 2015 and formally submitted to the W3C on 12 November 2015; the W3C created the Web Authentication Working Group on 17 February 2016.
The evidence that origin binding works arrived before the standard did. In July 2018 Brian Krebs reported that Google, having required physical security keys of all of its more than 85,000 employees from early 2017, had had no reported or confirmed account takeovers since. Four years later, on 9 August 2022, Cloudflare published an account of the phishing campaign later called 0ktapus, which compromised Twilio and more than a hundred other organizations: Cloudflare employees fell for the messages and entered credentials, and the attack failed anyway because every employee held a FIDO2 hardware key that would not produce an assertion for the attacker’s origin.
WebAuthn became a W3C Recommendation at Level 1 on 4 March 2019 and at Level 2 on 8 April 2021. Level 3’s first public working draft appeared on 15 December 2021. Passkeys as a consumer product date from the joint announcement by Apple, Google and Microsoft on 5 May 2022, built on the FIDO Alliance’s multi-device credentials white paper of that spring. Google made passkeys available for all personal Google Accounts on 3 May 2023 and the default sign-in method on 10 October 2023. As of August 2026, Level 3 remains a Candidate Recommendation, with a Candidate Recommendation Snapshot dated 26 May 2026 and an Editor’s Draft of 30 July 2026; Level 2 of April 2021 is still the most recent full Recommendation, which is a normal state of affairs for a specification being implemented faster than it is being formally advanced.
23.98 Common wrong ideas#
Wrong: A passkey is a password stored more securely. Right: There is no shared secret at any point; the authenticator holds a private key, the relying party holds only the matching public key, and the public key is safe to publish, so a full breach of the relying party’s database yields nothing that can be used to sign in anywhere, which is a different category of protection from storing a password well.
Wrong: Passkeys are phishing-proof, so a user with passkeys cannot be robbed. Right: Origin binding defeats credential relay, which is the mechanism behind most account takeovers, but it does not defeat a site that still accepts a password as a fallback, session cookie theft after a legitimate sign-in, or user-interface attacks such as the DOM-based extension clickjacking Marek Toth demonstrated at DEF CON 33 on 9 August 2025, which obtained valid assertions from eight of eleven tested password managers while the origin binding itself held perfectly.
Wrong: The private key never leaves the device. Right: That is true only of device-bound credentials, where the backup eligibility flag is zero; for a synced passkey the private key is encrypted and copied to a sync fabric by design, which is why NIST Special Publication 800-63B-4 caps syncable authenticators at authentication assurance level 2 and states that syncing violates the non-exportability requirement of AAL3.
Wrong: The relying party should check the origin by looking for its domain name in the origin string. Right: The check in step 12 of section 7.2 is a comparison against a list of expected origins, because a substring test is passed by the origin kedbyte.com.secure-login.example and by any attacker origin containing the name, which converts the single control that makes WebAuthn phishing-resistant into no control at all.
Wrong: The signature counter detects cloned authenticators. Right: It is defined as a signal and not proof even for hardware keys, and it cannot work at all for synced credentials, which legitimately exist on several devices with no shared counter and overwhelmingly report zero forever, so the specification tells relying parties to skip the comparison entirely when both the stored and reported values are zero.
Wrong: Request attestation so you know what kind of authenticator the user has. Right: Attestation identifies a model or a batch and not a device, deliberately so, since the FIDO UAF specification asks that at least 100,000 devices share an attestation certificate; it is absent unless you opt in, since the creation options default the attestation preference to “none”; verifying it properly means maintaining the FIDO Metadata Service BLOB and deciding what to do about unlisted authenticators; and if what you actually wanted was to know whether the credential syncs, the backup eligibility flag tells you that for free in every assertion.
Wrong: The relying party identifier is the website’s URL. Right: It is a domain with no scheme and no port, it defaults to the effective domain of the calling origin, it may only be set to a registrable domain suffix of that origin, and the credential’s scope is that domain and all of its subdomains, which means registering at accounts.kedbyte.com produces a credential that will not work on kedbyte.com and cannot be widened afterwards.
Wrong: One passkey per account is enough because it syncs. Right: Syncing protects against losing a device and not against losing access to the sync account itself, in which case every passkey in that vault becomes unavailable at once, which is why the specification recommends acting on the backup eligibility flag by ensuring a second authenticator or a recovery path exists, and why chapter 24 exists.
Wrong: Passkeys work across your sites automatically because you own all the domains. Right: Scope follows the registrable domain, so kedbyte.co.uk and kedbyte.de are unrelated as far as WebAuthn is concerned; sharing credentials across them requires related origin requests under section 5.11, which means serving a JSON document at /.well-known/webauthn listing the origins, and being aware that clients need only support five distinct registrable origin labels and that in practice none support more.
Wrong: The authenticator checks the website, so the browser is not really trusted. Right: The authenticator receives only the SHA-256 hash of the client data and the relying party identifier the client hands it, so it cannot read the origin at all; its independent contribution is confirming that the requested RP ID matches the scope recorded in the credential, and the honest reporting of the actual origin is a property of the client, which is squarely inside the trusted computing base.
23.99 Chapter summary in 20 lines#
- A passkey is a public key credential: an asymmetric key pair generated on an authenticator and scoped to exactly one relying party, with the private key retained by the authenticator and only the public key given to the website.
- Because the relying party stores only a public key and a credential identifier, a full breach of its database yields nothing that can be replayed anywhere, which removes the shared secret that every earlier authenticator depended on.
- Because each relying party gets an independently generated key pair, two websites comparing their records cannot tell that the same person holds both credentials.
- Registration is a ceremony in which the relying party sends a random challenge of at least 16 bytes and receives back the exact client data serialization and an attestation object containing the format, the attestation statement and the authenticator data.
- Authentication is a ceremony in which the relying party sends a fresh challenge and receives an assertion consisting of the client data, the authenticator data, a signature and, for discoverable credentials, the user handle.
- The signature is computed over the binary concatenation of the authenticator data and the SHA-256 hash of the exact client data bytes, which is why a relying party must hash the bytes it received rather than re-serializing a parsed copy.
- The client data carries the type, either webauthn.create or webauthn.get, the base64url challenge, the fully qualified origin, and in Level 3 the optional crossOrigin and topOrigin members.
- The origin field is the whole anti-phishing mechanism because it is written by the browser and not by the page, it covers scheme and port, it is protected by the signature through the hash, and it is compared by the relying party against a list of expected origins.
- An origin check implemented as a substring test rather than a comparison against expected origins removes the entire security property while appearing to implement it.
- Authenticator data is 37 bytes or more: a 32-byte SHA-256 hash of the relying party identifier, one flags byte, a four-byte big-endian counter, then optional attested credential data and extensions.
- The six defined flags are user present at bit 0, user verified at bit 2, backup eligible at bit 3, backup state at bit 4, attested credential data at bit 6 and extension data at bit 7, with bits 1 and 5 reserved and required to be zero.
- Backup eligibility is fixed at creation and marks a credential as syncable; backup state may change over time, the combination of eligibility clear with state set is not allowed, and relying parties should store both and react to transitions.
- The relying party identifier is a domain without scheme or port, defaults to the caller’s effective domain, may only be set to a registrable domain suffix of it, and scopes the credential to that domain and its subdomains.
- Related origin requests, added in Level 3 section 5.11, let one relying party identifier serve genuinely different domains by publishing a JSON document with an origins array at /.well-known/webauthn, subject to a client limit on distinct registrable origin labels that is at least five and in practice exactly five.
- CTAP is the authenticator-facing half of FIDO2, with commands as single bytes and CBOR parameter maps, and as of August 2026 the current version is CTAP 2.3, a Proposed Standard of 26 February 2026 that is backwards compatible with CTAP 2.2 of 14 July 2025.
- CTAP defines USB HID, NFC, Bluetooth Low Energy and hybrid transports, and the hybrid flow uses a QR code carrying a key and a secret plus a short-range Bluetooth advertisement, so that physical proximity is proved and a remote attacker cannot complete the flow.
- The specification defines eight attestation statement formats, but attestation defaults to none, identifies a batch rather than a device by design, requires metadata service infrastructure to verify properly, and answers the syncing question worse than the backup eligibility flag does.
- Synced credentials survive device loss but add the sync provider as a trusted party, and NIST Special Publication 800-63B-4 caps them at authentication assurance level 2 with specific requirements on encryption strength and access control, stating that syncing violates the non-exportability requirement of level 3.
- Discoverable credentials store the user handle on the authenticator so that a user can sign in without typing an identifier, at the cost of finite storage on hardware keys and of orphaned credentials that the Level 3 signal methods only partially clean up.
- As of August 2026 the FIDO Alliance estimates 5 billion passkeys in use, the Passkey Index of October 2025 reported 36 per cent of accounts enrolled and a 93 per cent sign-in success rate against 63 per cent for other methods, and the remaining weak point is not the cryptography but the recovery path, which is chapter 24.
Chapter sources: W3C, “Web Authentication: An API for accessing Public Key Credentials - Level 3”, Candidate Recommendation Snapshot of 26 May 2026, with the Editor’s Draft of 30 July 2026 consulted; sections used are 4 for terminology, 5.1.3 for the create internal method and attestation conveyance handling, 5.1.8 and 5.1.9 for the JSON parsing methods, 5.1.10 for signalUnknownCredential, signalAllAcceptedCredentials and signalCurrentUserDetails, 5.4 for PublicKeyCredentialCreationOptions and its attestation default of “none”, 5.8.1 for CollectedClientData and 5.8.1.2 for the limited verification algorithm, 5.11 and 5.11.1 for related origins and the validation procedure that fetches the well-known webauthn resource at the path /.well-known/webauthn, 6.1 for the authenticator data table and flag bits, 6.1.1 for signature counter considerations, 6.1.3 for the backup flag combinations, 6.5.1 for attested credential data with its 16-byte AAGUID and 1,023-byte credential identifier limit, 6.5.3 for the attestation types Basic, Self, Attestation CA, Anonymization CA and None, 7.1 for the 29 registration verification steps, 7.2 for the 25 authentication steps including step 12 on origin and step 21 on signature verification, 8.2 to 8.9 for the eight attestation statement formats, 10.1.4 for the prf extension, 13.4.3 for the challenge entropy recommendation of at least 16 bytes, 14.4.1 for attestation privacy and the FIDO UAF requirement of at least 100,000 devices per attestation certificate, 15.1 for the ceremony timeout range of 300,000 to 600,000 milliseconds, and 18.1.1 for the substantive changes since Level 2, including JSON serialization, cross-origin iframe creation, conditional mediation for create, topOrigin, related origins, the prf extension and the removal of AAGUID zeroing under attestation “none”. FIDO Alliance, “Client to Authenticator Protocol (CTAP)”, version 2.3, Proposed Standard of 26 February 2026, and version 2.2, Proposed Standard of 14 July 2025, with version 2.1 of 15 June 2021 for the CTAP2 canonical CBOR encoding form; sections 11.2 to 11.5 for the USB HID, NFC, Bluetooth Low Energy and hybrid transports, with the QR URI scheme FIDO:/ quoted from the CTAP 2.2 review draft of 21 March 2023, and the command values 0x01 through 0x0D listed in this chapter. FIDO Alliance, “FIDO2 Server Requirements (WebAuthn Level 3 and CTAP 2.3)”, 2026, adding ML-DSA-44, ML-DSA-65 and ML-DSA-87 as Recommended alongside the fully specified ESP256, ESP384, ESP512 and Ed25519. FIDO Alliance Credential Exchange Format version 1.0, Proposed Standard of 14 August 2025, with the Credential Exchange Protocol alongside it. IANA COSE Algorithms registry for the values ES256 -7, EdDSA -8, ESP256 -9, Ed25519 -19, ES384 -35, ES512 -36, ML-DSA-44 -48, ML-DSA-65 -49, ML-DSA-87 -50, ESP384 -51, ESP512 -52, Ed448 -53 and RS256 -257. NIST Special Publication 800-63-4 and its volume 800-63B-4, final of 31 July 2025 and superseding SP 800-63-3 from 1 August 2025, with the syncable authenticator requirements in the normative appendix on cloning authentication keys, following the supplement to SP 800-63B on syncable authenticators of April 2024; NIST SP 800-131A for the 112-bit minimum security strength; FIPS 204 of 13 August 2024 for ML-DSA. RFC 6454 for the web origin concept, RFC 8615 for well-known URIs, RFC 8949 for CBOR, and RFC 9052 and RFC 9053 for COSE keys and algorithms. History and measurement: the FIDO Alliance founding in February 2013; U2F 1.0 Proposed Standard of 9 October 2014 and UAF 1.0 of 8 December 2014; FIDO 2.0 Proposed Standard of 4 September 2015, submitted to the W3C on 12 November 2015, with the Web Authentication Working Group created on 17 February 2016; WebAuthn Level 1 Recommendation of 4 March 2019, Level 2 of 8 April 2021 and Level 3’s first public working draft of 15 December 2021; Brian Krebs, “Google: Security Keys Neutralized Employee Phishing”, KrebsOnSecurity, July 2018, on more than 85,000 employees and no confirmed account takeovers since early 2017; Cloudflare, “The mechanics of a sophisticated phishing scam and how we stopped it”, 9 August 2022, on the campaign later named 0ktapus; the joint Apple, Google and Microsoft announcement of 5 May 2022 and the FIDO Alliance white paper “Multi-Device FIDO Credentials”; Google, “The beginning of the end of the password”, 3 May 2023, and “Passwordless by default”, 10 October 2023; Microsoft Security Blog, “Pushing passkeys forward”, 1 May 2025, for passwordless-by-default new accounts, the 98 per cent versus 32 per cent success figures, the eight-times-faster claim and nearly one million passkeys registered daily; Chrome for Developers, “Introducing hints, Related Origin Requests and JSON serialization for WebAuthn in Chrome”, August 2024, for Chrome 129; Marek Toth, “DOM-based Extension Clickjacking”, 9 August 2025, presented at DEF CON 33, testing eleven password manager extensions with passkey exploitation demonstrated against eight; the FIDO Alliance Passkey Index of 14 October 2025, pooling data from Amazon, Google, LY Corporation, Mercari, Microsoft, NTT DOCOMO, PayPal, Target and TikTok; FIDO Alliance, “The State of Passkeys 2026”, published on World Passkey Day, 7 May 2026, surveying 11,000 consumers in ten countries and 1,400 enterprise decision-makers through Sapio Research in April 2026, with margins of error of 0.9 and 2.6 percentage points at 95 per cent confidence, and the Alliance’s own estimate of 5 billion passkeys in use; the Yubico YubiKey Technical Manual for the firmware 5.7.4 capacity of 100 discoverable credentials, 24 PIV certificates and 64 OATH seeds; the community passkey-authenticator-aaguids list for the AAGUID fbfc3007-154e-4ecc-8c0b-6e020557d7bd. All challenge, client data, hash, authenticator data, COSE key and ECDSA signature values in this chapter were computed on 18 August 2026 with Python 3 and the cryptography library version 46.0.7 over the NIST P-256 curve, and the signature was verified against the stated public key before publication.