Understanding WebAuthn Attestation Objects.
A relying party that decodes attestation but never verifies it has a decorative pipeline. This is the engineer's walkthrough: what the structure means, what each fmt requires, how MDS fits in, and what the verification path actually does.
What attestation actually proves
An attestation statement is a signed assertion from the authenticator that says, in effect: "this credential was just generated on a device of this make and model, by an authenticator I am willing to vouch for". The signature is performed by an attestation key — not the freshly generated credential key — and the signing material is the concatenation of authenticatorData || SHA-256(clientDataJSON).
Three things sit just outside what attestation proves, and conflating them is where most integrations come unstuck. Attestation does not say who the user is — that is the relying party's job. It does not say the user verified themselves — that is the UV flag. And it does not by itself say the device is currently in a trustworthy state — only that the device was a known make and model at the moment of registration. Everything else is policy that the relying party builds on top.
The reason attestation matters is operational. Without it, an enterprise has no protocol-level mechanism to require that user-facing credentials are stored in certified hardware. Anyone can ship a software authenticator that says "trust me". Attestation is what shifts the trust decision from a vendor's claim to a manufacturer-signed proof.
The attestationObject as a CBOR map
The wire structure is a single CBOR map with three string keys: fmt, attStmt, and authData. The map is serialised at the authenticator and emerges from navigator.credentials.create() as a base64url-encoded blob inside the response.attestationObject field of the returned PublicKeyCredential.
The first key, fmt, is a short string identifying the attestation format. The second, attStmt, is a sub-map whose schema depends on fmt. The third, authData, is a binary blob in the WebAuthn-specific authenticatorData layout: a 32-byte rpIdHash, one byte of flags, four bytes of signCount, optionally followed by attested-credential data and CBOR extensions.
When you walk an attestation in the field, the first move is always: paste the bytes into the CBOR decoder, look at the top-level map, and confirm those three keys are present. If they are not, you have a malformed envelope.
The attestation formats, one by one
fmt: "none"
The simplest possible attestation. attStmt is the empty map. authData still contains the freshly generated public key — you just have no proof of where it came from. This is the right default for consumer flows. The relying party still gets all the security properties of WebAuthn (origin binding, hardware-bound keys), it just cannot enforce a specific make-and-model policy.
Apple consumer flows (Touch ID, Face ID) historically returned none. They have since added apple and apple-appattest for richer cases, but consumer apps that don't require hardware proof are best served by attestation: 'none' in the request options — that's what Microsoft, Google, and Apple recommend for the public-internet account creation flow.
fmt: "packed"
The FIDO Alliance generic attestation format, defined in §8.2 of the WebAuthn spec. attStmt contains alg (the COSE algorithm identifier of the attestation signature, typically -7 for ES256), sig (the signature bytes), and optionally x5c (an array of X.509 certificates representing the attestation cert chain).
Three sub-flavours exist. With an x5c chain, the format is basic attestation — the attestation key is a per-cohort key whose public counterpart sits in the leaf of the x5c. Without an x5c but with a credential-key-bound signature (self attestation), the credential signs its own attestation; you get no make/model assurance, just key consistency. The third flavour is ECDAA, a privacy-preserving group signature scheme that has not seen production deployment.
Most certified roaming authenticators (YubiKey, Feitian, Token2, OnePass) ship packed with a basic-attestation x5c chain. The chain typically has a leaf cert (per-batch attestation key), one or two intermediates, and terminates at a manufacturer root. The FIDO MDS BLOB lists the manufacturer root for each AAGUID; that's the trust anchor your RP validates the chain against.
fmt: "tpm"
Used by Windows Hello when the platform authenticator is bound to a TPM (most modern Windows laptops). The format mirrors a TPM2_Quote: ver, alg, x5c, sig, plus two TPM-specific structures: certInfo (the TPMS_ATTEST that the TPM signed) and pubArea (the TPMT_PUBLIC describing the new credential key).
The verification rule is more involved than packed: parse certInfo, verify the TPM signed it (using the AIK leaf in x5c), verify certInfo.attested.name equals nameAlg(pubArea), then verify the AIK chain to a TPM manufacturer root. The Trusted Computing Group publishes the canonical TPM EK roots; FIDO MDS includes equivalent material indexed by AAGUID.
If your enterprise wants TPM-bound platform credentials accepted, you'll need both the WebAuthn attestation pipeline and a TPM-aware verifier. Most FIDO server libraries handle this transparently, but worth confirming if you're rolling your own.
fmt: "android-key" and "android-safetynet"
android-key uses the Android KeyStore Attestation feature: a hardware-backed Keymaster module produces a key, and Android signs an x5c chain that includes a custom key-description X.509 extension carrying the key properties (origin, security level, verifiedBootState, verifiedBootHash). The verification rule is to walk the x5c to a Google attestation root, then parse the key-description extension and enforce policy on its fields.
android-safetynet is a JWS payload from Google's SafetyNet API. Google deprecated SafetyNet in 2023 in favour of the Play Integrity API; production RPs should not adopt it for new deployments. Existing assertions can still be verified against the SafetyNet root, but new flows should require android-key instead.
fmt: "fido-u2f"
The legacy U2F envelope, kept for backwards compatibility. attStmt contains x5c and sig. The signed bytes are constructed differently from packed: 0x00 || rpIdHash || clientDataHash || credentialId || publicKey, where the public key is the uncompressed P-256 point (0x04 || x || y). You'll see this from older authenticators or new authenticators in U2F-compat mode.
fmt: "apple"
Apple Anonymous Attestation. attStmt contains an x5c chain rooted in the Apple Anonymous Attestation CA. The format is anonymous-by-design — there is no per-device AAGUID; the leaf certifies a per-cohort attestation key. RPs cannot enforce per-device policy, only per-OS-version cohort policy. For the consumer privacy profile this is intentional.
fmt: "apple-appattest"
Strictly speaking, App Attest is not a WebAuthn attestation; it's an iOS app-integrity assertion. You'll see it in WebView-mediated WebAuthn flows on iOS where the OS surfaces App Attest. Verification follows the App Attest specification (Apple developer docs), not the WebAuthn one.
The verification pipeline, end to end
Putting it all together, here is what a relying party actually does on registration:
- Decode the
attestationObjectas CBOR, extractfmt,attStmt,authData. - Decode
authData: split rpIdHash | flags | signCount | attestedCredentialData | extensions. Verify rpIdHash equals SHA-256(rpId). Verify flags include UP and (if policy requires) UV; verify AT is set; reject the invalid BE=0 ∧ BS=1 combination. - Decode the credentialPublicKey from attestedCredentialData as a COSE_Key (CBOR map). Verify
kty,algare in your accepted set; for EC2 confirmcrvis one of P-256 / P-384 / P-521. - If
fmt !== "none": verify the attestation signature againstauthData || SHA-256(clientDataJSON)using the attestation public key (extracted from the leaf ofx5cfor basic attestation, or from credentialPublicKey for self attestation). Validate the x5c chain against the trust anchor for the AAGUID in your MDS cache. - Look up the AAGUID in MDS. Reject if status indicates compromise. Reject if AAGUID not in your enterprise allow-list (where applicable).
- Persist credentialId, credentialPublicKey, signCount, AAGUID, BE, BS, transports.
The MDS BLOB — what it is, what's in it
The FIDO Metadata Service publishes a signed JSON document called the MDS BLOB at https://mds.fidoalliance.org/. It is signed using JWS with a key chained to the FIDO Alliance MDS Root CA. Each entry contains a metadata statement describing one authenticator family: AAGUID, vendor, model, attestation root certificates, supported algorithms, transports, certification level, and a statusReports array tracking status changes (initial certification, revocations, compromise reports).
Production RPs should fetch the BLOB on a daily cadence, verify the JWS, cache the entries by AAGUID, and use the cache during ceremony verification — never fetch on the hot path. A daily-stale cache is a far better reliability posture than coupling auth availability to FIDO Alliance uptime.
The fields that drive enterprise policy:
statusReports[].status—FIDO_CERTIFIED,NOT_FIDO_CERTIFIED,USER_VERIFICATION_BYPASS,ATTESTATION_KEY_COMPROMISE,USER_KEY_REMOTE_COMPROMISE,USER_KEY_PHYSICAL_COMPROMISE. Any of the compromise statuses should cause a registration to be rejected outright.authenticatorVersion— firmware version. For high-assurance environments, you may pin a minimum version.userVerificationDetails— what UV mechanisms the authenticator supports. Useful when constructing UV-required policy.keyProtection,matcherProtection,tcDisplay— enumerations of the protection level for keys / matcher / transaction confirmation.HARDWARE,SECURE_ELEMENT,TEEare what you want;SOFTWAREis what you reject for high-assurance use.
The AAGUID allow-list pattern
For workforce identity in a regulated industry, the recommended deployment pattern is an explicit AAGUID allow-list maintained by your security team. The allow-list contains the AAGUIDs of authenticators your organisation has procured and certified for use; everything else is rejected at registration time, with a clear UI message.
The allow-list is a runtime configuration in your relying party. It pairs with two background processes: an MDS sync job that fetches and verifies the BLOB on a schedule, and a procurement workflow that adds new AAGUIDs to the allow-list when a new device family is approved. The MDS sync should also remove AAGUIDs from effective circulation when their MDS status changes to a compromise indicator — either by removing from the allow-list or by emitting an alert that prompts a security review.
Common pitfalls
Five issues account for most attestation-pipeline incidents:
Pitfall 1: not actually verifying the signature. Some libraries decode the attestation, surface the AAGUID, and call it done — without verifying the cryptographic signature. The result is an attestation pipeline that is decorative, not load-bearing. Always verify.
Pitfall 2: not validating the x5c chain. Same shape: decoding the chain but never validating it against a known trust anchor. Anyone can construct an x5c chain that appears well-formed; only chain validation against an MDS-derived trust anchor turns it into an actual proof.
Pitfall 3: trusting a stale MDS cache. If a vendor publishes an MDS update flagging a compromise, and your cache is six months stale, you'll keep accepting compromised devices. Set up a sync job, alert on staleness, and have a runbook for rapid AAGUID purges.
Pitfall 4: blocking on MDS at registration time. Don't fetch on the hot path. Cache and serve from the cache; let the sync job be eventual.
Pitfall 5: confusing fmt: "none" with "registration failed". fmt: "none" is a perfectly valid registration. The user has a fresh credential bound to the RP. You just don't have make/model proof. For consumer flows that's fine; the absence of attestation is not the absence of security.
Practical: walking a real attestation
Pull a sample registration off your stack. Base64url-decode response.attestationObject and walk it with the attestation decoder, the authData parser, the COSE key inspector, and the X.509 viewer. Half an hour on a real attestation from your own fleet is worth more than any spec PDF.
What attestation does not solve
Attestation does not protect against a compromised attestation root, a compromised individual device after issuance, or a compromised relying party. MDS status reports cover the first; counter monitoring + UV the second; WebAuthn's origin binding limits the blast radius of the third.
The summary
Attestation is the policy primitive that lets an enterprise enforce hardware requirements for authentication. It is not magic. It is a signed assertion you verify against a manufacturer's root, indexed by AAGUID, status-reported by the FIDO Metadata Service. Used well, it lets you say "this credential lives in certified hardware" with cryptographic backing. Used poorly — decoded but not verified — it is decorative. Use it well.