WebAuthn engineering reference#
This is a developer-portal reference. It assumes you are integrating WebAuthn into a relying-party backend or a JavaScript client; it does not assume you have read the W3C spec end-to-end. Every section links into the AmbiSecure tools that let you walk the binary structures while you read.
If you only have ten minutes, read Registration and Authentication — everything else is depth.
Prerequisites#
- HTTPS origin — localhost is the only HTTP exception (development).
- RP ID — pick the broadest registrable domain you want credentials to roam across.
- CSPRNG — for challenge generation.
- Persistence — for credentialId, credentialPublicKey, signCount, BE/BS, AAGUID.
Your first ceremony#
The two ceremonies are nearly symmetric. navigator.credentials.create() creates a fresh credential; .get() uses an existing one. Both produce a PublicKeyCredential object with attached binary fields.
// Registration — issued by the RP, called by the client const credential = await navigator.credentials.create({ publicKey: { challenge: challengeFromServer, // Uint8Array, 32 bytes rp: { name: "Example Corp", id: "example.com" }, user: { id: userIdBytes, // opaque, max 64 bytes name: "alice@example.com", displayName: "Alice" }, pubKeyCredParams: [ { type: "public-key", alg: -7 }, // ES256 { type: "public-key", alg: -257 } // RS256 ], authenticatorSelection: { authenticatorAttachment: "cross-platform", requireResidentKey: true, userVerification: "required" }, attestation: "direct" } });
Registration ceremony#
The relying party server has six things to verify on registration. Skip any of these and you've broken the security model.
1. Verify clientDataJSON
Decode the base64url clientDataJSON as UTF-8 JSON. Verify type === "webauthn.create", the challenge matches the one your server issued (single-use), and the origin is in your allow-list (exact match — case-insensitive host, scheme, port).
Use the clientDataJSON decoder to walk the structure during integration.
2. Verify authData.rpIdHash
Compute SHA-256(rpId). Compare against the first 32 bytes of authenticatorData. Mismatch → reject. Common cause: rpId mismatch between client options and server expectation.
3. Verify flags
Verify UP bit (0x01) is set; verify UV bit (0x04) if your policy requires user verification. AT (0x40) MUST be set on registration. Reject if BE=0 ∧ BS=1 (invalid combination per WebAuthn level 3).
4. Verify attestation
If fmt !== "none", validate the attestation signature against authData || SHA-256(clientDataJSON). Validate the x5c chain against your trust anchors and the FIDO MDS BLOB. Walk attestStmt with the attestation decoder.
5. Enforce AAGUID policy
Look up the AAGUID in your cached MDS BLOB. Reject if status is USER_VERIFICATION_BYPASS, ATTESTATION_KEY_COMPROMISE, or any other revocation. Reject if AAGUID is not in your allow-list (enterprise).
6. Persist credential
Store: credentialId (bytes), credentialPublicKey (COSE_Key bytes), signCount (uint32), AAGUID, BE flag, BS flag, transports, and the user binding. The BE flag is immutable for the credential’s lifetime; BS is mutable and may be re-evaluated on every assertion.
Authentication ceremony#
Authentication is the easier half — fewer fields to verify, no attestation, no attestedCredentialData. The signature is the load-bearing primitive.
const assertion = await navigator.credentials.get({ publicKey: { challenge: challengeFromServer, // fresh per-session rpId: "example.com", allowCredentials: [ { type: "public-key", id: credentialIdBytes } ], userVerification: "required" } });
Server verification:
- Decode
clientDataJSON; verify type, challenge, origin. - Compute
SHA-256(rpId); compare with authData.rpIdHash. - Verify UP / UV per policy.
- Verify the signature:
verify(publicKey, authData || SHA-256(clientDataJSON), signature). - Verify
signCount > storedSignCount(or both 0 — many platform authenticators). - Update stored signCount; mint session.
authenticatorData reference#
The exact byte layout of authenticatorData. Use the authData parser to walk live data.
clientDataJSON reference#
JSON object built by the user agent. The bytes are what the authenticator's signature covers (after SHA-256).
{
"type": "webauthn.get",
"challenge": "aabbccddeeff...", // base64url
"origin": "https://login.example.com",
"crossOrigin": false
}
Attestation reference#
The attestationObject is a CBOR map keyed by short strings. See technologies / attestation for the full deep dive on each fmt.
Flags — BE / BS / UP / UV#
| Bit | Name | Meaning |
|---|---|---|
| 0x01 | UP | User Present (touch / tap). |
| 0x04 | UV | User Verified (PIN / biometric). |
| 0x08 | BE | Backup Eligible. Set at registration; immutable. |
| 0x10 | BS | Backup State (currently backed up). Mutable. |
| 0x40 | AT | Attested credential data included (registration only). |
| 0x80 | ED | Extension data included. |
Error handling#
Common DOMExceptions during a ceremony:
- NotAllowedError — user cancelled or timed out, or browser refused due to RP ID / origin mismatch.
- InvalidStateError — credential already registered (de-dup mismatch on registration).
- NotSupportedError — none of the requested algorithms are supported by the available authenticator.
- SecurityError — RP ID is not a registrable suffix of the origin.
- ConstraintError — userVerification: "required" but the authenticator can't perform UV.
Found a bug or have a suggestion? File at /contact/. This portal is static and version-pinned; planned migration target is docs.ambimat.com for a future Phase.