Interactive

Passkey Town

Most explanations of passkeys stop at “it’s like a key, but digital”. This one is a town you can walk around. Four walkthroughs, every payload shown as it really appears, and a last one that runs against your own authenticator.

Drag to orbit, scroll to zoom, click any building. Space plays, arrow keys step. Open full screen →

What you are looking at

Four parties, and the whole protocol makes sense once you know who trusts whom.

01
Alicethe user
A passkey ceremony needs a live human gesture. Nothing here works from a script running at 3am.
02
Authenticatorher phone
The secure element. Generates key pairs, keeps the private halves, signs on request. There is no export path.
03
Clientthe browser
Enforces domain scoping and writes the origin into clientDataJSON, where page JavaScript cannot reach it.
04
Bob's serverthe relying party
Issues challenges, verifies signatures, stores public keys. A dump of that database is not worth much.

The client is the load-bearing part. It is the only party that knows for certain which site the user is actually on, and it writes that fact into the signed data where an attacker cannot touch it. That single design choice is why passkeys resist phishing and passwords do not.

Why the phishing flow matters

Run the third walkthrough. Mallory gets three attempts and loses all three, each for a different reason:

  • At the client. A page on a lookalike domain cannot request an rpId it does not own. The call fails before the authenticator is ever woken up.
  • At the authenticator. Asking under its own domain is allowed, but no credential was ever registered there, so there is nothing to return.
  • At the server. Even granting the attacker a signature by some other means, clientDataJSON still records the origin the browser was on, and that string is inside the signature. Editing it breaks the signature. Re-signing needs a key that never left the secure element.

A password would have been handed over at step one. So would an SMS code, a TOTP code, or a push approval — an attacker proxying the real site only needs a few seconds. Passkeys move the judgement call off the human and onto the browser, and the browser is not fooled by a homoglyph.

The last flow is real

“Try it for real” is not a cartoon. It calls navigator.credentials.create() against your actual authenticator, decodes authenticatorData byte by byte, then converts the DER signature to raw r‖s and verifies it with WebCrypto against the public key it stored. Nothing is sent anywhere: the public key lives in localStorage and there is no server involved.

It opens in its own tab, and that is not a convenience. Browsers refuse to create a passkey inside an embedded frame: Chrome rejects the call and Safari blocks it entirely, on the grounds that a credential should only ever be created by the site showing in the address bar. The walkthrough above is embedded, so the restriction applies to it — a small live demonstration of the same origin-binding principle the phishing flow is about.

That does mean you end up with a real passkey scoped to this domain, saved wherever your device saves them. “Forget it” clears our copy of the public key, but the passkey itself lives in your password manager or secure element — remove it there if you want it gone.

Deliberately not modelled

CBOR and COSE encoding, attestation statement formats, the hybrid transport that makes phone-as-authenticator work, and conditional UI. All of them matter, and none of them help on a first pass. The signature verification handles ES256 only; RS256 credentials register fine but the in-browser check will say it skipped them.

If you want a passkey manager that keeps credentials on your own devices rather than in someone else’s cloud, that is what Fenko Vault is for.