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,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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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)`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user