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:

AAGUIDAuthenticator
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
Test this yourself: Our FIDO2 Attestation Parser decodes authData, attestation statements, and COSE keys from base64url or hex — paste your attestationObject and get instant byte-by-byte breakdown.

6. Common Attestation Debugging Scenarios

ProblemCheck
Signature verification failsVerify you're hashing authData || hash(clientDataJSON), not clientDataJSON directly
AT flag not setCheck attestation: "none" wasn't passed — then AT=0 and no attestedCredentialData
AAGUID is all zerosNormal for U2F authenticators migrated to FIDO2; use metadata service fallback
COSE key parsing errorCBOR integer keys are negative for EC parameters (crv=-1, x=-2, y=-3)
TPM certInfo magic mismatchVerify 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