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,65 @@
# Compact serialization
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
RFC 7515 §3.1 + §7.1 compact-form JWS — the three-segment `<protected>.<payload>.<signature>` string format that is the default representation for JWS-as-JWT and the only form most downstream consumers will ever encounter. This module owns `compactDecode` (parse), `compactVerify` (RFC §5.2 verify), and `compactSign` (RFC §5.1 sign).
## Features
- Strict structural parser: exactly two `.` separators, URL-alphabet bytes only.
- Empty middle segment accepted (RFC 7515 §A.5 detached form).
- Mandatory `algorithms` allowlist at verify time — algorithm-confusion-CVE defence.
- `compactSign` returns the joined string; consumers can split on `.` to inspect segments.
## Usage: Basic Example
```ts
import { compactDecode, compactSign, compactVerify } from "@suaveplan/jws";
const token = await compactSign("hello", secret, "HS256");
const decoded = compactDecode(token);
// decoded.protected / decoded.payload / decoded.signature
const result = await compactVerify(token, secret, { algorithms: ["HS256"] });
// result.verified === true
```
## Usage: Advanced Example
```ts
import { compactVerify, type KeyResolver, JwsError } from "@suaveplan/jws";
const keyResolver: KeyResolver = async (kid) => keyStore.get(kid);
const verify = async (token: string) => {
try {
return await compactVerify(token, undefined, {
algorithms: ["RS256", "ES256", "EdDSA"],
allowEmbeddedJwk: false,
allowX5c: false,
keyResolver,
acceptableCriticalHeaders: ["exp", "nbf"],
});
} catch (err) {
if (err instanceof JwsError) {
if (err.code === "JWS.ALG_NOT_ALLOWED") return null;
if (err.code === "JWS.SIG_INVALID") return null;
if (err.code === "JWS.KEY_NOT_FOUND") return null;
}
throw err;
}
};
```
## API Reference
- `compactDecode(token: string): CompactJws` — throws `JWS.MALFORMED`.
- `compactVerify(token: string, key: unknown, opts: VerifyOptions): Promise<VerifyResult>`
- `compactSign(payload: string | Uint8Array, key: unknown, alg: string, opts?: SignOptions): Promise<string>`
## Implementation Notes
Verification enforces the algorithm allowlist BEFORE any cryptographic work — `opts.algorithms.includes(header.alg)` is checked immediately after parsing the header, so a token presenting `alg: "HS256"` against an RSA public key cannot even reach the HMAC impl, neutralising the entire alg-confusion class. The pipeline also enforces `crit` allowlist before alg dispatch, so a token whose `crit` parameter is unknown to the verifier never runs cryptography either; this matters because cryptographic verification side-channels can leak information about the verifier's state (cache state, branch prediction) even when the verifier rejects the token.

View File

@@ -0,0 +1,54 @@
# JSON Flattened serialization
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
RFC 7515 §3.2 + §7.2.2 JSON-flattened JWS — `{ protected?, header?, payload, signature }`. The flat form is the single-signature variant of the general form; producers use it when they need to ship an unprotected header alongside the protected one (e.g. for non-signed metadata like `kid` rotation hints) without going through the compact form's protected-header-only constraint.
## Features
- `flatDecode` — Zod-validated decode; at least one of `protected` / `header` must be present.
- `flatVerify` — verify against the recomputed `protected || "." || payload` signing input.
- `flatSign` — sign and emit the flat JSON.
- Unprotected header preserved on the verify result via `.unprotectedHeader`.
## Usage: Basic Example
```ts
import { flatSign, flatVerify } from "@suaveplan/jws";
const flat = await flatSign("hello", secret, "HS256", {
unprotectedHeader: { kid: "k1" },
});
// { protected: "...", header: { kid: "k1" }, payload: "...", signature: "..." }
const result = await flatVerify(flat, secret, { algorithms: ["HS256"] });
// result.unprotectedHeader === { kid: "k1" }
```
## Usage: Advanced Example
```ts
import { flatVerify, JwsError } from "@suaveplan/jws";
const verified = await flatVerify(flat, undefined, {
algorithms: ["RS256"],
allowEmbeddedJwk: false,
keyResolver: async (kid) => keyStore.get(kid),
acceptableCriticalHeaders: ["exp"],
});
// verified.header === protected header (signed)
// verified.unprotectedHeader === unprotected header (advisory only)
```
## API Reference
- `flatDecode(json: unknown): FlatJws`
- `flatVerify(json: unknown, key: unknown, opts: VerifyOptions): Promise<VerifyResult & { unprotectedHeader?: Record<string, unknown> }>`
- `flatSign(payload: string | Uint8Array, key: unknown, alg: string, opts?: SignOptions): Promise<FlatJws>`
## Implementation Notes
The unprotected header is informational — it is NOT covered by the signature, so consumers MUST NOT rely on it for security decisions. The library surfaces it on the verify result so callers can use it for non-security routing (rotation hints, message-id correlation) without re-decoding the wire shape. Verification enforces a header-key collision check between protected and unprotected headers per RFC 7515 §5.2 step 4: any duplicate key throws `JWS.HEADER_MALFORMED` because the RFC explicitly disallows it (an attacker could otherwise plant a different `alg` in the unprotected header and confuse a careless verifier).

View File

@@ -0,0 +1,69 @@
# JSON General serialization (multi-signature)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
RFC 7515 §3.2 + §7.2.1 JSON-general JWS — `{ payload, signatures: JwsSignature[] }`. Each entry in `signatures` carries its own `protected` / `header` / `signature` so the same payload can be co-signed by N parties. Common use cases: cross-tenant message attestation, multi-party authorisation, signed installer manifests where multiple maintainers' signatures must all attach to the same artifact.
## Features
- `generalDecode` — Zod-validated decode; `signatures` must be non-empty.
- `generalVerify` — verifies every signer via a per-signer key resolver; returns `{ verifiedIndices, failedIndices }`.
- `generalSign` — signs the same payload under N signers; rejects mixed-`b64` signers.
- `requireAll: true` causes the first failing index to throw `JWS.MULTI_SIG_FAILED` with `error.context.failedIndex`.
## Usage: Basic Example
```ts
import { generalSign, generalVerify } from "@suaveplan/jws";
const general = await generalSign("hello", [
{ key: secret, alg: "HS256" },
{ key: rsaPriv, alg: "RS256" },
]);
const result = await generalVerify(
general,
async (i) => (i === 0 ? secret : rsaPub),
{ algorithms: ["HS256", "RS256"] }
);
// result.verifiedIndices === [0, 1]
```
## Usage: Advanced Example
```ts
import { generalVerify, JwsError } from "@suaveplan/jws";
try {
await generalVerify(
general,
async (i, protectedHdr) => keyStoreByIssuer.get(protectedHdr["iss"] as string),
{
algorithms: ["HS256", "RS256", "ES256"],
requireAll: true,
}
);
} catch (err) {
if (err instanceof JwsError && err.code === "JWS.MULTI_SIG_FAILED") {
const failedIndex = (err.context as { failedIndex: number }).failedIndex;
// err.cause === underlying JwsError for that signer.
}
}
```
## API Reference
- `generalDecode(json: unknown): GeneralJws`
- `generalVerify(json: unknown, keyResolver, opts: GeneralVerifyOptions): Promise<GeneralVerifyResult>`
- `keyResolver: (signerIndex, protectedHeader, unprotectedHeader?) => Promise<unknown>`
- `GeneralVerifyOptions extends VerifyOptions { requireAll?: boolean }`
- `GeneralVerifyResult = { verifiedIndices, failedIndices, payloadString }`
- `generalSign(payload, signers: GeneralSigner[]): Promise<GeneralJws>`
- `GeneralSigner = { key, alg, protectedHeader?, unprotectedHeader? }`
## Implementation Notes
The default `requireAll` value is `false` — more flexible for use cases like "any one of N trust roots verifies." Callers needing the stricter "every signer must verify" semantics opt in via `requireAll: true`. Mixed `b64` settings across signers are NOT supported because each signer's signing input would differ in interpretation and the receiver-side verifier cannot know which interpretation to apply — `generalSign` rejects mixed-`b64` arrangements with `JWS.MALFORMED`. The payload segment is computed from the first signer's b64 setting and then asserted byte-identical across the rest.