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 the none format 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 from none in 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.

Attestation conveyance decision tree Decision tree: if provenance not required, choose none; if required and devices managed, choose enterprise; if required and unmanaged, choose direct with MDS3 verification. New registration Must verify provenance? no none consumer passkeys yes Managed devices? yes enterprise serial-level id no direct + MDS3 verify AAGUID

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