Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# `jwkToCryptoKey`
|
||||
|
||||
## Purpose
|
||||
|
||||
Parse an RFC 7517 §4 / RFC 7518 §6 JSON Web Key into the typed key shape expected by the JWS algorithm impl for the declared `alg`. `kty` / `crv` / coordinate lengths are validated against the declared algorithm; mismatch throws `JWS.JWK_INVALID` for shape issues or `JWS.KEY_ALG_MISMATCH` for valid-shape-but-wrong-algorithm cases.
|
||||
|
||||
## Features
|
||||
|
||||
- RSA — `kty: "RSA"` → `RsaPublicKey` via `@suaveplan/crypto-rsa.parseRsaPublicKeyJwk`.
|
||||
- EC — `kty: "EC"` with `crv: "P-256" | "P-384" | "P-521"` → uncompressed SEC1 bytes for the JWS ECDSA impl.
|
||||
- OKP — `kty: "OKP"` with `crv: "Ed25519"` → 32-byte raw key for `EdDSA`.
|
||||
- Symmetric — `kty: "oct"` with `k` → `Uint8Array` for `HS256/384/512`.
|
||||
- Coordinate length validation per curve.
|
||||
- Ed448 explicitly rejected with `KEY_ALG_MISMATCH`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { jwkToCryptoKey } from "@suaveplan/jws";
|
||||
|
||||
const parsed = jwkToCryptoKey(
|
||||
{
|
||||
kty: "EC",
|
||||
crv: "P-256",
|
||||
x: "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
|
||||
y: "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
|
||||
},
|
||||
"ES256"
|
||||
);
|
||||
// parsed.kind === "ec"; parsed.key is uncompressed SEC1.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { JwsError, jwkToCryptoKey } from "@suaveplan/jws";
|
||||
|
||||
// Algorithm-confusion defence: RSA JWK declared for an ES256 token rejected.
|
||||
try {
|
||||
jwkToCryptoKey({ kty: "RSA", n: "...", e: "AQAB" }, "ES256");
|
||||
} catch (err) {
|
||||
if (err instanceof JwsError && err.code === "JWS.JWK_INVALID") {
|
||||
// kty: RSA vs alg: ES256 mismatch.
|
||||
}
|
||||
}
|
||||
|
||||
// Ed448 rejected:
|
||||
try {
|
||||
jwkToCryptoKey({ kty: "OKP", crv: "Ed448", x: "..." }, "EdDSA");
|
||||
} catch (err) {
|
||||
if (err instanceof JwsError && err.code === "JWS.KEY_ALG_MISMATCH") {
|
||||
// Only Ed25519 supported today.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `jwkToCryptoKey(jwk: Record<string, unknown>, alg: string): ParsedKey`
|
||||
- `ParsedKey = { kind: "rsa", key: RsaPublicKey } | { kind: "ec", key: Uint8Array } | { kind: "okp", key: Uint8Array }`
|
||||
- Throws `JwsError(JWS.JWK_INVALID)` for shape issues.
|
||||
- Throws `JwsError(JWS.KEY_ALG_MISMATCH)` for valid-shape-but-wrong-curve cases.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Coordinate lengths (32/48/66 bytes for P-256/P-384/P-521) are strictly enforced. The P-521 curve uses 66-byte coordinates because the curve order is 521 bits (66 bytes), not the often-assumed 64; producers that left-pad to 64 fail validation here, which is correct — those tokens are malformed per RFC 7518 §6.2.1.2 even though many lenient libraries accept them. RSA keys route through `@suaveplan/crypto-rsa.parseRsaPublicKeyJwk` so the same key-policy checks (min/max modulus length, validated public exponent, malformed-base64url rejection) apply uniformly across the project.
|
||||
@@ -0,0 +1,59 @@
|
||||
# `KeyResolver`
|
||||
|
||||
## Purpose
|
||||
|
||||
Caller-supplied key-resolution function consulted when `header.kid` is present and no key has been supplied via `opts.key`, `header.jwk`, or `header.x5c`. The resolver receives the parsed header so dispatch can branch on additional parameters (`iss`, custom tenant headers, environment-specific kid namespaces). Returning `undefined` surfaces as `JwsError(JWS.KEY_NOT_FOUND)`; throwing propagates verbatim so callers can map their own error taxonomy onto JWS verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Async or sync — both signatures are accepted.
|
||||
- Receives `(kid, header)`; the header is the parsed `JoseHeader`, including unknown parameters.
|
||||
- `undefined` / `null` return → typed `JWS.KEY_NOT_FOUND` thrown by the pipeline.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { compactVerify, type KeyResolver } from "@suaveplan/jws";
|
||||
|
||||
const keyResolver: KeyResolver = async (kid) => keyStore.get(kid);
|
||||
|
||||
await compactVerify(token, undefined, {
|
||||
algorithms: ["RS256"],
|
||||
keyResolver,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { compactVerify, JwsError, type KeyResolver } from "@suaveplan/jws";
|
||||
|
||||
// Dispatch on a custom tenant header:
|
||||
const keyResolver: KeyResolver = async (kid, header) => {
|
||||
const tenant = header["tenant"] as string | undefined;
|
||||
if (tenant === "acme") return acmeKeyStore.get(kid);
|
||||
if (tenant === "globex") return globexKeyStore.get(kid);
|
||||
return undefined; // → KEY_NOT_FOUND
|
||||
};
|
||||
|
||||
try {
|
||||
await compactVerify(token, undefined, {
|
||||
algorithms: ["RS256"],
|
||||
keyResolver,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof JwsError && err.code === "JWS.KEY_NOT_FOUND") {
|
||||
// Inspect err.context.kid for the unresolved id.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type KeyResolver = (kid: string, header: JoseHeader) => Promise<unknown> | unknown`
|
||||
|
||||
Returning anything other than `undefined` / `null` is treated as the verification key and passed to the algorithm impl unchanged. The pipeline performs no type-check on the returned value — it relies on the algorithm impl to throw `JWS.KEY_ALG_MISMATCH` if the resolved key doesn't match the declared `alg`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Resolution priority in the verify pipeline is `opts.key → header.jwk → header.x5c → opts.keyResolver(header.kid)`. The resolver only runs when none of the earlier sources resolved a key; this means a token carrying both `kid` AND `jwk` will route through `jwk` first (assuming `allowEmbeddedJwk: true`). That ordering is deliberate: when a producer self-describes a key in the token, the consumer's stored key for the same `kid` should be re-verified against the embedded one — but doing that match is the consumer's responsibility, not this library's. The pipeline simply uses whichever source the caller has opted in to first.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `extractX5cLeafKey`
|
||||
|
||||
## Purpose
|
||||
|
||||
Decode an RFC 7515 §4.1.6 `x5c` certificate chain, delegate trust-path validation to `@suaveplan/x509.validateChain`, and extract the leaf `SubjectPublicKeyInfo` shaped to match the declared JOSE `alg`. This is the entry point that `@suaveplan/webauthn` will call for SafetyNet attestation verification: SafetyNet tokens carry their attestation cert chain in `x5c`, and the verifier needs to anchor it against Google's pinned attestation roots before trusting the embedded RSA public key.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts either standard-base64 (RFC's preference) or base64url-encoded chain entries.
|
||||
- Delegates chain validation entirely to `@suaveplan/x509.validateChain`.
|
||||
- Leaf-public-key algorithm matched against declared JOSE `alg`.
|
||||
- Chain failure → `JWS.X5C_CHAIN_INVALID` with the underlying x509 error chained as `cause`.
|
||||
- Leaf-alg mismatch → `JWS.KEY_ALG_MISMATCH`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { extractX5cLeafKey } from "@suaveplan/jws";
|
||||
import { parseCertificate } from "@suaveplan/x509";
|
||||
|
||||
const trustAnchors = [parseCertificate(googleAttestationRootDer)];
|
||||
const leaf = await extractX5cLeafKey(token.protected.x5c, "RS256", {
|
||||
trustAnchors,
|
||||
});
|
||||
// leaf.kind === "rsa"; leaf.key is the SafetyNet leaf cert's RSA public key.
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { compactVerify, extractX5cLeafKey, JwsError } from "@suaveplan/jws";
|
||||
|
||||
// Use through compactVerify's automatic x5c resolution:
|
||||
const result = await compactVerify(safetynetToken, undefined, {
|
||||
algorithms: ["RS256"],
|
||||
allowX5c: true,
|
||||
x5cTrustAnchors: trustAnchors,
|
||||
});
|
||||
|
||||
// Direct call with a deadline:
|
||||
try {
|
||||
const ac = new AbortController();
|
||||
setTimeout(() => ac.abort(), 1_000);
|
||||
await extractX5cLeafKey(chain, "RS256", { trustAnchors, signal: ac.signal });
|
||||
} catch (err) {
|
||||
if (err instanceof JwsError && err.code === "JWS.X5C_CHAIN_INVALID") {
|
||||
// err.cause is the underlying X509Error — inspect cause.code for
|
||||
// CERT_EXPIRED / NAME_CHAIN_BROKEN / etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `extractX5cLeafKey(x5c: readonly string[], alg: string, options: ExtractX5cOptions): Promise<ParsedKey>`
|
||||
- `interface ExtractX5cOptions { trustAnchors: readonly X509Certificate[]; clock?: Clock; signal?: AbortSignal }`
|
||||
- Returns the same `ParsedKey` union as `jwkToCryptoKey`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`x5c` array entries are base64-encoded DER certificates per RFC 7515 §4.1.6. The decoder accepts both alphabets because real-world producers vary; the strict choice is base64 standard alphabet with optional padding, but base64url is also surfaced from some Google attestation chains. The leaf-alg check is intentionally separate from chain validation: a malicious producer could submit a valid Google-rooted chain whose leaf is an ECDSA cert and claim `alg: "RS256"` — chain validation would pass (Google did sign that cert), but the JOSE-level alg doesn't match the leaf SPKI, and the verifier MUST reject. `validateChain` runs the full RFC 5280 §6.1 algorithm (expiry, name chaining, key usage, EKU, name constraints, optional OCSP), so this module owns only the JOSE-layer "does the leaf match `alg`" check.
|
||||
Reference in New Issue
Block a user