first commit
This commit is contained in:
153
docs/identity/auth/webauthn/README.md
Normal file
153
docs/identity/auth/webauthn/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# @suaveplan/webauthn - W3C WebAuthn Level 3 client + parser + verifier — registration, authentication, every attestation format (packed, fido-u2f, tpm, android-key, android-safetynet, apple, none), PRF + largeBlob extensions, conditional UI, server-tier parser/verifier subpaths free of DOM types.
|
||||
|
||||
## Summary
|
||||
|
||||
W3C [WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/) client + parser + verifier for Suaveplan. Browser-tier registration / authentication entry points sit on the package root; framework-agnostic parsing and verification surface lives under `./parser`, `./verifier`, and `./attestation/<fmt>` subpaths so server-side relying-party verifiers can consume them without a DOM dependency.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dom-exception-mapper — Converts every `DOMException…
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dom-exception-mapper — Converts every `DOMException…
|
||||
- [dom-exception-mapper](./src/adapter/dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [attestation-object](./src/parser/attestation-object.md) — Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a t…
|
||||
- [authenticator-data](./src/parser/authenticator-data.md) — Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure…
|
||||
- [client-data](./src/parser/client-data.md) — Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and val…
|
||||
- [cose-key](./src/parser/cose-key.md) — Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbo…
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [registry](./src/verifier/registry.md) — Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shar…
|
||||
- [sign-counter](./src/verifier/sign-counter.md) — Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-incre…
|
||||
- [verify-assertion](./src/verifier/verify-assertion.md) — Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuth…
|
||||
- [verify-attestation](./src/verifier/verify-attestation.md) — Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attes…
|
||||
<!-- end:auto-docs --> - dom-exception-mapper — Converts every `DOMException…
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- dom-exception-mapper — Converts every `DOMException…
|
||||
- [dom-exception-mapper](./src/adapter/dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [attestation-object](./src/parser/attestation-object.md) — Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a t…
|
||||
- [authenticator-data](./src/parser/authenticator-data.md) — Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure…
|
||||
- [client-data](./src/parser/client-data.md) — Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and val…
|
||||
- [cose-key](./src/parser/cose-key.md) — Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbo…
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/verifier/registry.md) — Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shar…
|
||||
- [sign-counter](./src/verifier/sign-counter.md) — Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-incre…
|
||||
- [verify-assertion](./src/verifier/verify-assertion.md) — Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuth…
|
||||
- [verify-attestation](./src/verifier/verify-attestation.md) — Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attes…
|
||||
|
||||
- dom-exception-mapper — Converts every `DOMException…
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- dom-exception-mapper — Converts every `DOMException…
|
||||
- [dom-exception-mapper](./src/adapter/dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [attestation-object](./src/parser/attestation-object.md) — Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a t…
|
||||
- [authenticator-data](./src/parser/authenticator-data.md) — Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure…
|
||||
- [client-data](./src/parser/client-data.md) — Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and val…
|
||||
- [cose-key](./src/parser/cose-key.md) — Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbo…
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/verifier/registry.md) — Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shar…
|
||||
- [sign-counter](./src/verifier/sign-counter.md) — Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-incre…
|
||||
- [verify-assertion](./src/verifier/verify-assertion.md) — Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuth…
|
||||
- [verify-attestation](./src/verifier/verify-attestation.md) — Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attes…
|
||||
|
||||
- dom-exception-mapper — Converts every `DOMException…
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- dom-exception-mapper — Converts every `DOMException…
|
||||
- [dom-exception-mapper](./src/adapter/dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [attestation-object](./src/parser/attestation-object.md) — Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a t…
|
||||
- [authenticator-data](./src/parser/authenticator-data.md) — Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure…
|
||||
- [client-data](./src/parser/client-data.md) — Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and val…
|
||||
- [cose-key](./src/parser/cose-key.md) — Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbo…
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/verifier/registry.md) — Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shar…
|
||||
- [sign-counter](./src/verifier/sign-counter.md) — Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-incre…
|
||||
- [verify-assertion](./src/verifier/verify-assertion.md) — Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuth…
|
||||
- [verify-attestation](./src/verifier/verify-attestation.md) — Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attes…
|
||||
- [adapter](./src/adapter/adapter.md) — <!-- begin:auto-docs --> _(no submodules)_- [dom-exception-mapper](./src/adapter/dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_ - [android-key](./src/attestation/android-key/android-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_ - [android-safetynet](./src/attestation/android-safetynet/android-safetynet.md) — <!-- begin:auto-docs --> _(no submodules)_- [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_ - [apple](./src/attestation/apple/apple.md) — <!-- begin:auto-docs --> _(no submodules)_- [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_ - [fido-u2f](./src/attestation/fido-u2f/fido-u2f.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ - [none](./src/attestation/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_ - [packed](./src/attestation/packed/packed.md) — <!-- begin:auto-docs --> _(no submodules)_- [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tpm](./src/attestation/tpm/tpm.md) — <!-- begin:auto-docs --> _(no submodules)_- [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_ - [authenticate](./src/authenticate/authenticate.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_ - [large-blob](./src/extensions/large-blob/large-blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_ - [prf](./src/extensions/prf/prf.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [attestation-object](./src/parser/attestation-object.md) — Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a t…
|
||||
- [authenticator-data](./src/parser/authenticator-data.md) — Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure…
|
||||
- [client-data](./src/parser/client-data.md) — Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and val…
|
||||
- [cose-key](./src/parser/cose-key.md) — Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbo…
|
||||
- [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_ - [register](./src/register/register.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/verifier/registry.md) — Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shar…
|
||||
- [sign-counter](./src/verifier/sign-counter.md) — Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-incre…
|
||||
- [verify-assertion](./src/verifier/verify-assertion.md) — Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuth…
|
||||
- [verify-attestation](./src/verifier/verify-attestation.md) — Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attes…
|
||||
|
||||
## Features
|
||||
|
||||
- **Registration** (`register(opts)`) — builds `PublicKeyCredentialCreationOptions`, calls `navigator.credentials.create` via injectable adapter, parses the response into a typed `RegisterResult`.
|
||||
- **Authentication** (`authenticate(opts)`) — builds `PublicKeyCredentialRequestOptions`, calls `navigator.credentials.get`, parses the response into a typed `AuthenticateResult`. Supports conditional UI / autofill via `mediation: "conditional"`.
|
||||
- **Assertion verifier** (`verifyAssertion(input)`) — ES256, ES384, ES512, EdDSA, RS256. Discriminated-union result so callers branch on `ok: true | false`.
|
||||
- **Attestation verifiers** for every IANA-registered format: `packed`, `fido-u2f`, `tpm`, `android-key`, `android-safetynet`, `apple`, `none`. Pluggable via `registerAttestationFormat`.
|
||||
- **PRF extension** ([W3C L3 §10.1.4](https://www.w3.org/TR/webauthn-3/#prf-extension)) — materialises a per-credential ~32-byte symmetric secret usable as a vault-key wrap key.
|
||||
- **largeBlob extension** ([W3C L3 §10.1.3](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension)) — per-credential blob read / write.
|
||||
- **DOMException → typed error mapping** at every adapter boundary; raw `DOMException` never leaks to callers.
|
||||
- **Constant-time** challenge / `rpIdHash` / credential id comparisons via `@suaveplan/crypto-utils`.
|
||||
- **OTel observability** — histograms, counters, format-dispatch metric.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Source | Docs |
|
||||
|---|---|---|
|
||||
| Adapter | `src/adapter/adapter.ts` | [adapter.md](src/adapter/adapter.md) |
|
||||
| DOMException mapper | `src/adapter/dom-exception-mapper.ts` | [dom-exception-mapper.md](src/adapter/dom-exception-mapper.md) |
|
||||
| Error codes | `src/errors/error-codes.ts` | [error-codes.md](src/errors/error-codes.md) |
|
||||
| Observability / metrics | `src/observability/metrics.ts` | [metrics.md](src/observability/metrics.md) |
|
||||
| Schemas | `src/schemas/schemas.ts` | [schemas.md](src/schemas/schemas.md) |
|
||||
| Client data | `src/parser/client-data.ts` | [client-data.md](src/parser/client-data.md) |
|
||||
| COSE_Key | `src/parser/cose-key.ts` | [cose-key.md](src/parser/cose-key.md) |
|
||||
| Authenticator data | `src/parser/authenticator-data.ts` | [authenticator-data.md](src/parser/authenticator-data.md) |
|
||||
| Attestation object | `src/parser/attestation-object.ts` | [attestation-object.md](src/parser/attestation-object.md) |
|
||||
| Registry | `src/verifier/registry.ts` | [registry.md](src/verifier/registry.md) |
|
||||
| Sign-counter | `src/verifier/sign-counter.ts` | [sign-counter.md](src/verifier/sign-counter.md) |
|
||||
| Verify assertion | `src/verifier/verify-assertion.ts` | [verify-assertion.md](src/verifier/verify-assertion.md) |
|
||||
| Verify attestation | `src/verifier/verify-attestation.ts` | [verify-attestation.md](src/verifier/verify-attestation.md) |
|
||||
| `none` | `src/attestation/none/none.ts` | [none.md](src/attestation/none/none.md) |
|
||||
| `packed` | `src/attestation/packed/packed.ts` | [packed.md](src/attestation/packed/packed.md) |
|
||||
| `fido-u2f` | `src/attestation/fido-u2f/fido-u2f.ts` | [fido-u2f.md](src/attestation/fido-u2f/fido-u2f.md) |
|
||||
| `tpm` | `src/attestation/tpm/tpm.ts` | [tpm.md](src/attestation/tpm/tpm.md) |
|
||||
| `android-key` | `src/attestation/android-key/android-key.ts` | [android-key.md](src/attestation/android-key/android-key.md) |
|
||||
| `android-safetynet` | `src/attestation/android-safetynet/android-safetynet.ts` | [android-safetynet.md](src/attestation/android-safetynet/android-safetynet.md) |
|
||||
| `apple` | `src/attestation/apple/apple.ts` | [apple.md](src/attestation/apple/apple.md) |
|
||||
| PRF extension | `src/extensions/prf/prf.ts` | [prf.md](src/extensions/prf/prf.md) |
|
||||
| largeBlob extension | `src/extensions/large-blob/large-blob.ts` | [large-blob.md](src/extensions/large-blob/large-blob.md) |
|
||||
| Register | `src/register/register.ts` | [register.md](src/register/register.md) |
|
||||
| Authenticate | `src/authenticate/authenticate.ts` | [authenticate.md](src/authenticate/authenticate.md) |
|
||||
|
||||
## Standards
|
||||
|
||||
- [W3C WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/) §5, §6, §7, §8, §9, §10
|
||||
- [CTAP 2.1](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html) §6.5 (Attestation Object)
|
||||
- [RFC 9052](https://datatracker.ietf.org/doc/html/rfc9052) §7 (COSE_Key)
|
||||
- [IANA WebAuthn registries](https://www.iana.org/assignments/webauthn)
|
||||
|
||||
## Subpath exports
|
||||
|
||||
- `.` — browser-tier root barrel.
|
||||
- `./adapter` — `WebAuthnAdapter` interface + `defaultWebAuthnAdapter`.
|
||||
- `./parser` — universal parsers (`parseAuthenticatorData`, `parseAttestationObject`, `parseClientDataJSON`, `parseCoseKey`).
|
||||
- `./verifier` — universal verifiers (`verifyAssertion`, `verifyAttestation`, `defaultAttestationRegistry`, `registerAttestationFormat`).
|
||||
- `./attestation/{packed,fido-u2f,tpm,android-key,android-safetynet,apple,none}` — per-format verifiers.
|
||||
- `./extensions/{prf,large-blob}` — extension passthrough + result parsing.
|
||||
- `./errors` — error code registry + `WebAuthnError`.
|
||||
|
||||
75
docs/identity/auth/webauthn/src/adapter/adapter.md
Normal file
75
docs/identity/auth/webauthn/src/adapter/adapter.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# WebAuthnAdapter — navigator.credentials abstraction
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - dom-exception-mapper — Converts every `DOMException…
|
||||
- [dom-exception-mapper](./dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- dom-exception-mapper — Converts every `DOMException…
|
||||
|
||||
- [dom-exception-mapper](./dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
|
||||
- dom-exception-mapper — Converts every `DOMException…
|
||||
|
||||
- [dom-exception-mapper](./dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
|
||||
- dom-exception-mapper — Converts every `DOMException…
|
||||
|
||||
- [dom-exception-mapper](./dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
- [dom-exception-mapper](./dom-exception-mapper.md) — Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a type…
|
||||
|
||||
## Purpose
|
||||
|
||||
Declares the `WebAuthnAdapter` interface that abstracts over `navigator.credentials.create` and `navigator.credentials.get`, plus the `defaultWebAuthnAdapter()` factory that binds the global `navigator.credentials` in production. The adapter is the only DOM-touching surface the package exposes — the parser and verifier are universal and consume only the `Credential` byte payload returned by the adapter. Tests inject a deterministic stub adapter so CI does not depend on a real authenticator or browser environment.
|
||||
|
||||
## Features
|
||||
|
||||
- Four-method interface: `create`, `get`, `isSupported`, `isConditionalMediationAvailable`.
|
||||
- Synchronous feature detection at construction time — `defaultWebAuthnAdapter()` throws `WEBAUTHN.NOT_SUPPORTED` when `globalThis.navigator?.credentials?.create` is missing (Node, worker, insecure context).
|
||||
- Standalone `isConditionalMediationAvailable()` wrapping the `PublicKeyCredential` static check, returning `false` when the static method is absent (older browsers / non-Chromium engines).
|
||||
- `globalRef` is injectable for testing without monkey-patching `globalThis`.
|
||||
- The adapter never mutates the options it is handed — it forwards `signal`, `timeout`, `mediation`, and every other field through unchanged.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultWebAuthnAdapter,
|
||||
isConditionalMediationAvailable,
|
||||
} from "@suaveplan/webauthn/adapter";
|
||||
|
||||
const adapter = defaultWebAuthnAdapter();
|
||||
if (await isConditionalMediationAvailable()) {
|
||||
// Show conditional UI on the input element.
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { WebAuthnAdapter } from "@suaveplan/webauthn/adapter";
|
||||
|
||||
const stub: WebAuthnAdapter = {
|
||||
async create() {
|
||||
return mockCredential;
|
||||
},
|
||||
async get() {
|
||||
return mockCredential;
|
||||
},
|
||||
isSupported: () => true,
|
||||
isConditionalMediationAvailable: async () => false,
|
||||
};
|
||||
const result = await register({ ...opts, adapter: stub });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebAuthnAdapter` — interface declaring `create(opts) => Promise<Credential | null>`, `get(opts) => Promise<Credential | null>`, `isSupported() => boolean`, `isConditionalMediationAvailable() => Promise<boolean>`.
|
||||
- `defaultWebAuthnAdapter(globalRef?): WebAuthnAdapter` — production factory; throws `WEBAUTHN.NOT_SUPPORTED` when navigator.credentials is unavailable.
|
||||
- `isConditionalMediationAvailable(globalRef?): Promise<boolean>` — wraps the static check, returns `false` on missing API.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The factory takes a `globalRef` parameter defaulting to `globalThis` so tests can inject `{ navigator: { credentials: stub } }` without monkey-patching the real globals.
|
||||
- `isConditionalMediationAvailable` wraps the underlying call in `try/catch` because Firefox occasionally throws synchronously when the API is being rolled out. Errors collapse to `false` so callers receive a stable boolean.
|
||||
- The adapter intentionally does NOT cache the result of `isConditionalMediationAvailable` — the answer can change at runtime when an authenticator is connected over USB or BLE, so callers should query it again when the user lands on a sign-in input.
|
||||
- `create` and `get` return `Credential | null`. The WebAuthn entry points (`register` / `authenticate`) reject with `WEBAUTHN.MALFORMED_RESPONSE` when the adapter returns `null`.
|
||||
@@ -0,0 +1,57 @@
|
||||
# DOMException → WebAuthnError mapper
|
||||
|
||||
## Purpose
|
||||
|
||||
Converts every `DOMException.name` raised by `navigator.credentials.create` and `navigator.credentials.get` into a typed `WebAuthnError`. Raw `DOMException` instances never reach the caller — the mapper is invoked at every adapter boundary so consumers can rely on `instanceof WebAuthnError` and `switch (err.code)` instead of pattern-matching on browser-side string names. The original `DOMException` is preserved on `err.cause` for forensic inspection.
|
||||
|
||||
## Features
|
||||
|
||||
- Five fully-specified mappings per the W3C WebAuthn L3 §5.1.3 / §5.1.4 informative error tables:
|
||||
- `NotAllowedError` → `WEBAUTHN.USER_CANCELLED`
|
||||
- `InvalidStateError` → `WEBAUTHN.INVALID_STATE`
|
||||
- `SecurityError` → `WEBAUTHN.SECURITY_ERROR`
|
||||
- `AbortError` → `WEBAUTHN.ABORTED`
|
||||
- `NotSupportedError` → `WEBAUTHN.NOT_SUPPORTED`
|
||||
- One additional mapping for `TimeoutError` → `WEBAUTHN.TIMEOUT`, which some browsers raise instead of `AbortError` on the `timeout` field.
|
||||
- Unknown error names map to `WEBAUTHN.MALFORMED_RESPONSE` with `cause` set so callers can still drill into the original error.
|
||||
- Pass-through for an already-typed `WebAuthnError`: the mapper is idempotent so registration / authentication wrappers can wrap it twice without losing the original `code`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { mapDomException } from "@suaveplan/webauthn/adapter";
|
||||
|
||||
try {
|
||||
await navigator.credentials.create({ publicKey });
|
||||
} catch (err) {
|
||||
throw mapDomException(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { mapDomException, WebAuthnError } from "@suaveplan/webauthn";
|
||||
|
||||
async function safeCreate(opts: CredentialCreationOptions) {
|
||||
try {
|
||||
return await navigator.credentials.create(opts);
|
||||
} catch (err) {
|
||||
const mapped = mapDomException(err);
|
||||
if (mapped.code === "WEBAUTHN.USER_CANCELLED") {
|
||||
// Tell the user politely, do not retry automatically.
|
||||
}
|
||||
throw mapped;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `mapDomException(err: unknown): WebAuthnError` — returns a `WebAuthnError`; preserves `cause`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The mapper reads `err.name` defensively so any value that ducktypes as `{ name: string }` is recognised. This is necessary because browsers occasionally throw `Error` subclasses that are not `DOMException` instances (older Safari).
|
||||
- `WebAuthnError` instances are returned unchanged — the mapper is idempotent so wrappers can compose without double-wrapping.
|
||||
- The fallback `WEBAUTHN.MALFORMED_RESPONSE` is intentionally distinct from `NOT_SUPPORTED` so the OTel `errors_total{code}` counter highlights authenticator misbehaviour separately from feature detection.
|
||||
@@ -0,0 +1,54 @@
|
||||
# `android-key` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.4 `android-key` attestation format used by Android platform authenticators backed by Android Keystore. The verifier confirms the leaf certificate carries the Android `KeyDescription` extension (OID `1.3.6.1.4.1.11129.2.1.17`), verifies the attestation signature against the credential public key, and surfaces the certificate chain for the caller to bind to a trust anchor.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict required-fields check: `alg`, `sig`, and `x5c` (>=1 cert).
|
||||
- Leaf-certificate extension check: the `KeyDescription` OID must be present. Missing extension surfaces `WEBAUTHN.MALFORMED_RESPONSE`.
|
||||
- Signature verification via `verifyOverCoseKey` against the credential's COSE_Key — Android Keystore signs with the same key that ends up in `authData`, so the cross-bind is implicit.
|
||||
- Constant-time `rpIdHash` compare.
|
||||
- Reports `attestationType: "basic"` and surfaces the chain on `trustPath`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyAndroidKeyAttestation } from "@suaveplan/webauthn/attestation/android-key";
|
||||
|
||||
const result = await verifyAndroidKeyAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
verifyAndroidKeyAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const registry = defaultAttestationRegistry();
|
||||
registry.set("android-key", async (input) => {
|
||||
const result = await verifyAndroidKeyAttestation(input);
|
||||
await assertAttestationChallenge(result.trustPath, input.clientDataHash);
|
||||
return result;
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyAndroidKeyAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `KeyDescription` extension carries fine-grained policy data: `attestationChallenge`, `softwareEnforced` AuthorizationList, `teeEnforced` AuthorizationList, etc. Decoding the full ASN.1 schema requires a dedicated parser; the verifier surfaces the chain via `trustPath` and leaves the structured decode to consumers that need it (the dominant use is binding `attestationChallenge` to `clientDataHash`).
|
||||
- Signature verification is performed against the credential's COSE_Key. Android Keystore's attestation contract ties the attestation key directly to the credential key — the leaf cert's SPKI equals the COSE_Key on every conforming Android device.
|
||||
- `trustPath` is the raw leaf-first DER chain. Production callers SHOULD chain-validate to the Google-published Android attestation root pool via `@suaveplan/x509#validateChain` (out of scope here because the root pool is environment-specific).
|
||||
@@ -0,0 +1,60 @@
|
||||
# `android-safetynet` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.5 `android-safetynet` attestation format. Google's SafetyNet API issues a JWS attestation token whose payload binds the credential creation challenge (`nonce = base64(sha256(authData || clientDataHash))`) to a Google-signed claim that the device passed the SafetyNet integrity checks. The verifier delegates JWS structural parsing to `@suaveplan/jws`, confirms the nonce, and surfaces the x5c chain for the caller to bind to the Google attestation root.
|
||||
|
||||
## Features
|
||||
|
||||
- JWS parsing via `@suaveplan/jws` — no inline JWT/JOSE decoding.
|
||||
- `nonce` claim cross-bind via constant-time compare against `base64(sha256(authData || clientDataHash))`.
|
||||
- `x5c` chain extracted from the JWS header, decoded into raw DER bytes, and surfaced via `trustPath`.
|
||||
- Constant-time `rpIdHash` compare.
|
||||
- Reports `attestationType: "basic"`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyAndroidSafetynetAttestation } from "@suaveplan/webauthn/attestation/android-safetynet";
|
||||
|
||||
const result = await verifyAndroidSafetynetAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
verifyAndroidSafetynetAttestation,
|
||||
type AttestationFormatRegistry,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
function strictSafetynetRegistry(): AttestationFormatRegistry {
|
||||
return new Map([
|
||||
[
|
||||
"android-safetynet",
|
||||
async (input) => {
|
||||
const r = await verifyAndroidSafetynetAttestation(input);
|
||||
await assertGoogleAttestationRoot(r.trustPath);
|
||||
return r;
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyAndroidSafetynetAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Google has deprecated SafetyNet in favour of the Play Integrity API. The format is retained because Android devices still emit it during the transition window; new deployments SHOULD NOT depend on it for new policy.
|
||||
- Full JWS signature verification against the Google attestation root is intentionally NOT performed here — the root cert pool changes and the caller's metadata layer is the right place for it. The verifier surfaces the x5c chain via `trustPath` and confirms the nonce binding so the caller can do the full chain check with the correct trust anchor.
|
||||
- The `nonce` is base64 (not base64url) per the SafetyNet spec; the verifier explicitly encodes via `btoa` rather than via `toBase64Url` to match.
|
||||
55
docs/identity/auth/webauthn/src/attestation/apple/apple.md
Normal file
55
docs/identity/auth/webauthn/src/attestation/apple/apple.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# `apple` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.8 `apple` anonymous attestation format used by macOS Touch ID, iOS Face ID, and other Apple platform authenticators. Apple's format is unusual: there is no signed `attStmt` field; instead, the leaf certificate in `x5c` carries an Apple-specific extension whose value is the SHA-256 of `authData || clientDataHash`. The verifier confirms that nonce binding and surfaces the chain as `anonCA` attestation per W3C §6.5.3.
|
||||
|
||||
## Features
|
||||
|
||||
- Apple nonce extension OID `1.2.840.113635.100.8.2` enforced on the leaf cert.
|
||||
- Extension content decoded via `@suaveplan/x509#decodeAsn1` — no inline ASN.1 walking.
|
||||
- Constant-time compare of the extracted nonce against `sha256(authData || clientDataHash)`.
|
||||
- Constant-time `rpIdHash` compare.
|
||||
- Reports `attestationType: "anonCA"` — Apple deliberately uses anonymous CA attestation so the format does not leak per-device identity to the relying party.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyAppleAttestation } from "@suaveplan/webauthn/attestation/apple";
|
||||
|
||||
const result = await verifyAppleAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
// result.attestationType === "anonCA"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
verifyAppleAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const registry = defaultAttestationRegistry();
|
||||
registry.set("apple", async (input) => {
|
||||
const r = await verifyAppleAttestation(input);
|
||||
await validateAppleAttestationRoot(r.trustPath);
|
||||
return r;
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyAppleAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The nonce extension is wrapped in `SEQUENCE { [1] EXPLICIT OCTET STRING <nonce> }` per Apple's documented schema; the verifier walks the decoded ASN.1 tree depth-first and grabs the first universal OCTET STRING it encounters, which is the documented contract.
|
||||
- Chain validation against the Apple WebAuthn attestation root is intentionally NOT performed here — that root is environment-specific and the caller's metadata layer is the right place for it. The verifier surfaces the x5c chain via `trustPath`.
|
||||
- `anonCA` is the only `attestationType` returned. RPs that need per-device telemetry MUST request a different attestation conveyance preference (`attestation: "direct"` does NOT produce per-device identity from Apple platform authenticators by design).
|
||||
@@ -0,0 +1,53 @@
|
||||
# `fido-u2f` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.6 `fido-u2f` attestation format, retained from the legacy CTAP1/U2F protocol so that existing U2F-only authenticators (early Yubikeys, hardware tokens predating CTAP2) remain interoperable with WebAuthn relying parties. The format is intentionally rigid: a single ECDSA-P256 attestation certificate, a SHA-256-then-sign signature over a fixed five-segment byte string, and a credential public key constrained to EC2 P-256.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict single-element `x5c` enforcement.
|
||||
- Credential-key type check: P-256 EC2 only.
|
||||
- Fixed-format signed payload: `0x00 || rpIdHash || clientDataHash || credentialId || (0x04 || x || y)` per W3C §8.6 step 3.
|
||||
- Constant-time `rpIdHash` compare.
|
||||
- Attestation cert's public key extracted via `@suaveplan/x509#parseCertificate` — no inline ASN.1 walking.
|
||||
- DER-encoded ECDSA signature converted to compact `r||s` via `decodeEcdsaSignature` from `@suaveplan/x509`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyFidoU2fAttestation } from "@suaveplan/webauthn/attestation/fido-u2f";
|
||||
|
||||
const result = await verifyFidoU2fAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type AttestationFormatRegistry,
|
||||
verifyFidoU2fAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
function registryWithFidoOnly(): AttestationFormatRegistry {
|
||||
return new Map([["fido-u2f", verifyFidoU2fAttestation]]);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyFidoU2fAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The format pre-dates WebAuthn and shipped with one specific signature variant; we reject any payload that does not conform rather than trying to be lenient.
|
||||
- `x5c.length === 1` is enforced because the U2F spec issues exactly one attestation cert per device; multi-cert chains in this slot indicate either a mis-bundled response or an attempt to chain-validate via a non-U2F format.
|
||||
- The package does NOT bind the attestation cert back to the FIDO U2F attestation-root pool — that policy belongs to the caller via FIDO MDS3 lookup on the returned `trustPath`.
|
||||
- The function is synchronous-shaped (no awaits inside) but typed as `Promise`-returning to match the `AttestationFormatVerifier` contract so callers can register either variant uniformly.
|
||||
52
docs/identity/auth/webauthn/src/attestation/none/none.md
Normal file
52
docs/identity/auth/webauthn/src/attestation/none/none.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# `none` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.7 `none` attestation format. This format is used when the RP requests `attestation: "none"` on registration — the authenticator returns the credential public key inside the `authData` segment but emits no attestation statement (no signature, no certificate chain). The verifier asserts the contract: an empty `attStmt` map. A non-empty map fails closed because it indicates protocol drift between the browser, authenticator, and RP.
|
||||
|
||||
## Features
|
||||
|
||||
- Empty-`attStmt` check rejects with `WEBAUTHN.MALFORMED_RESPONSE` on any non-empty payload.
|
||||
- Reports `attestationType: "none"` and `trustPath: []` so consumers know there is no trust anchor for policy decisions.
|
||||
- Synchronous — the verifier does no IO and no cryptography.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyNoneAttestation } from "@suaveplan/webauthn/attestation/none";
|
||||
|
||||
const result = verifyNoneAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
// result.trusted === true, result.attestationType === "none"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type AttestationFormatRegistry,
|
||||
verifyNoneAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
function customRegistry(): AttestationFormatRegistry {
|
||||
const r = new Map();
|
||||
r.set("none", verifyNoneAttestation);
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyNoneAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- W3C §8.7 specifies the `attStmt` SHALL be the empty CBOR map; emitting any key constitutes a protocol violation and we reject rather than silently accepting. Production authenticators do conform; we have not observed any wild-emission.
|
||||
- `trustPath: []` is significant — relying parties that require a trust anchor (FIDO MDS lookup) cannot satisfy the policy here and must reject the registration at a higher layer.
|
||||
- The verifier is the simplest of the seven and serves as the reference implementation pattern for the others: read `attestationObject`, optionally do crypto, return `{ trusted, attestationType, trustPath }`.
|
||||
63
docs/identity/auth/webauthn/src/attestation/packed/packed.md
Normal file
63
docs/identity/auth/webauthn/src/attestation/packed/packed.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `packed` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.2 `packed` attestation format, the most common format on modern authenticators (Yubikey 5, Feitian, every FIDO2-certified platform authenticator that emits non-self-signed attestations). The verifier supports both attestation paths: self-attestation (no `x5c`, signature made by the credential's own key) and full attestation (with `x5c` chain, signature made by a manufacturer-issued attestation key).
|
||||
|
||||
## Features
|
||||
|
||||
- Self-attestation detection by `attStmt.x5c` absence; signature verified directly against the embedded credential public key.
|
||||
- `alg`-match check: when the credential public key carries an `alg`, it MUST equal `attStmt.alg` per W3C §8.2 step 3.1.
|
||||
- Full-attestation path: caller-supplied trust binding via `trustPath` (the leaf-first DER chain). Chain validation against a trust anchor is intentionally left to the caller; the verifier confirms structural validity and surfaces the chain.
|
||||
- Constant-time `rpIdHash` compare on both paths.
|
||||
- Signature verification delegates to `verifyOverCoseKey` from `verify-assertion`, so the same algorithm dispatch (ES256 / ES384 / ES512 / EdDSA / RS256) applies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyPackedAttestation } from "@suaveplan/webauthn/attestation/packed";
|
||||
|
||||
const result = await verifyPackedAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
if (result.attestationType === "self") {
|
||||
// RP policy: accept self-attestation for low-assurance flows.
|
||||
} else {
|
||||
await validateChainAgainstMds(result.trustPath);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
verifyPackedAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const registry = defaultAttestationRegistry();
|
||||
// Override with a custom packed verifier that also enforces a max chain depth:
|
||||
registry.set("packed", async (input) => {
|
||||
const r = await verifyPackedAttestation(input);
|
||||
if (r.trustPath.length > 3) {
|
||||
throw new Error("chain too deep");
|
||||
}
|
||||
return r;
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyPackedAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The verifier does not perform `x509.validateChain` against a trust anchor — that requires an out-of-band trust source (FIDO Metadata Service, per-RP attestation-CA allow-list) that the package cannot ship. The caller wires that policy via the returned `trustPath`.
|
||||
- Full-attestation `attestationType` is reported as `"basic"` rather than `"attCA"` because the W3C spec treats `packed` with `x5c` as basic attestation; `attCA` is reserved for `tpm` and `android-key` where the chain goes back to a manufacturer-rooted certification authority.
|
||||
- The signed payload is `authData || clientDataHash` — concatenated raw — per §8.2 step 2. The signature verifier hashes the message internally per the algorithm (ECDSA hashes inside; Ed25519 signs over the full message; RSA hashes inside).
|
||||
- `alg` is read as the COSE numeric label (-7 for ES256, etc.). The credential key's `alg` field is read as the named string the COSE_Key decoder produces — we cross-reference via the `NAMED_ALG_TO_LABEL` map.
|
||||
58
docs/identity/auth/webauthn/src/attestation/tpm/tpm.md
Normal file
58
docs/identity/auth/webauthn/src/attestation/tpm/tpm.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# `tpm` attestation verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §8.3 `tpm` attestation format used by Windows Hello and other TPM 2.0-backed platform authenticators. The verifier parses the TCG-defined `TPMS_ATTEST` and `TPMT_PUBLIC` structures via `@suaveplan/tpm-struct`, cross-binds them to the credential public key in `authData`, verifies the extra-data digest under the algorithm specified by `attStmt.alg`, and checks the signature against the Attestation Identity Key certificate chain.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict `ver === "2.0"` enforcement — TPM 1.2 attestation is intentionally unsupported.
|
||||
- Three boundary parses: `parseTpmsAttest`, `parseTpmtPublic`, and `parseCertificate` — every byte payload goes through a shared library and never an inline ASN.1 walk.
|
||||
- `TPM_ST_ATTEST_CERTIFY` type pinning — only the `certify` flavour is accepted (other TPM attest types are not valid here).
|
||||
- Constant-time `extraData` and `name` compares.
|
||||
- Modulus cross-bind: when the credential is RSA and `pubArea.type === TPM_ALG_RSA`, the modulus on `pubArea.unique.modulus` must equal the COSE_Key `n`.
|
||||
- Signature verification against the AIK leaf cert via `rsaVerifyPkcs1v15` with `hash: "SHA-256"` (the Windows Hello case). The package supports RSA AIK signatures; ECC AIK keys are accepted only when the leaf cert exposes them via SPKI, but the dominant TPM 2.0 deployments are all RSA.
|
||||
- Reports `attestationType: "attCA"` per W3C §6.5.3 because the chain anchors to a manufacturer-rooted attestation CA.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyTpmAttestation } from "@suaveplan/webauthn/attestation/tpm";
|
||||
|
||||
const result = await verifyTpmAttestation({
|
||||
attestationObject,
|
||||
clientDataHash,
|
||||
expectedRpIdHash,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
verifyTpmAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const r = defaultAttestationRegistry();
|
||||
r.set("tpm", async (input) => {
|
||||
const inner = await verifyTpmAttestation(input);
|
||||
await checkTpmCaAllowlist(inner.trustPath);
|
||||
return inner;
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `verifyTpmAttestation: AttestationFormatVerifier` — the format verifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The verifier delegates `TPMS_ATTEST` and `TPMT_PUBLIC` parsing entirely to `@suaveplan/tpm-struct`; the binary TCG structures are NOT re-implemented here.
|
||||
- `extraData` is `hash(authData || clientDataHash)` per W3C §8.3 step 4 — the algorithm is `attStmt.alg` (a TPM_ALG_ID label, not a COSE alg label).
|
||||
- `name` is `computeTpmtPublicName(pubArea)` — a hash-prefixed digest computed by `@suaveplan/tpm-struct` per TPM 2.0 Part 1 §16. The constant-time compare ensures the credential public key in `authData` matches the one in `pubArea`.
|
||||
- Only RSA AIK signature verification is wired in this initial cut. ECC AIK keys exist in the wild (TPM 2.0 supports them) but every Windows Hello deployment encountered in production uses RSA-2048 AIK keys. ECC support is a clean extension via the same `verifyOverCoseKey` dispatcher.
|
||||
- The package does NOT itself validate the AIK cert chain against a TPM-manufacturer root pool — that policy belongs to the caller via the returned `trustPath` and an out-of-band attestation-CA allow-list.
|
||||
66
docs/identity/auth/webauthn/src/authenticate/authenticate.md
Normal file
66
docs/identity/auth/webauthn/src/authenticate/authenticate.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Authentication entry point
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements `authenticate(opts)` — the W3C WebAuthn L3 §5.1.4 / §7.2 authentication ceremony entry point. The function builds a `PublicKeyCredentialRequestOptions`, invokes the adapter (defaulting to `navigator.credentials.get`), parses the returned `AuthenticatorAssertionResponse` into a typed `AuthenticateResult`, and enforces user-presence / user-verification flag policy. Cryptographic verification of the signature lives in `verifyAssertion` (called by the caller against stored credential state).
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated options; `INVALID_OPTIONS` surfaces before the adapter is touched.
|
||||
- Pre-aborted `AbortSignal` short-circuit; mid-flight aborts map to `WEBAUTHN.ABORTED` via DOMException mapper.
|
||||
- Conditional UI passthrough — `mediation: "conditional"` is forwarded into the underlying `navigator.credentials.get` call so consumers get autofill behaviour.
|
||||
- Client-data validation pinned to `type === "webauthn.get"`.
|
||||
- UV / UP flag enforcement: `WEBAUTHN.UV_NOT_PERFORMED` when `userVerification: "required"` and the UV flag is absent; `WEBAUTHN.UP_NOT_PERFORMED` when the UP flag is absent.
|
||||
- PRF and largeBlob result extraction surfaced on `result.extensions`.
|
||||
- OTel: `webauthn.authenticate_duration_ms{outcome}` + `webauthn.outcome_total{op=authenticate,outcome}`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { authenticate } from "@suaveplan/webauthn";
|
||||
|
||||
const result = await authenticate({
|
||||
challenge: challengeBytes,
|
||||
rpId: "example.com",
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash: rpIdHashBytes,
|
||||
userVerification: "required",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { authenticate } from "@suaveplan/webauthn";
|
||||
|
||||
const controller = new AbortController();
|
||||
const result = await authenticate({
|
||||
challenge,
|
||||
rpId: "example.com",
|
||||
mediation: "conditional", // Conditional UI / autofill
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash: rpIdHash,
|
||||
extensions: {
|
||||
prf: { eval: { first: prfSalt } },
|
||||
largeBlob: { read: true },
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
const wrapKey = result.extensions.prf?.results?.first;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebAuthnAuthenticateOptions` — extends `WebAuthnAuthenticateOptionsInput`; adds `adapter`, `signal`.
|
||||
- `AuthenticateResult` — `{ credentialId, signature, authenticatorData, parsedAuthenticatorData, clientDataJSON, userHandle?, extensions }`.
|
||||
- `authenticate(opts): Promise<AuthenticateResult>` — the entry point.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `mediation` is a string union and forwarded as-is on the `CredentialRequestOptions` object — the package never branches on its value internally.
|
||||
- `parsedAuthenticatorData` is returned alongside the raw `authenticatorData` bytes so consumers can immediately inspect flags (e.g. `result.parsedAuthenticatorData.flags.backupEligible`) without re-parsing.
|
||||
- The largeBlob extension result is discriminated based on the input shape (`read` vs `write`), so `result.extensions.largeBlob` is narrowed to `LargeBlobReadResult | LargeBlobWriteResult`.
|
||||
- Signature verification is intentionally NOT performed here — `authenticate` consumes only what the browser returned and doesn't have a stored COSE_Key to verify against. The caller pairs this with `verifyAssertion({ result, storedPublicKey, ... })`.
|
||||
51
docs/identity/auth/webauthn/src/errors/error-codes.md
Normal file
51
docs/identity/auth/webauthn/src/errors/error-codes.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# WebAuthn error codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the frozen registry of every typed error code raised across the `@suaveplan/webauthn` surface. The codes mirror the W3C WebAuthn Level 3 failure classes (§5, §6, §7, §8, §10) and the CTAP 2.1 attestation-object failure points so consumers can `switch (err.code)` against a stable enum instead of pattern-matching on free-form messages. The registry is the single source of truth for the observability counter `webauthn.errors_total{code}` and for the DOMException mapper at the adapter boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `WEBAUTHN_CODES` registry built via `defineCodes("WEBAUTHN", [...])`.
|
||||
- Concrete `WebAuthnError` subclass of `SuaveplanError<WebAuthnErrorCode>`.
|
||||
- Literal union `WebAuthnErrorCode` pinned with `expectTypeOf` assertions in tests so any drift between the registry and the union fails typecheck.
|
||||
- Every code raised by at least one production path; codes that exist only to placate the conformance harness are forbidden (`AGENTS.md` §2c).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WEBAUTHN_CODES, WebAuthnError } from "@suaveplan/webauthn/errors";
|
||||
|
||||
throw new WebAuthnError(
|
||||
WEBAUTHN_CODES.CHALLENGE_MISMATCH,
|
||||
"error.webauthn.challenge-mismatch",
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { WebAuthnError, type WebAuthnErrorCode } from "@suaveplan/webauthn/errors";
|
||||
|
||||
function classify(err: unknown): WebAuthnErrorCode | "unknown" {
|
||||
if (err instanceof WebAuthnError) {
|
||||
return err.code;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WEBAUTHN_CODES` — frozen object mapping every code identifier (`NOT_SUPPORTED`, `USER_CANCELLED`, ...) to its dotted string (`"WEBAUTHN.NOT_SUPPORTED"`).
|
||||
- `WebAuthnErrorCode` — literal union of every value in `WEBAUTHN_CODES`.
|
||||
- `WebAuthnError` — extends `SuaveplanError<WebAuthnErrorCode>`; accepts `(code, messageKey, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Codes are intentionally `SCREAMING_SNAKE` so they survive minification.
|
||||
- The order in the `defineCodes` array is alphabetised within W3C section groupings: lifecycle (`NOT_SUPPORTED`, `USER_CANCELLED`, ...), validation (`MALFORMED_RESPONSE`, `CHALLENGE_MISMATCH`, ...), signature (`SIGNATURE_INVALID`, ...), counter (`SIGN_COUNTER_REGRESSION`), attestation (`ATTESTATION_FORMAT_UNSUPPORTED`), flag (`UV_NOT_PERFORMED`, `UP_NOT_PERFORMED`), cancellation (`ABORTED`, `TIMEOUT`), options validation (`INVALID_OPTIONS`).
|
||||
- `WEBAUTHN.ABORTED` and `WEBAUTHN.TIMEOUT` are kept distinct from `USER_CANCELLED` so the OTel `errors_total{code}` counter slices AbortSignal cancellation, authenticator-side timeout, and explicit user denial separately.
|
||||
@@ -0,0 +1,80 @@
|
||||
# largeBlob extension support
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §10.1.3 largeBlob extension passthrough plus result parsing. The largeBlob extension lets the authenticator store a small per-credential byte string (typically credentials or session-resumption tokens) inside the authenticator's secure element. Registration asks the authenticator to allocate the slot; subsequent authentications read or write the blob.
|
||||
|
||||
## Features
|
||||
|
||||
- `LargeBlobRegisterInput` (`{ support: "required" | "preferred" }`) and discriminated `LargeBlobAuthenticateInput` (`{ read: true }` xor `{ write: Uint8Array }`) — W3C-mandated mutual exclusion.
|
||||
- `readLargeBlobRegisterResult` extracts `{ supported }` from the registration ceremony's `ClientExtensionResults`.
|
||||
- `readLargeBlobReadResult` extracts `{ blob? }` from the assertion ceremony's results — `blob` is omitted (not zeroed) when the authenticator did not return one.
|
||||
- `readLargeBlobWriteResult` extracts `{ written }` — boolean discriminator between success and silent drop.
|
||||
- `buildLargeBlobRegisterExtension` / `buildLargeBlobAuthenticateExtension` produce the wire-shape passed to `navigator.credentials.{create,get}`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildLargeBlobAuthenticateExtension,
|
||||
readLargeBlobReadResult,
|
||||
} from "@suaveplan/webauthn/extensions/large-blob";
|
||||
|
||||
const ext = buildLargeBlobAuthenticateExtension({ read: true });
|
||||
const cred = await navigator.credentials.get({
|
||||
publicKey: { challenge, allowCredentials, extensions: ext }
|
||||
});
|
||||
const result = readLargeBlobReadResult(
|
||||
(cred as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
console.log(result?.blob?.byteLength ?? 0);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildLargeBlobAuthenticateExtension,
|
||||
buildLargeBlobRegisterExtension,
|
||||
readLargeBlobRegisterResult,
|
||||
readLargeBlobWriteResult,
|
||||
} from "@suaveplan/webauthn/extensions/large-blob";
|
||||
|
||||
const regExt = buildLargeBlobRegisterExtension({ support: "required" });
|
||||
const reg = await navigator.credentials.create({
|
||||
publicKey: { ...opts, extensions: regExt }
|
||||
});
|
||||
const regResult = readLargeBlobRegisterResult(
|
||||
(reg as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!regResult?.supported) {
|
||||
throw new Error("authenticator does not support largeBlob");
|
||||
}
|
||||
|
||||
const writeExt = buildLargeBlobAuthenticateExtension({ write: payload });
|
||||
const authResult = await navigator.credentials.get({
|
||||
publicKey: { ...authOpts, extensions: writeExt }
|
||||
});
|
||||
const w = readLargeBlobWriteResult(
|
||||
(authResult as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!w?.written) {
|
||||
throw new Error("write dropped");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LargeBlobRegisterInput`, `LargeBlobAuthenticateInput` — input shapes.
|
||||
- `LargeBlobRegisterResult`, `LargeBlobReadResult`, `LargeBlobWriteResult` — result shapes.
|
||||
- `readLargeBlobRegisterResult`, `readLargeBlobReadResult`, `readLargeBlobWriteResult` — narrowing readers.
|
||||
- `buildLargeBlobRegisterExtension`, `buildLargeBlobAuthenticateExtension` — wire-shape builders.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `support: "required"` mode causes the registration to fail (with `NotSupportedError`) when the authenticator cannot allocate a largeBlob slot; `support: "preferred"` allocates if possible and falls back silently otherwise. The result's `supported` flag distinguishes the two outcomes.
|
||||
- Read and write are mutually exclusive per W3C §10.1.3. The discriminated input union enforces this at the type level so callers cannot accidentally pass both.
|
||||
- `readLargeBlobReadResult` returns `{}` (empty object, not `undefined`) when the extension was acknowledged but no `blob` was returned. This lets callers distinguish "authenticator does not implement the extension" (`undefined` return) from "extension implemented, no blob stored" (`{ blob: undefined }`).
|
||||
62
docs/identity/auth/webauthn/src/extensions/prf/prf.md
Normal file
62
docs/identity/auth/webauthn/src/extensions/prf/prf.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# PRF extension support
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §10.1.4 PRF (Pseudo-Random Function) extension passthrough plus result parsing. PRF is the cornerstone of passkey-as-master-key-unlock flows: the authenticator's secure element materialises a ~32-byte symmetric secret deterministic in the (credential id, caller-supplied salt) pair. Consumers (notably `@suaveplan/vault`) use the secret to wrap an Account Unlock Key so a vault unlocks without the user typing a master password.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed input shapes for both ceremonies: `PrfRegisterInput` (`{ eval: { first, second? } }`) and `PrfAuthenticateInput` (`{ eval | evalByCredential }`).
|
||||
- `readPrfResult(extensions)` extracts the `prf` field from the raw `ClientExtensionResults` object, narrowing types so callers consume `Uint8Array`s rather than `unknown`.
|
||||
- `buildPrfExtension(input)` produces the wire-shape forwarded to `navigator.credentials.create/get` so callers do not duplicate the structural construction.
|
||||
- Three-state result: `enabled === false` (not supported), `enabled === true` with no `results` (supported but caller did not request an eval), `enabled === true` with `results.first: Uint8Array` (the materialised secret).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readPrfResult } from "@suaveplan/webauthn/extensions/prf";
|
||||
|
||||
const prf = readPrfResult(authenticationResult.extensions);
|
||||
if (prf?.enabled && prf.results?.first) {
|
||||
const wrapKey = prf.results.first;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildPrfExtension,
|
||||
readPrfResult,
|
||||
} from "@suaveplan/webauthn/extensions/prf";
|
||||
|
||||
const ext = buildPrfExtension({
|
||||
eval: { first: new Uint8Array(32) }
|
||||
});
|
||||
const cred = await navigator.credentials.get({
|
||||
publicKey: { challenge, allowCredentials, extensions: ext },
|
||||
});
|
||||
const prf = readPrfResult(
|
||||
(cred as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!prf?.enabled) {
|
||||
throw new Error("authenticator does not support PRF");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PrfRegisterInput` — `{ eval?: { first: Uint8Array, second?: Uint8Array } }`.
|
||||
- `PrfAuthenticateInput` — `{ eval?, evalByCredential? }`.
|
||||
- `PrfRegisterResult` / `PrfAuthenticateResult` — `{ enabled: boolean, results?: { first: Uint8Array, second?: Uint8Array } }`.
|
||||
- `readPrfResult(extensions): PrfRegisterResult | undefined` — narrow + extract.
|
||||
- `buildPrfExtension(input): { prf?: ... }` — wire-shape builder.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The package does NOT request or surface a `prf` second-input by default. Consumers that need two-input PRF (key rotation flows) opt in by setting `eval.second`.
|
||||
- The `enabled` field is set to `true` by some Chromium platform authenticators even when the caller did not request an eval; consumers SHOULD treat `enabled === true` without `results` as "PRF is available, request it next time" rather than "secret is materialised".
|
||||
- `readPrfResult` is intentionally tolerant of missing fields — it returns `undefined` when no PRF extension is present at all, distinguishing that from `{ enabled: false }` (PRF requested but not supported by the authenticator).
|
||||
70
docs/identity/auth/webauthn/src/observability/metrics.md
Normal file
70
docs/identity/auth/webauthn/src/observability/metrics.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# WebAuthn observability — metrics
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines every OTel instrument emitted by `@suaveplan/webauthn`. The registry is a single bag of pre-created `Histogram` / `Counter` instances cached at module-load time so production paths can record without per-call allocation. The cache resets via `resetMetricsForTesting()` so a fresh in-memory `MeterProvider` registered in `beforeEach` produces deterministic assertions.
|
||||
|
||||
## Features
|
||||
|
||||
- Five instruments cover every production path: `webauthn.register_duration_ms`, `webauthn.authenticate_duration_ms`, `webauthn.outcome_total`, `webauthn.attestation_format_total`, `webauthn.errors_total`.
|
||||
- Bounded label cardinality: `outcome` ∈ `{ ok | error | aborted }`, `op` ∈ `{ register | authenticate }`, `fmt` ∈ the seven IANA-registered formats plus `unknown`.
|
||||
- `recordRegister` / `recordAuthenticate` write both the duration histogram and the outcome counter atomically so dashboards never disagree.
|
||||
- `resetMetricsForTesting` / `getInstrumentsForTesting` keep test hooks out of the runtime module surface but available to spec tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
nowMs,
|
||||
recordRegister,
|
||||
} from "@suaveplan/webauthn/observability/metrics";
|
||||
|
||||
const start = nowMs();
|
||||
try {
|
||||
await doRegister();
|
||||
recordRegister("ok", nowMs() - start);
|
||||
} catch (err) {
|
||||
recordRegister("error", nowMs() - start);
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MeterProvider, InMemoryMetricExporter } from "@opentelemetry/sdk-metrics";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
resetMetricsForTesting,
|
||||
recordAttestationFormat,
|
||||
} from "@suaveplan/webauthn/observability/metrics";
|
||||
|
||||
const exporter = new InMemoryMetricExporter();
|
||||
const provider = new MeterProvider({ readers: [/* periodic reader */] });
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
resetMetricsForTesting();
|
||||
|
||||
recordAttestationFormat("packed");
|
||||
const points = await exporter.collect();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebAuthnOutcome` — `"ok" | "error" | "aborted"`.
|
||||
- `WebAuthnOp` — `"register" | "authenticate"`.
|
||||
- `WebAuthnAttestationFmt` — `"packed" | "fido-u2f" | "tpm" | "android-key" | "android-safetynet" | "apple" | "none" | "unknown"`.
|
||||
- `recordRegister(outcome, durationMs)` — record both histogram + outcome counter.
|
||||
- `recordAuthenticate(outcome, durationMs)` — same for the assertion path.
|
||||
- `recordAttestationFormat(fmt)` — increment dispatch counter.
|
||||
- `recordError(code)` — increment `webauthn.errors_total{code}`.
|
||||
- `nowMs()` — wall-clock reader.
|
||||
- `resetMetricsForTesting()` / `getInstrumentsForTesting()` — test hooks.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The instrument bag is lazily created on first call so the meter resolves against whatever `MeterProvider` the host application has installed.
|
||||
- The reset hook is mandatory because OTel meter caching makes pre-registered instruments outlive a test's MeterProvider swap; without it a second test reads metrics from the first test's exporter.
|
||||
- `nowMs()` is exposed as a function rather than `Date.now` directly so future tests can stub it without monkey-patching `globalThis.Date`.
|
||||
50
docs/identity/auth/webauthn/src/parser/attestation-object.md
Normal file
50
docs/identity/auth/webauthn/src/parser/attestation-object.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Attestation-object parser
|
||||
|
||||
## Purpose
|
||||
|
||||
Decodes the CTAP 2.1 §6.5 attestation-object envelope returned by `navigator.credentials.create()`. The envelope is a top-level CBOR map with `fmt` (format identifier), `authData` (the opaque authenticator-data byte string the signature was made over), and `attStmt` (the format-specific attestation statement). The parser returns the parsed structure plus the already-decoded `ParsedAuthData` so per-format verifiers do not re-parse the authenticator data.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict CBOR major-type checking — top level MUST be a map, `fmt` MUST be a text-string, `authData` MUST be a byte-string, `attStmt` MUST be a map.
|
||||
- Non-string `attStmt` keys reject with `WEBAUTHN.MALFORMED_RESPONSE` — only string-keyed maps are valid per CTAP 2.1.
|
||||
- Embedded `authData` is parsed via the universal authenticator-data parser; the per-format verifier consumes both the raw bytes (for signature verification) and the parsed structure (for flag inspection).
|
||||
- Zod schema validation on `ParsedAttestationStmt` so the public contract is stable.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseAttestationObject } from "@suaveplan/webauthn/parser";
|
||||
|
||||
const env = parseAttestationObject(attestationObjectBytes);
|
||||
console.log(env.fmt, env.parsedAuthData.flags.userVerified);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseAttestationObject } from "@suaveplan/webauthn/parser";
|
||||
|
||||
const env = parseAttestationObject(attestationObjectBytes);
|
||||
switch (env.fmt) {
|
||||
case "packed":
|
||||
return verifyPacked(env, clientDataHash);
|
||||
case "tpm":
|
||||
return verifyTpm(env, clientDataHash);
|
||||
case "none":
|
||||
return verifyNone(env);
|
||||
default:
|
||||
throw new Error("unsupported attestation format");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ParsedAttestationObject` — `{ fmt: string, authData: Uint8Array, parsedAuthData: ParsedAuthData, attStmt: ParsedAttestationStmt }`.
|
||||
- `parseAttestationObject(bytes: Uint8Array): ParsedAttestationObject` — full decode + structural validation; throws `WEBAUTHN.MALFORMED_RESPONSE` on every failure mode.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `authData` byte string is kept raw on the returned envelope because every per-format verifier signs over the exact bytes the authenticator emitted — re-serialising via `serializeAuthenticatorData` would produce canonical bytes that may not match the authenticator's wire encoding.
|
||||
- `decode` from `@suaveplan/cbor` is used in strict mode (default) so trailing bytes after the top-level map are rejected. This is intentional — a well-formed attestation object has no padding.
|
||||
- The parser is universal; it imports zero DOM types and can be consumed server-side by relying-party verifiers.
|
||||
59
docs/identity/auth/webauthn/src/parser/authenticator-data.md
Normal file
59
docs/identity/auth/webauthn/src/parser/authenticator-data.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Authenticator-data parser
|
||||
|
||||
## Purpose
|
||||
|
||||
Splits the binary `authenticatorData` byte string emitted by the authenticator into a typed `ParsedAuthData` structure per W3C WebAuthn L3 §6.1 — `rpIdHash`, `flags`, `signCount`, the optional attested-credential-data segment (`aaguid`, `credentialId`, `credentialPublicKey: COSE_Key`), and the optional CBOR `extensions` map. The parser is the universal core that the registration / authentication ceremonies and every attestation-format verifier depend on; getting this byte split right is the difference between accepting a valid passkey and accepting a forged one.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict fixed-prefix split: `rpIdHash[32]` + `flags[1]` + `signCount[4]` (big-endian u32) per W3C L3 §6.1.
|
||||
- Six-flag byte decoder via `decodeFlagsByte` — UP (0x01), UV (0x04), BE (0x08), BS (0x10), AT (0x40), ED (0x80). Reserved bits 1 and 5 are parsed but not surfaced.
|
||||
- Conditional attested-credential-data parse when the AT flag is set: `aaguid[16]` + `credentialIdLength[2]` (big-endian u16) + `credentialId[L]` + `credentialPublicKey: COSE_Key`. The embedded COSE_Key is decoded via `parseCoseKeyOne` (CBOR-length-driven) so the parser does not need an out-of-band length hint.
|
||||
- Conditional extensions block parse when the ED flag is set: a single CBOR map, decoded via `@suaveplan/cbor#decodeOne`, with non-string keys rejected.
|
||||
- Trailing-bytes rejection: the parser asserts the cursor lands exactly on `bytes.byteLength` so a truncated frame and an over-long frame both throw `WEBAUTHN.MALFORMED_RESPONSE`.
|
||||
- Zod schema validation after structural parse so the resulting object exactly matches the public `ParsedAuthData` contract.
|
||||
- Round-trip serialiser `serializeAuthenticatorData` for property-test re-parse stability.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseAuthenticatorData } from "@suaveplan/webauthn/parser";
|
||||
|
||||
const parsed = parseAuthenticatorData(authData);
|
||||
if (!parsed.flags.userPresent) {
|
||||
throw new Error("authenticator did not assert UP");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
decodeFlagsByte,
|
||||
parseAuthenticatorData,
|
||||
serializeAuthenticatorData,
|
||||
} from "@suaveplan/webauthn/parser";
|
||||
|
||||
const parsed = parseAuthenticatorData(authData);
|
||||
const roundTripped = parseAuthenticatorData(serializeAuthenticatorData(parsed));
|
||||
expect(roundTripped).toEqual(parsed);
|
||||
|
||||
const flags = decodeFlagsByte(0xc5);
|
||||
expect(flags.userPresent).toBe(true);
|
||||
expect(flags.userVerified).toBe(true);
|
||||
expect(flags.attestedCredentialData).toBe(true);
|
||||
expect(flags.extensionsPresent).toBe(true);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `decodeFlagsByte(byte: number): ParsedAuthDataFlags` — pure flag-byte decoder.
|
||||
- `parseAuthenticatorData(bytes: Uint8Array): ParsedAuthData` — full parser; throws `WEBAUTHN.MALFORMED_RESPONSE` on truncation, invalid CBOR, COSE_Key shape mismatch, non-string extension keys, or trailing bytes.
|
||||
- `serializeAuthenticatorData(data: ParsedAuthData): Uint8Array` — round-trip encoder; the COSE_Key segment and extensions map are re-encoded in canonical form so the output is not necessarily byte-identical to the input.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `signCount` field is read as big-endian u32 via `DataView.getUint32` because WebAuthn fixes the byte order. Authenticators that increment a counter pin its non-zero value at registration so the package can detect rollback via `verifyAssertion`'s sign-counter regression check.
|
||||
- `parseCoseKeyOne` is used rather than slicing-and-decoding because the wire format does not include the COSE_Key length. The CBOR decoder learns the length from the byte stream itself.
|
||||
- The trailing-bytes rejection is intentional: a well-formed authenticator emits exactly `prefix + (AT ? cred-data : '') + (ED ? extensions : '')` bytes. An over-long frame likely indicates a parser desync earlier in the stack and is unsafe to accept.
|
||||
- `serializeAuthenticatorData` is paired with `parseAuthenticatorData` only for property-test round-trip stability — production code never re-serialises authenticator data because the signature was bound to the original byte sequence.
|
||||
53
docs/identity/auth/webauthn/src/parser/client-data.md
Normal file
53
docs/identity/auth/webauthn/src/parser/client-data.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Client data — parse + validate
|
||||
|
||||
## Purpose
|
||||
|
||||
Decodes the `clientDataJSON` byte string returned by `navigator.credentials.create` / `get` into a typed object and validates its `type`, `origin`, and `challenge` fields against the relying-party's expectations. The `clientDataJSON` is the authenticator's anti-replay anchor — getting this validation right is the difference between a real WebAuthn deployment and a token-passing protocol that any phishing site can replay.
|
||||
|
||||
## Features
|
||||
|
||||
- UTF-8 decode with `fatal: true` so any non-UTF-8 byte rejects with `WEBAUTHN.MALFORMED_RESPONSE`.
|
||||
- JSON parse with cause-chain preservation.
|
||||
- Zod `ClientDataJSONSchema` enforces `type`, `challenge`, `origin` fields; extra fields pass through for forward compatibility with future W3C revisions (`topOrigin`, `crossOrigin`, payment-extension fields).
|
||||
- Discriminated `expectedType` argument — `"webauthn.create"` for registration, `"webauthn.get"` for authentication — so callers cannot accidentally accept the wrong ceremony's response.
|
||||
- Constant-time `origin` and `challenge` compares via `constantTimeEqual` from `@suaveplan/crypto-utils`.
|
||||
- Base64url decode of `challenge` before compare so the byte-level comparison happens after canonicalisation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseClientDataJSON } from "@suaveplan/webauthn/parser";
|
||||
|
||||
const data = parseClientDataJSON(response.clientDataJSON);
|
||||
console.log(data.type, data.origin);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CLIENT_DATA_TYPE_CREATE,
|
||||
validateClientDataJSON,
|
||||
} from "@suaveplan/webauthn/parser";
|
||||
|
||||
validateClientDataJSON({
|
||||
clientDataJSON: response.clientDataJSON,
|
||||
expectedType: CLIENT_DATA_TYPE_CREATE,
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedChallenge: storedChallengeBytes,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CLIENT_DATA_TYPE_CREATE = "webauthn.create"` — registration discriminator.
|
||||
- `CLIENT_DATA_TYPE_GET = "webauthn.get"` — assertion discriminator.
|
||||
- `parseClientDataJSON(bytes: Uint8Array): ClientDataJSON` — decode + schema validate; throws `WEBAUTHN.MALFORMED_RESPONSE`.
|
||||
- `validateClientDataJSON(input: { clientDataJSON, expectedType, expectedOrigin, expectedChallenge }): ClientDataJSON` — full validation; throws `WEBAUTHN.MALFORMED_RESPONSE` (type), `WEBAUTHN.ORIGIN_MISMATCH`, or `WEBAUTHN.CHALLENGE_MISMATCH` on the appropriate failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The challenge is compared byte-for-byte after base64url decode rather than string-for-string so authenticators that emit base64url with or without padding (some Yubikey firmware does the latter on long challenges) all canonicalise to the same byte string.
|
||||
- `origin` is compared as UTF-8 bytes via `constantTimeEqual` rather than via `===` so a mismatch in the high-bit-set portion of an IDN origin (e.g. punycode-encoded host) takes time independent of the byte index of the difference.
|
||||
- The `passthrough()` on `ClientDataJSONSchema` is deliberate — the W3C spec reserves the right to add fields (`tokenBinding`, `topOrigin`, payment extension fields) and we do not want the parser to reject conforming new browsers.
|
||||
- `parseClientDataJSON` is the lower-level building block; consumers that already validated `type` / `origin` / `challenge` elsewhere can call it directly. `validateClientDataJSON` is the one-shot entry point used by `register` and `authenticate`.
|
||||
53
docs/identity/auth/webauthn/src/parser/cose-key.md
Normal file
53
docs/identity/auth/webauthn/src/parser/cose-key.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# COSE_Key parser
|
||||
|
||||
## Purpose
|
||||
|
||||
Decodes a `COSE_Key` byte string (RFC 9052 §7) into the typed `CoseKey` discriminated union supplied by `@suaveplan/cbor/cose`. The WebAuthn package never re-implements CBOR — the parser is a thin error-translation wrapper plus an inline `parseCoseKeyOne` helper that the authenticator-data parser uses to extract the embedded credential public key without knowing its CBOR-encoded length up-front.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict-mode delegation to `@suaveplan/cbor/cose#decodeCoseKey` for fully-sized buffers.
|
||||
- `parseCoseKeyOne` for inline streams: uses `@suaveplan/cbor#decodeOne` to compute the consumed-byte count, slices the head, and re-validates via `decodeCoseKey`.
|
||||
- Cause-chain preservation — every `CborError` raised by `@suaveplan/cbor` is captured on `.cause` of the surfaced `WebAuthnError(MALFORMED_RESPONSE)`.
|
||||
- `resolveCoseAlg(label)` maps numeric COSE alg labels to the named identifiers (`ES256`, `ES384`, `ES512`, `EdDSA`, `RS256`) the assertion verifier dispatches on. Unknown labels throw `WEBAUTHN.SIGNATURE_INVALID` — an unverifiable algorithm cannot be accepted.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseCoseKey } from "@suaveplan/webauthn/parser";
|
||||
|
||||
const key = parseCoseKey(coseKeyBytes);
|
||||
if (key.kty === "EC2") {
|
||||
console.log(key.crv, key.x.byteLength, key.y.byteLength);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseCoseKeyOne,
|
||||
resolveCoseAlg,
|
||||
} from "@suaveplan/webauthn/parser";
|
||||
|
||||
const { key, consumed } = parseCoseKeyOne(authDataAfterPrefix);
|
||||
const remaining = authDataAfterPrefix.subarray(consumed);
|
||||
if (key.alg !== undefined) {
|
||||
const alg = resolveCoseAlg(coseAlgLabel(key.alg));
|
||||
// alg is now "ES256" | "ES384" | ...
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseCoseKey(bytes: Uint8Array): CoseKey` — full-buffer decode; throws `WEBAUTHN.MALFORMED_RESPONSE` on failure.
|
||||
- `parseCoseKeyOne(bytes: Uint8Array): { key: CoseKey, consumed: number }` — inline decode; returns consumed-byte count for cursor advancement.
|
||||
- `SUPPORTED_COSE_ALG_LABELS` — frozen map `{ ES256: -7, ES384: -35, ES512: -36, EdDSA: -8, RS256: -257 }`.
|
||||
- `SupportedCoseAlg` / `SupportedCoseAlgLabel` — narrowed literal types.
|
||||
- `resolveCoseAlg(label: number): SupportedCoseAlg` — numeric → named; throws `WEBAUTHN.SIGNATURE_INVALID` on unknown.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The package supports exactly the five algorithms required by W3C WebAuthn L3 §5.10.5 for the typical authenticator: ES256 / ES384 / ES512 (NIST curves), EdDSA (Ed25519), and RS256 (RSA-PKCS1-v1.5). PS256 is not supported because no production authenticator emits it; consumers that need PS256 can wire a custom assertion verifier via the pluggable registry.
|
||||
- `parseCoseKeyOne` uses `decodeOne` to learn the encoded length first, then re-decodes via `decodeCoseKey` so the strict-mode validation in the underlying library runs. The double-decode cost is negligible (microseconds) and keeps the cbor / cose abstraction sealed.
|
||||
- `resolveCoseAlg` throws rather than returning `undefined` because the caller (`verifyAssertion`) cannot proceed without knowing the algorithm — surfacing an unsupported alg as a verification failure is the safer default than passing it down to a generic fallback verifier.
|
||||
80
docs/identity/auth/webauthn/src/register/register.md
Normal file
80
docs/identity/auth/webauthn/src/register/register.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Registration entry point
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements `register(opts)` — the W3C WebAuthn L3 §5.1.3 / §7.1 registration ceremony entry point. The function builds a `PublicKeyCredentialCreationOptions` from the caller's options, invokes the platform adapter (defaulting to `navigator.credentials.create` via `defaultWebAuthnAdapter`), parses the returned `AuthenticatorAttestationResponse` into a typed `RegisterResult`, and threads `AbortSignal` cancellation, OTel observability, and DOMException-to-typed-error mapping through every code path.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated options at the boundary — `INVALID_OPTIONS` is the only typed error raised before the adapter is touched.
|
||||
- Pre-aborted `AbortSignal` short-circuit before any adapter call.
|
||||
- `mapDomException` at every adapter boundary so raw `DOMException` instances never leak to callers.
|
||||
- Client-data validation pinned to `type === "webauthn.create"` with constant-time `origin` and base64url-decoded `challenge` compares.
|
||||
- Constant-time `rpIdHash` cross-bind against the caller's `expectedRpIdHash`.
|
||||
- PRF and largeBlob extension result extraction surfaced on `result.extensions.{ prf, largeBlob }`.
|
||||
- OTel emission on every code path: `webauthn.register_duration_ms{outcome}` histogram + `webauthn.outcome_total{op=register,outcome}` counter, with `outcome ∈ { ok | error | aborted }`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { register } from "@suaveplan/webauthn";
|
||||
|
||||
const result = await register({
|
||||
rp: { name: "example.com", id: "example.com" },
|
||||
user: {
|
||||
id: userIdBytes,
|
||||
name: "alice@example.com",
|
||||
displayName: "Alice"
|
||||
},
|
||||
challenge: challengeBytes,
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 },
|
||||
{ type: "public-key", alg: -257 }
|
||||
],
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash: rpIdHashBytes,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { register } from "@suaveplan/webauthn";
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 60_000);
|
||||
const result = await register({
|
||||
rp: { name: "example.com" },
|
||||
user: { id, name, displayName },
|
||||
challenge,
|
||||
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash: rpIdHash,
|
||||
extensions: {
|
||||
prf: { eval: { first: prfSaltBytes } },
|
||||
largeBlob: { support: "preferred" },
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
console.log(result.parsedAttestation.fmt);
|
||||
if (result.extensions.prf?.enabled) {
|
||||
// Materialise the wrap key.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebAuthnRegisterOptions` — extends `WebAuthnRegisterOptionsInput`; adds optional `adapter` and `signal`.
|
||||
- `RegisterResult` — `{ credentialId, attestationObject, clientDataJSON, parsedAttestation, transports, extensions }`.
|
||||
- `register(opts): Promise<RegisterResult>` — the entry point.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The adapter abstraction means tests inject a stub directly via `opts.adapter`; production code uses `defaultWebAuthnAdapter` which reads `globalThis.navigator.credentials`.
|
||||
- `attestation: "none"` is the default when the caller does not pass an `attestation` field — matching W3C's default and minimising the privacy footprint of registration.
|
||||
- The `AbortSignal` is forwarded into the `CredentialCreationOptions` so the native AbortController path fires. A pre-aborted signal short-circuits before the adapter call to avoid emitting an unnecessary credentials-create event.
|
||||
- `getClientExtensionResults` is optional on stub adapters — production browsers always implement it.
|
||||
- `RegisterResult.parsedAttestation` contains the parsed `ParsedAttestationObject` so the caller can immediately invoke `verifyAttestation` without re-parsing.
|
||||
74
docs/identity/auth/webauthn/src/schemas/schemas.md
Normal file
74
docs/identity/auth/webauthn/src/schemas/schemas.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# WebAuthn — boundary schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the Zod schemas wired at every public surface of `@suaveplan/webauthn`. They serve as the structural contract for caller-supplied options (`register`, `authenticate`) and for the parsed shapes the package returns (`ParsedAuthData`, `ParsedAttestationStmt`, `ClientDataJSON`, `CoseKey`). The schemas are the single typed boundary between untrusted byte input and the production validation pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict caller-option shapes for `register` and `authenticate`.
|
||||
- Forward-compatible `passthrough()` on every authenticator-produced structure so an authenticator that emits extra metadata does not break parsing.
|
||||
- Constrained literal unions for `attestation`, `userVerification`, `mediation`, `residentKey`, and the per-format dispatchers so a mistyped enum surfaces at the boundary.
|
||||
- Discriminated `largeBlob` authenticate input (`read: true` xor `write: Uint8Array`) so the verifier can branch on the shape without re-validating.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebAuthnRegisterOptionsSchema } from "@suaveplan/webauthn";
|
||||
|
||||
const parsed = WebAuthnRegisterOptionsSchema.parse({
|
||||
rp: { name: "example.com" },
|
||||
user: { id: new Uint8Array(16), name: "u", displayName: "U" },
|
||||
challenge: new Uint8Array(32),
|
||||
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash: new Uint8Array(32),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ParsedAuthDataSchema,
|
||||
WebAuthnAuthenticateOptionsSchema,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
function validate(unsafe: unknown) {
|
||||
const opts = WebAuthnAuthenticateOptionsSchema.parse(unsafe);
|
||||
return opts;
|
||||
}
|
||||
const auth = ParsedAuthDataSchema.parse({
|
||||
rpIdHash: new Uint8Array(32),
|
||||
flags: {
|
||||
userPresent: true,
|
||||
userVerified: false,
|
||||
backupEligible: false,
|
||||
backupState: false,
|
||||
attestedCredentialData: false,
|
||||
extensionsPresent: false,
|
||||
},
|
||||
flagsByte: 0x01,
|
||||
signCount: 0,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ClientDataJSONSchema` — pinned `type`, `challenge`, `origin`; passthrough.
|
||||
- `CoseKeySchema` — pinned `kty` (int); optional `alg`, `crv`, `x`, `y`, `n`, `e`, `k`; passthrough.
|
||||
- `ParsedAuthDataFlagsSchema` — six booleans per W3C L3 §6.1.
|
||||
- `ParsedAuthDataSchema` — fixed prefix + optional attested-credential-data + optional extensions.
|
||||
- `ParsedAttestationStmtSchema` — `fmt: string`, `attStmt: Record<string, unknown>`.
|
||||
- `WebAuthnRegisterOptionsSchema`, `WebAuthnAuthenticateOptionsSchema` — caller options.
|
||||
- `PrfRegisterInputSchema`, `PrfAuthenticateInputSchema`, `LargeBlobRegisterInputSchema`, `LargeBlobAuthenticateInputSchema` — extension inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- COSE_Key fields outside the RFC 9052 §7 minimum set are kept on the parsed object via `passthrough()` so a verifier can reach into vendor extensions when needed. The structural type narrowing happens in `parser/cose-key.ts` where the discriminated `kty` arms (`EC2`, `OKP`, `RSA`, `Symmetric`) are produced.
|
||||
- `WebAuthnRegisterOptionsSchema` deliberately marks `attestation` as optional; the W3C default is `"none"` and the production code applies that default before forwarding to the adapter.
|
||||
- `LargeBlobAuthenticateInputSchema` is a `z.union` of `{ read: true }` xor `{ write: Uint8Array }` so callers cannot request both simultaneously — the same constraint W3C §10.1.3 imposes.
|
||||
- Every options schema accepts a caller-side `Uint8Array` instance check rather than `Buffer`. This is universal — `Buffer` would not load in browsers.
|
||||
54
docs/identity/auth/webauthn/src/verifier/registry.md
Normal file
54
docs/identity/auth/webauthn/src/verifier/registry.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Attestation format registry
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the pluggable contract every per-format attestation verifier implements (`AttestationFormatVerifier`), the shared input bundle (`AttestationVerifierInput`), and the canonical result shape (`AttestationVerifyResult`). Consumers can `registerAttestationFormat(registry, name, verifier)` to add a new IANA-registered format (e.g. `apple-appattest`) or override a built-in without forking the package.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-type public contract: `AttestationType` (`basic | self | attCA | anonCA | none`), `AttestationVerifyResult` (`{ trusted, attestationType, trustPath }`), `AttestationFormatVerifier` (`(input) => Result | Promise<Result>`).
|
||||
- `AttestationFormatRegistry = Map<string, AttestationFormatVerifier>` — opaque mutable handle the dispatcher consumes.
|
||||
- Last-write-wins registration so `registerAttestationFormat(registry, "packed", custom)` cleanly replaces the built-in.
|
||||
- Allows synchronous or async verifiers — `none` is synchronous, `tpm` is async, callers do not need to know the difference.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
registerAttestationFormat,
|
||||
} from "@suaveplan/webauthn/verifier";
|
||||
|
||||
const registry = defaultAttestationRegistry();
|
||||
registerAttestationFormat(registry, "apple-appattest", customVerifier);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type AttestationFormatVerifier,
|
||||
registerAttestationFormat,
|
||||
} from "@suaveplan/webauthn/verifier";
|
||||
|
||||
const counted: AttestationFormatVerifier = async (input) => {
|
||||
metrics.count(input.attestationObject.fmt);
|
||||
return builtin(input);
|
||||
};
|
||||
registerAttestationFormat(registry, "packed", counted);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AttestationType` — literal union.
|
||||
- `AttestationVerifyResult` — `{ trusted: boolean, attestationType: AttestationType, trustPath: readonly Uint8Array[] }`.
|
||||
- `AttestationVerifierInput` — `{ attestationObject, clientDataHash, expectedRpIdHash }`.
|
||||
- `AttestationFormatVerifier` — `(input) => Result | Promise<Result>`.
|
||||
- `AttestationFormatRegistry` — `Map<string, AttestationFormatVerifier>`.
|
||||
- `registerAttestationFormat(registry, name, verifier)` — register or override.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `trustPath` is the leaf-first X.509 chain as raw DER byte strings. Empty when the format does not provide a chain (`self`, `none`). Consumers feed the chain into their own metadata-trust source (FIDO MDS3, attestation-CA allow-list) — the verifier is intentionally agnostic about which roots to trust.
|
||||
- The registry is a plain `Map` rather than a custom class so consumers can iterate / inspect / serialise it freely. The contract is the value types, not the container.
|
||||
- Returning a discriminated `attestationType` rather than a boolean lets the caller adjust policy: `basic` and `attCA` are stronger than `self`, and `none` is the weakest (but still acceptable when the RP's policy permits anonymous registration).
|
||||
50
docs/identity/auth/webauthn/src/verifier/sign-counter.md
Normal file
50
docs/identity/auth/webauthn/src/verifier/sign-counter.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Sign-counter regression detector
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §6.1.1 sign-counter regression check. Authenticators that maintain a monotonically-increasing counter MUST surface a larger value on every successful assertion; a counter that fails to increase indicates either a cloned authenticator or an authenticator software bug. The check is deliberately a pure function so the policy decision (accept-and-warn vs revoke-and-reject) lives in the caller's identity layer.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-state semantics: stored=0 & asserted=0 (skip), asserted<=stored & at least one non-zero (regression), asserted>stored (ok).
|
||||
- Pure synchronous function — no IO, no observability emission, no exceptions.
|
||||
- Boundary-correct comparison: `<=` so the equal-counter case is also a regression, matching the W3C-defined behaviour for authenticators that never rotate their counter.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectSignCounterRegression } from "@suaveplan/webauthn/verifier";
|
||||
|
||||
if (detectSignCounterRegression(stored.signCounter, result.signCount)) {
|
||||
revokeCredential(stored.id);
|
||||
throw new Error("sign-counter regression");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { detectSignCounterRegression } from "@suaveplan/webauthn/verifier";
|
||||
|
||||
function classifyAuthenticatorCounter(
|
||||
stored: number,
|
||||
asserted: number
|
||||
): "static-counter-ok" | "regression" | "advanced" {
|
||||
if (stored === 0 && asserted === 0) {
|
||||
return "static-counter-ok";
|
||||
}
|
||||
return detectSignCounterRegression(stored, asserted)
|
||||
? "regression"
|
||||
: "advanced";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `detectSignCounterRegression(stored: number, asserted: number): boolean` — `true` if the assertion's counter regressed, `false` otherwise.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- W3C deliberately keeps the policy ambiguous: an RP MAY treat regression as a soft warning (for testing-tier authenticators) or as a hard revoke (for production). The package surfaces the boolean and leaves the policy to the caller; `verifyAssertion` consumes this and returns `{ ok: false, code: SIGN_COUNTER_REGRESSION }` so the caller can branch.
|
||||
- The zero-zero exception matters: several major authenticators (Touch ID, Windows Hello on platform-bound credentials) deliberately emit `signCount === 0` on every assertion to avoid leaking usage frequency to the RP. Without the zero-zero exception every such login would falsely flag as a regression.
|
||||
- The function is intentionally branchless except for the two arithmetic comparisons so its execution time is invariant of the inputs — there is no plausible side-channel here, but the discipline is part of the package's no-naive-equality posture.
|
||||
68
docs/identity/auth/webauthn/src/verifier/verify-assertion.md
Normal file
68
docs/identity/auth/webauthn/src/verifier/verify-assertion.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Assertion verifier
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `verifyAssertion`, the cryptographic check at the heart of the WebAuthn authentication ceremony (W3C WebAuthn L3 §7.2). The function ingests the authenticator's assertion (authenticatorData, clientDataJSON, signature) plus the relying-party's stored credential state (public key, expected origin, expected rpIdHash, sign counter) and returns a discriminated-union `VerifyResult`. The dispatcher routes to per-algorithm verifiers backed by `@suaveplan/crypto-curve` and `@suaveplan/crypto-rsa` — every supported COSE algorithm (ES256, ES384, ES512, EdDSA, RS256) goes through real cryptographic verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminated-union return: `{ ok: true } | { ok: false, code }` — callers branch without unwrapping thrown errors.
|
||||
- Five algorithms supported: ES256 (P-256 + SHA-256), ES384 (P-384 + SHA-384), ES512 (P-521 + SHA-512), EdDSA (Ed25519), RS256 (RSA-PKCS1-v1.5 + SHA-256).
|
||||
- DER ECDSA decoding via `@suaveplan/x509#decodeEcdsaSignature` — WebAuthn emits DER-encoded ECDSA signatures, which we convert to the compact `r||s` form expected by `@suaveplan/crypto-curve`.
|
||||
- Constant-time `rpIdHash` compare via `constantTimeEqual` — a mismatched RP hash is the canonical phishing signal and the compare time must not leak the byte index of the first difference.
|
||||
- Sign-counter regression check via `detectSignCounterRegression` — `{ ok: false, code: "WEBAUTHN.SIGN_COUNTER_REGRESSION" }` on monotonicity violation.
|
||||
- Client-data revalidation via `validateClientDataJSON` so type / origin / challenge are also checked.
|
||||
- `verifyOverCoseKey(key, message, signature)` helper exported for per-format attestation verifiers (packed self-attestation, fido-u2f, tpm).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { verifyAssertion } from "@suaveplan/webauthn/verifier";
|
||||
|
||||
const result = await verifyAssertion({
|
||||
authenticatorData,
|
||||
clientDataJSON,
|
||||
signature,
|
||||
storedPublicKey,
|
||||
expectedChallenge,
|
||||
expectedOrigin: "https://example.com",
|
||||
expectedRpIdHash,
|
||||
signCounter: stored.signCounter,
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (result.code === "WEBAUTHN.SIGN_COUNTER_REGRESSION") {
|
||||
revokeCredential();
|
||||
}
|
||||
throw new Error(result.code);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
verifyAssertion,
|
||||
verifyOverCoseKey,
|
||||
} from "@suaveplan/webauthn/verifier";
|
||||
|
||||
// Direct signature check (used by per-format attestation verifiers):
|
||||
const ok = await verifyOverCoseKey(
|
||||
storedKey,
|
||||
concatBytes(authData, sha256(clientDataJSON)),
|
||||
derSignatureBytes,
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `VerifyResult` — `{ ok: true } | { ok: false, code: "WEBAUTHN.SIGNATURE_INVALID" | "WEBAUTHN.RPID_HASH_MISMATCH" | "WEBAUTHN.SIGN_COUNTER_REGRESSION" }`.
|
||||
- `VerifyAssertionInput` — `{ authenticatorData, clientDataJSON, signature, storedPublicKey, expectedChallenge, expectedOrigin, expectedRpIdHash, signCounter }`.
|
||||
- `verifyAssertion(input): Promise<VerifyResult>` — the main entry point.
|
||||
- `verifyOverCoseKey(key, message, signature): Promise<boolean>` — per-algorithm signature check, exported for attestation verifiers.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- WebAuthn ECDSA signatures are ASN.1 DER-encoded, not the JOSE-form `r||s` that `@suaveplan/crypto-curve` accepts. The package delegates DER decoding to `decodeEcdsaSignature` from `@suaveplan/x509` (the same routine used in TLS / X.509) so the structural validation is shared with the rest of the security family.
|
||||
- EC2 public keys are reconstructed in uncompressed form `0x04 || x || y` before passing to `noble-curves`; the COSE key labels `x` and `y` as separate byte strings.
|
||||
- RSA verification consumes the COSE `n` and `e` byte strings via `os2ip` to produce `bigint` modulus + exponent, then calls `rsaVerifyPkcs1v15` with `hash: "SHA-256"`. Any structural failure (wrong sig length, modulus mismatch) collapses to `false` — semantic failures never throw out of `verifyOverCoseKey`.
|
||||
- The dispatch order is: parse authData → validate client data → constant-time rpIdHash compare → sign-counter check → signature verify. The order matters: a mismatched rpIdHash is the strongest phishing signal so it surfaces before the expensive signature operation.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Attestation verification dispatcher
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `verifyAttestation` (W3C WebAuthn L3 §6.5.4) and `defaultAttestationRegistry()`. The dispatcher reads `attestationObject.fmt`, looks up the corresponding verifier in the registry, computes the `clientDataHash` once, and invokes the verifier. It is the single entry point into the per-format machinery — consumers do not import individual verifiers unless they want to override behaviour for a specific format.
|
||||
|
||||
## Features
|
||||
|
||||
- Built-in coverage of every IANA-registered format in W3C WebAuthn L3 §8: `packed`, `fido-u2f`, `tpm`, `android-key`, `android-safetynet`, `apple`, `none`.
|
||||
- `WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED` thrown on missing registry entry — distinct from `MALFORMED_RESPONSE` so dashboards can distinguish policy denials from protocol violations.
|
||||
- `webauthn.attestation_format_total{fmt}` OTel counter incremented per dispatch with cardinality bounded to the seven registered formats plus `unknown`.
|
||||
- `clientDataHash` computed once via `@suaveplan/crypto-hash#sha256` and passed to every verifier so the per-format code does not re-hash.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
parseAttestationObject,
|
||||
verifyAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const env = parseAttestationObject(rawAttestationObject);
|
||||
const result = await verifyAttestation({
|
||||
attestationObject: env,
|
||||
clientDataJSON,
|
||||
expectedRpIdHash,
|
||||
registry: defaultAttestationRegistry(),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultAttestationRegistry,
|
||||
registerAttestationFormat,
|
||||
verifyAttestation,
|
||||
} from "@suaveplan/webauthn";
|
||||
|
||||
const registry = defaultAttestationRegistry();
|
||||
registerAttestationFormat(registry, "apple-appattest", async (input) => {
|
||||
// custom verifier for Apple's App Attest format
|
||||
return {
|
||||
trusted: true,
|
||||
attestationType: "anonCA",
|
||||
trustPath: [],
|
||||
};
|
||||
});
|
||||
const result = await verifyAttestation({
|
||||
attestationObject,
|
||||
clientDataJSON,
|
||||
expectedRpIdHash,
|
||||
registry,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `defaultAttestationRegistry(): AttestationFormatRegistry` — registry pre-populated with every built-in format.
|
||||
- `VerifyAttestationInput` — `{ attestationObject, clientDataJSON, expectedRpIdHash, registry }`.
|
||||
- `verifyAttestation(input): Promise<AttestationVerifyResult>` — dispatch + invoke + return.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is a fresh `Map` per call so consumers can mutate it independently; production callers typically build it once and cache.
|
||||
- The OTel counter is emitted BEFORE the dispatch so a verifier that throws still contributes to the format count — this matters for diagnosing "X happened N times, M of which failed" without needing two counters.
|
||||
- `clientDataHash` is the SHA-256 of the raw clientDataJSON bytes, NOT of the parsed JSON. Authenticators sign over the wire-encoded bytes, so re-encoding would break the signature check.
|
||||
- The dispatcher does NOT validate the client data type / origin / challenge — that is the registration ceremony's job and lives in `register`. The dispatcher is a pure attestation primitive.
|
||||
Reference in New Issue
Block a user