Attestation Conveyance Policy Selection Guide
attestationConveyancePreference is one word in your registration options — none, indirect, direct, or enterprise — but it decides whether you learn what kind of authenticator a user registered, how much verification code you must run, and what privacy and compliance obligations you take on. Requesting direct when you will not verify the statement is wasted attack surface; requesting none in a regulated workforce fails an audit. This page is the decision guide, grounded in WebAuthn Level 2 §5.4.7 and the attestation formats it produces. It sits under Backend Verification and Secure Credential Storage.
Concept Definition and Spec Grounding
The preference is a request to the client; the authenticator and platform decide how much to reveal, and synced passkeys frequently downgrade to none regardless. The four values (WebAuthn L2 §5.4.7):
none— do not request attestation. The client may replace the statement with thenoneformat and zero the AAGUID. Best privacy, zero provenance. The default for consumer passkeys.indirect— the client may anonymise attestation (e.g. via an anonymization CA) while still conveying some provenance. Rarely distinct fromnonein practice today.direct— request the authenticator’s attestation statement unaltered, including the real AAGUID and attestation certificate. Enables model verification against FIDO MDS3.enterprise— request individually-identifying attestation (may include a serial number). Only honoured for authenticators/platforms configured to permit it, typically on managed devices. See enterprise attestation explained.
The golden rule: only request the level you will actually verify. Every level above none obliges you to parse and validate the attestation statement (packed, tpm, android-key, fido-u2f), or you gain attack surface with no benefit.
Architecture and Data Flow
The decision tree below routes a deployment to its preference based on whether provenance is required and whether devices are managed.
Implementation Guide
Step 1 — Set the preference to match your policy
import { generateRegistrationOptions } from '@simplewebauthn/server';
const options = await generateRegistrationOptions({
rpID: 'example.com',
userID: account.userHandle,
userName: account.username,
attestationType: 'none', // 'none' | 'indirect' | 'direct' | 'enterprise'
});
Step 2 — Verify only to the depth you requested
For none, accept the statement without certificate-chain validation. For direct/enterprise, validate the format and the certificate path, as detailed in validating attestation statements on the server.
import { verifyRegistrationResponse } from '@simplewebauthn/server';
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin: 'https://app.example.com',
expectedRPID: 'example.com',
// requireUserVerification per your AAL target
});
Step 3 — Check the AAGUID against MDS3 (direct/enterprise)
const aaguid = registrationInfo?.aaguid; // zeroed for many synced passkeys
if (policy === 'direct' && !mds3.isTrusted(aaguid)) {
throw Object.assign(new Error('Authenticator model not in trust store'), { status: 403 });
}
Step 4 — Store the attestation outcome
Record the format, AAGUID, and whether the statement was verified, so audits and signature verification debugging have ground truth.
Validation Checklist
Error Reference Table
| Error / Condition | HTTP Status | Trigger | Diagnostic |
|---|---|---|---|
Unexpected none format |
400 | direct requested but client downgraded (synced passkey) |
Accept downgrade or relax to none for platform attachment |
| Untrusted AAGUID | 403 | AAGUID not in MDS3 store under direct |
Compare AAGUID hex to MDS3; update metadata blob |
| Certificate chain invalid | 400 | Attestation cert path fails to a trusted root | Verify intermediate certs; refresh MDS3 roots |
| Enterprise attestation absent | 400 (policy) | enterprise requested on an unmanaged device |
Fall back to direct; confirm device management |
| Over-collection of PII | — (privacy) | enterprise serial stored without need |
Restrict to required fields; document retention |
Platform and Library Notes
@simplewebauthn/server
attestationType maps to the conveyance preference; verifyRegistrationResponse validates supported formats and returns aaguid and credentialDeviceType. Pair with a metadata service for MDS3 lookups.
Apple / Google synced passkeys
Frequently return none and a zeroed AAGUID even when direct is requested — do not treat this as an attack under a consumer policy.
fido2-lib / WebAuthn4J
Both validate packed, tpm, android-key, and fido-u2f formats; ensure your MDS3 metadata blob is current so certificate roots resolve.
py_webauthn
Supports attestation verification and AAGUID extraction; supply your own MDS3 trust store for model allowlisting.
Pitfalls and Security Hardening
1. Requesting direct but not verifying. Root cause: copying an example that sets direct. Mitigation: use none unless you run the verification path.
2. Rejecting zeroed AAGUIDs from passkeys. Root cause: assuming all authenticators attest. Mitigation: allow none/zero AAGUID for platform attachment.
3. Expecting enterprise from consumer devices. Root cause: misreading the value. Mitigation: only request it on managed fleets.
4. Stale MDS3 metadata. Root cause: never refreshing the blob. Mitigation: schedule MDS3 updates; roots and AAGUIDs change.
5. Over-collecting identifying attestation. Root cause: storing enterprise serials indefinitely. Mitigation: minimise and set retention (GDPR).
Related
- Backend Verification and Secure Credential Storage — the parent pillar and registration pipeline
- Enterprise Attestation Conveyance Explained — when serial-level attestation is available and how to handle it
- Validating Attestation Statements on the Server — the per-format verification this policy triggers
- Platform vs Roaming Authenticator Trade-offs — how attachment drives the attestation you can expect
- WebAuthn Security Boundaries for Enterprise Apps — AAGUID policy and MDS3 metadata verification