Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
# `@suaveplan/webauthn-mds3` — FIDO Metadata Service v3 integration
**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`)
## 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 to a follow-up:
> "MDS3 (FIDO Metadata Service) integration — `verifyAttestation` returns the `trustPath: X509[]` and `attestationType` so callers can perform MDS3 lookups against their own metadata source. Bundling MDS3 fetch + caching expands scope into a separate package and is a follow-up."
> — `openspec/archive/2026-05-11-enterprise-webauthn/proposal.md`, "Out of scope"
Without MDS3 a relying party cannot answer the production-critical questions every passkey deployment eventually has to answer: is this AAGUID a known certified authenticator, is its certification still valid, is it on the revocation list, what attestation root CAs are trusted for this AAGUID, and what attachment / user-verification / key-protection statuses did the FIDO Alliance certify it at. Today every genesis consumer that wants those answers has to hand-roll the MDS3 blob fetch, JWS verification, FIDO root-CA pin, AAGUID lookup, and refresh schedule — exactly the duplication this monorepo exists to eliminate.
This change introduces a sibling package, `@suaveplan/webauthn-mds3`, that owns the MDS3 wire protocol end-to-end (download, JWS verify against the pinned FIDO Alliance root, cache, periodic refresh, AAGUID-keyed lookup) and exposes an injectable `MetadataResolver` that `@suaveplan/webauthn`'s attestation pipeline consumes through a new optional seam. Absent injection, the existing `verifyAttestation` contract is preserved byte-for-byte (strict back-compat).
## What changes
### New package: `@suaveplan/webauthn-mds3`
Universal-tier package at `packages/auth/webauthn-mds3/`. No browser-only or Node-only APIs — `FetchLike` and `StorageLike` are injected from `@suaveplan/types`.
#### MDS3 blob download + JWS verification
- ADDED: `fetchMetadataBlob({ fetch, url? }): Promise<RawBlob>` — fetches the MDS3 JWT (`application/jwt`) from `url` (default `https://mds.fidoalliance.org/`) using the injected `FetchLike` so consumers can layer in retries, proxies, or a Cloudflare-Workers `fetch`.
- ADDED: `verifyMetadataBlob({ jws, trustAnchors? }): Promise<MetadataBlobPayload>` — verifies the compact-JWS signature via `@suaveplan/jws` using the FIDO Alliance MDS3 root CA chain. The root CA fingerprint is pinned in `constants/fido-root.ts`; consumers can override via `trustAnchors` for staging / private metadata services. Verifies `x5c` chain to root, validates not-before / not-after, enforces RS256 / ES256 alg allow-list, decodes the JSON payload, and Zod-validates it against `MetadataBlobPayloadSchema` (FIDO MDS3 §3.1.7).
- ADDED: `MetadataBlobPayload`, `MetadataStatement`, `AuthenticatorStatus`, `StatusReport`, `BiometricStatusReport`, `EcdaaTrustAnchor` — Zod schemas tracking the published MDS3 v3 schema verbatim.
#### Cache + scheduled refresh
- ADDED: `MetadataCache` — `StorageLike<MetadataBlobPayload>`-backed cache with `legalHeader`, `no`, `nextUpdate`, and `entries` preserved. Cache key namespace `webauthn-mds3:blob:v1`.
- ADDED: `createMetadataRefresher({ fetch, storage, clock, logger, intervalMs?, signal? }): MetadataRefresher` — schedules refresh at `payload.nextUpdate - jitter`; falls back to `intervalMs` (default 24h) when the blob omits a `nextUpdate`. Honors `AbortSignal`; never starts a second refresh while one is in flight; emits OTel `webauthn_mds3.refresh_total{outcome}` counter.
#### AAGUID lookup API
- ADDED: `MetadataResolver` interface — `resolveByAaguid(aaguid: Uint8Array): Promise<MetadataLookup | undefined>` and `resolveByAttestationCertificateKeyId(keyId: Uint8Array): Promise<MetadataLookup | undefined>` (U2F authenticators are MDS3-keyed by attestation cert SKI, not AAGUID — FIDO MDS3 §3.1.6).
- ADDED: `MetadataLookup` — `{ statement, currentStatus, statusReports, attestationRootCertificates: Uint8Array[], aaguid?, attestationCertificateKeyIds?, lastUpdate }`.
- ADDED: `createMemoryResolver({ blob }): MetadataResolver` — synchronous in-memory resolver over a verified payload. Used by tests and by consumers that ship their own refresh loop.
- ADDED: `createCachedResolver({ cache, refresher? }): MetadataResolver` — production resolver that reads the cache and optionally drives a refresher.
#### Integration seam back into `@suaveplan/webauthn`
- ADDED (in `@suaveplan/webauthn`): optional `metadata?: MetadataResolver` field on `VerifyAttestationInput`. When supplied, after the per-format verifier returns a `trustPath`, `verifyAttestation` resolves the AAGUID (or attestation-cert SKI for `fido-u2f`) and, when a `MetadataLookup` is returned, augments the result with `metadata: MetadataLookup` and downgrades `trusted` to `false` when `currentStatus` is `REVOKED`, `USER_KEY_REMOTE_COMPROMISE`, `USER_KEY_PHYSICAL_COMPROMISE`, `ATTESTATION_KEY_COMPROMISE`, or any other FIDO `AuthenticatorStatus` in the configurable revocation set.
- ADDED (in `@suaveplan/webauthn`): `AttestationVerifyResult.metadata?: MetadataLookup` field. Absent when no resolver is injected — strict back-compat.
#### Errors
- ADDED: `WEBAUTHN_MDS3.*` codes via `defineCodes`: `BLOB_FETCH_FAILED`, `BLOB_MALFORMED`, `BLOB_SIGNATURE_INVALID`, `BLOB_CHAIN_UNTRUSTED`, `BLOB_EXPIRED`, `BLOB_ALG_DISALLOWED`, `BLOB_SCHEMA_MISMATCH`, `CACHE_READ_FAILED`, `CACHE_WRITE_FAILED`, `RESOLVER_LOOKUP_FAILED`, `REFRESHER_ALREADY_RUNNING`, `REFRESHER_ABORTED`.
#### Test strategy
- Vectors of the FIDO Alliance MDS3 sample blob (`__fixtures__/mds3-sample-blob.jwt` + parsed payload) sourced offline from the FIDO Alliance conformance suite. Tests use the `FetchLike` injection seam to return the fixture bytes — no live network in CI.
- Negative fixtures: signature-flipped blob, chain-broken blob, expired `nextUpdate`, schema-violation payload.
### Subpath exports
- `.` — root barrel: `MetadataResolver`, `createMemoryResolver`, `createCachedResolver`, every type, every error code.
- `./fetch` — `fetchMetadataBlob`.
- `./verify` — `verifyMetadataBlob`, `MetadataBlobPayloadSchema`.
- `./cache` — `MetadataCache`, cache-key constants.
- `./refresher` — `createMetadataRefresher`, `MetadataRefresher`.
- `./errors` — `WEBAUTHN_MDS3_CODES`, `WebAuthnMds3Error`.
### Dependencies
- `@suaveplan/types workspace:^` — `FetchLike`, `StorageLike`, `Clock`, `LoggerLike`.
- `@suaveplan/error workspace:^` — `defineCodes`, base error.
- `@suaveplan/jws workspace:^` — JWS verification with `x5c` chain.
- `@suaveplan/x509 workspace:^` — chain validation to pinned FIDO root.
- `@suaveplan/crypto-hash workspace:^` — SHA-256 for AAGUID key derivation and cert thumbprints.
- `@suaveplan/crypto-utils workspace:^` — `constantTimeEqual`, base64url.
- `@opentelemetry/api catalog:observability` — refresh + lookup metrics.
- `tslib catalog:build`.
- `zod catalog:forms` — boundary validation.
## Out of scope
- **MDS v2 / MDS v1 compatibility.** MDS3 is the only metadata version on the FIDO Alliance roadmap; bundling deprecated MDS2 expands scope and ships dead code for new deployments.
- **Per-RP custom metadata sources.** The package only speaks the FIDO Alliance MDS3 wire format. Custom metadata catalogues (enterprise allow-lists) should implement `MetadataResolver` directly — that interface is the integration seam, not the MDS3 wire format.
- **Authenticator-status policy decisions.** The package surfaces `currentStatus` and `statusReports`; the relying party decides what status set to treat as revocation. The default revocation set is opinionated but configurable.
- **CRL / OCSP polling for attestation-root chains.** Out of band for MDS3 — root pinning + the published metadata `nextUpdate` cadence is the FIDO-Alliance-blessed freshness model.
## Source mapping
| Requirement | Source | Notes |
|---|---|---|
| Why an MDS3 integration is needed | `openspec/archive/2026-05-11-enterprise-webauthn/proposal.md` "Out of scope" | Verbatim deferral text |
| `trustPath` + `attestationType` consumed downstream | `packages/auth/webauthn/src/verifier/registry.ts` L14-L18 | `AttestationVerifyResult` is the integration seam |
| AAGUID parse + carry on attested-credential-data | `packages/auth/webauthn/src/parser/authenticator-data.ts` L73-L84 | Already available at attestation-verification time |
| `none`/`fido-u2f`/`packed` doc anchors flag MDS3 as caller responsibility | `packages/auth/webauthn/src/attestation/{packed,fido-u2f,none}/*.md` | Today's wording moves into the `MetadataResolver` |
| MDS3 wire format | FIDO Alliance "Metadata Service Specification" v3.0 §3.1 | External standard anchor |
| FIDO Alliance root CA pin | <https://mds.fidoalliance.org/Root.cer> | Pinned in `constants/fido-root.ts` |
| JWS verification | RFC 7515 + `@suaveplan/jws` | Internal contract |
## Verification approach
Every requirement in `specs/webauthn-mds3/spec.md` and every modified requirement in `specs/webauthn/spec.md` carries a `**Verified by:**` line naming planned test files. Test files (all paths relative to `packages/auth/webauthn-mds3/` unless noted):
- `src/fetch/fetch-blob.test.ts` — happy path via injected `FetchLike`; non-2xx surfaces `BLOB_FETCH_FAILED`; `AbortSignal` cancellation.
- `src/verify/verify-blob.test.ts` — sample-blob signature verifies; flipped signature → `BLOB_SIGNATURE_INVALID`; broken chain → `BLOB_CHAIN_UNTRUSTED`; expired `nextUpdate` → `BLOB_EXPIRED`; HS256 → `BLOB_ALG_DISALLOWED`; schema mismatch → `BLOB_SCHEMA_MISMATCH`.
- `src/cache/metadata-cache.test.ts` — round-trip through `StorageLike`; read/write failure mapping.
- `src/refresher/metadata-refresher.test.ts` — refreshes at `nextUpdate - jitter`; falls back to `intervalMs`; reentrancy guard fires `REFRESHER_ALREADY_RUNNING`; `signal.abort()` fires `REFRESHER_ABORTED`; OTel counter records per outcome.
- `src/resolver/memory.test.ts` — AAGUID lookup hit / miss; `attestationCertificateKeyId` lookup for U2F.
- `src/resolver/cached.test.ts` — reads cache; lazy-refreshes when stale.
- `src/errors/errors.test.ts` — every `WEBAUTHN_MDS3.*` code is thrown by at least one production path.
- `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"augments result with metadata when resolver injected"` AND `"::"downgrades trusted=false when currentStatus is revoked"` AND `"::"omits metadata field when no resolver injected (back-compat)"` — integration into the existing `@suaveplan/webauthn` verifier.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/webauthn-mds3` after landing — at draft time the package is new.
- `@suaveplan/webauthn` — [x] tests run / [x] docs reviewed / [ ] no action needed. Gains the optional `metadata?: MetadataResolver` field on `VerifyAttestationInput` and the optional `metadata?: MetadataLookup` field on `AttestationVerifyResult`. Both are additive; existing call sites that do not inject a resolver see byte-identical behaviour.
- `@suaveplan/vault` — [ ] tests run / [x] docs reviewed / [x] no action needed (reason: vault unlock consumes `verifyAssertion`, not `verifyAttestation`; MDS3 surfaces only on registration paths the vault doesn't drive).
- `@suaveplan/react-auth` — [ ] tests run / [x] docs reviewed / [x] no action needed (reason: hook surface unchanged; MDS3 plugs into server-side verifier).
## Migration
No prior MDS3 surface — additive only. Behaviour change for `verifyAttestation` consumers is opt-in (only fires when `metadata: MetadataResolver` is passed). Consumers that previously open-coded an MDS3 lookup over the returned `trustPath` MAY migrate to the injected seam at their own pace; the old "ignore `metadata`, do it yourself" path is permanently supported.