Passkeys are often described first as “log in after a fingerprint scan.” The more important question is why they can suppress several of the most stubborn password risks at once. “It uses public-key cryptography” is not enough, because many systems use public keys and still fail against phishing, replay, and credential copying.
Passkeys sit on top of a protocol stack that works together: WebAuthn organizes registration and login, CTAP carries messages between the browser or operating system and the authenticator, and the authenticator signs the domain, challenge, and user-verification state together. What changes is not “password becomes private key.” What changes is how login is bound to the site, the request, and the user’s approval.
Where the Problem Comes From
Passwords are not only hard because users forget them. The deeper problems are:
- Password reuse across sites
- Servers storing reusable verification material
- Phishing sites forwarding usernames, passwords, and OTPs in real time
- Authentication success proving only that someone knew a secret, not that the secret was valid only for this site
Traditional MFA only patches this partially. TOTP may reduce reuse after a leak, but it cannot stop a user from typing the code into a phishing site.
FIDO2 and WebAuthn solve a lower-level trust boundary: the authenticator knows which relying party it is signing for, and the server stores only a public key instead of a shared secret.
Where It Came From
Passkeys are not a brand-new protocol that appeared out of nowhere. They are the latest deployment form of the FIDO stack for the Web. U2F and UAF came first, then WebAuthn standardized the browser-side interface, and CTAP filled in the client-to-authenticator path. Together they make what people now call FIDO2.
That history explains a few common misunderstandings:
- Passkeys are not a separate new standard; they are a credential form and deployment model based on FIDO2
- WebAuthn handles the browser and website boundary
- CTAP handles the browser / operating-system to authenticator boundary
It also explains why passkeys may look inconsistent in practice: some credentials are synced, some are device-bound, some use built-in platform auth, and some use an external security key. Those are compatibility and deployment choices, not protocol confusion.
What They Actually Solve
In one sentence:
Passkeys create a per-site key pair and bind each login to this site, this request, and this user approval, while the server keeps only the public key.
The chain has four actors:
- Relying Party (RP): creates the challenge and verifies the response
- Client: browser or OS that passes the WebAuthn request to the authenticator
- Authenticator: stores the private key, performs user verification, and signs
- User: approves the operation with PIN, biometrics, or a touch gesture
Passkeys are about registration and authentication. They do not solve session expiry, authorization, risk policy, or account recovery by themselves.
Why They Are Designed This Way
The Main Path
At registration:
- The server creates a challenge and declares
rp.id, user identity, and accepted algorithms - The client passes that to the authenticator
- The authenticator creates a key pair for that RP and returns signed registration material
- The server verifies it and stores only the public key and credential ID
At login:
- The server creates a new challenge
- The client asks the authenticator for an assertion for the given
rpId - The authenticator verifies user presence or user verification and signs the authentication data
- The server verifies the challenge, domain binding, signature, and counter
The smallest skeleton is:
Registration:
RP -> Client: creation options(challenge, rp, user, algs)
Client -> Authenticator: makeCredential(...)
Authenticator -> RP: attestationObject + clientDataJSON
Authentication:
RP -> Client: request options(challenge, rpId, allowCredentials?)
Client -> Authenticator: getAssertion(...)
Authenticator -> RP: authenticatorData + signature + clientDataJSON
Site Binding
If one credential could be reused across sites, it would just recreate the password problem. WebAuthn’s critical choice is that each credential is naturally bound to an RP.
That gives you:
- A site leak does not affect every other site
- The protocol naturally splits credential use by site
The tradeoff is that credential management now has to handle users with multiple devices, multiple authenticators, and multiple sites.
What the Signature Actually Covers
The authenticator does not sign only a random challenge. It signs:
authenticatorData || SHA-256(clientDataJSON)
That binds three things at once:
- The site
- The request
- The semantic type of the operation
The server should verify at least:
clientDataJSON.typeclientDataJSON.challengeclientDataJSON.originauthenticatorData.rpIdHash- the signature itself
Layered Responsibilities
WebAuthn faces the website and browser boundary. CTAP faces the client-to-authenticator boundary.
That split lets:
- Websites avoid caring whether the authenticator is USB, NFC, BLE, or platform-native
- Authenticators avoid dealing with page context directly
- The ecosystem evolve without rewriting the whole web stack
It also means debugging has to ask where the failure is: WebAuthn, CTAP, or the authenticator itself.
Presence, Verification, and Counters
UP, UV, and signCount are not cosmetic fields.
UPsays the user was presentUVsays the user passed a stronger local checksignCountgives the server an anomaly signal if a credential may have been copied
Three Easy Misreads
Domain Binding
The anti-phishing property comes mostly from domain binding, not from public-key cryptography alone.
originbelongs to the browser security boundaryrpIdbelongs to the credential scope
They are related, but not the same.
Challenge
Without a fresh challenge for each login, a valid assertion could be replayed. The challenge must be created and consumed correctly.
Discoverable Credentials
Discoverable credentials change lookup and account selection. The authenticator can list matching credentials for the RP, which improves usability but also changes storage and account-management behavior.
Engineering Use
What to Verify First
First verify:
- Challenge freshness
originandrpIdHash- Signature validity on
authenticatorData || SHA-256(clientDataJSON) signCountbehavior- Whether attestation and credential extraction are handled correctly
Troubleshoot by Layer
When debugging, separate:
- Server challenge handling
- Browser
origin/clientDataJSON - Authenticator capabilities
- Registration flow (
makeCredential) vs login flow (getAssertion)
Most Dangerous Assumption
Passkeys do not make the rest of account security disappear. They just change the authentication chain to one that is much harder to phish.