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,49 @@
# `pem/labels`
## Purpose
Centralised PEM textual labels per RFC 7468 §5/§10/§11 used by the public
and private PEM parsers and serialisers in this package. Keeping the labels
in one file means the parser whitelist and serialiser emission share a
single source of truth.
## Features
- `PEM_LABEL_PUBLIC_KEY` — `"PUBLIC KEY"` (SPKI body).
- `PEM_LABEL_RSA_PUBLIC_KEY` — `"RSA PUBLIC KEY"` (PKCS#1 public body).
- `PEM_LABEL_PRIVATE_KEY` — `"PRIVATE KEY"` (PKCS#8 body).
- `PEM_LABEL_RSA_PRIVATE_KEY` — `"RSA PRIVATE KEY"` (PKCS#1 private body).
- `PEM_LINE_LENGTH = 64` — RFC 7468 §3 maximum body line length.
## Usage: Basic Example
```ts
import { PEM_LABEL_PUBLIC_KEY } from "@suaveplan/crypto-rsa/pem";
const armor = `-----BEGIN ${PEM_LABEL_PUBLIC_KEY}-----`;
```
## Usage: Advanced Example
```ts
import {
PEM_LABEL_PRIVATE_KEY,
PEM_LABEL_RSA_PRIVATE_KEY,
} from "@suaveplan/crypto-rsa/pem";
function classifyPrivatePem(label: string): "pkcs8" | "pkcs1" | "unknown" {
if (label === PEM_LABEL_PRIVATE_KEY) return "pkcs8";
if (label === PEM_LABEL_RSA_PRIVATE_KEY) return "pkcs1";
return "unknown";
}
```
## API Reference
All exports are `string` constants.
## Implementation Notes
RFC 7468 §3 fixes the body line length at 64 base64 characters. Some legacy
emitters use 76 (the historic RFC 1421 PEM length); we accept both on parse
but always emit 64.

View File

@@ -0,0 +1,50 @@
# `pem/parse-private`
## Purpose
Parse an RSA private key from RFC 7468 PEM textual encoding. Accepts the
two canonical private-key armor labels — `"PRIVATE KEY"` (PKCS#8 per RFC
5208 / RFC 5958) and `"RSA PRIVATE KEY"` (PKCS#1 per RFC 8017 Appendix
A.1.2) — and dispatches to the matching DER parser.
## Features
- Dual-label support (`"PRIVATE KEY"` and `"RSA PRIVATE KEY"`).
- Delegates to existing DER parsers.
- Strict-grammar body validation via `decodePem`.
## Usage: Basic Example
```ts
import { parseRsaPrivateKeyPem } from "@suaveplan/crypto-rsa/pem";
const key = parseRsaPrivateKeyPem(`
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAAS...
-----END PRIVATE KEY-----
`);
```
## Usage: Advanced Example
```ts
import { parseRsaPrivateKeyPem } from "@suaveplan/crypto-rsa/pem";
const fromPkcs8 = parseRsaPrivateKeyPem(pkcs8Pem);
const fromPkcs1 = parseRsaPrivateKeyPem(pkcs1Pem);
```
## API Reference
### `parseRsaPrivateKeyPem(pem: string): RsaPrivateKey`
Throws `CryptoRsaError(CRYPTO_RSA.PEM_MALFORMED)` on framing errors,
`CRYPTO_RSA.INVALID_KEY` / `CRYPTO_RSA.UNSUPPORTED_FORMAT` on inner-DER errors.
## Implementation Notes
Same dispatcher pattern as `parse-public.ts` — `Record<label, parser>` with
the allowlist derived from the dispatcher keys. Encrypted PEM bodies
(legacy `Proc-Type: 4,ENCRYPTED` headers, OpenSSL `DEK-Info` lines) are
not supported ; consumers needing those should decrypt to a
plain PKCS#8 body before calling this function.

View File

@@ -0,0 +1,52 @@
# `pem/parse-public`
## Purpose
Parse an RSA public key from RFC 7468 PEM textual encoding. Accepts both
canonical armor labels — `"PUBLIC KEY"` (SPKI per RFC 5280) and
`"RSA PUBLIC KEY"` (PKCS#1 per RFC 8017) — and dispatches to the
appropriate DER parser. Mismatched BEGIN/END, unknown labels, or non-base64
body content surface as `CRYPTO_RSA.PEM_MALFORMED`.
## Features
- Dual-label support (`"PUBLIC KEY"` and `"RSA PUBLIC KEY"`).
- Delegates to existing DER parsers — no DER logic duplicated.
- Strict-grammar body validation via `decodePem`.
## Usage: Basic Example
```ts
import { parseRsaPublicKeyPem } from "@suaveplan/crypto-rsa/pem";
const key = parseRsaPublicKeyPem(`
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
`);
```
## Usage: Advanced Example
```ts
import { parseRsaPublicKeyPem } from "@suaveplan/crypto-rsa/pem";
// Either label is accepted.
const fromSpki = parseRsaPublicKeyPem(spkiPem);
const fromPkcs1 = parseRsaPublicKeyPem(pkcs1Pem);
```
## API Reference
### `parseRsaPublicKeyPem(pem: string): RsaPublicKey`
Throws `CryptoRsaError(CRYPTO_RSA.PEM_MALFORMED)` on framing errors and
`CryptoRsaError(CRYPTO_RSA.INVALID_KEY | CRYPTO_RSA.UNSUPPORTED_FORMAT)`
on inner-DER errors per the underlying DER parsers.
## Implementation Notes
The dispatcher is a `Record<string, parser>` lookup keyed by label. Building
the allowed-label list from the dispatcher's keys ensures the codec's
allowlist and the dispatcher's coverage cannot drift — adding a new label
requires changing exactly one place.

View File

@@ -0,0 +1,61 @@
# `pem/pem-codec`
## Purpose
Underlying RFC 7468 textual-encoding codec used by the four PEM
parse/serialise entry points in this module. Splits the codec from the
public-key-specific dispatchers so the parser logic (label whitelist,
mismatched BEGIN/END detection, base64 strict-grammar enforcement) lives
in exactly one place.
## Features
- `decodePem(pem, allowedLabels)` — line-tolerant scan for matching
BEGIN/END armor, label whitelist enforcement, body base64 round-trip.
- `encodePem(label, body)` — canonical 64-char body line emission with
trailing newline.
- All failure modes raise `CryptoRsaError(CRYPTO_RSA.PEM_MALFORMED)`.
## Usage: Basic Example
```ts
import { decodePem, encodePem } from "@suaveplan/crypto-rsa/pem";
const decoded = decodePem(pemString, ["PUBLIC KEY"]);
const back = encodePem("PUBLIC KEY", decoded.body);
```
## Usage: Advanced Example
```ts
import { decodePem } from "@suaveplan/crypto-rsa/pem";
const { label, body } = decodePem(input, ["PUBLIC KEY", "RSA PUBLIC KEY"]);
// Dispatch on label to the appropriate DER parser.
```
## API Reference
### `decodePem(pem: string, allowedLabels: readonly string[]): DecodedPem`
Returns `{ label, body }`. Throws `PEM_MALFORMED` on missing BEGIN line,
unknown label, missing matching END line, empty body, non-base64 body
characters, or base64-decode failure.
### `encodePem(label: string, body: Uint8Array): string`
Returns the canonical PEM block.
### `interface DecodedPem { readonly label: string; readonly body: Uint8Array }`
## Implementation Notes
The body grammar is enforced before base64 decoding because some base64
implementations silently accept non-canonical characters (whitespace, `_`,
`-`) that violate strict RFC 7468. We require the strict alphabet
`[A-Za-z0-9+/=]` to ensure round-trip determinism.
The codec accepts both LF and CRLF line endings on parse but always emits
LF — the canonical form recommended by RFC 7468 §2 for systems with
flexible line-ending support. CRLF emission is left to the consumer (e.g.
HTTP frameworks) as a transport-layer concern.

View File

@@ -0,0 +1,43 @@
# `pem/serialize-private`
## Purpose
Inverse of `parseRsaPrivateKeyPem`. Emits PEM textual encoding for an RSA
private key in either PKCS#8 (`"PRIVATE KEY"`, the modern default) or
PKCS#1 (`"RSA PRIVATE KEY"`, the legacy OpenSSL form) per RFC 7468 §3.
## Features
- 64-char body line emission.
- Discriminated `format` option (`"pkcs8"` default | `"pkcs1"`).
- Reuses the underlying DER serialisers.
## Usage: Basic Example
```ts
import { serializeRsaPrivateKeyPem } from "@suaveplan/crypto-rsa/pem";
const pem = serializeRsaPrivateKeyPem(key);
// -----BEGIN PRIVATE KEY----- ...
```
## Usage: Advanced Example
```ts
import { serializeRsaPrivateKeyPem } from "@suaveplan/crypto-rsa/pem";
const legacy = serializeRsaPrivateKeyPem(key, { format: "pkcs1" });
// -----BEGIN RSA PRIVATE KEY----- ...
```
## API Reference
### `serializeRsaPrivateKeyPem(key, options?: { format?: "pkcs8" | "pkcs1" }): string`
Returns the PEM string with trailing newline.
## Implementation Notes
We default to PKCS#8 for the same reason `serialize-public` defaults to
SPKI — modern PKI tooling, KMS providers, and JOSE libraries all consume
PKCS#8. PKCS#1 remains for legacy OpenSSL and ssh-keygen interop.

View File

@@ -0,0 +1,44 @@
# `pem/serialize-public`
## Purpose
Inverse of `parseRsaPublicKeyPem`. Emits PEM textual encoding for an RSA
public key in either SPKI (`"PUBLIC KEY"`, the default modern form) or
PKCS#1 (`"RSA PUBLIC KEY"`, the legacy OpenSSL form) per RFC 7468 §3.
## Features
- 64-char body line emission (RFC 7468 §3 canonical).
- Discriminated `format` option (`"spki"` default | `"pkcs1"`).
- Reuses the underlying DER serialisers — no DER logic duplicated.
## Usage: Basic Example
```ts
import { serializeRsaPublicKeyPem } from "@suaveplan/crypto-rsa/pem";
const pem = serializeRsaPublicKeyPem(key);
// -----BEGIN PUBLIC KEY----- ... (SPKI form)
```
## Usage: Advanced Example
```ts
import { serializeRsaPublicKeyPem } from "@suaveplan/crypto-rsa/pem";
const legacyOpenSsl = serializeRsaPublicKeyPem(key, { format: "pkcs1" });
// -----BEGIN RSA PUBLIC KEY----- ... (PKCS#1 form)
```
## API Reference
### `serializeRsaPublicKeyPem(key, options?: { format?: "spki" | "pkcs1" }): string`
Returns the PEM string with a trailing newline.
## Implementation Notes
The default is `format: "spki"` because the modern OpenSSL toolchain and
all PKI standards (X.509 / TLS / OIDC / WebAuthn) emit and consume SPKI-
labeled PEM. The PKCS#1 variant remains for compatibility with legacy
OpenSSL `openssl rsa -RSAPublicKey_out` and ssh-keygen tooling.