Understanding Packed Attestation Format

Packed is the FIDO2 default and the format most likely to reach a relying party that asked for direct attestation. This page covers its three variants, the four checks a correct verification performs, and the AAGUID cross-check that is skipped in most implementations. It sits under Attestation vs Assertion Explained, which establishes why attestation is a registration-time policy input rather than part of authentication.


Statement Reference

Field Present when Meaning
alg always the algorithm the attestation signature uses
sig always signature over authData || clientDataHash
x5c full basic attestation the certificate chain, leaf first
x5c absent self attestation signed by the credential key itself

The presence or absence of the chain is the whole distinction between the two variants, and it changes what the statement is worth.

The three shapes of a packed statement Packed attestation comes in three variants, and they are verified differently. One format name, three verification paths full basic an attestation certificate chains to a vendor root — the common case self attestation signed by the credential key itself; proves nothing about the device AAGUID cross-check the certificate extension must match the AAGUID in authData missing x5c absence of the chain is what distinguishes self from basic

Root Cause Analysis

1. Signing input assembled wrongly. The attestation signature covers authenticatorData concatenated with the SHA-256 of clientDataJSON — the same construction as an assertion signature, over the same bytes. Implementations that hash the concatenation, or that concatenate the parsed forms, produce a verification failure that looks like a bad certificate.

2. Chain not anchored. Verifying the leaf certificate’s signature over the statement proves internal consistency. Without building a path to a root you pinned in advance, nothing about the manufacturer has been established.

3. Certificate constraints unchecked. An attestation certificate has requirements: a specific version, a basic-constraints extension marking it as an end-entity certificate, and an organisational unit identifying it as an authenticator attestation certificate. Skipping these accepts certificates that were issued for something else entirely.

4. AAGUID cross-check skipped. The certificate may carry an extension naming the AAGUID it was issued for. When present, it must equal the AAGUID in authenticatorData. Without this check, a valid certificate from one model can vouch for a claimed AAGUID from another.

5. Self attestation treated as basic. A statement with no chain is signed by the credential key. It verifies, which is why it slips through — but it establishes nothing beyond what the assertion already would.

Verifying a packed statement Signature over the concatenation, then the chain, then the certificate constraints, then the AAGUID cross-check. Four checks, and the last one is the one most often skipped signature over authData + hash chain to a pinned vendor root constraints basic constraints, version, OU AAGUID extension must match authData Skipping the final cross-check lets a valid certificate vouch for an AAGUID it was never issued for.

Step-by-Step Resolution

Step 1 — dispatch on the presence of a chain

Branch immediately: chain present means full basic, chain absent means self. The two paths share only the signature check.

Step 2 — rebuild the signed input from raw bytes

Concatenate the untouched authenticatorData with the SHA-256 of the untouched clientDataJSON. Verify the statement signature over that buffer.

Step 3 — build and validate the certificate path

Anchor to a root you pinned. Check validity dates, and consult revocation information where the chain provides it.

Step 4 — enforce the certificate constraints

Version, basic constraints, and the attestation-certificate organisational unit. A certificate failing any of these was not issued for this purpose.

Step 5 — cross-check the AAGUID

If the certificate carries the AAGUID extension, compare it byte-for-byte with the value in authenticatorData. A mismatch is a hard rejection.

Step 6 — treat self attestation as no device claim

Accept the registration if your policy accepts unattested credentials; record that no model claim was made.

Self attestation is not weak attestation A self-attested statement is signed by the credential key, so it adds no information beyond the assertion itself. Treat self attestation as "none" for policy purposes full basic signed by an attestation key chains to a vendor root identifies a certified model usable as a policy input self attestation signed by the credential key no chain, no root identifies nothing equivalent to no attestation It is not an attack; it is simply an authenticator declining to make a device claim.

Verification and Testing

Keep a real packed attestation from a certified hardware key as a fixture, together with the trust anchor that validates it. Assert that it verifies, and assert that a single altered byte in authData makes it fail — that second assertion is what proves the signing input is assembled from the right bytes.

Add a fixture whose certificate does not chain to your pinned root. It must be rejected at the path-building step rather than accepted on the strength of a valid leaf signature.

Add a self-attested fixture and assert that your code classifies it as carrying no device claim rather than routing it down the basic-attestation path.

Finally, add a fixture where the certificate’s AAGUID extension disagrees with authenticatorData. If it passes, the cross-check is missing.


Pitfalls

1. Hashing the concatenation. The signature is over authData plus the client-data hash, not over a hash of both.

2. Accepting a leaf without a path. A self-consistent signature proves nothing about provenance.

3. Letting an expired batch certificate fail registrations silently. Vendor certificates expire; monitor for a rise in rejections concentrated on one vendor.


Frequently Asked Questions

Why is packed attestation called “packed”?

Because the statement is a compact CBOR map rather than a format inherited from an earlier protocol. It was designed for FIDO2 specifically, which is why it is the default for authenticators certified against it.

Do all hardware keys use packed?

Most certified ones do, but not all. Older devices may use formats inherited from U2F, and platform authenticators use their own vendor formats. A relying party that accepts direct attestation should implement the formats it actually receives and treat the rest as carrying no device claim.

Should self attestation be rejected?

Only if your policy rejects unattested credentials generally, since that is effectively what it is. Rejecting it specifically — while accepting registrations with no statement at all — is an inconsistency that will be hard to defend in a review.

What does the batch certificate actually identify?

A production batch of a model, not an individual device. Thousands of units share one certificate, which is deliberate: it means attestation identifies a product without identifying a person. That property is what makes direct attestation acceptable from a privacy standpoint.

How do I get the vendor roots?

From the metadata service, which publishes them per model, or directly from vendors for hardware you procure yourself. Either way they must be pinned in your deployment rather than taken from the statement being verified.

Does the attestation signature use the same algorithm as the credential?

Not necessarily. The alg field in the statement describes the attestation signature specifically, and it can differ from the algorithm of the credential key. Dispatch on the statement’s own field rather than reusing the credential’s.

What should be stored after verification?

The format name, the AAGUID, the verification outcome and the time — not the raw statement. The statement is only meaningful together with the trust context that validated it, and that context changes as roots rotate.


How do expired batch certificates show up in production?

As a rise in registration failures concentrated on one vendor, beginning on a specific date and affecting devices that worked perfectly the day before. Because the fault is a date rather than a defect, it is easy to misdiagnose as a service problem or as a change in the authenticator. The signature to look for is that every affected registration comes from the same model, and that the same payload verifies when the expiry check is relaxed. Monitoring rejections by vendor rather than in aggregate is what makes the pattern visible; without that split, a few hundred failures a day look like ordinary noise until somebody complains.

The same split is worth keeping for successful verifications, not only failures. Knowing which vendors your certified population actually comes from turns an abstract policy discussion about acceptable models into a concrete one about the hardware your users are holding today, and it makes the cost of tightening an allow-list something you can estimate rather than guess at.

Is it worth implementing packed if I only expect platform authenticators?

Probably not on its own, but the dispatch table that routes to it costs almost nothing and means a security key registering against your service does not fall into the unknown-format path. The realistic posture for a general-purpose relying party is to implement packed, implement the platform format for whichever platforms dominate your traffic, and treat everything else as carrying no device claim rather than as an error.

Related