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,59 @@
# `b64` (RFC 7797) header
## Purpose
RFC 7797 §3 implements the `b64` header parameter, allowing JWS producers to embed unencoded payloads directly into the signing input. When `b64: false`, the second compact-form segment is the raw payload bytes themselves (not base64url-encoded) and the signing input becomes `BASE64URL(protected) || "." || rawPayloadBytes`. The RFC's MUST clauses require that any `b64: false` header also list `"b64"` in `crit` so verifiers ignorant of RFC 7797 refuse the token rather than misinterpret it.
## Features
- `resolveB64(header)` — returns the effective boolean (`true` when absent).
- `b64: false` without `"b64"` in `crit` → `JWS.B64_INVALID`.
- Non-boolean `b64` value → `JWS.B64_INVALID`.
- Absent `b64` defaults to base64url encoding (RFC 7515 §3.1 default).
## Usage: Basic Example
```ts
import { resolveB64 } from "@suaveplan/jws";
resolveB64({ alg: "HS256" }); // true (default)
resolveB64({ alg: "HS256", b64: true }); // true
```
## Usage: Advanced Example
```ts
import { compactSign, compactVerify, resolveB64, JwsError } from "@suaveplan/jws";
// Produce a b64: false token (raw-payload signing input).
const token = await compactSign("hello", key, "HS256", {
protectedHeader: { b64: false, crit: ["b64"] },
b64: false,
});
const result = await compactVerify(token, key, {
algorithms: ["HS256"],
acceptableCriticalHeaders: ["b64"],
});
// result.payloadString === "hello" — raw, not base64url-decoded
// Missing crit listing — verifier rejects.
try {
resolveB64({ alg: "HS256", b64: false });
} catch (err) {
if (err instanceof JwsError && err.code === "JWS.B64_INVALID") {
// RFC 7797 §3 MUST violation.
}
}
```
## API Reference
- `resolveB64(header: JoseHeader): boolean`
- Returns `true` when `b64` is absent or `b64: true`.
- Returns `false` only when `b64: false` AND `crit` includes `"b64"`.
- Throws `JwsError(JWS.B64_INVALID)` for non-boolean values or `false` without `crit`.
## Implementation Notes
The RFC 7797 contract is asymmetric: producers may emit `b64: true` without `crit` (default behaviour), but `b64: false` MUST be marked critical. This module enforces only the verifier side; the sign pipeline emits the `crit: ["b64"]` companion automatically when callers pass `b64: false` so producers do not have to remember the dual-write. Raw-payload signing input requires special handling — the payload bytes cannot pass through `TextEncoder` when the producer's payload contains the `.` byte (0x2e), since that's also the segment separator; the compact-form parser handles this by treating the segment as bytes once `b64: false` is resolved, not as a UTF-8 string.

View File

@@ -0,0 +1,57 @@
# `crit` enforcement
## Purpose
RFC 7515 §4.1.11 enforcement: every entry in `header.crit` MUST be in the caller's `acceptableCriticalHeaders` allowlist. The RFC's intent is that callers refuse to verify any token whose protected header carries critical parameters they do not understand — otherwise the producer could downgrade security guarantees (e.g. an `exp` claim becoming critical against a verifier that ignores it). This module owns that gate.
## Features
- Empty `crit` array rejected.
- Non-array `crit` rejected.
- Non-string entry rejected.
- `crit` referencing itself rejected (RFC 7515 §4.1.11 MUST).
- Entry not in caller's allowlist → `JWS.CRIT_UNRECOGNIZED`.
- Entry in allowlist accepted.
## Usage: Basic Example
```ts
import { enforceCrit, parseHeader } from "@suaveplan/jws";
const header = parseHeader("eyJhbGciOiJIUzI1NiIsImNyaXQiOlsiZXhwIl0sImV4cCI6MTYwfQ");
enforceCrit(header, ["exp"]); // does not throw
```
## Usage: Advanced Example
```ts
import { enforceCrit, JwsError } from "@suaveplan/jws";
try {
enforceCrit({ alg: "HS256", crit: ["exp", "tenant"] }, ["exp"]);
} catch (err) {
if (err instanceof JwsError && err.code === "JWS.CRIT_UNRECOGNIZED") {
// Reject the token: a critical parameter is outside the allowlist.
}
}
// Self-reference rejected:
try {
enforceCrit({ alg: "HS256", crit: ["crit"] }, ["crit"]);
} catch (err) {
if (err instanceof JwsError && err.code === "JWS.CRIT_MALFORMED") {
// RFC 7515 §4.1.11 MUST.
}
}
```
## API Reference
- `enforceCrit(header: JoseHeader, acceptableCriticalHeaders: readonly string[] | undefined): void`
- No-op when `header.crit` is absent.
- Throws `JwsError(JWS.CRIT_MALFORMED)` for shape failures (non-array, empty, non-string entry, self-reference).
- Throws `JwsError(JWS.CRIT_UNRECOGNIZED)` for entries not on the allowlist.
## Implementation Notes
The allowlist is supplied at every verify call rather than configured globally; this is deliberate because the allowlist is fundamentally a property of the consumer's threat model, not the JWS library. Callers integrating an RFC 7797 `b64: false` token MUST add `"b64"` to their `acceptableCriticalHeaders` even though this package implements the RFC 7797 spec internally — the gate is "do you know what to do with this parameter," not "does the library implement it." This makes it impossible to accidentally accept a `b64: false` token through a verifier that wasn't expecting one.

View File

@@ -0,0 +1,57 @@
# `header`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Parse and serialise RFC 7515 §4.1 JOSE protected headers. Every registered parameter (`alg`, `jku`, `jwk`, `kid`, `x5u`, `x5c`, `x5t`, `x5t#S256`, `typ`, `cty`, `crit`, plus RFC 7797 `b64`) is shape-validated through the Zod schema; non-registered parameters survive round-trip via the schema's catch-all. The `alg` field is the only mandatory parameter; everything else is optional and the decoder preserves bytes verbatim so callers can re-emit the header without canonicalisation surprises.
## Features
- `parseHeader(base64urlString | object)` — base64url-decode + JSON-parse + Zod-shape-check; returns a typed `JoseHeader`.
- `serializeHeader(header)` — `JSON.stringify` + base64url-encode in one call.
- `JoseHeader` — strongly-typed shape (every registered parameter).
- Unknown parameters survive round-trip.
## Usage: Basic Example
```ts
import { parseHeader, serializeHeader } from "@suaveplan/jws";
const decoded = parseHeader("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
// { alg: "HS256", typ: "JWT" }
const reencoded = serializeHeader(decoded);
```
## Usage: Advanced Example
```ts
import { JwsError, parseHeader } from "@suaveplan/jws";
try {
parseHeader("eyJhbGciOjQyfQ"); // alg as number
} catch (err) {
if (err instanceof JwsError && err.code === "JWS.HEADER_MALFORMED") {
// The Zod boundary refused a non-string alg.
}
}
// Custom parameter survives round-trip:
const h = parseHeader({ alg: "ES256", custom: { tenant: "acme" } });
console.log(h["custom"]); // { tenant: "acme" }
```
## API Reference
- `parseHeader(input: string | Record<string, unknown>): JoseHeader`
- Decodes a base64url-encoded JSON object header, OR validates an already-parsed object.
- Throws `JwsError(JWS.HEADER_MALFORMED)` on shape failure.
- Throws `JwsError(JWS.BASE64URL_INVALID)` on base64url failure.
- `serializeHeader(header: JoseHeader | Record<string, unknown>): string`
- `interface JoseHeader` — every RFC 7515 §4.1 registered parameter, all readonly.
## Implementation Notes
The schema is `.passthrough()` so consumers retain non-registered parameters — required for downstream RFCs (RFC 7519 JWT, RFC 9202 EAT-JWT, custom tenant claims) that layer additional parameters into the same header structure. Encoding always uses `JSON.stringify` with no replacer, which means insertion order is preserved; callers needing canonical-order headers (e.g. for byte-identical reproduction of an upstream signer's output) should sort their object keys before passing in. The decoder uses a strict UTF-8 `TextDecoder` so malformed UTF-8 sequences fail rather than silently inserting replacement characters into the parsed JSON — a deliberate strictness that catches producers that emit windows-1252 or latin-1 bytes through a misconfigured pipeline.