Ambimat GroupAmbimatAmbiSecureeSIM InitiativeEngineering BlogAhmedabad · India · Est. 1981
Technology

WebAuthn — a working engineer's reference.

WebAuthn replaces shared-secret passwords with public-key cryptography bound to the relying-party origin. The credential lives on the authenticator (smart card, USB key, platform TPM, secure enclave). The server never sees a secret. Origin binding is what kills the entire phishing class.

Two specs working in concert

WebAuthn is a Web API. CTAP2 is the wire protocol that backs it. WebAuthn is what your relying-party JavaScript calls (navigator.credentials.create() for registration, .get() for authentication). CTAP2 is what the browser speaks to a roaming authenticator over USB-HID, NFC, or BLE. Platform authenticators (Windows Hello, Apple Touch ID, Android Keystore) skip CTAP and go through OS-native APIs — but the WebAuthn surface is identical from the RP's perspective.

Registration ceremony

RP → client1. Issue PublicKeyCredentialCreationOptions

Server generates a 32-byte random challenge, picks an RP ID, sets user.id (opaque), pubKeyCredParams (e.g. ES256, EdDSA), and an attestation policy (none / direct / enterprise).

browser2. Build clientDataJSON

Browser serialises the type, origin, and challenge into a JSON object. The SHA-256 of these bytes is what the authenticator signs.

authenticator3. Generate keypair, sign attestation

Authenticator generates a fresh key, stores private side internally, returns public key inside an attestationObject (CBOR with fmt, attStmt, authData).

RP4. Verify and store

RP verifies clientData, computes SHA-256(rpId), checks rpIdHash, validates attestation if required, persists credentialId + publicKey + signCount.

Use the clientDataJSON decoder and the attestation decoder to walk these structures byte-by-byte during integration.

Authentication ceremony

RP → client1. Issue PublicKeyCredentialRequestOptions

Server generates a fresh challenge, optionally lists allowCredentials (credentialIds it accepts for this user), sets userVerification policy.

browser ↔ authenticator2. Authenticator signs assertion

Authenticator builds authenticatorData (rpIdHash, flags, signCount), signs authData || SHA-256(clientDataJSON), returns the signature plus the credentialId.

RP3. Verify signature

Lookup stored publicKey by credentialId, verify signature, check rpIdHash, verify UP / UV / counter monotonicity.

RP4. Issue session

If everything checks, mint a session. Total round-trip on USB-HID is sub-second; NFC is sub-300ms.

Trust-zone diagram

USER CLIENT NETWORK RELYING PARTY User touch / PIN / bio Authenticator private key SECURE ELEMENT Browser WebAuthn API USER AGENT TLS / HTTPS origin-bound RP front-end JavaScript RP server verifies signature PUBLIC KEY ONLY
User and authenticator are paired physically. The browser mediates between authenticator and the RP. The RP only ever holds a public key. Origin binding inside the browser is what makes phishing structurally impossible.

authenticatorData — the load-bearing structure

authenticatorData is the binary the authenticator builds and signs over. The structure is fixed: rpIdHash(32) || flags(1) || signCount(4), optionally followed by attested-credential data (registration only) and CBOR extensions.

rpIdHash SHA-256(rpId) — anti-phishing anchor flags UP | UV | BE | BS | AT | ED signCount monotonic counter (anti-clone) [attestedCredentialData] aaguid 16-byte make/model id credLen 2 bytes BE credId opaque to RP credentialPublicKey COSE_Key (CBOR map) [extensions] CBOR map (e.g. credProtect, hmac-secret)

The flags byte is where most policy decisions live. UP (User Present) is the touch / tap. UV (User Verified) is local user verification — PIN, biometric, or pattern. BE (Backup Eligible) and BS (Backup State) tell you whether the credential is a syncable passkey. AT indicates attested-credential data is present (registration only). ED indicates extension data follows.

For high-assurance enterprise environments where you want a device-bound credential, require BE = 0. For consumer flows where convenience trumps assurance, accept BE = 1.

RP ID rules — §5.1.3

The RP ID is the scope of a credential. It must be a registrable domain suffix of, or equal to, the origin's effective domain. In practice this lets you set rpId = example.com on a page served from login.example.com — the credential becomes usable across all *.example.com hosts.

  • Browsers reject public-suffix RP IDs (you can't register at com or co.uk).
  • HTTPS is required — localhost is the only HTTP exception (development).
  • Origin verification on the RP must be exact — case-insensitive host match, scheme + port comparison, no substring matching.

Use the RP ID validator to dry-run combinations before shipping.

Attestation, AAGUIDs, and the FIDO MDS

An authenticator can prove what it is via an attestation statement — signed by the manufacturer's root key. Each model has an AAGUID recorded in the FIDO Metadata Service (MDS) BLOB. Consumer apps usually skip attestation (attestationType: 'none'); enterprise apps that require certified hardware verify the attestation against MDS and enforce AAGUID allow-lists.

The MDS BLOB is signed by the FIDO Alliance and updated periodically; production RPs should fetch and verify it on a schedule, not at runtime.

For diagnostic work the AmbiSecure AAGUID lookup covers a curated subset of well-known devices.

Verification checklists

Registration verification (server-side)
  1. Decode clientDataJSON; verify type === "webauthn.create", challenge matches the one your RP issued (single-use), origin is in your allow-list.
  2. Compute SHA-256(rpId) and verify authData.rpIdHash matches.
  3. Verify UP flag is set; verify UV if your policy requires user verification.
  4. If you require an attestation policy stronger than none, verify the attestation signature against authData || SHA-256(clientDataJSON), and validate the x5c chain against your trust anchors and the FIDO MDS BLOB.
  5. Enforce the AAGUID allow-list (enterprise).
  6. Persist credentialId, credentialPublicKey, signCount, and the BE/BS flags for policy use.
Authentication verification (server-side)
  1. Decode clientDataJSON; verify type === "webauthn.get", challenge matches, origin matches.
  2. Look up the stored credentialPublicKey by credentialId.
  3. Compute SHA-256(rpId); verify authData.rpIdHash.
  4. Verify UP set; UV if required.
  5. Verify the signature over authData || SHA-256(clientDataJSON) using the stored public key.
  6. Verify the new signCount > stored signCount (or both 0 — many platform authenticators keep counter at 0).
  7. Update stored signCount; mint session.