Safeguard
Supply Chain Security•

FIDO2 Explained: How WebAuthn and CTAP Work Together

FIDO2 is two protocols: W3C WebAuthn between site and browser, FIDO CTAP between browser and authenticator. How both work over USB, NFC, BLE and hybrid.

Safeguard Security Team
37 min read

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 authenticatorData and the SHA-256 hash of clientDataJSON. 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.

PieceWho talksEncodingRuns overOwner and current document
WebAuthnRelying party web application and the WebAuthn client (browser)JavaScript objects; clientDataJSON is JSON; authenticatorData is binaryHTTPS between server and page; JavaScript API inside the browserW3C, Web Authentication Level 3, Recommendation, 25 August 2026
CTAP2Client platform (browser or operating system) and the authenticatorCTAP2 canonical CBORUSB HID, NFC, BLE, hybrid tunnelFIDO Alliance, CTAP 2.3, Proposed Standard, 26 February 2026
CTAP1 / U2FClient platform and older second factor authenticatorsU2F binary messagesSame transportsFIDO Alliance; still supported for interoperability, version string U2F_V2
Metadata ServiceRelying party and the FIDO AllianceSigned JSON (a JWT style BLOB)HTTPSFIDO 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:

MemberMeaningWhat the server checks
typeThe ceremony typeMust equal webauthn.get
challengeBase64url of the challenge the server issuedMust equal the stored challenge for this ceremony
originThe origin of the page that called the API, as the browser saw itMust be an origin the relying party expects
crossOriginTrue when called from a cross origin iframeIf true, the relying party must expect iframe use
topOriginLevel 3: the top level origin when embeddedIf 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:

FieldSizeMeaning
rpIdHash32 bytesSHA-256 of the RP ID the credential is scoped to
flags1 byteBit 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)
signCount4 bytes, big endianSignature counter; zero if the authenticator does not implement one
attestedCredentialDataVariablePresent at registration (AT set); normally absent in assertions
extensionsVariable, CBORPresent 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:

  1. If you sent a non empty allowCredentials, check the returned credential ID is one of them.
  2. 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 userHandle must match it. If you did not (a discoverable credential, often called usernameless sign in), userHandle must be present and must identify an account that owns this credential.
  3. Decode clientDataJSON as UTF-8 and parse it. Check type is webauthn.get.
  4. Check challenge equals the base64url encoding of the challenge you issued.
  5. Check origin is one you expect, and handle crossOrigin and topOrigin if present.
  6. Check rpIdHash in authenticatorData equals SHA-256 of your RP ID.
  7. Check the UP flag is set.
  8. 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 userVerification to required.
  9. 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.
  10. Hash clientDataJSON with SHA-256 and verify the signature over authenticatorData plus that hash, using the stored public key.
  11. Apply signature counter logic (below), process any extension outputs you asked for, and be ready for extension outputs you did not ask for.
  12. Update the stored signCount and 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:

CodeCommandPurpose
0x01authenticatorMakeCredentialCreate a credential (registration)
0x02authenticatorGetAssertionProduce a signature (authentication)
0x04authenticatorGetInfoReport versions, extensions, options, limits such as maxMsgSize
0x06authenticatorClientPINSet or change the PIN, obtain a pinUvAuthToken
0x07authenticatorResetWipe all credentials and the PIN
0x08authenticatorGetNextAssertionFetch further assertions when several credentials match
0x09authenticatorBioEnrollmentManage fingerprint templates (added in 2.1)
0x0AauthenticatorCredentialManagementList and delete discoverable credentials (added in 2.1)
0x0BauthenticatorSelectionAsk the user to pick one of several connected authenticators (added in 2.1)
0x0CauthenticatorLargeBlobsRead and write per credential blobs (added in 2.1)
0x0DauthenticatorConfigEnterprise 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) and CTAPHID_KEEPALIVE (0x3B) are mandatory; CTAPHID_WINK (0x08) and CTAPHID_LOCK (0x04) are optional.
Initialization packet offsetLengthField
04CID, channel identifier
41CMD, command identifier with bit 7 set
51BCNTH, high byte of payload length
61BCNTL, low byte of payload length
7packet size minus 7DATA

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

DocumentDateStatusWhat changed
WebAuthn Level 14 March 2019W3C RecommendationFirst standard browser API for public key credentials
CTAP 2.030 January 2019FIDO Proposed StandardCBOR 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.115 June 2021; errata 21 June 2022FIDO Proposed StandardPIN/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 28 April 2021W3C RecommendationMatches CTAP 2.1 features, adds credProps and largeBlob extensions and the crossOrigin member for use in cross origin iframes
CTAP 2.214 July 2025FIDO Proposed StandardHybrid transport, JSON based messages, PIN complexity policy, persistent PIN/UV auth token for read only credential management, thirdPartyPayment, hmac-secret-mc
MDS 3.1.15 January 2026FIDO Proposed StandardCurrent Metadata Service revision (3.0 was May 2021, 3.1 May 2025)
CTAP 2.326 February 2026FIDO Proposed StandardCurrent revision; adds long touch for reset among other changes; version string FIDO_2_3
WebAuthn Level 325 August 2026 (Candidate Recommendation 26 May 2026)W3C RecommendationBackup 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

MethodHow it proves possessionPhishing resistantReplay protectionMain operational costFits best
FIDO2 / WebAuthn passkeysSignature from a key scoped to the RP ID, origin checked by the browserYes (verifier name binding, per NIST)Server challengeRecovery design, library upkeep, user enrolment of a second credentialWeb and app sign in for workforce and customers
FIDO2 with device bound hardware keys and attestationAs above, key non exportable, model attestedYes; can meet AAL3 when non exportableServer challengeBuying, issuing and replacing keys; metadata policyAdministrators, high value accounts, regulated workforce
TOTP authenticator appShared secret generates a time based codeNo; codes can be relayed by a fake pageTime window onlySecret stored on server; resetsLegacy systems that cannot do WebAuthn
Push approval (with number matching)Approve a prompt on an enrolled phoneNo; CISA lists it as phishable, though number matching resists push bombingServer sidePush fatigue handling, phone enrolmentInterim step while moving to FIDO2
SMS or voice codeCode sent to a phone numberNoShort validitySIM 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 TLSYes (CISA lists PKI based MFA as phishing resistant; NIST cites client authenticated TLS as channel binding)Protocol nonceCertificate lifecycle, readers, middlewareGovernment 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.