Security Engineering · Authentication
The WebAuthn Ceremony and Origin Binding
What the browser signs, what the relying party verifies, and why the challenge and the origin check are what make the credential resistant to relay and phishing.
A password is a secret the user sends to the server. A one-time code is a secret the user reads and sends. Both travel through the same channel the attacker may control, and both are therefore phishable in real time. WebAuthn replaces the shared secret with a key pair generated inside an authenticator, and it makes the browser a participant in the protocol: the browser constructs the signed message, and the message contains the origin of the page that requested it. A credential created for one origin does not produce a signature that verifies for another, and that is the property no shared-secret scheme has.
What the relying party asks for, and what comes back
Registration begins with the server generating a challenge: at least 16 bytes from a cryptographically secure random source, encoded in the options object it sends to the client. Along with the challenge, the server sends its identity as rp.id, a user handle, and a list of acceptable algorithms, typically ES256 and RS256 in COSE algorithm identifiers.
The browser resolves rp.id against the page’s origin. The identifier must be equal to the origin’s effective domain or a registrable suffix of it; a page at https://login.example.com may use example.com as the relying party identifier, but not other.example.com, and never a suffix that is a public suffix such as com. That constraint is enforced in the browser before any authenticator is involved, which is what prevents a page at an attacker-controlled subdomain from minting credentials that the parent domain will accept.
The authenticator generates a key pair scoped to the relying party identifier, keeps the private key, and returns the public key along with an attestation object. The object is CBOR-encoded, per RFC 8949, and contains the authenticator data and, optionally, an attestation statement. The authenticator data is a binary structure: a 32-byte SHA-256 hash of the relying party identifier, a flags byte, a 4-byte signature counter, and then the credential’s identifier and public key for a registration.
The flags byte carries the properties that matter for policy. Bit 0 is user presence, set when the user physically interacted with the authenticator. Bit 2 is user verification, set when the authenticator established the user’s identity locally, by PIN, biometric, or password. Bit 6 is the attested credential data flag, set during registration. Bit 3 indicates backup eligibility and bit 4 indicates whether the credential is currently backed up, which is how a relying party distinguishes a device-bound credential from a synchronised passkey.
The browser assembles the client data, a JSON structure containing the type of operation, the challenge as received, and the origin of the caller, and hashes it with SHA-256. The authenticator signs the concatenation of the authenticator data and that hash. The signature therefore covers three things at once: the relying party identifier, the challenge, and the origin. None of them can be changed without invalidating the signature.
The verification the server must perform
The server’s job is to reconstruct what was signed and check it, in an order that fails closed.
The client data JSON must be parsed and its type must match the operation, webauthn.create for registration or webauthn.get for authentication. The challenge must equal the challenge the server issued, compared as base64url of the exact bytes. The origin must equal the expected origin for the relying party, which is a string comparison against a configured value, not a suffix match. That last point is a recurring implementation defect: accepting any origin that ends with the expected domain accepts https://example.com.attacker.test if the comparison is written carelessly.
The relying party identifier hash in the authenticator data must equal SHA-256 of the configured rp.id. The user presence flag must be set; a credential that reports no user interaction has not been authorised by a person. The user verification flag is checked against policy, which is a decision the relying party makes per operation rather than a property of the protocol.
For registration, the credential identifier must be new to the relying party, and the public key must be parsed from the COSE encoding. For authentication, the signature must verify over the authenticator data concatenated with the client data hash, using the stored public key and the algorithm recorded at registration.
The signature counter is the last check and the most misunderstood. It is a 32-bit value the authenticator increments on each assertion, and the relying party stores the last seen value. A counter that fails to increase suggests a cloned authenticator. The check is worth performing and worth treating as a signal rather than a verdict, because many authenticators do not implement a counter at all and always report zero, which is legal under the specification. A verifier that rejects a non-increasing counter outright will lock out every user of such an authenticator after the first login. The correct handling is to skip the check when the stored value and the presented value are both zero, and to treat a genuine decrease as a risk signal.
Why the challenge and the origin are the security
The challenge defeats replay. It is generated per ceremony, single-use, and verified byte-for-byte. An attacker who captures a complete assertion cannot present it again, because the server will not have issued the same challenge twice. The challenge does not need to be secret; it needs to be unpredictable and bound to the session that requested it, so that an assertion produced in one session cannot be redeemed in another.
The origin defeats phishing. A user who is tricked into visiting https://examp1e.com and typing a password has given the attacker the password. The same user visiting that site with a WebAuthn credential produces nothing useful: the browser computes the client data hash over the phishing origin, the authenticator signs it because the site asked correctly and the user consented, and the signature fails verification at the real relying party because the origin string is wrong. The private key never left the authenticator, and the assertion is bound to a domain the attacker does not control.
The relying party identifier hash adds a second layer. An authenticator stores credentials per relying party identifier, and it will not produce an assertion for a credential scoped to example.com when the requesting origin resolves to a different identifier. Even if a browser were subverted, or a user manually approved a request on a device with no display, the credential is not reachable from a foreign origin.
This is why the protocol is described as origin-bound rather than merely public-key. Public-key authentication without origin binding, such as a raw signature over a server-supplied nonce, is still phishable: a proxy can obtain a signature over the nonce the real server will accept. The origin string inside the signed data is what makes the relay fail.
Attestation, and when it is worth the cost
Attestation is a statement, signed by the authenticator, about how the key was created and stored. Its purpose is to let a relying party apply a policy: only accept credentials from authenticators that are hardware-backed, or from a specific vendor, or that meet a certification level.
The costs are concrete. Attestation certificates can be unique per authenticator, which makes them a tracking identifier if the relying party stores them and they leak. The privacy-preserving alternative is a batch attestation certificate shared across many devices, which weakens the identification but preserves the trust signal. Apple’s and Google’s platforms offer attestation that is anonymous in this sense, and many deployments choose none for consumer registration and rely on the protocol’s other properties.
For an enterprise that must prove credentials are hardware-bound, attestation is the only mechanism that carries that claim. For a consumer service, requiring it excludes authenticators that do not implement it, which is most platform authenticators in their default configuration, and gains little: the security of the credential comes from the key never leaving the authenticator, which is true whether or not the authenticator is willing to attest to it.
The trust chain is the other cost. Verifying an attestation statement means validating a certificate chain to a root the relying party trusts, checking the certificate’s authorisation for attestation, and checking that the credential’s key meets the claimed protection level. That is a metadata service, a root store, and a policy engine, and it has to be maintained as vendors rotate certificates.
Registration and authentication as separate ceremonies
The two ceremonies differ in what they establish and in what they can go wrong.
Registration creates a credential and must be protected against an attacker who completes a registration for a user who did not. The user handle and the challenge must be bound to the authenticated account, which means registration for an existing account happens inside an authenticated session or behind a fresh proof of the existing factor. A registration endpoint that accepts an arbitrary user handle and creates a credential for it is an account takeover primitive.
Registration also has to handle the case where the authenticator already holds a credential for the relying party. The options object can carry an exclusion list of credential identifiers, and the browser passes it to the authenticator, which refuses to create a duplicate and reports the existing credential instead. An application that omits the exclusion list lets a user create several credentials for the same account on the same device without noticing, and one that treats the resulting error as a generic failure leaves the user with no guidance when it fires.
Authentication is the ceremony with the counter and the signature. It returns an assertion that identifies which credential was used, so the server must look up the stored public key by credential identifier and must not trust any user identifier supplied by the client. The user handle in the response is a convenience for discoverable credentials and must be checked against the credential’s owner, not used to select it.
Discoverable credentials, where the authenticator stores the user handle and the browser can offer an account picker without the server sending an allow-list, change the flow: the server issues a challenge without knowing who is authenticating, receives an assertion, looks up the credential by identifier, and learns the user from the stored record. That inverts the usual order and makes the credential identifier the primary key of the authentication path.
What remains
WebAuthn does not protect against an attacker who controls the endpoint. Malware that can drive the browser through the same API the user’s page uses inherits the user’s authority, because the browser’s origin check is satisfied. Nor does it protect a user who is persuaded to approve a ceremony they did not intend; user presence is a consent signal, not a statement about intent.
It also does not remove the need for a recovery path, and that path is where most deployments lose the property they just gained. A fallback to a password reset over email means an attacker who controls the email account bypasses the credential entirely. The question of whether that fallback is acceptable is the same one that governs factor independence, and answering it honestly is harder than deploying the ceremony.
For the server side, the credential is a public key and a signature counter, both of which are safe to store in the clear and safe to back up. That is a genuine operational advantage over every shared-secret factor, whose storage is itself a liability, as discussed in password hashing. The one thing that must not be lost is the binding between the credential identifier and the account, because that binding is the authentication decision.
References
- Web Authentication: An API for accessing Public Key Credentials Level 2 — W3C
- Client to Authenticator Protocol (CTAP) 2.1 — FIDO Alliance, 2021
- CBOR: Concise Binary Object Representation — IETF