first commit
This commit is contained in:
59
docs/security-stack/security/jws/src/header/b64.md
Normal file
59
docs/security-stack/security/jws/src/header/b64.md
Normal 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.
|
||||
57
docs/security-stack/security/jws/src/header/crit.md
Normal file
57
docs/security-stack/security/jws/src/header/crit.md
Normal 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.
|
||||
57
docs/security-stack/security/jws/src/header/header.md
Normal file
57
docs/security-stack/security/jws/src/header/header.md
Normal 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.
|
||||
Reference in New Issue
Block a user