Lion Cipher Lab
Verify WebAuthn/FIDO2 registrations and assertions from scratch (no library)
- Identity & Access Management
- Advanced
- ~90 min
- Free · no account required
Why it matters
WebAuthn/FIDO2 (the technology behind passkeys) is the strongest widely-deployed answer to phishing, because the secret that proves who you are -- a private key -- never leaves your device and never travels to a server that could leak or be tricked into revealing it. Instead, your server (the RELYING PARTY) sends a random CHALLENGE, the authenticator signs it, and the server VERIFIES the signature. But that verification is the whole ballgame: a relying party that checks the signature but forgets to check the ORIGIN throws away the phishing resistance entirely, because an attacker's site could…
What you'll learn
- the WebAuthn/FIDO2 model: two ceremonies (registration = navigator.credentials.create, authentication = .get), the relying party (your server) issuing a random challenge and VERIFYING the response, and the private key that never leaves the authenticator
- why origin binding -- the relying party checking clientDataJSON.origin against its own expected origin -- is what makes passkeys phishing-resistant
- building a headless SOFTWARE AUTHENTICATOR (a P-256/ES256 keypair) that constructs authenticatorData + clientDataJSON exactly as a browser/authenticator would, purely to produce reproducible test vectors
- hand-rolling a tiny CBOR decoder and parsing the attestationObject (fmt/attStmt/authData) and a COSE public key with no fido2 library
- parsing authenticatorData byte by byte: 32-byte rpIdHash, the flag byte (UP/UV/AT), the 4-byte signCount, then attestedCredentialData (16-byte AAGUID, 2-byte credId length, credId, COSE key)
- verifying registration from scratch: type==webauthn.create, challenge match, origin match, rpIdHash == SHA-256(rpId), required flags, then extracting and storing the COSE P-256 key + credId + signCount
- verifying an assertion from scratch: recomputing the signed bytes as authenticatorData || SHA-256(clientDataJSON), verifying the ES256 signature with the stored key, and requiring signCount to strictly increase
- proving the four rejections yourself: replay (stale challenge), wrong origin (the phishing case, refused even though the signature is valid), tampered rpIdHash, and a cloned-authenticator counter rollback
- the judgment: none vs packed/full attestation, UP vs UV as the MFA distinction, the server-generated single-use time-boxed challenge rule, the signCount caveat for synced passkeys, and why production uses a maintained FIDO-conformant server library
Steps in this lab
- The model: two ceremonies, one relying party, and origin binding
- Build a software authenticator to make real test vectors (with a tiny CBOR codec)
- Registration verification from scratch: parse the attestationObject byte by byte
- Authentication verification from scratch: recompute the signed bytes, verify ES256, check the counter
- Prove the rejections: replay, wrong origin, wrong rpId, cloned authenticator
- The judgment: attestation, UP vs UV, the challenge rules, the counter caveat, and the library