Files
suaveplan/openspec/changes/complete-webauthn-mds3/specs/webauthn/spec.md
2026-05-27 23:29:53 +01:00

56 lines
5.0 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: webauthn (webauthn)
**Package**: `@suaveplan/webauthn`
**Status**: PROPOSED (companion delta of `complete-webauthn-mds3`)
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: Attestation verification dispatch
The package SHALL export `verifyAttestation(input: VerifyAttestationInput): Promise<AttestationVerifyResult>` from the `./verifier` subpath. The function SHALL dispatch on `input.attestationObject.fmt` to the per-format verifier registered in `input.registry`, throwing `WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED` when no verifier is registered. `VerifyAttestationInput` SHALL accept an optional `metadata?: MetadataResolver` field and an optional `statusFilter?: StatusFilter` field (both typed via `import type` from `@suaveplan/webauthn-mds3` — runtime-optional). `AttestationVerifyResult` SHALL carry an optional `metadata?: MetadataLookup` field. When `input.metadata` is `undefined` the returned `AttestationVerifyResult` SHALL be byte-identical to the pre-modification contract (strict back-compat). When `input.metadata` is supplied, after the per-format verifier returns, `verifyAttestation` SHALL resolve the AAGUID — or, for `fmt === "fido-u2f"` or an all-zero AAGUID, the SHA-1 SKI of the leaf attestation certificate (FIDO MDS3 §3.1.6) — call `lookup` / `lookupByCertKeyId` on the supplied resolver, set `result.metadata` to the returned `MetadataLookup` (or omit when `undefined`), and downgrade `result.trusted` to `false` when `(input.statusFilter ?? DEFAULT_STATUS_FILTER).accept(lookup.currentStatus) === false`.
#### Scenario: dispatches to registered format verifier
- **WHEN** `verifyAttestation({ attestationObject: { fmt: "packed", ... }, registry: defaultAttestationRegistry(), ... })` is called
- **THEN** the result is produced by the `packed` verifier and includes `trustPath`, `attestationType`, `trusted`
#### Scenario: throws WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED on unknown fmt
- **WHEN** `verifyAttestation({ attestationObject: { fmt: "unknown-fmt", ... }, registry: emptyRegistry })` is called
- **THEN** the call rejects with `WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED`
#### Scenario: omits metadata field when no resolver injected (back-compat)
- **WHEN** `verifyAttestation(input)` is called with `input.metadata === undefined`
- **THEN** the resolved `AttestationVerifyResult` has `metadata === undefined` and is byte-for-byte identical to the legacy (pre-modification) result for the same inputs
#### Scenario: augments result with metadata when resolver injected
- **GIVEN** a `MetadataResolver` whose `lookup` returns a `MetadataLookup` with `currentStatus === "FIDO_CERTIFIED_L1"`
- **WHEN** `verifyAttestation({ ..., metadata: resolver })` is called against a `packed` attestation carrying a known AAGUID
- **THEN** `result.metadata` deep-equals the lookup AND `result.trusted` is the verifier's original value (unchanged)
#### Scenario: downgrades trusted=false when default filter rejects status
- **GIVEN** a `MetadataResolver` whose `lookup` returns `currentStatus === "REVOKED"` and no caller-supplied `statusFilter`
- **WHEN** `verifyAttestation({ ..., metadata: resolver })` is called and the per-format verifier returned `trusted: true`
- **THEN** the merged `result.trusted` is `false` AND `result.metadata.currentStatus === "REVOKED"`
#### Scenario: caller-supplied statusFilter overrides default
- **GIVEN** a `MetadataResolver` returning `currentStatus === "REVOKED"` and `statusFilter: createStatusFilter({ allow: new Set(["REVOKED"]) })`
- **WHEN** `verifyAttestation({ ..., metadata: resolver, statusFilter })` is called and the verifier returned `trusted: true`
- **THEN** the merged `result.trusted` is `true` AND `result.metadata.currentStatus === "REVOKED"`
#### Scenario: resolves U2F by attestation-cert SKI
- **GIVEN** a `MetadataResolver` and a `fido-u2f` attestation whose leaf cert has SKI `0xDEAD…`
- **WHEN** `verifyAttestation({ ..., metadata: resolver })` is called
- **THEN** the resolver's `lookupByCertKeyId(0xDEAD…)` is invoked exactly once AND `lookup` is never called
**Verified by:** `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"dispatches to registered format verifier"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"throws WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED on unknown fmt"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"omits metadata field when no resolver injected (back-compat)"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"augments result with metadata when resolver injected"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"downgrades trusted=false when default filter rejects status"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"caller-supplied statusFilter overrides default"` AND `packages/auth/webauthn/src/verifier/verify-attestation.test.ts::"resolves U2F by attestation-cert SKI"`