FIDO2 is two protocols that let a website sign a user in with a public key credential instead of a password. W3C WebAuthn runs between the website (the relying party) and the browser. The FIDO Alliance Client to Authenticator Protocol (CTAP) carries the browser's commands to the authenticator over USB, NFC, Bluetooth Low Energy or hybrid.
This post follows a sign in request all the way down: the WebAuthn authentication ceremony field by field, then the CTAP2 authenticatorGetAssertion command and how each transport carries it. If you want the registration ceremony, passkey sync or attestation in depth, we have already covered them in how passkeys work and why they resist phishing, the WebAuthn registration ceremony and what your server must check, synced versus device bound passkeys and the backup flags and what attestation means. This post stays on the protocol split, the transports and the authentication ceremony.
Key takeaways
- FIDO2 is two specifications with two owners. W3C WebAuthn Level 3 became a W3C Recommendation on 25 August 2026, following Level 2 (8 April 2021) and Level 1 (4 March 2019). FIDO CTAP 2.3 became a FIDO Alliance Proposed Standard on 26 February 2026, following CTAP 2.2 (14 July 2025), CTAP 2.1 (15 June 2021, errata 21 June 2022) and CTAP 2.0 (30 January 2019).
- The website never talks to the authenticator. The relying party talks WebAuthn to the browser; the client platform talks CTAP, in CBOR, to the authenticator. Origin checks happen in the client.
- In an authentication ceremony the authenticator signs the concatenation of
authenticatorDataand the SHA-256 hash ofclientDataJSON. The server must check type, challenge, origin, the RP ID hash, the user present and user verified flags, the signature, and then decide what to do with the signature counter. - CTAP runs over four transport families: USB HID (64 byte packets, FIDO usage page 0xF1D0), NFC (ISO 7816 applet with AID A0000006472F0001), Bluetooth Low Energy (GATT service 0xFFFD, encrypted link required) and hybrid (QR code, a BLE proximity advert and an encrypted tunnel, added in CTAP 2.2).
- NIST SP 800-63B-4 (July 2025) names WebAuthn as an example of phishing resistance through "verifier name binding", requires verifiers at AAL2 to offer a phishing resistant option, and bars syncable authenticators from AAL3. CISA calls FIDO/WebAuthn the "gold standard" of MFA.
- Most real FIDO2 failures are relying party failures: loose origin checks, client trusted challenges, unchecked UV flags and weak recovery paths.
What FIDO2 is for, where it sits and which standards define it
Passwords and one time codes share one flaw: a secret typed into a convincing fake page works on the real site. FIDO2 removes the typed secret. The authenticator holds a private key created for one relying party and only signs data that includes that relying party's identity, as checked by the browser.
FIDO2 is an application layer protocol pair, but the two halves run in very different places.
| Piece | Who talks | Encoding | Runs over | Owner and current document |
|---|---|---|---|---|
| WebAuthn | Relying party web application and the WebAuthn client (browser) | JavaScript objects; clientDataJSON is JSON; authenticatorData is binary | HTTPS between server and page; JavaScript API inside the browser | W3C, Web Authentication Level 3, Recommendation, 25 August 2026 |
| CTAP2 | Client platform (browser or operating system) and the authenticator | CTAP2 canonical CBOR | USB HID, NFC, BLE, hybrid tunnel | FIDO Alliance, CTAP 2.3, Proposed Standard, 26 February 2026 |
| CTAP1 / U2F | Client platform and older second factor authenticators | U2F binary messages | Same transports | FIDO Alliance; still supported for interoperability, version string U2F_V2 |
| Metadata Service | Relying party and the FIDO Alliance | Signed JSON (a JWT style BLOB) | HTTPS | FIDO Alliance, MDS 3.1.1, Proposed Standard, 5 January 2026 |
The WebAuthn abstract describes the goal as "strong, attested, scoped, public key-based credentials". The CTAP abstract describes an application layer protocol between a roaming authenticator and a client platform, plus bindings to several transports. When the authenticator is built into the device (a platform authenticator), the browser reaches it through operating system interfaces instead of CTAP.
FIDO2 has no IANA port. WebAuthn rides on your existing HTTPS login page; CTAP rides on local transports, except hybrid, which uses a WebSocket tunnel over HTTPS. The identifiers that matter are the USB HID usage page, the NFC application identifier and the Bluetooth service UUID, plus the webauthn well known URI (RFC 8615) that Level 3 uses for related origin requests.
How the WebAuthn authentication ceremony works
Registration creates the credential and stores its public key on the server; we covered it step by step in the registration ceremony post. Authentication is the ceremony that runs every time a user signs in, and it is where most production bugs live.
The message flow
Relying party server Browser (WebAuthn client) Authenticator
| | |
|-- options: challenge, rpId, | |
| allowCredentials, UV pref->| |
| | check rpId vs page origin |
| | build clientDataJSON: |
| | type, challenge, origin |
| | hash it (SHA-256) |
| |-- CTAP authenticatorGetAssertion
| | rpId, clientDataHash, |
| | allowList, options -------->|
| | | find credential for rpId
| | | user presence (touch)
| | | user verification (PIN/bio)
| | | signCount += n
| | | sign(authData + clientDataHash)
| |<-- credential, authData, |
| | signature, user handle ----|
|<-- id, clientDataJSON, | |
| authenticatorData, | |
| signature, userHandle ----| |
| verify (section 7.2 steps) | |
The three blobs the server receives
clientDataJSON is built by the browser, not the authenticator. For an authentication ceremony it contains:
| Member | Meaning | What the server checks |
|---|---|---|
type | The ceremony type | Must equal webauthn.get |
challenge | Base64url of the challenge the server issued | Must equal the stored challenge for this ceremony |
origin | The origin of the page that called the API, as the browser saw it | Must be an origin the relying party expects |
crossOrigin | True when called from a cross origin iframe | If true, the relying party must expect iframe use |
topOrigin | Level 3: the top level origin when embedded | If present, must match a page the relying party expects to be framed in |
The authenticator never sees this JSON. It sees only its SHA-256 hash, clientDataHash, and signs over it. That is how the browser's view of the origin ends up inside the signature.
authenticatorData is binary, built and signed by the authenticator:
| Field | Size | Meaning |
|---|---|---|
rpIdHash | 32 bytes | SHA-256 of the RP ID the credential is scoped to |
flags | 1 byte | Bit 0 UP (user present), bit 1 reserved, bit 2 UV (user verified), bit 3 BE (backup eligible), bit 4 BS (backup state), bit 5 reserved, bit 6 AT (attested credential data included), bit 7 ED (extension data included) |
signCount | 4 bytes, big endian | Signature counter; zero if the authenticator does not implement one |
attestedCredentialData | Variable | Present at registration (AT set); normally absent in assertions |
extensions | Variable, CBOR | Present when ED is set |
signature is the authenticator's signature, with the credential private key, over authenticatorData concatenated with clientDataHash. WebAuthn notes this format is compatible with signatures generated by FIDO U2F authenticators.
The response also carries the credential ID (which is not signed; WebAuthn section 13.1 explains why that is harmless) and an optional userHandle, the opaque user ID set at registration.
What the server must verify
WebAuthn Level 3 section 7.2 lists the relying party's steps. Condensed into the order that matters for security:
- If you sent a non empty
allowCredentials, check the returned credential ID is one of them. - Find the credential record. If you identified the user before the ceremony (username or cookie), the credential must belong to that account, and a returned
userHandlemust match it. If you did not (a discoverable credential, often called usernameless sign in),userHandlemust be present and must identify an account that owns this credential. - Decode
clientDataJSONas UTF-8 and parse it. Checktypeiswebauthn.get. - Check
challengeequals the base64url encoding of the challenge you issued. - Check
originis one you expect, and handlecrossOriginandtopOriginif present. - Check
rpIdHashinauthenticatorDataequals SHA-256 of your RP ID. - Check the UP flag is set.
- If your policy requires user verification, check the UV flag is set. The specification says user verification should be required if, and only if, you set
userVerificationtorequired. - If BE is not set, check BS is not set. If you use backup state in policy, compare BE and BS with what you stored.
- Hash
clientDataJSONwith SHA-256 and verify the signature overauthenticatorDataplus that hash, using the stored public key. - Apply signature counter logic (below), process any extension outputs you asked for, and be ready for extension outputs you did not ask for.
- Update the stored
signCountand backup state.
Step 4 is replay protection. Steps 5 and 6 are phishing resistance as seen from the server. Step 8 is the difference between "someone touched a key" and "the account owner unlocked the key".
The signature counter, read correctly
Authenticators should keep a signature counter, per credential or global, and increment it on every successful assertion. If either the stored or the received value is non zero and the new value is not greater than the stored one, WebAuthn says this is "a signal, but not proof" of a cloned authenticator. It may also mean a malfunctioning authenticator or assertions processed out of order. The specification leaves the response to the relying party's risk policy.
Authenticators without a counter leave it at zero, and the check only runs when one side is non zero, so a constant zero is legitimate. Synced credentials cannot keep one strictly increasing counter across devices, which is one reason many report zero. Treat a regression as a risk signal for a device bound credential, not a hard failure for every credential.
How CTAP2 carries the request to the authenticator
When the authenticator is roaming (a hardware key or a phone acting as one), the browser or operating system turns the WebAuthn request into a CTAP2 command.
The CTAP2 command set
Every CTAP2 request is a one byte command code followed by a CBOR map with small integer keys, in CTAP2 canonical CBOR. The responses are a one byte status code plus a CBOR map. CTAP 2.3 defines:
| Code | Command | Purpose |
|---|---|---|
| 0x01 | authenticatorMakeCredential | Create a credential (registration) |
| 0x02 | authenticatorGetAssertion | Produce a signature (authentication) |
| 0x04 | authenticatorGetInfo | Report versions, extensions, options, limits such as maxMsgSize |
| 0x06 | authenticatorClientPIN | Set or change the PIN, obtain a pinUvAuthToken |
| 0x07 | authenticatorReset | Wipe all credentials and the PIN |
| 0x08 | authenticatorGetNextAssertion | Fetch further assertions when several credentials match |
| 0x09 | authenticatorBioEnrollment | Manage fingerprint templates (added in 2.1) |
| 0x0A | authenticatorCredentialManagement | List and delete discoverable credentials (added in 2.1) |
| 0x0B | authenticatorSelection | Ask the user to pick one of several connected authenticators (added in 2.1) |
| 0x0C | authenticatorLargeBlobs | Read and write per credential blobs (added in 2.1) |
| 0x0D | authenticatorConfig | Enterprise attestation, always require UV, minimum PIN length and similar settings (added in 2.1) |
authenticatorGetAssertion carries rpId (key 0x01), clientDataHash (0x02), an optional allowList (0x03), extensions (0x04), options such as up and uv (0x05), and pinUvAuthParam with pinUvAuthProtocol (0x06 and 0x07) when the platform has obtained a PIN or UV token. The response returns credential (0x01), authData (0x02), signature (0x03), user (0x04), and, when several discoverable credentials match, numberOfCredentials (0x05).
The pinUvAuthParam matters for security. When the user types a PIN into the browser, the platform and authenticator first agree a shared secret (a key agreement inside authenticatorClientPIN), the PIN travels encrypted, and the platform receives a pinUvAuthToken. It then proves it holds that token by computing an authentication code over clientDataHash. CTAP 2.1 added a second PIN/UV auth protocol and permissions on the token, so a token obtained for one purpose cannot be reused for another.
authenticatorGetInfo and version strings
The versions member of authenticatorGetInfo tells the platform what the authenticator speaks. CTAP 2.3 lists FIDO_2_3, FIDO_2_1, FIDO_2_0, FIDO_2_1_PRE (preview features from before 2.1 was final) and U2F_V2 (CTAP1). It also notes that the string FIDO_2_2 was never defined for CTAP 2.2 and must not appear. If you inventory authenticators by version string, a 2.2 device will report FIDO_2_1.
USB HID
USB is the original CTAP transport, and CTAPHID is a framing layer on top of USB human interface device reports. The FIDO Alliance has its own HID usage page, 0xF1D0, which is how the operating system recognises an authenticator without a driver.
- Channels. Each application gets a 32 bit channel ID allocated by the authenticator. Channel 0 is reserved and 0xFFFFFFFF is the broadcast channel used to allocate one with
CTAPHID_INIT. - Packets. Reports are fixed size, 64 bytes for full speed devices. Continuation packets carry the channel ID, a sequence number from 0x00 to 0x7F and data, so the maximum message is 7,609 bytes.
- Commands.
CTAPHID_MSG(0x03) carries CTAP1/U2F messages,CTAPHID_CBOR(0x10) carries CTAP2 messages,CTAPHID_INIT(0x06),CTAPHID_PING(0x01),CTAPHID_CANCEL(0x11),CTAPHID_ERROR(0x3F) andCTAPHID_KEEPALIVE(0x3B) are mandatory;CTAPHID_WINK(0x08) andCTAPHID_LOCK(0x04) are optional.
| Initialization packet offset | Length | Field |
|---|---|---|
| 0 | 4 | CID, channel identifier |
| 4 | 1 | CMD, command identifier with bit 7 set |
| 5 | 1 | BCNTH, high byte of payload length |
| 6 | 1 | BCNTL, low byte of payload length |
| 7 | packet size minus 7 | DATA |
CTAP notes the protocol relies on each client application behaving "politely": a misbehaving application on the same host can disrupt other channels, an availability concern rather than a key exposure one.
NFC
Over NFC the authenticator is a smart card style applet speaking ISO 7816 APDUs over ISO 14443. The client must explicitly select the FIDO applet before any command, with AID RID 0xA000000647 and PIX 0x2F0001 (A0000006472F0001), using the standard SELECT command (CLA 0x00, INS 0xA4, P1 0x04, P2 0x00). CTAP2 messages travel in NFCCTAP_MSG (0x10), with NFCCTAP_GETRESPONSE (0x11) for responses that need polling or fragmentation.
CTAP section 11.1 adds a rule that matters for multi application cards: the authenticator shall not allow the FIDO applet to be implicitly selected, and FIDO state (credentials, counters, PINs) must not be observable or alterable through non FIDO interfaces.
Bluetooth Low Energy
Over BLE the authenticator exposes a GATT primary service with UUID 0xFFFD. Requests and responses are framed with a command byte and length, and fragmented to fit the negotiated attribute size.
The BLE binding is the most explicit about its own risks. CTAP says BLE "may be subject to monitoring, injection, and other network-level attacks", requires both sides to create and use a long term link key, requires all communication to be encrypted, and requires the authenticator to enforce Security Mode 1 Level 2 or Level 3 before any FIDO message is exchanged. It also notes BLE has poor ranging, so pairing is the only mechanism that ensures the client talks to the intended authenticator, and that manufacturers should tell users to avoid pairing in public places such as cafes or train stations. Finally, pairing is often system wide, so any application on a paired computer might be able to reach the authenticator.
Hybrid transport
Hybrid, added to CTAP in version 2.2 and to the WebAuthn AuthenticatorTransport enumeration as "hybrid" in Level 3, is how a phone acts as the authenticator for a nearby laptop. CTAP describes it as decoupling the proof that the devices are physically close from the transport of the messages.
Laptop (client platform) Phone (authenticator) Tunnel service
| show QR code: | |
| FIDO:/ + digit encoded CBOR | |
| (P-256 public key, 16 byte | |
| secret, flow hint, channels) | |
| | scan QR |
| |-- open tunnel (WebSocket) ---->|
|<----- BLE advert (encrypted, proves the phone is nearby) --------- |
|-- connect to same tunnel --------------------------------------> |
|<========= Noise handshake (KNpsk0), then encrypted CTAP ==========>|
| authenticatorGetAssertion ... | |
The QR code carries a compressed P-256 public key, a 16 byte random secret, the number of known tunnel server domains, a flow hint (ga for get assertion, mc for make credential) and optional fields. The phone must prove proximity with a BLE advert encrypted under keys derived from that secret, so a QR code relayed to someone far away is not enough. Later sign ins can use "state assisted" transactions: the laptop keeps linking information from a previous QR exchange and contacts the phone through the cached tunnel service without a new QR code; that handshake uses the Noise pattern NKpsk0.
WebAuthn section 13.2 is the reason proximity is designed in. An authenticator that can be invoked remotely loses part of its strength as a "something you have" factor, because "users can be tricked into authorizing remotely initiated malicious requests". Hybrid keeps the remote convenience but adds a local radio check.
Versions and history
| Document | Date | Status | What changed |
|---|---|---|---|
| WebAuthn Level 1 | 4 March 2019 | W3C Recommendation | First standard browser API for public key credentials |
| CTAP 2.0 | 30 January 2019 | FIDO Proposed Standard | CBOR command set (make credential, get assertion, get info, client PIN, reset, get next assertion), USB, NFC and BLE bindings, CTAP1/U2F interoperation, hmac-secret extension |
| CTAP 2.1 | 15 June 2021; errata 21 June 2022 | FIDO Proposed Standard | PIN/UV auth protocol two and token permissions, credential management, bio enrollment, large blobs, authenticator selection and config, credProtect, always require UV, enterprise attestation, minimum PIN length |
| WebAuthn Level 2 | 8 April 2021 | W3C Recommendation | Matches CTAP 2.1 features, adds credProps and largeBlob extensions and the crossOrigin member for use in cross origin iframes |
| CTAP 2.2 | 14 July 2025 | FIDO Proposed Standard | Hybrid transport, JSON based messages, PIN complexity policy, persistent PIN/UV auth token for read only credential management, thirdPartyPayment, hmac-secret-mc |
| MDS 3.1.1 | 5 January 2026 | FIDO Proposed Standard | Current Metadata Service revision (3.0 was May 2021, 3.1 May 2025) |
| CTAP 2.3 | 26 February 2026 | FIDO Proposed Standard | Current revision; adds long touch for reset among other changes; version string FIDO_2_3 |
| WebAuthn Level 3 | 25 August 2026 (Candidate Recommendation 26 May 2026) | W3C Recommendation | Backup eligibility and state flags, hybrid and smart-card transports, topOrigin, related origin requests, client capabilities, signal methods for credential changes, JSON serialisation helpers, user agent hints, prf extension |
A CTAP 2.3.1 Working Draft (29 May 2026) is also listed on the FIDO Alliance specifications page. Working drafts are not standards; build against 2.3.
In practice the deployed base is mixed. Older keys speak only CTAP1/U2F, which CTAP 2.x can still drive; many hardware keys report FIDO_2_0 or FIDO_2_1; phones and laptops gain new features with operating system updates. That is why WebAuthn's verification steps tolerate zero counters and missing or unsolicited extensions.
Security properties: what FIDO2 guarantees and what it does not
What it guarantees, when the relying party follows section 7
- Phishing resistance through origin binding. The browser refuses an RP ID that is not the page's effective domain or a registrable suffix of it, the authenticator scopes credentials to the RP ID and signs its hash, and the browser writes the real origin into
clientDataJSON, which is signed by hash. A lookalike domain cannot obtain a signature the real site will accept. NIST SP 800-63B-4 calls this "verifier name binding" and gives WebAuthn as its example; it also notes that channel binding (client authenticated TLS is its example) is stronger because it does not rely on the verifier's certificate. - No shared secret on the server. The server stores public keys, so a database leak exposes no reusable credential.
- Replay resistance, if challenges are random, at least 16 bytes, stored server side and single use.
- Man in the middle resistance for authentication, assuming registration completed securely.
- User presence and, optionally, user verification, reported in signed flags.
What it does not guarantee
- Registration integrity. WebAuthn section 13.4.4 says an attestation statement "provides no means" for the relying party to verify that an attestation object came from the authenticator the user intended rather than a man in the middle, for example injected script. TLS and page integrity protect registration.
- Session security. FIDO2 authenticates a sign in. The session cookie or token you issue afterwards is ordinary bearer state; if it is stolen, FIDO2 does not help.
- Protection from code running on your own origin. Section 13.4.8: malicious code on an origin within the credential's scope "has the potential to invalidate any and all security guarantees".
- Account recovery. WebAuthn defines no backup protocol and recommends registering several credentials. Whatever you build for lost authenticators sits outside the protocol.
- Proof of a specific device. Without attestation and policy, you know a key pair signed, not what hardware holds it.
Weakness classes, described as concepts
These come from the specifications' own security considerations and from the verification steps that relying parties most often get wrong.
Relying party misvalidation
The protocol's guarantees exist only if the server performs the checks. Common gaps: comparing the challenge against a value sent back by the client rather than one stored server side (section 13.4.3 says tolerating a mismatch "will compromise the security of the protocol"); not checking origin at all, or checking it with a loose substring match; not checking rpIdHash; not checking type, so a registration response could be confused with an assertion; and verifying the signature over the wrong bytes. Each of these turns a phishing resistant protocol back into something closer to a bearer token.
Over broad origin and subdomain acceptance
WebAuthn allows a relying party to accept a list of origins or any subdomain of the RP ID, but warns that user content hosted on a subdomain can then exercise credentials scoped to the parent. Its default advice is that the relying party should not allow a subdomain origin, and must not serve untrusted code on any subdomain it does allow. Related origin requests in Level 3 are a safer way to span several domains, because the list is explicit and published at /.well-known/webauthn over HTTPS.
Ignoring user verification
If policy requires that the account owner, not just someone holding the key, approved the sign in, the server must request userVerification: "required" and then check the UV flag. Requesting UV in options but never checking the flag means a client that skipped verification is accepted anyway.
Account recovery bypass
The strongest login is only as strong as the weakest way to get into the account. If losing a passkey drops the user back to an emailed link, an SMS code or a help desk password reset, an attacker targets that path instead. CISA's fact sheet ranks SMS and voice as the weakest forms of MFA, vulnerable to phishing, SS7 exploitation and SIM swap. NIST SP 800-63B-4 likewise requires a way to invalidate a lost authenticator immediately, which is part of the same recovery design.
Unprotected account detection
WebAuthn section 13.4.7 notes that if a site answers a username with a non empty allowCredentials for some accounts and a password prompt for others, an attacker learns which accounts are protected and which are not. Discoverable credentials, or uniform responses, close that signal.
Attestation misunderstandings
Attestation tells you, at registration, what kind of authenticator created the credential, if the authenticator and client provide it. It does not prove the registration was not intercepted, it is often absent ("none") for consumer passkeys, and its trust chain can be revoked. Section 13.4.5 recommends remembering the intermediate attestation CA used at registration so credentials registered after a CA compromise can be downgraded. The FIDO Metadata Service publishes status reports per authenticator model, including ATTESTATION_KEY_COMPROMISE, USER_VERIFICATION_BYPASS, USER_KEY_REMOTE_COMPROMISE, USER_KEY_PHYSICAL_COMPROMISE and REVOKED, plus certification levels. Relying parties that require attestation and then never refresh the metadata are enforcing a stale list.
Transport level concerns
BLE can be monitored and injected, which is why CTAP mandates an encrypted, bonded link; pairing in public weakens assurance about which device you paired. Remote or mediated authenticators move scope enforcement to whoever mediates them (section 13.2).
Embedding and clickjacking
Section 13.4.2 warns that WebAuthn in cross origin iframes may expose users to UI redressing. Level 3's topOrigin lets the relying party reject unexpected embedders.
How to verify relying party configuration in your own deployment
Only run these checks against systems you own or are authorised to test.
Step 1: write down the four values your server enforces
Every relying party has, explicitly or buried in library defaults, four settings. Find them in your configuration and make sure they are deliberate.
{
"rpId": "example.com",
"allowedOrigins": ["https://example.com", "https://login.example.com"],
"userVerification": "required",
"challengeBytes": 32
}
Check that rpId is the registrable domain you intend and will not change (changing it orphans every credential); that allowedOrigins is an explicit list with no origin serving user content; that the code checks the UV flag when userVerification is required; and that challenges are at least 16 random bytes stored server side with an expiry.
Step 2: check the related origins document, if you use one
If your relying party spans several domains with related origin requests, the document must be served over HTTPS at the RP ID with content type application/json, a 200 status and an origins array. The flags below are from the curl manual: -sS for silent output that still shows errors, -D - to print response headers, and --proto =https to refuse anything but HTTPS.
curl -sS -D - --proto =https https://example.com/.well-known/webauthn
Confirm the content-type header is application/json, that every origin listed is one you control, and that it matches the allowedOrigins list your server enforces.
Step 3: inventory the authenticators you issue
If your organisation issues roaming authenticators, the open source libfido2 command line tools report what each one supports. From the fido2-token manual page:
# List authenticators the operating system can see
fido2-token -L
# Show versions, extensions, options and PIN/UV settings for one device
fido2-token -I /dev/hidraw0
# Show resident (discoverable) credential metadata; prompts for the PIN
fido2-token -I -c /dev/hidraw0
# List relying parties with discoverable credentials on the device; prompts for the PIN
fido2-token -L -r /dev/hidraw0
The -I output includes the versions list from authenticatorGetInfo, the supported extensions and options. The same manual documents -S -l to set a minimum PIN length and -S -u to turn on CTAP 2.1 "user verification always". Note that fido2-token -R resets a device without asking for confirmation, so keep it out of scripts that touch user keys.
Step 4: test the negative cases in your own test suite
Keep automated tests of your own verification code, with recorded fixtures, that must each fail: a wrong type, a reused challenge, an origin not in your list, a subdomain you do not allow, a wrong rpIdHash, UV clear when policy requires it, BS set with BE clear, and a signature over altered authenticatorData. These are unit tests of your own code, not attacks on anyone else's.
Step 5: check the recovery and fallback paths
List every way into an account without a passkey (recovery codes, email links, SMS, support resets) and decide whether each is acceptable for the accounts it protects. A fallback that is not phishing resistant undoes the strong factor for anyone who can trigger it.
Step 6: keep metadata and libraries current
If you require attestation, refresh the FIDO Metadata Service BLOB before its nextUpdate date and act on status reports. And keep the server side WebAuthn library current: CBOR, COSE key and signature parsing are the code paths that handle attacker supplied bytes.
FIDO2 vs TOTP vs push MFA vs smart cards
| Method | How it proves possession | Phishing resistant | Replay protection | Main operational cost | Fits best |
|---|---|---|---|---|---|
| FIDO2 / WebAuthn passkeys | Signature from a key scoped to the RP ID, origin checked by the browser | Yes (verifier name binding, per NIST) | Server challenge | Recovery design, library upkeep, user enrolment of a second credential | Web and app sign in for workforce and customers |
| FIDO2 with device bound hardware keys and attestation | As above, key non exportable, model attested | Yes; can meet AAL3 when non exportable | Server challenge | Buying, issuing and replacing keys; metadata policy | Administrators, high value accounts, regulated workforce |
| TOTP authenticator app | Shared secret generates a time based code | No; codes can be relayed by a fake page | Time window only | Secret stored on server; resets | Legacy systems that cannot do WebAuthn |
| Push approval (with number matching) | Approve a prompt on an enrolled phone | No; CISA lists it as phishable, though number matching resists push bombing | Server side | Push fatigue handling, phone enrolment | Interim step while moving to FIDO2 |
| SMS or voice code | Code sent to a phone number | No | Short validity | SIM swap and SS7 exposure (CISA) | Only where nothing else is possible |
| Smart card (PKI, PIV style) | Private key on a card signs a challenge, often via client certificate TLS | Yes (CISA lists PKI based MFA as phishing resistant; NIST cites client authenticated TLS as channel binding) | Protocol nonce | Certificate lifecycle, readers, middleware | Government and enterprises already running PKI |
FIDO2 and smart cards are the two phishing resistant options CISA names. Smart cards bring a certificate authority, revocation and directory integration; FIDO2 credentials are per site key pairs with no user side PKI. Many organisations run both.
Common mistakes
- Treating FIDO2 as a browser feature only. The browser checks the origin; the server does every other check, and a skipped step is invisible to users.
- Accepting any subdomain as an origin. Convenient during development, dangerous in production if any subdomain hosts user content or third party code.
- Validating the challenge from the client's own copy. The challenge must come from server side storage, be single use and expire.
- Assuming UV happened. Requesting
userVerification: "preferred"and then assuming a PIN or biometric was used. Check the UV flag when it matters. - Rejecting every zero signature counter. Many authenticators, including synced ones, legitimately report zero.
- Changing the RP ID during a domain migration. Credentials are scoped to the RP ID; change it and users cannot sign in. Related origin requests exist for multi domain setups.
- Requiring attestation and never refreshing metadata. A frozen trust list misses revocations and compromise reports.
- Leaving SMS or email as a silent fallback. Attackers go around the strong factor rather than through it.
- Registering only one credential per user. WebAuthn recommends several, bound to different authenticators via
excludeCredentials.
FAQ
What is the difference between FIDO2, WebAuthn and CTAP?
FIDO2 is the umbrella term for two specifications. WebAuthn (W3C) is the browser API and the ceremony between a website and the browser. CTAP (FIDO Alliance) is the protocol between the browser or operating system and an external authenticator such as a hardware key or a phone.
What port does FIDO2 use?
None of its own. WebAuthn runs inside your existing HTTPS login page. CTAP runs over USB HID, NFC and Bluetooth Low Energy locally, and the hybrid transport uses a WebSocket tunnel over HTTPS plus a BLE proximity check.
Is FIDO2 encrypted?
FIDO2 relies on signatures rather than encryption for its core guarantee. The browser to server leg uses your TLS. CTAP over BLE must use an encrypted, bonded link, the hybrid tunnel is encrypted with a Noise handshake, and PINs travel encrypted under a key agreed with the authenticator. CTAP does not add its own encryption to USB HID or NFC framing; the values that need secrecy, such as the PIN, are protected inside the CTAP commands, and signatures carry no reusable secret.
How does WebAuthn prevent phishing?
The browser writes the real page origin into clientDataJSON and only allows an RP ID that matches the page's domain. The authenticator signs a hash of that data along with a hash of the RP ID. A credential registered for the real site cannot be used by a lookalike site, and a signature produced for the lookalike will fail the real server's origin and RP ID checks.
What is CTAP2?
The CBOR based command protocol, first published as CTAP 2.0 in January 2019, that succeeded CTAP1/U2F. The current revision is CTAP 2.3 (26 February 2026).
What is the hybrid transport?
Hybrid lets a phone act as an authenticator for another device. The computer shows a QR code, the phone scans it, the phone proves it is physically nearby with an encrypted Bluetooth advert, and the two exchange CTAP messages through an encrypted tunnel. It was added in CTAP 2.2.
What is signCount in WebAuthn?
A counter the authenticator increments on each signature. If a new value is not greater than the stored one, the relying party has a signal, not proof, that the authenticator may have been cloned. Authenticators without a counter, including many synced credentials, always return zero.
Is FIDO2 better than TOTP?
For phishing, yes. NIST SP 800-63B-4 says manually entered outputs such as one time codes are not phishing resistant, and CISA ranks app based codes below FIDO/WebAuthn.
Can passkeys meet NIST AAL3?
Device bound credentials with non exportable keys can, subject to the other AAL3 requirements. NIST SP 800-63B-4 says syncable authenticators require exportable keys and therefore shall not be used at AAL3; it covers their use at AAL2 in Appendix B.
Where Safeguard fits
Safeguard is not a relying party, authenticator or identity provider, and it does not inspect WebAuthn or CTAP traffic. Its connection is the code that implements FIDO2 inside your applications.
Your relying party rests on a WebAuthn server library and the CBOR, COSE and cryptography packages beneath it. Safeguard's software composition analysis, generally available, builds a full transitive dependency inventory that includes those libraries, and SBOM generation and analysis, also generally available, records them in SPDX or CycloneDX so you can answer "which services verify WebAuthn with which library version" in one query. Vulnerability management with EPSS and KEV enrichment and reachability analysis, both generally available, show whether a reported flaw sits on a code path your login service calls. First party DAST, generally available, tests the running HTTP and API endpoints of login services you own and have verified. Policies and gates at build, CI and release, generally available, can stop a build that pulls in a known vulnerable version of an authentication library. Our post on using SBOMs to find affected products fast shows that workflow end to end.
Fix it without waiting for upstream
FIDO2 itself is a pair of specifications, but the code that verifies an assertion is an open source library your login service depends on, along with CBOR parsers, COSE key handling and crypto packages. When one of those ships a flaw, the fix path is the same as for any other dependency.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show dependency, SBOM and remediation workflows, not authentication or authenticator management. You can download Safeguard and follow the CLI docs to try them on your own projects.
This demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. Pointed at the repository or container image of your login service, the same scan lists the WebAuthn, CBOR and crypto libraries inside it with their versions and known vulnerabilities.
Click through it at your own pace, or open it full screen.
This demo is a real CLI session: install, open the interactive terminal, ask it to fix the lodash CVE in a repository, and watch it read the finding and advisory, ask before each command, move lodash to 4.17.21, run npm install and npm test with approval, and print a summary without pushing anything. The same approve each step flow applies when the package to bump is the WebAuthn server library your sign in code depends on, with your negative case tests from Step 4 running before anything is committed.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Once connected, you can ask in plain language which of your services ship a particular version of an authentication library and get the answer from your own scan data.
Click through it at your own pace, or open it full screen.
Quick check
A phishing page on a lookalike domain relays your real site's WebAuthn options to the victim's browser. Why does the sign in still fail?
The victim's browser builds clientDataJSON with the lookalike's origin and will not let the lookalike request your RP ID, because the RP ID must match the page's own domain. Either the authenticator finds no credential for the lookalike's RP ID, or any signature it produces carries the wrong origin and RP ID hash, which your server rejects in steps 5 and 6 of verification.
Your server sees a signCount of 0 on every sign in from one user. Is their credential cloned?
Not on that evidence. The counter check only applies when the stored or received value is non zero. Authenticators without a counter, including many synced credentials, always return zero, which WebAuthn treats as legitimate. A regression from a non zero value on a device bound key is the case worth investigating.
Sources
- W3C: Web Authentication, Level 3, W3C Recommendation, 25 August 2026
- W3C: Web Authentication, Level 2, W3C Recommendation, 8 April 2021
- W3C: Web Authentication, Level 1, W3C Recommendation, 4 March 2019
- FIDO Alliance: Client to Authenticator Protocol 2.3, Proposed Standard, 26 February 2026
- FIDO Alliance: Client to Authenticator Protocol 2.2, Proposed Standard, 14 July 2025
- FIDO Alliance: Client to Authenticator Protocol 2.1 with errata, 21 June 2022
- FIDO Alliance: Client to Authenticator Protocol 2.0, Proposed Standard, 30 January 2019
- FIDO Alliance: specifications index
- FIDO Alliance: FIDO Metadata Service 3.1.1, Proposed Standard, 5 January 2026
- FIDO Alliance: Metadata Service overview
- NIST SP 800-63B-4: Digital Identity Guidelines, Authentication and Authenticator Management, July 2025
- NIST CSRC: SP 800-63B-4 publication record
- CISA: Implementing Phishing-Resistant MFA fact sheet, October 2022
- fido2-token(1) manual page (libfido2 tools)
- fido2-assert(1) manual page (libfido2 tools)
- curl manual page
Last reviewed: 23 October 2026. FIDO and W3C publish new revisions regularly; check the specifications index and the W3C WebAuthn page for the current versions before you rely on a version string or flag.