FIDO2 & CTAP Protocol: How Hardware Security Keys Work

Published July 28, 2026 · ~9,000 words · Authentication & Hardware Security

In this guide: The complete FIDO2 protocol stack — from the WebAuthn browser JavaScript API down to CTAP2 CBOR commands on the USB/NFC/BLE wire. Every step of credential creation and assertion, the authData byte structure in full detail, attestation statement formats, resident keys, hmac-secret extension, PIN/UV protocol v1 and v2, and the hardware architecture of FIDO2 authenticators.

1. FIDO2 Overview: WebAuthn + CTAP2

FIDO2 is the umbrella standard for passwordless authentication from the FIDO Alliance in collaboration with the W3C. It consists of two core specifications:

+-------------+     WebAuthn JS      +-----------+     CTAP2 (CBOR)      +-----------+
|  Relying    |<--- (HTTPS) -------->|  Browser  |<--- (USB/NFC/BLE)--->|  Security |
|  Party (RP) |                      | (Client)  |                       |  Key      |
+-------------+                      +-----------+                       +-----------+
                                           |
                                    +-----+------+
                                    |  Platform  |  (Windows Hello, Apple Touch ID,
                                    |  Authnr    |   Android biometric, etc.)
                                    +------------+
FIDO2 Architecture: RP WebAuthn JS → Browser CTAP2 → Authenticator

The client-side architecture has two paths:

CTAP1 (U2F) Backward Compatibility

CTAP1 is the original FIDO U2F protocol. CTAP2 authenticators must also support CTAP1 for backward compatibility with U2F-only relying parties. The key differences:

CTAP1 (U2F)CTAP2 (FIDO2)
EncodingFixed binary formatCBOR
Credential storageKey-wrapping (no on-device storage)Resident keys (on-device) + non-resident (wrapped)
User verificationUser Presence only (tap)UP + UV (PIN, biometric)
Multiple accounts per RPNoYes (resident keys)
Discoverable credentialsNo (username-first)Yes (usernameless login)
ExtensionsNonehmac-secret, credProtect, largeBlobKey, etc.

2. WebAuthn JavaScript API Deep Dive

Credential Creation (Registration)

When a user registers a security key with a website, the RP calls navigator.credentials.create():

const credential = await navigator.credentials.create({
    publicKey: {
        challenge: Uint8Array.from(randomChallenge, c => c.charCodeAt(0)),
        rp: { id: "example.com", name: "Example Corp" },
        user: {
            id: Uint8Array.from(userHandle, c => c.charCodeAt(0)),
            name: "alice@example.com",
            displayName: "Alice"
        },
        pubKeyCredParams: [
            { type: "public-key", alg: -7 },   // ES256
            { type: "public-key", alg: -257 }  // RS256
        ],
        timeout: 60000,
        attestation: "none",  // "none" | "indirect" | "direct" | "enterprise"
        authenticatorSelection: {
            authenticatorAttachment: "cross-platform",  // "platform" | "cross-platform"
            residentKey: "preferred",       // "discouraged" | "preferred" | "required"
            userVerification: "preferred"   // "discouraged" | "preferred" | "required"
        }
    }
});

// credential.response is AuthenticatorAttestationResponse
// Contains: attestationObject (CBOR), clientDataJSON

Credential Assertion (Authentication)

When a user logs in, the RP calls navigator.credentials.get():

const assertion = await navigator.credentials.get({
    publicKey: {
        challenge: Uint8Array.from(randomChallenge, c => c.charCodeAt(0)),
        rpId: "example.com",
        allowCredentials: [   // For non-discoverable credentials
            { id: credentialId, type: "public-key" }
        ],
        userVerification: "preferred",
        timeout: 30000
    }
});

// assertion.response is AuthenticatorAssertionResponse
// Contains: authenticatorData, signature, userHandle (if resident key)

3. authData Structure: Byte-by-Byte Breakdown

The authenticatorData (authData) is the core binary data structure produced by the authenticator. It is included in both registration (AuthenticatorAttestationResponse) and authentication (AuthenticatorAssertionResponse). We also provide an interactive FIDO2 CBOR Attestation Parser that decodes authData and attestation objects in-browser.

Structure Layout

Offset  Size  Field
------  ----  -----
 0      32    RP ID Hash — SHA-256(originRpId)
32       1    Flags byte
33       4    Signature Counter — uint32 big-endian

// ---- If AT flag (bit 6) is set: ----
37      16    AAGUID — Authenticator Attestation GUID
53       2    Credential ID Length — uint16 big-endian (L)
55       L    Credential ID — opaque byte string
55+L    var   COSE_Key — CBOR-encoded public key (variable length)

// ---- If ED flag (bit 7) is set: ----
...     var   Extension Data — CBOR map of extension outputs

Flags Byte (authData[32])

BitMaskNameMeaning
00x01UPUser Present — user tapped the key or performed a gesture
20x04UVUser Verified — PIN entered or biometric matched
60x40ATAttested Credential Data included (only in registration responses)
70x80EDExtension Data included

Signature Counter

The 4-byte signature counter starts at 0 and increments with every successful assertion. A constant value of 0 indicates a counter-less authenticator (most modern FIDO2 keys). Non-zero counters protect against authenticator cloning: if the RP sees a counter that hasn't increased, the key may have been duplicated.

4. CTAP2 Protocol: Commands and Wire Format

CommandCTAP CMDPurpose
authenticatorMakeCredential0x01Create a new credential (registration)
authenticatorGetAssertion0x02Sign a challenge (authentication)
authenticatorGetInfo0x04Get authenticator capabilities and metadata
authenticatorClientPIN0x06Set/change/verify PIN, get PIN token
authenticatorReset0x07Factory reset — erase all credentials
authenticatorGetNextAssertion0x08Get next assertion (multiple credentials for RP)
authenticatorBioEnrollment0x09Manage biometric templates (fingerprint enrollment)
authenticatorCredentialManagement0x0AEnumerate/delete resident credentials
authenticatorSelection0x0BSelect between multiple authenticators (USB hub)
authenticatorLargeBlobs0x0CRead/write large blob data (certificates, up to 4096 bytes)

authenticatorMakeCredential Parameters

{
  1: Uint8Array   // clientDataHash — SHA-256(clientDataJSON)
  2: {            // rp
     "id": "example.com",
     "name": "Example Corp"  // optional
  },
  3: {            // user
     "id": Uint8Array,  // userHandle
     "name": "alice@example.com",    // optional
     "displayName": "Alice"          // optional
  },
  4: [            // pubKeyCredParams
    { "type": "public-key", "alg": -7 },
    { "type": "public-key", "alg": -257 }
  ],
  5: Uint32Array,  // excludeList — credential IDs to exclude
  6: {             // extensions (optional)
     "hmac-secret": true,
     "credProtect": 2
  },
  7: {             // options
     "rk": true,   // residentKey
     "uv": true    // userVerification
  },
  8: {             // pinUvAuthParam (optional)
     "pinProtocol": 2,
     "pinAuth": Uint8Array  // HMAC of pinToken over CDH
  },
  9: 2             // pinUvAuthProtocol (1 or 2)
}

CTAP2 Response: authenticatorMakeCredential

{
  1: "packed",              // fmt — attestation statement format
  2: {                      // authData (CBOR byte string)
    // RP ID Hash (32 bytes) + flags (1 byte) + counter (4 bytes)
    // + AAGUID (16) + credIdLen (2) + credId (L) + COSE_Key
  },
  3: {                      // attStmt — format-specific
    "alg": -7,              // COSE AlgorithmIdentifier
    "sig": Uint8Array,      // ECDSA or RSA signature
    "x5c": [cert1, ...]     // X.509 certificate chain (optional)
  }
}

5. Attestation Formats: packed, TPM, U2F, Android Key

packed (FIDO2 Standard)

packed is the default FIDO2 attestation format. The authenticator generates an attestation key pair at manufacturing time or per-credential. The attStmt contains:

TPM 2.0

Used by Windows Hello platform authenticators. The attStmt contains a TPM-generated TPMS_ATTEST structure (CBOR-encoded) and TPMT_SIGNATURE. The TPM's Attestation Identity Key (AIK) signs the credential, providing a hardware root of trust back to the TPM manufacturer (Infineon, STM, Nuvoton, etc.).

U2F (FIDO U2F Attestation)

Legacy attestation format compatible with CTAP1/U2F. Contains:

attStmt = {
  "x5c": [attestationCert],
  "sig": ECDSA signature (ASN.1 DER encoded)
}

The X.509 certificate is issued by the authenticator vendor's attestation CA. The RP verifies the cert chain up to the vendor root.

Apple Anonymous Attestation

Apple's platform authenticator (iCloud Keychain / Face ID / Touch ID) uses the "apple" attestation format. It does NOT contain x5c — instead, it contains a credCert (X.509 credential certificate issued by Apple's WebAuthn CA). The credential is tied to the user's Apple ID and can be verified through Apple's attestation service.

6. Resident Keys (Discoverable Credentials)

A resident key (also called discoverable credential) is a credential whose private key AND user handle (user.id/user.name/user.displayName) are stored on the authenticator itself. This enables usernameless login — the user goes to a login page, taps their security key, and the authenticator provides the credential ID + user handle without the RP having to send an allowCredentials list.

Resident vs Non-Resident

Non-Resident (Wrapped)Resident (Discoverable)
StoragePrivate key encrypted with authenticator master key, stored off-device (RP keeps it as credential ID)Private key + user handle stored on authenticator flash
Credential IDContains encrypted private key + RP ID (opaque, ~250 bytes)Opaque index into authenticator storage (~30-80 bytes)
CapacityUnlimited (RP stores the blob)Limited by authenticator flash (typically 25-100 slots)
usernamelessNot possible (RP must provide allowCredentials)Yes — authenticator sends user handle
WebAuthn parameterresidentKey: "discouraged"residentKey: "required"

7. hmac-secret Extension and Other Extensions

hmac-secret (CTAP2 Extension)

The hmac-secret extension is one of the most powerful FIDO2 extensions. It allows a relying party to derive a symmetric secret from the credential's private key using HMAC. The authenticator computes:

HMAC-SHA-256(credRandom, salt1 || salt2)

Where credRandom is a 32-byte random value generated at credential creation time and stored securely on the authenticator. The RP provides up to two 32-byte salts, and the authenticator returns up to two 32-byte HMAC outputs.

This is the primitive that enables offline authentication and encryption use cases: password manager unlocking (Bitwarden's FIDO2 unlock), disk encryption key derivation, and cryptocurrency wallet seed encryption.

Other Important Extensions

ExtensionIdentifierPurpose
credProtectcredentialProtectionPolicySets minimum UV requirement for credential: 1=optional, 2=required, 3=hardware-enforced
credBlobcredBlobStores a small blob (up to 32 bytes) alongside the credential on the authenticator
largeBlobKeylargeBlobKeyDerives a 32-byte key for encrypting data stored in the authenticator's large blob (up to 4096 bytes per RP)
minPinLengthminPinLengthRP requests the authenticator's minimum PIN length policy
prfprfPseudorandom Function — successor to hmac-secret, more general-purpose

8. Client PIN Protocol and User Verification

CTAP2 defines two Client PIN Protocol versions:

PIN Protocol v1

PIN Protocol v2

User Verification Methods

UV MethodUV ByteDescription
Presence onlyNo UV — just tap the key (UP flag set, UV flag clear)
PIN4-63+ alphanumeric characters, verified via pinProtocol
Fingerprint0x02Built-in fingerprint sensor (e.g., Feitian BioPass)
Internal UV0x04Device-level biometric/TEE (e.g., YubiKey Bio)

9. FIDO2 Security Key Hardware Internals

A FIDO2 security key is essentially a secure element (SE) with a USB/NFC/BLE controller. Leading hardware platforms:

Common Hardware Platforms

PlatformChipMCU CoreKey StorageUsed By
NXP A700x / A71CHNXP Kinetis + SE050ARM Cortex-M4SE050 secure elementYubiKey 5 Series, Google Titan (2019+)
Infineon SLE 78Infineon SLE 7816-bit secure MCUOn-chip EEPROM (mask ROM)Feitian ePass, older YubiKeys
STM32 + ATECC608AMicrochip ATECC608AARM Cortex-M0/M3ATECC608A crypto elementSoloKeys Solo, open-source keys
Nordic nRF52840Nordic nRF52 + ARM CryptoCellARM Cortex-M4FCryptoCell-310 TRNG + Key VaultSoloKeys Solo 2, open-source NFC keys

USB HID Framing

CTAP2 over USB uses the HID (Human Interface Device) protocol with a custom FIDO usage page:

Summary

FIDO2 represents a fundamental shift in authentication — from shared secrets (passwords) to public-key cryptography where the private key never leaves the authenticator. Understanding the full stack — from navigator.credentials.create() down to the CBOR bytes on the USB wire — is essential for anyone building, deploying, or testing FIDO2 authenticators.

Key takeaways: