How to Debug FIDO2 Attestation — authData, Attestation Statement & Certificate Parsing
When a WebAuthn navigator.credentials.create() call succeeds, the authenticator returns an attestation response containing two critical structures: authData (37+ bytes) and an attestation statement (format depends on authenticator type). This guide walks through parsing both byte-by-byte, covering all attestation formats: Packed, TPM, Android Key, Android SafetyNet, Apple, and None.
1. The authData Structure — First 37 Bytes
Every attestation response includes authenticator data. The first 37 bytes are always present:
authData layout: [0..31] RP ID Hash (SHA-256, 32 bytes) [32] Flags (1 byte) [33..36] SignCount (4 bytes, big-endian uint32) [37+] Attested Credential Data (only if AT flag set) [..+N] Extensions (only if ED flag set)
2. Parsing the Flags Byte
Bit 0 (UP): User Present — user performed an authorization gesture
Bit 1 (RFU): Reserved
Bit 2 (UV): User Verified — PIN or biometric verified
Bit 3 (RFU): Reserved
Bit 4 (RFU): Reserved
Bit 5 (RFU): Reserved
Bit 6 (AT): Attested Credential Data included
Bit 7 (ED): Extension Data included
# Python flag parser
def parse_flags(byte):
return {
'UP': bool(byte & 0x01),
'UV': bool(byte & 0x04),
'AT': bool(byte & 0x40),
'ED': bool(byte & 0x80)
}
3. Parsing Attested Credential Data
If the AT flag is set, authData continues with attested credential data:
Attested Credential Data:
[AAGUID] 16 bytes — Authenticator Attestation GUID
[Credential ID Len] 2 bytes, big-endian
[Credential ID] variable
[COSE Public Key] CBOR-encoded COSE_Key map
def parse_attested_credential_data(data, offset):
aaguid = data[offset:offset+16]
offset += 16
cred_id_len = int.from_bytes(data[offset:offset+2], 'big')
offset += 2
cred_id = data[offset:offset+cred_id_len]
offset += cred_id_len
# Remaining bytes are CBOR-encoded COSE key
cose_key_bytes = data[offset:]
return aaguid, cred_id, cose_key_bytes
The AAGUID identifies the authenticator model. Common values:
| AAGUID | Authenticator |
|---|---|
00000000-0000-0000-... | Sent by U2F authenticators (FIDO2 spec allows null AAGUID) |
adce0002-35bc-c60a-... | YubiKey 5 Series |
95442b2e-f15e-4a5d-... | Google Titan Security Key |
b93fd961-f2e6-462f-... | Apple Touch ID / Face ID (platform authenticator) |
4. COSE Public Key Decoding
The credential public key is CBOR-encoded using COSE_Key format. Key parameters depend on the algorithm:
# COSE_Key common parameters (RFC 8152):
# kty (1): Key Type — 2=EC2, 3=RSA
# alg (3): Algorithm — -7=ES256, -257=RS256, -8=EdDSA
# crv (-1): Curve — 1=P-256, 6=Ed25519
# x (-2): X coordinate (EC2) or n (RSA)
# y (-3): Y coordinate (EC2) or e (RSA)
import cbor2
def parse_cose_key(cbor_bytes):
key = cbor2.loads(cbor_bytes)
kty = key.get(1) # Key type
alg = key.get(3) # Algorithm
if kty == 2: # EC2
return {
'algorithm': 'ES256' if alg == -7 else f'alg={alg}',
'curve': 'P-256' if key.get(-1) == 1 else 'unknown',
'x': key.get(-2).hex(),
'y': key.get(-3).hex()
}
return {'key_type': kty, 'algorithm': alg}
5. Attestation Statement Formats
Packed Attestation (most common for security keys)
Packed attestation statement:
{
"alg": -7, // ES256
"sig": <signature bytes>, // ASN.1 DER ECDSA signature
"x5c": [<cert bytes>] // Optional: attestation certificate chain
}
# If x5c is present: verify cert chain → validate signature
# If x5c absent (Self Attestation): verify with credential public key directly
TPM Attestation
TPM attestation statement:
{
"alg": -7,
"sig": <signature>,
"x5c": [<AIK cert>, <intermediate>],
"certInfo": <TPMS_ATTEST binary>, // TPM attestation structure
"pubArea": <TPMT_PUBLIC binary> // TPM public area
}
# certInfo contains:
# - magic: TPM_GENERATED_VALUE (0xFF544347)
# - type: TPM_ST_ATTEST_CERTIFY (0x8017)
# - qualifiedSigner: name of signing key
# - extraData: hash of attestation data
# - clockInfo: TPM clock state
# - firmwareVersion: TPM firmware
attestationObject and get instant byte-by-byte breakdown.6. Common Attestation Debugging Scenarios
| Problem | Check |
|---|---|
| Signature verification fails | Verify you're hashing authData || hash(clientDataJSON), not clientDataJSON directly |
| AT flag not set | Check attestation: "none" wasn't passed — then AT=0 and no attestedCredentialData |
| AAGUID is all zeros | Normal for U2F authenticators migrated to FIDO2; use metadata service fallback |
| COSE key parsing error | CBOR integer keys are negative for EC parameters (crv=-1, x=-2, y=-3) |
| TPM certInfo magic mismatch | Verify byte order: 0xFF544347 = "TPM" + GENERATED marker, big-endian |
Related Tools
FIDO2 Parser Tool — Parse attestation objects from base64url/hex | FIDO2 & CTAP Protocol Guide — Full protocol reference | FIDO2 Resident vs Non-Resident Keys — Key types comparison | ASN.1 DER Parser — Decode attestation certificates