first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.

View 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.

View 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`.

View 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.