Skip to content

FIDO2 / CTAP details

This page documents the FIDO2 / CTAP protocol surface the badge actually implements, as found in components/mod_fido2/. It is descriptive of the firmware, not of the FIDO specification: where a feature is advertised but not implemented, it is called out.

For the generated code reference, see the Code reference.

PropertyValue
TransportUSB HID, FIDO Alliance usage page (CTAPHID)
Report / packet size64 bytes
CTAPHID commands handledINIT, PING, WINK, CANCEL, CBOR, MSG
Capability flags (INIT)WINK | CBOR
Max message size constant2048 bytes

MSG carries CTAP1 / U2F APDUs (VERSION, REGISTER, AUTHENTICATE); CBOR carries CTAP2 commands.

The getInfo response is a 12-entry map:

KeyFieldValue
0x01versionsFIDO_2_0, FIDO_2_1, U2F_V2
0x02extensionsappid, credProtect, appidExclude
0x03aaguidCDCBAD6E39C30001BAD6E00100000001
0x04optionssee below
0x05maxMsgSize1200
0x06pinUvAuthProtocols[2]
0x07maxCredentialCountInList8
0x08maxCredentialIdLength64
0x09transports["usb"]
0x0AalgorithmsES256, EdDSA
0x0BmaxSerializedLargeBlobArray1024
0x0DminPINLengthcurrent floor (default 4)

Keys are emitted in CTAP canonical order (by length, then bytewise).

OptionValueMeaning
rktrueResident / discoverable keys supported
uptrueUser presence supported
uvfalseNo built-in user verification (e.g. biometric)
platfalseRemovable authenticator
alwaysUvcurrent stateWhether every operation requires user verification
credMgmttrueCredential management supported
authnrCfgtrueauthenticatorConfig supported
clientPintrueClientPIN supported
largeBlobstrueauthenticatorLargeBlobs supported
pinUvAuthTokentruepinUvAuthToken supported
setMinPINLengthtruesetMinPINLength supported
makeCredUvNotRqdtruemakeCredential allowed without user verification
CommandCodeStatus
makeCredential0x01Implemented
getAssertion0x02Implemented
getInfo0x04Implemented
clientPIN0x06Implemented
reset0x07Implemented (requires on-device user presence)
getNextAssertion0x08Implemented
credentialManagement0x0AImplemented
selection0x0BImplemented (user presence only)
largeBlobs0x0CImplemented
authenticatorConfig0x0DImplemented (toggleAlwaysUv, setMinPINLength)
bioEnrollment0x09Not dispatched (CTAP1_ERR_INVALID_COMMAND)
AlgorithmCOSE idCurveUsed for
ES256-7P-256 (secp256r1)Credential keys, attestation
EdDSA-8Ed25519Credential keys
ECDH-ES + HKDF-256-25P-256ClientPIN key agreement

makeCredential selects ES256 -> P-256 or EdDSA -> Ed25519 from pubKeyCredParams; any other requested algorithm yields CTAP2_ERR_UNSUPPORTED_ALGORITHM.

AspectValue
pinUvAuthProtocol2 (protocol 0 / unset also accepted)
Key agreementCOSE EC2, P-256, alg ECDH-ES+HKDF-256
Protocol-2 encryptionAES-256-CBC, 16-byte IV prefixed to ciphertext
pinUvAuthParamHMAC-SHA-256(pinToken, message), first 32 bytes (protocol 2)
PIN retries (max)8
UV retries (max)3

Subcommands:

SubcommandCodeStatus
getPINRetries0x01Implemented
getKeyAgreement0x02Implemented
setPIN0x03CTAP2_ERR_UNSUPPORTED_OPTION
changePIN0x04CTAP2_ERR_UNSUPPORTED_OPTION
getPinToken0x05Implemented
getPinUvAuthTokenUsingPinWithPermissions0x09Implemented

The PIN is not settable over the wire. The badge verifies the decrypted PIN hash against its own stored PIN hash, so the FIDO PIN is the badge PIN, set on-device. getPinToken returns CTAP2_ERR_PIN_NOT_SET when no PIN hash is available and CTAP2_ERR_PIN_BLOCKED after the retry counter reaches zero.

The permission bits are defined (mc, ga, cm, be, lbw, acfg). The credential-bound paths consume mc/ga/cm; lbw is required for authenticatorLargeBlobs writes and acfg for authenticatorConfig.

  • up must be true (false yields CTAP2_ERR_INVALID_OPTION); uv=true is rejected with CTAP2_ERR_UNSUPPORTED_OPTION (no internal UV).
  • When alwaysUv is enabled (see authenticatorConfig), a request without a verified pinUvAuthParam is rejected with CTAP2_ERR_PIN_REQUIRED.
  • User presence is always requested on the device before a key is created.
  • An existing credential for the same RP-ID + user handle is overwritten.
  • appidExclude matching an existing credential yields CTAP2_ERR_CREDENTIAL_EXCLUDED.
  • Attestation is always returned (see below).

The credProtect extension (levels 1-3) is parsed at registration, stored with the credential, echoed in the authenticator-data extensions, and reported by credential management (defaulting to level 1 when unset).

  • With no allowList, all resident credentials for the RP are returned (discoverable flow), with getNextAssertion iterating the rest.
  • authData flags: UP (0x01) is set when user presence was requested; UV (0x04) is set when a pinUvAuth token was verified for the request.
  • When alwaysUv is enabled, an assertion without a verified pinUvAuth token is rejected with CTAP2_ERR_PIN_REQUIRED.
  • ECDSA assertions are DER-encoded; EdDSA assertions are raw 64-byte signatures, both produced by the secure element.

Each credential has its own monotonic signature counter, incremented on every assertion and persisted in TROPIC01 R-Memory. (A separate global authentication counter is also kept in NVS, distinct from the per-credential counter reported in authData.)

  • Requires a valid pinUvAuthToken before any subcommand runs.
  • Implemented subcommands: getCredsMetadata, enumerateRPsBegin/GetNext, enumerateCredentialsBegin/GetNext, deleteCredential.
  • Responses include credProtect for each credential.

The badge implements authenticatorLargeBlobs as a single serialized large-blob array.

  • The array is stored in NVS, capped at 1024 bytes (maxSerializedLargeBlobArray). An unset store reads back as the canonical empty array (0x80 followed by the left 16 bytes of SHA-256(0x80)).
  • get returns a fragment from offset (no PIN required).
  • set writes fragments in order: the first fragment (offset 0) carries the total length; out-of-order offsets yield CTAP1_ERR_INVALID_SEQ, a total above the cap yields CTAP2_ERR_LARGE_BLOB_STORAGE_FULL. When a PIN is set, each write must carry a pinUvAuthParam with the lbw permission over 0xff x 32 || 0x0c || 0x00 || offset (LE32) || SHA-256(fragment).
  • On the final fragment the trailing 16-byte truncated SHA-256 checksum is verified; a mismatch yields CTAP2_ERR_INTEGRITY_FAILURE and the write is discarded.

Supported subcommands:

SubcommandCodeStatus
toggleAlwaysUv0x02Implemented
setMinPINLength0x03Implemented
enableEnterpriseAttestation0x01CTAP2_ERR_UNSUPPORTED_OPTION
vendorPrototype0xFFCTAP2_ERR_UNSUPPORTED_OPTION

When a PIN is set, the command requires a pinUvAuthParam with the acfg permission over 0xff x 32 || 0x0d || subCommand || subCommandParams.

  • toggleAlwaysUv flips a persistent flag. While set, every makeCredential and getAssertion without a verified pinUvAuth token is rejected with CTAP2_ERR_PIN_REQUIRED. The current value is reported as the alwaysUv getInfo option.
  • setMinPINLength raises the minimum badge-PIN length floor (reported as the minPINLength getInfo value). The new value must be between the current floor and 8 (the badge PIN maximum); a lower or larger value yields CTAP1_ERR_INVALID_PARAMETER. The floor is enforced on the next on-device PIN change and persists across reboots. The minPinLengthRPIDs list and forceChangePin flag are accepted (covered by the auth) but not acted upon; the minPinLength makeCredential extension is not reported to RPs.

makeCredential returns packed attestation with an x5c certificate array, a single self-signed per-device certificate, and an ES256 (P-256) signature over authData || clientDataHash. The attestation private key is a chip-bound key in TROPIC01 ECC slot 0. See FIDO2 attestation key & AAGUID for the certificate fields and key lifecycle.

LimitValueSource
FIDO2 ECC key slots26 (slots 5-30)main/tropic_slot_map.h
FIDO2 R-Memory slots27 (slots 5-31)main/tropic_slot_map.h
Max credentials26 (one ECC slot each)bounded by ECC slot count
Credential ID length64 bytesFIDO2_CRED_ID_LEN

Every credential, resident or not, consumes one ECC slot because its private key is generated and held in the secure element; there is no key-wrapping scheme for unlimited server-side credentials.