# `@suaveplan/webauthn-mds3` — FULL FIDO Metadata Service v3 capability **Package**: `@suaveplan/webauthn-mds3` (NEW) **Status**: PROPOSED **Type**: new-package (genesis-original) **Date**: 2026-05-17 **Path**: `packages/auth/webauthn-mds3/` **Design**: ./design.md **Spec delta**: ./specs/webauthn-mds3/spec.md **Companion delta**: ./specs/webauthn/spec.md (`## MODIFIED Requirements`) **Supersedes**: `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-webauthn-mds3/` ## Why The `enterprise-webauthn` change (archived 2026-05-11) shipped `verifyAttestation` returning a `trustPath: X509[]` plus an `attestationType`, but explicitly deferred FIDO Metadata Service v3 (MDS3) integration. Without MDS3 a relying party cannot answer the production-critical questions every passkey deployment eventually faces: is this AAGUID a known certified authenticator, is its certification valid, is it revoked, is it on a compromise list, what attestation roots are trusted for it, and what user-verification / key-protection level did the FIDO Alliance certify it at. The prior draft (`_superseded-narrow-deferrals-2026-05-17/add-webauthn-mds3/`) covered the bones (fetch / verify / cache / refresh / resolver / integration seam). This change supersedes it with the **full** MDS3 surface that a production relying party actually needs: conditional fetch (ETag/Last-Modified), rollback protection on the cache, configurable status allow/deny lists, a shipped bootstrap blob so cold-start works without the FIDO endpoint, a manual blob override seam for air-gapped deployments and tests, EventBus events on refresh outcomes, OTel spans across the full pipeline, and at least twelve `WEBAUTHN_MDS3.*` error codes covering every reachable failure path. ## What changes ### New package: `@suaveplan/webauthn-mds3` Universal-tier package at `packages/auth/webauthn-mds3/`. No browser-only or Node-only APIs; `FetchLike`, `StorageLike`, `Clock`, `LoggerLike`, `EventBusLike` are all injected from `@suaveplan/types`. #### Wire protocol — fetch + verify - ADDED: `fetchMetadataBlob({ fetch, url?, signal?, cache? }): Promise` — pluggable `FetchLike`; periodic refresh uses caller-managed timers; supports **conditional fetch** via `If-None-Match` (ETag) and `If-Modified-Since` (Last-Modified) when `cache` carries the previous response's validators; surfaces `304 Not Modified` as `{ kind: "not-modified" }` so the refresher can skip re-verification. - ADDED: `verifyMetadataBlob({ jws, trustAnchors?, clock? })` — compact-JWS verify against the pinned **FIDO Alliance MDS3 root CA**; full `x5c` chain validation via `@suaveplan/x509`; algorithm allow-list `{ RS256, ES256, ES384 }`; Zod-validated payload against `MetadataBlobPayloadSchema` (FIDO MDS3 §3.1.7); rejects expired blobs via `nextUpdate` check. - ADDED: Note — FIDO MDS3 blobs are JWS-signed JSON, **not CBOR**. No `@suaveplan/cbor` dep needed. #### Cache, rollback protection, bootstrap, manual override - ADDED: `MetadataCache` — wraps injected `StorageLike` under key namespace `webauthn-mds3:blob:v1`. Envelope carries `{ payload, etag?, lastModified?, fetchedAt }`. Writes are **atomic** (single key, last-writer-wins on the underlying `StorageLike`). **Rollback protection**: refuses to overwrite a cached payload with one whose `no` field is strictly lower (FIDO MDS3 §3.1.7 monotonicity). - ADDED: `BOOTSTRAP_BLOB` shipped at `src/bootstrap/blob.ts` — a verified FIDO Alliance blob captured at package release. Used on cold-start when the cache is empty and the first network fetch fails; replaced atomically by the first successful refresh. - ADDED: `createManualResolver({ jws })` / `createMemoryResolver({ payload })` — caller-injected blob overrides for testing, air-gapped deployments, or enterprise allow-lists; bypasses the network entirely. #### Lookup, status filtering - ADDED: `MetadataResolver` interface with `lookup(aaguid: Uint8Array): Promise` and `lookupByCertKeyId(ski: Uint8Array): Promise`. Returns the **full** `MetadataStatement` plus complete `statusReports[]` history (not just the most recent), so callers can render certification timelines. - ADDED: `MetadataLookup` — `{ statement, currentStatus, statusReports, attestationRootCertificates, aaguid?, attestationCertificateKeyIds?, lastUpdate }`. - ADDED: Indexing — `createMemoryResolver` builds an O(1) primary map by 16-byte AAGUID and a secondary O(1) map by `attestationCertificateKeyIdentifier` (SHA-1 SKI hex string). The two maps coexist so a payload that mixes FIDO2 (AAGUID) and U2F (SKI) entries works uniformly. - ADDED: `StatusFilter` — caller-configurable allow/deny lists over `AuthenticatorStatus`. `createStatusFilter({ allow?, deny? })` returns a predicate evaluating "deny wins"; defaults reject `REVOKED`, `USER_VERIFICATION_BYPASS`, `ATTESTATION_KEY_COMPROMISE`, `USER_KEY_REMOTE_COMPROMISE`, `USER_KEY_PHYSICAL_COMPROMISE`. #### Scheduled refresher - ADDED: `createMetadataRefresher({ fetch, storage, clock, logger, eventBus?, intervalMs?, signal? })` — schedules at `payload.nextUpdate - jitter` (uniform `[-5min, +5min]`) when present; falls back to `intervalMs` (default 24h). Reentrancy guard fires `REFRESHER_ALREADY_RUNNING`; honors `AbortSignal` with `REFRESHER_ABORTED`. Emits EventBus events `webauthn-mds3.refresh.success` / `webauthn-mds3.refresh.failure` and OTel spans `webauthn_mds3.refresh`. #### Integration seam back into `@suaveplan/webauthn` - MODIFIED (in `@suaveplan/webauthn`): optional `metadata?: MetadataResolver` and `statusFilter?: StatusFilter` on `VerifyAttestationInput`. Absent ⇒ **byte-identical** legacy behaviour (strict back-compat). Present ⇒ post-verifier resolution: AAGUID for FIDO2 formats, SHA-1 SKI of the leaf attestation cert for `fido-u2f` (per FIDO MDS3 §3.1.6); attaches `metadata: MetadataLookup` to the result; downgrades `trusted` to `false` when the filter rejects the resolved status. - ADDED (in `@suaveplan/webauthn`): `AttestationVerifyResult.metadata?: MetadataLookup`. #### Errors - ADDED: `WEBAUTHN_MDS3.*` codes via `defineCodes` (≥12 — current count: 14): `BLOB_FETCH_FAILED`, `BLOB_NOT_MODIFIED`, `BLOB_MALFORMED`, `BLOB_SIGNATURE_INVALID`, `BLOB_CHAIN_UNTRUSTED`, `BLOB_EXPIRED`, `BLOB_ALG_DISALLOWED`, `BLOB_SCHEMA_MISMATCH`, `CACHE_READ_FAILED`, `CACHE_WRITE_FAILED`, `CACHE_ROLLBACK_REFUSED`, `RESOLVER_LOOKUP_FAILED`, `REFRESHER_ALREADY_RUNNING`, `REFRESHER_ABORTED`. #### Observability - ADDED: EventBus topics `webauthn-mds3.refresh.success`, `webauthn-mds3.refresh.failure`, `webauthn-mds3.lookup.hit`, `webauthn-mds3.lookup.miss` (typed payloads via `@suaveplan/events`). - ADDED: OTel spans `webauthn_mds3.fetch`, `webauthn_mds3.verify`, `webauthn_mds3.refresh`, `webauthn_mds3.lookup`; counters `webauthn_mds3.refresh_total{outcome}`, `webauthn_mds3.lookup_total{outcome}`; histogram `webauthn_mds3.blob_age_ms`. #### Test strategy - Bundled FIDO Alliance sample blob fixtures (`__fixtures__/mds3-sample-blob.jwt`, parsed JSON, corrupted variants for negative tests). **No live network in CI** — every test injects a deterministic `FetchLike`. - 100% line / branch / function / statement coverage via istanbul. ### Subpath exports `.`, `./fetch`, `./verify`, `./cache`, `./refresher`, `./resolver`, `./bootstrap`, `./status-filter`, `./events`, `./errors`. ### Dependencies `@suaveplan/types`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/jws`, `@suaveplan/x509`, `@suaveplan/crypto-hash`, `@suaveplan/crypto-utils` (all `workspace:^`); `@opentelemetry/api catalog:observability`, `tslib catalog:build`, `zod catalog:forms`. ## Out of scope - **Certifying new authenticators.** That is the FIDO Alliance's exclusive job; this package only **consumes** their published metadata blob. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/webauthn-mds3` — at draft time the package is new. - `@suaveplan/webauthn` — [x] tests run / [x] docs reviewed / [ ] no action needed. Gains optional `metadata?: MetadataResolver` and `statusFilter?: StatusFilter` fields on `VerifyAttestationInput` plus optional `metadata?: MetadataLookup` on `AttestationVerifyResult`. Additive only — byte-identical when not supplied. - `@suaveplan/vault` — [ ] tests run / [x] docs reviewed / [x] no action needed (reason: vault unlock consumes `verifyAssertion`, not `verifyAttestation`). - `@suaveplan/react-auth` — [ ] tests run / [x] docs reviewed / [x] no action needed (reason: hook surface unchanged; MDS3 plugs into the server-side verifier). ## Migration No prior MDS3 surface — additive only. `verifyAttestation` consumers see byte-identical behaviour unless they opt in by injecting `metadata`. Consumers that previously open-coded an MDS3 lookup over the returned `trustPath` MAY migrate to the injected seam at their own pace; the "ignore `metadata`, do it yourself" path stays permanently supported.