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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# keys/jwk-policy
## Purpose
Cross-check a JWK's `key_ops`, `use`, and `alg` members against the
requested operation per RFC 7517 §4.2 / §4.3 / §4.4 + RFC 7518 §6.3.
Prevents the common operational mistake of using a JWK marked
`use: "sig"` for encryption (or vice-versa), which leaks both the key
purpose and the operator intent into the wrong code path.
## Features
- Validates `key_ops` ⊃ requested operation (with the standard
encrypt → wrapKey, decrypt → unwrapKey aliases)
- Validates `use === "sig"` permits only sign/verify and `use === "enc"`
permits only encrypt/decrypt
- Validates `alg` is one of the RSA-compatible JWA registry values
(`RS{256,384,512}`, `PS{256,384,512}`, `RSA-OAEP{,−256,−384,−512}`,
`RSA1_5`) AND is appropriate for the requested operation class
- Each violation throws `CRYPTO_RSA.KEY_OP_NOT_PERMITTED` with a
`reason` attribute (`"key_ops"` / `"use"` / `"alg"`) plus context
identifying the offending member
- Skips checks when the JWK omits the corresponding member (RFC 7517
semantics: absence ≠ denial)
## Usage: Basic Example
```ts
import { enforceJwkPolicy } from "@suaveplan/crypto-rsa/keys/jwk-policy";
const jwk = {
kty: "RSA",
n: "...",
e: "AQAB",
use: "sig",
alg: "RS256",
key_ops: ["sign"],
};
enforceJwkPolicy(jwk, "sign"); // no-op
enforceJwkPolicy(jwk, "encrypt"); // throws KEY_OP_NOT_PERMITTED
```
## Usage: Advanced Example
Wire policy enforcement into a higher-level signer that accepts both
canonical `RsaPrivateKey` and JWK inputs; only run the policy check on
the JWK path because canonical keys carry no policy fields:
```ts
import { enforceJwkPolicy, parseRsaPrivateKey } from "@suaveplan/crypto-rsa";
function loadSigningKey(input: RsaJwk | Uint8Array): RsaPrivateKey {
if (typeof input === "object" && "kty" in input) {
enforceJwkPolicy(input, "sign");
return parseRsaPrivateKey(input, { format: "jwk" });
}
return parseRsaPrivateKey(input, { format: "pkcs8" });
}
```
## API Reference
### `enforceJwkPolicy(jwk, operation): void`
- `jwk: RsaJwk`
- `operation: "sign" | "verify" | "encrypt" | "decrypt"`
- **throws** `CryptoRsaError(CRYPTO_RSA.KEY_OP_NOT_PERMITTED)` on any
cross-policy violation; the error context carries `reason` (which
member triggered the rejection) plus the member value
### `JwkOperation`
`"sign" | "verify" | "encrypt" | "decrypt"`.
## Implementation Notes
- The canonical `RsaPublicKey` / `RsaPrivateKey` shapes have no
`key_ops` / `use` / `alg` fields, so direct callers using the
canonical types bypass policy enforcement entirely. Wrap the policy
check at the JWK ingestion boundary if you need it; the package's
built-in sign / encrypt surfaces accept canonical keys precisely so
they don't impose JWK-specific semantics.
- The encrypt → wrapKey / decrypt → unwrapKey aliases follow RFC 7517
§4.3 (`key_ops` value definitions); a JWK marked `key_ops: ["wrapKey"]`
is permitted to encrypt because the cryptographic operation is the
same (RSA public encryption).
- When `use` and `alg` both appear they MUST be consistent (RFC 7517
§4.4); this implementation does not cross-check them against each
other because the JWA registry ties algorithm IDs to a single use,
so the per-member checks already cover the cross-product.
- `key_ops` and `use` together are RFC-permitted but discouraged
(RFC 7517 §4.3 last paragraph); both checks fire independently when
both are present.

View File

@@ -0,0 +1,53 @@
# `keys/oid`
## Purpose
Centralised OID constants for the RSA wire formats handled by this package.
Currently exports `OID_RSA_ENCRYPTION` (`1.2.840.113549.1.1.1`), the algorithm
identifier RFC 8017 uses for `rsaEncryption` inside `SubjectPublicKeyInfo`
and `PrivateKeyInfo` `AlgorithmIdentifier` slots. Future stages may extend
this file with the signature-OID family (`sha256WithRSAEncryption` etc.) once
the sign/verify modules need them; this Stage-1 file covers the universal
key-format OID alone.
## Features
- One definition per OID — eliminates copy-paste drift between parsers.
- String-typed OIDs (`"1.2.840.113549.1.1.1"`) match the dotted-decimal form
produced by `DerReader.readObjectIdentifier`.
## Usage: Basic Example
```ts
import { OID_RSA_ENCRYPTION } from "@suaveplan/crypto-rsa";
if (oid === OID_RSA_ENCRYPTION) {
// dispatch to RSA path
}
```
## Usage: Advanced Example
```ts
import { OID_RSA_ENCRYPTION } from "@suaveplan/crypto-rsa";
import { encodeNull, encodeObjectIdentifier, encodeSequence } from "@suaveplan/crypto-rsa/asn1";
const algorithmIdentifier = encodeSequence(
encodeObjectIdentifier(OID_RSA_ENCRYPTION),
encodeNull()
);
```
## API Reference
### `OID_RSA_ENCRYPTION: "1.2.840.113549.1.1.1"`
The PKCS #1 `rsaEncryption` algorithm identifier per RFC 3279 §2.3.1, RFC 5280
Appendix A, and RFC 8017 Appendix A.1.
## Implementation Notes
The constant is exposed as a string rather than a typed brand because
`DerReader.readObjectIdentifier` returns plain strings, and direct equality
checks remain trivially constant-time at the byte level. A branded type would
add complexity without security benefit at this layer.

View File

@@ -0,0 +1,80 @@
# `keys/parse-jwk`
## Purpose
Parses RSA JWKs per RFC 7517 §6.3.1 (public) and §6.3.2 (private). Accepts
the JWK as a plain JS object literal, as a JSON string, or as a `Uint8Array`
of UTF-8 JSON bytes — all common ways the format is carried in the wild
(headers vs. bodies vs. binary protocol fields).
JWK is the JOSE / OIDC / WebAuthn canonical RSA representation and the
required input format for `@suaveplan/jws`'s RSA algorithms (`RS256`,
`PS256`, etc.).
## Features
- Triple-form input (`RsaJwk` object / JSON string / UTF-8 bytes).
- Explicit `kty: "RSA"` enforcement (`UNSUPPORTED_FORMAT` otherwise).
- Base64url decode per RFC 7515 §2 (URL-safe alphabet, no padding) via
`@suaveplan/crypto-utils`.
- Private-form requires every CRT field (`d`, `p`, `q`, `dp`, `dq`, `qi`) —
partial private JWKs reject with `INVALID_KEY`.
## Usage: Basic Example
```ts
import { parseRsaPublicKeyJwk } from "@suaveplan/crypto-rsa";
const key = parseRsaPublicKeyJwk({
kty: "RSA",
n: "0vx7agoeb...",
e: "AQAB",
});
```
## Usage: Advanced Example
```ts
import {
parseRsaPrivateKeyJwk,
parseRsaPublicKeyJwk,
} from "@suaveplan/crypto-rsa";
const fromJson = parseRsaPublicKeyJwk(jsonString);
const fromBytes = parseRsaPublicKeyJwk(utf8Bytes);
const privateKey = parseRsaPrivateKeyJwk({
kty: "RSA",
n, e, d, p, q, dp, dq, qi,
});
```
## API Reference
### `parseRsaPublicKeyJwk(input: RsaJwk | string | Uint8Array): RsaPublicKey`
Decodes the JWK, validates `kty === "RSA"`, and decodes `n` + `e`.
### `parseRsaPrivateKeyJwk(input: RsaJwk | string | Uint8Array): RsaPrivateKey`
Same as above plus the CRT parameters. Every field is mandatory.
Both throw `CRYPTO_RSA.UNSUPPORTED_FORMAT` for non-`RSA` `kty`, and
`CRYPTO_RSA.INVALID_KEY` for missing members, empty values, or invalid
base64url.
## Implementation Notes
The JWK input may be an `RsaJwk` literal (already-typed), a JSON string
(common in headers), or a UTF-8 `Uint8Array` (common in network framing).
Internally we normalise to a plain object before extracting members so
the validation logic stays one path.
Base64url decoding goes through `@suaveplan/crypto-utils`'s `fromBase64Url`
which accepts both padded and unpadded inputs; JWA mandates unpadded, but
some implementations leak padding in the wild and we accept that gracefully.
The JWK private-key `qi` member is RFC 7517's name for what RFC 8017 calls
`qInv` (the CRT coefficient `q⁻¹ mod p`). The canonical `RsaPrivateKey`
shape uses `qInv` for consistency with RFC 8017, so we translate `qi → qInv`
in the parser.

View File

@@ -0,0 +1,66 @@
# `keys/parse-pkcs1-private`
## Purpose
Parses a DER-encoded `RSAPrivateKey` per RFC 8017 Appendix A.1.2 into the
canonical `RsaPrivateKey` shape with mandatory CRT parameters
(`p`, `q`, `dp`, `dq`, `qInv`). Also exports `finalisePrivateKey` so the
PKCS#8 and JWK parsers share the same field-positivity validation.
Multi-prime RSA (`version == 1` with non-empty `otherPrimeInfos`) is recognised
but explicitly rejected with `CRYPTO_RSA.UNSUPPORTED_FORMAT` — the wider
package contract assumes two-prime RSA throughout.
## Features
- Strict-DER `RSAPrivateKey` parser via `DerReader`.
- Version validation (`0` two-prime, `1` multi-prime — both detected; only
`0` accepted).
- Multi-prime detection via either `version === 1` OR a trailing SEQUENCE
(covers conformant and non-conformant emitters).
- Per-field positivity gate via `finalisePrivateKey`.
- Bit-length derivation from `n.toString(2).length`.
## Usage: Basic Example
```ts
import { parseRsaPrivateKeyPkcs1 } from "@suaveplan/crypto-rsa";
const key = parseRsaPrivateKeyPkcs1(derBytes);
// key.n, key.e, key.d, key.p, key.q, key.dp, key.dq, key.qInv, key.bitLength
```
## Usage: Advanced Example
```ts
import { finalisePrivateKey } from "@suaveplan/crypto-rsa/keys";
const key = finalisePrivateKey({ n, e, d, p, q, dp, dq, qInv });
```
## API Reference
### `parseRsaPrivateKeyPkcs1(bytes: Uint8Array): RsaPrivateKey`
Throws `CryptoRsaError(CRYPTO_RSA.INVALID_KEY)` on structural problems and
on any non-positive field. Throws `CryptoRsaError(CRYPTO_RSA.UNSUPPORTED_FORMAT)`
on multi-prime input.
### `finalisePrivateKey(inputs): RsaPrivateKey`
Validates each of `n, e, d, p, q, dp, dq, qInv` is `> 0n` and computes
`bitLength` from `n`. Surfaces the same `INVALID_KEY` error class as the
DER parser.
## Implementation Notes
The parser uses both `version === 1n` and a trailing-SEQUENCE peek to detect
multi-prime keys. Some emitters set `version` to `0` even when they include
`otherPrimeInfos`; we reject either flag to avoid silently truncating the
prime set.
The fields are read in fixed order with `seq.readInteger()` calls; if any
field is missing the underlying `DerReader` raises `INVALID_KEY` with a
truncation context. The final `seq.assertEnd()` ensures no extra fields
slip through (other than the multi-prime trailing SEQUENCE that we explicitly
detected and rejected).

View File

@@ -0,0 +1,60 @@
# `keys/parse-pkcs1-public`
## Purpose
Parses a DER-encoded `RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }`
per RFC 8017 Appendix A.1.1 into the canonical `RsaPublicKey` shape used throughout
`@suaveplan/crypto-rsa`. Also exports `finalisePublicKey(n, e)` so the SPKI and JWK
parsers can share the same `(n, e)` validation and `bitLength` computation without
re-running DER parsing.
The parser rejects `e < 3` and even public exponents at this layer because every
downstream operation (verify, encrypt) assumes those constraints; rejecting at parse
time means the operations never have to defensively re-check.
## Features
- Strict-DER `RSAPublicKey` parser via `DerReader`.
- Public-exponent validation: rejects `e < 3` and even `e`.
- Bit-length derivation from `n.toString(2).length` (no off-by-one risk because
the leading `1` of any positive bigint is always present).
- `finalisePublicKey(n, e)` exported for sibling parsers.
## Usage: Basic Example
```ts
import { parseRsaPublicKeyPkcs1 } from "@suaveplan/crypto-rsa";
const key = parseRsaPublicKeyPkcs1(derBytes);
// key.n, key.e, key.bitLength
```
## Usage: Advanced Example
```ts
import { finalisePublicKey } from "@suaveplan/crypto-rsa/keys";
// SPKI / JWK parsers share the validation logic.
const key = finalisePublicKey(n, e);
```
## API Reference
### `parseRsaPublicKeyPkcs1(bytes: Uint8Array): RsaPublicKey`
Throws `CryptoRsaError(CRYPTO_RSA.INVALID_KEY)` on any structural problem
(malformed DER, missing fields, trailing data, even or sub-3 exponent,
non-positive modulus).
### `finalisePublicKey(n: bigint, e: bigint): RsaPublicKey`
Validates `(n, e)` and computes `bitLength`. Surfaces the same `INVALID_KEY`
errors as `parseRsaPublicKeyPkcs1`.
## Implementation Notes
`bitLength` derives from the binary representation of `n`. For a 2048-bit
key the modulus has bit length exactly 2048; the leading bit of `n` is
always `1` because the underlying primes are sampled with the high bit
fixed. Consumers that need strict round-number assertions should compare
against the canonical {2048, 3072, 4096} set in their own gate.

View File

@@ -0,0 +1,55 @@
# `keys/parse-pkcs8`
## Purpose
Parses a PKCS#8 `PrivateKeyInfo` per RFC 5208 §5 — also accepts the
RFC 5958 `OneAsymmetricKey` shape, which adds optional `[0] attributes`
and `[1] publicKey` trailing fields. The inner `privateKey OCTET STRING`
carries a PKCS#1 `RSAPrivateKey` which is delegated to the PKCS#1 parser.
PKCS#8 is the canonical wire format for any private key persisted to
disk (OpenSSL `PRIVATE KEY` PEM, PKCS#12 / PFX `EncryptedData` payloads
after decryption, JWE `kty: RSA` private-form decryption keys).
## Features
- DER outer SEQUENCE → version → AlgorithmIdentifier → OCTET STRING walk.
- Algorithm OID equality against `OID_RSA_ENCRYPTION`.
- RFC 5958 `[0] attributes` and `[1] publicKey` tolerant — both consumed
and discarded.
- Delegates to `parseRsaPrivateKeyPkcs1` for the inner RSAPrivateKey body.
## Usage: Basic Example
```ts
import { parseRsaPrivateKeyPkcs8 } from "@suaveplan/crypto-rsa";
const key = parseRsaPrivateKeyPkcs8(pkcs8DerBytes);
```
## Usage: Advanced Example
```ts
import { parseRsaPrivateKeyPkcs8 } from "@suaveplan/crypto-rsa";
// RFC 5958 OneAsymmetricKey form with optional trailing publicKey [1].
const key = parseRsaPrivateKeyPkcs8(oneAsymmetricKeyBytes);
```
## API Reference
### `parseRsaPrivateKeyPkcs8(bytes: Uint8Array): RsaPrivateKey`
Throws `CryptoRsaError(CRYPTO_RSA.UNSUPPORTED_FORMAT)` for non-RSA OIDs.
Throws `CryptoRsaError(CRYPTO_RSA.INVALID_KEY)` on structural problems
(unsupported `version`, missing fields, trailing non-context-specific bytes).
## Implementation Notes
PKCS#8 `version` is `0` for RFC 5208 and `1` for the RFC 5958 successor
(the version bumps when `publicKey` is present). Both are accepted; the
RFC 5958 trailing fields are walked through as opaque context-specific
TLVs without attempting to parse them, because Stage 1 has no need for
the public-key cross-check or the attribute set. Stage 4 may add a
verification that the `[1] publicKey` matches the inner PKCS#1
`(n, e)`.

View File

@@ -0,0 +1,61 @@
# `keys/parse-spki`
## Purpose
Parses an X.509 `SubjectPublicKeyInfo` per RFC 5280 §4.1 + Appendix A and
unwraps the inner PKCS#1 `RSAPublicKey`. SPKI is the canonical wire format
for RSA public keys in TLS certificates (`Certificate.tbsCertificate.subjectPublicKeyInfo`),
JWK headers (`x5c`/`x5t#S256` reference chains), and the WebAuthn TPM
attestation statement.
The parser enforces the `1.2.840.113549.1.1.1` `rsaEncryption` algorithm OID
and the canonical `NULL` algorithm parameters per RFC 3279 §2.3.1.
## Features
- DER outer-SEQUENCE → AlgorithmIdentifier → BIT STRING walk.
- Algorithm OID equality check against `OID_RSA_ENCRYPTION`.
- `NULL` algorithm-parameters enforcement (RFC 3279 §2.3.1).
- Delegates inner PKCS#1 parse to `parseRsaPublicKeyPkcs1`.
## Usage: Basic Example
```ts
import { parseRsaPublicKeySpki } from "@suaveplan/crypto-rsa";
const key = parseRsaPublicKeySpki(spkiDerBytes);
```
## Usage: Advanced Example
```ts
import { parseRsaPublicKeySpki } from "@suaveplan/crypto-rsa";
// SPKI carrying an EC OID rejects with UNSUPPORTED_FORMAT.
try {
parseRsaPublicKeySpki(ecSpkiBytes);
} catch (error) {
if (error instanceof CryptoRsaError && error.code === "CRYPTO_RSA.UNSUPPORTED_FORMAT") {
// dispatch to @suaveplan/crypto-curve
}
}
```
## API Reference
### `parseRsaPublicKeySpki(bytes: Uint8Array): RsaPublicKey`
Throws `CryptoRsaError(CRYPTO_RSA.UNSUPPORTED_FORMAT)` for non-RSA OIDs.
Throws `CryptoRsaError(CRYPTO_RSA.INVALID_KEY)` on structural problems
(missing fields, trailing data, unused-bits ≠ 0 in the `BIT STRING`).
## Implementation Notes
The `BIT STRING` payload is copied (defensive copy in `DerReader.readBitString`)
so the inner PKCS#1 reader never aliases the input buffer. This costs one
allocation per parse but eliminates a class of subtle bugs where downstream
mutation of the inner buffer would corrupt the SPKI input.
`alg.assertEnd()` and `outer.assertEnd()` together guarantee no extra fields
slipped through. RFC 3279 forbids extra parameters in the `rsaEncryption`
algorithm identifier, and RFC 5280 forbids extra fields in `SubjectPublicKeyInfo`.

View File

@@ -0,0 +1,62 @@
# `keys/parse`
## Purpose
Public dispatcher entry-point unifying the four key-format parsers
(PKCS#1, SPKI, PKCS#8, JWK) under one ergonomic call. Selects the
underlying parser via a `format` discriminator on the options object,
matching the round-trip pattern used elsewhere in the crypto
family.
## Features
- `parseRsaPublicKey(input, { format: "pkcs1" | "spki" | "jwk" })`.
- `parseRsaPrivateKey(input, { format: "pkcs1" | "pkcs8" | "jwk" })`.
- Binary formats accept `Uint8Array`; JWK accepts object / JSON string /
UTF-8 bytes.
- `default` branches surface `CRYPTO_RSA.UNSUPPORTED_FORMAT` so any future
format added to the type union without a switch arm fails loudly.
## Usage: Basic Example
```ts
import { parseRsaPublicKey } from "@suaveplan/crypto-rsa";
const key = parseRsaPublicKey(spkiBytes, { format: "spki" });
```
## Usage: Advanced Example
```ts
import {
parseRsaPrivateKey,
parseRsaPublicKey,
} from "@suaveplan/crypto-rsa";
const fromPkcs8 = parseRsaPrivateKey(pkcs8Bytes, { format: "pkcs8" });
const fromJwk = parseRsaPrivateKey(jwkObject, { format: "jwk" });
const fromJsonJwk = parseRsaPrivateKey(jwkJsonString, { format: "jwk" });
```
## API Reference
### `parseRsaPublicKey(input, options): RsaPublicKey`
Throws `CRYPTO_RSA.INVALID_KEY` when a non-`Uint8Array` is passed for the
binary formats.
### `parseRsaPrivateKey(input, options): RsaPrivateKey`
Same input/error contract on the private side.
## Implementation Notes
The dispatcher is exhaustive over the literal union via `assertNeverFormat`,
which TypeScript verifies statically. Adding a new format requires both a
new type member and a new switch arm — the compiler enforces both. This is
the only safe way to evolve a discriminated dispatcher without silently
falling through to a default branch.
The `requireBytes` helper is shared between the PKCS#1/SPKI/PKCS#8 paths
because they all consume `Uint8Array`. JWK input is forwarded through the
JWK parser which handles its own normalisation.

View File

@@ -0,0 +1,54 @@
# `keys/serialize-jwk`
## Purpose
Inverse of `parseRsaPublicKeyJwk` and `parseRsaPrivateKeyJwk`. Emits an
`RsaJwk` with every numeric field encoded as base64url per RFC 7515 §2
(URL-safe alphabet, no padding) over the minimum-length big-endian
representation of each `bigint` per RFC 7518 §6.3.
## Features
- Per-field base64url encoding via `@suaveplan/crypto-utils`'s `toBase64Url`.
- Minimum-length big-endian byte derivation — no leading-zero padding.
- Public-form emits `{ kty, n, e }` only; private-form adds the full CRT set.
- Maps the canonical `qInv` field name to JWA's `qi` per RFC 7517 §6.3.2.
## Usage: Basic Example
```ts
import { serializeRsaPublicKeyJwk } from "@suaveplan/crypto-rsa";
const jwk = serializeRsaPublicKeyJwk(key);
// { kty: "RSA", n: "...", e: "AQAB" }
```
## Usage: Advanced Example
```ts
import {
parseRsaPrivateKeyJwk,
serializeRsaPrivateKeyJwk,
} from "@suaveplan/crypto-rsa";
const jwk = serializeRsaPrivateKeyJwk(key);
const roundTripped = parseRsaPrivateKeyJwk(jwk);
```
## API Reference
### `serializeRsaPublicKeyJwk(key: RsaPublicKey): RsaJwk`
Returns `{ kty: "RSA", n, e }`.
### `serializeRsaPrivateKeyJwk(key: RsaPrivateKey): RsaJwk`
Returns `{ kty: "RSA", n, e, d, p, q, dp, dq, qi }`.
## Implementation Notes
The minimum-length encoding implies that `e === 65537n` (the universal
default RSA exponent) emits as the 3-byte sequence `0x010001`, which
base64url-encodes to `"AQAB"` — the same canonical string every JWK
ecosystem produces. Encoding via i2osp with the byte length derived from
`hex.length / 2` ensures we never accidentally emit a leading-zero byte.

View File

@@ -0,0 +1,52 @@
# `keys/serialize-pkcs1-private`
## Purpose
Inverse of `parseRsaPrivateKeyPkcs1`. Emits a DER `RSAPrivateKey` SEQUENCE
per RFC 8017 Appendix A.1.2 from a canonical `RsaPrivateKey`. Always emits
two-prime form with `version 0`. Multi-prime keys cannot be constructed
through this package's parsers (the parser rejects them) so the serialiser
also never has to emit the multi-prime branch.
## Features
- Strict-DER nine-INTEGER SEQUENCE emission in canonical field order.
- Always emits `version 0` (two-prime).
- Pure function — no validation overhead.
## Usage: Basic Example
```ts
import { serializeRsaPrivateKeyPkcs1 } from "@suaveplan/crypto-rsa";
const der = serializeRsaPrivateKeyPkcs1(key);
```
## Usage: Advanced Example
```ts
import {
parseRsaPrivateKeyPkcs1,
serializeRsaPrivateKeyPkcs1,
} from "@suaveplan/crypto-rsa";
const roundTripped = parseRsaPrivateKeyPkcs1(
serializeRsaPrivateKeyPkcs1(key)
);
```
## API Reference
### `serializeRsaPrivateKeyPkcs1(key: RsaPrivateKey): Uint8Array`
Returns the canonical DER encoding.
## Implementation Notes
The field order matches RFC 8017 Appendix A.1.2 exactly: `version`,
`modulus`, `publicExponent`, `privateExponent`, `prime1`, `prime2`,
`exponent1`, `exponent2`, `coefficient`. Any reorder would still parse
under a tolerant reader but would fail strict-DER validation.
`version` is encoded as `0n` rather than `0` (a `bigint`) because
`encodeInteger` is monomorphised over `bigint` for the two-prime case.

View File

@@ -0,0 +1,53 @@
# `keys/serialize-pkcs1-public`
## Purpose
Inverse of `parseRsaPublicKeyPkcs1`. Emits a DER `RSAPublicKey` SEQUENCE
per RFC 8017 Appendix A.1.1 from a canonical `RsaPublicKey`. The output
is byte-identical for any input that previously round-tripped through
the parser, and is the canonical body wrapped by SPKI public-key
emission and PEM `-----BEGIN RSA PUBLIC KEY-----` framing.
## Features
- Single-call DER emission via `encodeSequence(encodeInteger, encodeInteger)`.
- Strict-DER output (minimal length, minimal integer encoding).
- Pure function — no allocation beyond the byte buffers.
## Usage: Basic Example
```ts
import { serializeRsaPublicKeyPkcs1 } from "@suaveplan/crypto-rsa";
const der = serializeRsaPublicKeyPkcs1(key);
```
## Usage: Advanced Example
```ts
import {
parseRsaPublicKeyPkcs1,
serializeRsaPublicKeyPkcs1,
} from "@suaveplan/crypto-rsa";
// Round-trip invariant.
const roundTripped = parseRsaPublicKeyPkcs1(
serializeRsaPublicKeyPkcs1(key)
);
// roundTripped equals key.
```
## API Reference
### `serializeRsaPublicKeyPkcs1(key: RsaPublicKey): Uint8Array`
Returns the canonical DER encoding. Never throws on a structurally valid
`RsaPublicKey`.
## Implementation Notes
The serialiser does not re-validate `(n, e)`; that is the parser's job.
Callers that reach this path have either constructed the key locally
(in which case validation is the constructor's job) or routed through
the parser (already validated). Adding a redundant check here would
double the cost without adding any safety guarantee.

View File

@@ -0,0 +1,49 @@
# `keys/serialize-pkcs8`
## Purpose
Inverse of `parseRsaPrivateKeyPkcs8`. Wraps a serialised PKCS#1
`RSAPrivateKey` in a DER `PrivateKeyInfo` per RFC 5208 §5, emitting the
canonical `version 0` form (RFC 5958 `version 1` requires the optional
`publicKey [1]` field which Stage 1 does not emit).
## Features
- Standard PKCS#8 emission with `version 0`.
- Reuses `serializeRsaPrivateKeyPkcs1` so the inner OCTET STRING body
matches the PKCS#1 output bit-for-bit.
- RFC 3279 §2.3.1 conformant `NULL` algorithm parameters.
## Usage: Basic Example
```ts
import { serializeRsaPrivateKeyPkcs8 } from "@suaveplan/crypto-rsa";
const pkcs8 = serializeRsaPrivateKeyPkcs8(key);
```
## Usage: Advanced Example
```ts
import {
parseRsaPrivateKeyPkcs8,
serializeRsaPrivateKeyPkcs8,
} from "@suaveplan/crypto-rsa";
const roundTripped = parseRsaPrivateKeyPkcs8(
serializeRsaPrivateKeyPkcs8(key)
);
```
## API Reference
### `serializeRsaPrivateKeyPkcs8(key: RsaPrivateKey): Uint8Array`
Returns the DER-encoded `PrivateKeyInfo`.
## Implementation Notes
We emit `version 0` (RFC 5208) rather than `version 1` (RFC 5958) because
Stage 1 does not have a public-key derivation pipeline yet. Stage 4 may
add a `version 1` emission path that includes the `[1] publicKey` field
for ecosystems (cosign, sigstore) that prefer the OneAsymmetricKey form.

View File

@@ -0,0 +1,47 @@
# `keys/serialize-spki`
## Purpose
Inverse of `parseRsaPublicKeySpki`. Wraps a serialised PKCS#1 `RSAPublicKey`
in a DER `SubjectPublicKeyInfo` per RFC 5280 §4.1, with the canonical
`AlgorithmIdentifier { algorithm = 1.2.840.113549.1.1.1, parameters = NULL }`
inner SEQUENCE.
## Features
- Standard X.509 SPKI emission.
- RFC 3279 §2.3.1 conformant `NULL` algorithm parameters.
- Reuses `serializeRsaPublicKeyPkcs1` so the inner body matches the PKCS#1
output bit-for-bit.
## Usage: Basic Example
```ts
import { serializeRsaPublicKeySpki } from "@suaveplan/crypto-rsa";
const spki = serializeRsaPublicKeySpki(key);
```
## Usage: Advanced Example
```ts
import { serializeRsaPublicKeySpki } from "@suaveplan/crypto-rsa";
// Compute the SPKI fingerprint downstream consumers expect for cert pinning.
const spki = serializeRsaPublicKeySpki(key);
const fingerprint = await crypto.subtle.digest("SHA-256", spki);
```
## API Reference
### `serializeRsaPublicKeySpki(key: RsaPublicKey): Uint8Array`
Returns the DER-encoded `SubjectPublicKeyInfo`.
## Implementation Notes
The output is the canonical RFC 5280 SPKI; many PKI libraries accept variants
with implicit-NULL parameters (omitted entirely) but emit the explicit-NULL
form, so we follow the de-facto majority. The PKCS#1 body inside the
`BIT STRING` is wrapped with one leading `0x00` "unused bits" byte per
RFC 5280 §4.1 (handled by `encodeBitString`).

View File

@@ -0,0 +1,52 @@
# `keys/serialize`
## Purpose
Public dispatcher entry-point inverse of `parse`. Selects the per-format
serialiser via the same `format` discriminator. Returns `Uint8Array` for
binary formats (`pkcs1`, `spki`, `pkcs8`) and a structured `RsaJwk` object
for JWK so consumers can post-process the result without re-parsing JSON.
## Features
- `serializeRsaPublicKey(key, { format })` over the PKCS#1 / SPKI / JWK set.
- `serializeRsaPrivateKey(key, { format })` over the PKCS#1 / PKCS#8 / JWK set.
- `default` branch surfaces `CRYPTO_RSA.UNSUPPORTED_FORMAT` for safety.
## Usage: Basic Example
```ts
import { serializeRsaPublicKey } from "@suaveplan/crypto-rsa";
const der = serializeRsaPublicKey(key, { format: "spki" });
```
## Usage: Advanced Example
```ts
import {
serializeRsaPrivateKey,
serializeRsaPublicKey,
} from "@suaveplan/crypto-rsa";
const jwk = serializeRsaPublicKey(key, { format: "jwk" });
const pkcs8 = serializeRsaPrivateKey(key, { format: "pkcs8" });
```
## API Reference
### `serializeRsaPublicKey(key, options): Uint8Array | RsaJwk`
Returns bytes for `pkcs1` / `spki`, a structured `RsaJwk` for `jwk`.
### `serializeRsaPrivateKey(key, options): Uint8Array | RsaJwk`
Returns bytes for `pkcs1` / `pkcs8`, a structured `RsaJwk` for `jwk`.
## Implementation Notes
The mixed `Uint8Array | RsaJwk` return type intentionally pushes the
discrimination back to the caller — picking `format: "jwk"` is a deliberate
choice and the caller already knows the result will be an object. Type
narrowing on the `format` literal is left to consumer-side discriminated
union helpers because Stage 1 deliberately keeps the dispatcher minimal.