<div align="center">

<img src="assets/pkijs-logo.png" alt="@blamejs/pki" width="200" />

# @blamejs/pki

**PKI in pure JavaScript, with its own ASN.1 codec and crypto engine.**

X.509, ASN.1/DER, OID, CMS, OCSP, CRL, timestamping, enrollment, and the PKCS
formats. The fail-closed DER codec and the post-quantum-first algorithm registry
are in-tree and public, so strictness and algorithm coverage are decisions this
toolkit makes rather than inherits, and neither is capped by what Web Crypto
exposes. No runtime dependencies, no TypeScript, no build step.

[![npm version](https://img.shields.io/npm/v/@blamejs/pki.svg?label=%40blamejs%2Fpki&color=2563eb)](https://www.npmjs.com/package/@blamejs/pki)
[![npm downloads](https://img.shields.io/npm/dm/@blamejs/pki.svg?color=2563eb)](https://www.npmjs.com/package/@blamejs/pki)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![node](https://img.shields.io/node/v/@blamejs/pki.svg)](https://nodejs.org)

[![CI](https://github.com/blamejs/pki/actions/workflows/ci.yml/badge.svg)](https://github.com/blamejs/pki/actions/workflows/ci.yml)
[![CodeQL](https://github.com/blamejs/pki/actions/workflows/codeql.yml/badge.svg)](https://github.com/blamejs/pki/actions/workflows/codeql.yml)
[![Fuzzing](https://github.com/blamejs/pki/actions/workflows/cflite_batch.yml/badge.svg)](https://github.com/blamejs/pki/actions/workflows/cflite_batch.yml)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/blamejs/pki/badge)](https://scorecard.dev/viewer/?uri=github.com/blamejs/pki)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13562/badge)](https://www.bestpractices.dev/projects/13562)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev/spec/v1.0/levels#build-l3)

[![Zero runtime deps](https://img.shields.io/badge/runtime%20deps-0-2ea043)](#security-posture)
[![PQC-first](https://img.shields.io/badge/crypto-PQC--first-2563eb)](#security-posture)
[![No TypeScript](https://img.shields.io/badge/TypeScript-not%20required-2ea043)](#why-this-toolkit)
[![strict DER](https://img.shields.io/badge/DER-strict%20%2F%20fail--closed-2ea043)](#security-posture)

[pkijs.com](https://pkijs.com) · [Roadmap](ROADMAP.md) · [Security](SECURITY.md) · [Changelog](CHANGELOG.md)

</div>

---

## Why this toolkit

Most JavaScript PKI code gets its ASN.1 parser and its algorithm coverage from
somewhere else: an external DER library with its own CVE history, or the Web
Crypto API with its limits on streaming, opaque keys, and algorithm reach.
`@blamejs/pki` owns those layers.

- **Its own DER codec.** Strict, canonical, and bounded. Malformed input is
  rejected in bounded time rather than walked into a stack overflow.
- **An OID-named algorithm registry.** Every algorithm, attribute, and extension
  is named through one two-way OID table (`pki.oid`). Adding a signature or KEM
  algorithm, post-quantum ones included, is a registry entry rather than a case
  in `parse`. Sign and verify resolve algorithms through the same table the
  parsers read.
- **Fail-closed everywhere.** Every parse, sign, and verify path throws on
  failure. No path returns zero, a default, or partial output in place of a
  verdict.
- **Nothing in your `package.json`.** The cryptography runs on Node's built-in
  `node:crypto`: the classical set plus post-quantum ML-DSA and SLH-DSA
  signatures via the platform OpenSSL 3.5, and ML-KEM key generation,
  encapsulation, and decapsulation. Nothing is vendored and nothing is
  installed, so there is no dependency tree for `npm audit` to report on.

## Install

```sh
npm i @blamejs/pki
```

Requires Node.js 24.21+ and runs on the shipped runtime, with no build step and
no transpilation.

```js
var pki = require("@blamejs/pki");
```

An ESM caller can import the whole namespace object or any namespace by name:

```js
import pki from "@blamejs/pki";
import { x509, cms, path, schema } from "@blamejs/pki";
```

## Quickstart

### Parse an X.509 certificate

`pki.schema.x509.parse` accepts a DER `Buffer` or a PEM string or Buffer. It
returns a fully decoded, validated certificate: distinguished names rendered and
structured, the validity window as real `Date`s, algorithms and extensions named
through the OID registry, and the exact signed `tbsBytes` for a downstream
verifier.

```js
var pki = require("@blamejs/pki");
var fs  = require("node:fs");

var pem  = fs.readFileSync("cert.pem", "utf8");
var cert = pki.schema.x509.parse(pem);

cert.subject.dn;                    // "CN=example.com, O=Example Org, C=US"
cert.issuer.dn;                     // "CN=example.com, O=Example Org, C=US"
cert.serialNumberHex;              // "7057e1ebeec2e5f7…"
cert.signatureAlgorithm.name;      // "sha256WithRSAEncryption"
cert.subjectPublicKeyInfo.algorithm.name;  // "rsaEncryption"
cert.validity.notAfter;            // Date — 2027-07-04T07:16:15.000Z

cert.extensions.forEach(function (ext) {
  ext.name;      // "subjectKeyIdentifier" (or null when the OID is unknown)
  ext.critical;  // boolean
  ext.value;     // Buffer — the raw extnValue OCTET STRING contents
});
```

Malformed bytes throw a typed error rather than returning a half-parsed object:

```js
try {
  pki.schema.x509.parse(Buffer.from([0x30, 0x03, 0x02, 0x01, 0x00]));
} catch (e) {
  e.constructor.name;  // "CertificateError"
  e.code;              // e.g. "x509/not-a-certificate" — stable domain/reason string
}
```

### Convert PEM ↔ DER

```js
var der = pki.schema.x509.pemDecode(pem, "CERTIFICATE");   // Buffer of DER bytes
var out = pki.schema.x509.pemEncode(der, "CERTIFICATE");   // 64-column PEM string
```

### Decode and build ASN.1 / DER directly

The codec under every structure is public. Decode returns a zero-copy node tree,
and the builders emit canonical DER.

```js
// Build a canonical-DER SEQUENCE, then decode it back.
var der = pki.asn1.build.sequence([
  pki.asn1.build.oid("2.5.4.3"),          // commonName
  pki.asn1.build.utf8("example.com"),
]);

var node = pki.asn1.decode(der);
node.tagNumber === pki.asn1.TAGS.SEQUENCE;   // true
node.children.length;                        // 2
pki.asn1.read.oid(node.children[0]);         // "2.5.4.3"
pki.asn1.read.string(node.children[1]);      // "example.com"
```

The decoder is strict by construction, so non-DER shapes are refused:

```js
try {
  pki.asn1.decode(Buffer.from([0x30, 0x80, 0x00, 0x00]));  // indefinite length
} catch (e) {
  e.constructor.name;  // "Asn1Error"
  e.code;              // "asn1/indefinite-length"
}
```

Size and depth are bounded before a byte is walked. Override the caps per call:

```js
pki.asn1.decode(der, { maxBytes: pki.C.BYTES.mib(4), maxDepth: 32 });
```

### Resolve object identifiers

An OID names every algorithm, attribute type, and extension. The registry
is a two-way map, seeded with the RFC 5280 set, the classical algorithm set, and
the NIST post-quantum arcs (ML-DSA, ML-KEM, SLH-DSA).

```js
pki.oid.name("1.2.840.113549.1.1.11");  // "sha256WithRSAEncryption"
pki.oid.byName("sha256");               // "2.16.840.1.101.3.4.2.1"
pki.oid.toArcs("2.5.4.3");              // [2, 5, 4, 3]

// Extend it with your own arc:
pki.oid.register("1.3.6.1.4.1.99999.1", "acmeCorpExtension");
```

### Sign with post-quantum ML-DSA, or any classical algorithm

`pki.webcrypto` is a standard W3C WebCrypto (`SubtleCrypto`) engine over
`node:crypto`. The post-quantum suite lives in the same API as RSA, ECDSA, and
EdDSA: pick the algorithm, and the rest is identical.

```js
var subtle = pki.webcrypto.subtle;
var data   = Buffer.from("sign me");

// FIPS 204 ML-DSA-65 — a post-quantum signature.
var kp  = await subtle.generateKey({ name: "ML-DSA-65" }, true, ["sign", "verify"]);
var sig = await subtle.sign({ name: "ML-DSA-65" }, kp.privateKey, data);
var ok  = await subtle.verify({ name: "ML-DSA-65" }, kp.publicKey, sig, data); // true

// The classical set — ECDSA, RSA-PSS, Ed25519, AES-GCM, ECDH, HKDF, … — is the
// same call shape, and every key it exports is OpenSSL/NSS-interoperable.
```

### Sign with a key this process cannot export

Every signing verb takes a `node:crypto` `KeyObject` wherever it takes a private
key, and signs through `node:crypto`, so a key an OpenSSL engine or provider
refuses to export still signs.

```js
var keyObject = crypto.createPrivateKey({ key: pem });
var cert = await pki.x509.sign(spec, { key: keyObject, cert: caCert });
```

For a key that is not in this process at all, held in a hardware security module,
a cloud key management service, a PKCS#11 token, a PIV slot or a Trusted Platform
Module, every signing verb takes a signer object in the same place.

```js
var signer = {
  algorithm: { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
  publicKey: spkiDer,                       // the SPKI DER of the public half
  sign: function (bytes) { return kms.sign(keyId, bytes); },   // bytes or a promise of them
};

var cert = await pki.x509.sign(spec, { key: signer, cert: caCert });
```

`sign(bytes)` is handed the bytes WebCrypto would be handed and returns the bytes
WebCrypto would return: for ECDSA the fixed-width `r || s` of RFC 9053 §2.1, not a
DER `SEQUENCE`. `algorithm` is held to the certificate's key algorithm, `publicKey`
is held to the key the signature is verified against, and the signature is
verified before the artifact is returned. The bytes handed to the callback are a
copy, so rewriting them rewrites nothing the artifact carries. Hold the client and
any mutable state in the callback's closure, as the example does: a signing verb
snapshots its options, so the signer object the callback sees is a copy of the one
you passed.

## What ships today

Everything below is callable now; nothing is a stub. The whole documented
surface is stable under the deprecation policy: a deprecation warning ships at
least one minor release before any removal, and a minor release makes no silent
breaking change. Each primitive's `@status` appears on its wiki page, and
[LTS-CALENDAR.md](LTS-CALENDAR.md) explains the status lifecycle. The
table is an index — the per-function reference, generated from the source
comment blocks, is at [pkijs.com](https://pkijs.com).

| Namespace | What it does |
|---|---|
| `pki.asn1` | Strict, bounded DER codec. `decode` returns a zero-copy node tree, `build.*` emits canonical DER values, `read.*` are typed leaf readers. A universal tag X.680 Table 1 reserves and assigns to no type is refused as `asn1/reserved-tag`, and `reservedUniversalTag` answers which tags those are without attempting a decode. Plus `encode`, `TAGS`, OID content encode/decode, and the `isPrintableString` string predicate |
| `pki.cbor` | Strict, bounded deterministic CBOR codec (RFC 8949). `decode` returns a zero-copy node tree, plus `read.*` typed leaf readers including the keyed lookup `read.mapGet` (text or COSE-label integer key, with the map's major type asserted in the accessor). Fail-closed on every non-canonical shape: indefinite length, non-minimal argument, unsorted or duplicate map keys, non-shortest float, trailing bytes. The `build.*` encoders hold an integer argument to being an integer rather than converting one: a string in any radix, a padded or empty string, and a boolean are each refused, so an unsupplied value cannot encode as zero |
| `pki.oid` | Two-way OID ↔ name registry, seeded with RFC 5280 and the NIST PQC arcs — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`, and the `isDottedDecimal` string predicate |
| `pki.webcrypto` | A W3C `SubtleCrypto` engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2 and SHA, plus post-quantum ML-DSA-44/65/87, SLH-DSA, and ML-KEM-512/768/1024 key generation with certificate and PKCS#8 import. The RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare seed or an internally inconsistent key is rejected with a typed error. `encapsulateBits` and `decapsulateBits` are the ML-KEM key-establishment pair the CMS `KEMRecipientInfo` arm rides on, with the FIPS 203 §7.3 ciphertext-length check enforced here so a direct caller inherits it. Cross-implementation use is undefined in the specification, so a `CryptoKey` from a different WebCrypto implementation is refused here with a typed fault naming its origin; the `pki.*` verbs adopt such a key instead, whether it came from the platform or from a separately installed copy of this toolkit |
| `pki.tls` | RFC 8879 certificate compression and the RFC 8446 §4.4.2 Certificate message it carries (RFC 8446 is obsoleted by RFC 9846), which is the handshake's largest payload. `decompressCertificate` decodes a `CompressedCertificate` and returns the algorithm, the declared uncompressed length, the recovered message raw, and its per-entry certificate DER. Decompression carries the two-sided bound §5 requires: capped at the message's own declared length so a bomb is refused mid-stream, then compared to that declaration exactly, which catches the under-length direction a cap alone cannot see. An algorithm outside the RFC 8879 registry, one the runtime cannot decompress, or one the receiver never advertised is refused before any decompressor is handed the bytes; an empty compressed body is a framing violation (`tls/bad-framing`); and trailing bytes, after the message or after the compressed frame, are refused (`tls/trailing-data`), so one chain has exactly one encoding. `compressCertificate` is the inverse and round-trips its own output before returning. `parseCertificateMessage` decodes the RFC 8446 §4.4.2 message on its own, surfacing each entry's certificate DER ready for `pki.schema.x509.parse` alongside its raw extensions; a separate extension negotiates `certificate_type`, so it is declared, never guessed. All three registered algorithms (zlib, brotli, zstd) are implemented, each offered only where the running Node decompresses it safely — one whose decompressor answers a truncated frame with a short result instead of a fault is dropped at startup rather than advertised with a truncation it cannot detect. Structure only: no handshake is spoken and no certificate verified — `decompressCertificate`, `compressCertificate`, `parseCertificateMessage` |
| `pki.schema` | The schema family. `parse` detects which PKI format a DER or PEM input encodes and routes it to the right parser; `all` enumerates the registered formats; `pem.decodeBundle` reads every object in a multi-object PEM file (a `fullchain.pem`, a `ca-certificates.crt`, a key beside its certificate), carrying each object's label, DER and offset into the text, with explanatory text around the blocks read as text and the file bounded in both the number of objects and the bytes they decode to; `pem.encodeBundle` writes that list back; `pki.schema.pkcs1` reads and writes the RFC 8017 `RSA PRIVATE KEY` and `RSA PUBLIC KEY` encodings, with every component relation the specification states checked; `pki.schema.sec1` reads and writes the RFC 5915 `EC PRIVATE KEY` encoding, holding the scalar to its curve's width and the parameters field to a named curve; the engine and per-format members are grouped here |
| `pki.schema.x509` | Certificates (RFC 5280) parsed into structured, validated fields. Each extension is surfaced by name with its `extnValue` bytes; the shared extension decoders that read those bytes are what `pki.lint.certificate`, `pki.inspect` and `pki.path.validate` consume, and they cover the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification), the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker. Unknown statements stay opaque, fail-closed. `decodeExtensions` is the read door onto that table: it returns one row per extension carrying the decoded value, the raw `extnValue` octets, and a `state` of `decoded`, `unrecognized` or `undecodable`, so a reader can tell an extension nothing decodes from one whose value does not read. The RFC 3779 address and AS number blocks, their RFC 8360 identifiers, and the RFC 7633 TLS feature extension decode here too Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
| `pki.schema.c509` | C509 certificates (draft-ietf-cose-cbor-encoded-cert), the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR. `encode` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte, so the original signature still verifies. A `parse` result re-emits its native array in canonical deterministic CBOR, with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms: the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, inhibitAnyPolicy, OCSP No Check, TLS Feature), the general-name-bearing extensions (subjectAltName, issuer alternative name, name constraints, CRL distribution points, freshest CRL, authority and subject information access, and the full authority key identifier) over one shared GeneralNames codec, certificate policies with their CPS-URI and UserNotice qualifiers, policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers) with their RFC 8360 v2 twins, whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets. A value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact; a certificate outside the invertible set throws a typed `C509Error`. A type-3 `parse` also reports `signatureValueMinimalWidth`, the narrowest width that represents the fixed-width `r || s` value: the same signature padded to a wider accepted width reconstructs byte-identical DER, so three C509 encodings can name one certificate, and comparing the field with `signatureValue.length` answers whether the encoding in hand is the canonical one. Called explicitly, since it is CBOR rather than DER and so is not auto-routed |
| `pki.schema.crl` | X.509 CRLs (RFC 5280 §5): revoked serials with real-`Date` revocation times, named and partly decoded extensions, each record carrying the raw `extnValue` octets as `valueBytes`. `decodeExtensions` returns the CRL's own extensions and every revoked entry's in one array, each row naming the scope it was read at, and covers the whole §5.2 and §5.3 profile including the issuing distribution point and the delta CRL indicator. `decodeExtension` takes the scope explicitly, since RFC 6960 §4.4.5 carries a CRL entry extension into an OCSP response Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
| `pki.schema.csr` | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature. `decodeExtensions` is what a CA reads to see what a request asked for, returning the rows of its one `extensionRequest` attribute; a request carrying more than one is refused, and `groups: true` reads each separately Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
| `pki.schema.trustanchor` | RFC 5914 trust anchor lists: a trust anchor as a structure rather than as a certificate, carrying the constraints a root program otherwise states out of band. `parse` reads a `TrustAnchorList` into `{ anchors }`, each naming which of the three `TrustAnchorChoice` arms it came from: a `certificate`, a bare `tbsCert` (an anchor has no signature to check), or a `taInfo`. A `taInfo` carries the public key, the key identifier, an optional title, and a `certPath` stating the name the anchor is known by plus the policies it may assert, the namespace it is trusted for, and how far below it a path may run. Every rule RFC 5914 states is enforced: an empty `taName`, a policy set carrying qualifiers, a negative path length, and a `requireExplicitPolicy` with no policy set are each refused, and where the anchor carries a certificate its subject, public key and `subjectKeyIdentifier` must match the anchor's own. The four extension types the RFC excludes from `exts` are reported and left out rather than refused, as it says to ignore them. `parseInfo` reads a bare `TrustAnchorInfo`. `pki.trust.parseTrustAnchorList` turns a list into anchors `pki.path.validate` consumes, and `pki.trustanchor.build` writes one Fail-closed — `parse`, `parseInfo`, `pemDecode`, `pemEncode` |
| `pki.schema.pkcs8` | PKCS#8 private keys (RFC 5208 / 5958): algorithm, raw key bytes, attributes, optional public key. Encrypted keys are recognized but not decrypted. Fail-closed — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
| `pki.schema.cms` | CMS (RFC 5652 / 5083 / 9629): SignedData (§5, signer infos plus raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds including RFC 5753 key agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface plus raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation). §11 attribute placement is enforced, countersignatures validate recursively, certificates and CRLs are validated against the closed CHOICE sets and kept raw, a `crls` entry in the `other [1]` arm is read as RFC 5652 §10.2.1 `OtherRevocationInfoFormat` with an RFC 5940 OCSP response surfaced as a record the revocation verbs take directly, and every result is tagged `contentTypeName` Fail-closed — `parse`, `pemDecode`, `pemEncode` |
| `pki.schema.ocsp` | OCSP requests and responses (RFC 6960): per-certificate status (good, revoked, unknown), responder identity, raw tbs bytes for external verification, certificates kept raw. A response's nonce, and a single response's archive cutoff (§4.4.4) and CRL reference (§4.4.2), are decoded onto their extension entries by name; a malformed one is refused. Non-basic response types are recognized but not decoded. Fail-closed — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
| `pki.schema.tsp` | RFC 3161 timestamp requests, responses, and tokens: the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule. Fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
| `pki.schema.attrcert` | Attribute certificates (RFC 5755): holder and issuer identities as validated GeneralNames, the validity window as real `Date`s, and the privilege attributes and extensions decoded to structured values (role, clearance, service and access identity, group, charging identity; audit identity, target and proxy information, no-rev-avail, AA controls), with the raw signed region for a verifier. Unknown types stay opaque, and the obsolete v1 form is recognized and deferred. `decodeExtensions` returns the same row shape the other formats return, reporting the criticality RFC 5755 fixes for each extension it profiles Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
| `pki.schema.crmf` | Certificate request messages (RFC 4211) — the CMP and EST enrollment body. The requested-certificate template (subject, public key, validity, extensions), proof of possession, and registration controls, with the raw `CertRequest` region surfaced for the caller to hash. Names are dual-accepted, IMPLICIT and EXPLICIT. This toolkit ships no proof-of-possession verifier yet, so confirming that a requester holds the key it asks to have certified remains the CA's own step. `decodeExtensions` reads the extensions every message's template asks for, each row naming its message Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
| `pki.schema.pkcs12` | PKCS#12 (PFX) stores (RFC 7292) from DER, BER, or PEM: key bags via the PKCS#8 parser, shrouded keys with the algorithm surfaced and the ciphertext opaque, cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` and `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification. BER is accepted exactly where §4.1 requires it Fail-closed — `parse`, `pemDecode`, `pemEncode` |
| `pki.schema.cmp` | CMP messages (RFC 9810): the header (version, sender and recipient including the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` and `bodyBytes` slices an external verifier reconstructs the protected part from. The CMP-before-OCSP dispatch order is enforced Fail-closed — `parse`, `pemDecode`, `pemEncode` |
| `pki.schema.csrattrs` | EST CSR Attributes (`CsrAttrs`, RFC 8951 §3.5 / RFC 9908) — the `AttrOrOID` items a server sends to shape an enrollment: bare OIDs, attributes with raw values, and decoded views of the RFC 9908 meaningful types (extension requests, EC and RSA key-type conventions, the certification-request-info template). Unknown types are surfaced raw; structure and the RFC 9908 semantic MUSTs are fail-closed — `parse` |
| `pki.est` | Enrollment over Secure Transport (RFC 7030 / 8951 / 9908 / 7616). The client verbs `cacerts`, `simpleenroll`, `simplereenroll`, `serverkeygen`, `csrattrs`, and `fullcmc` drive the RFC 7030 flow over `pki.transport` (inject your own via `opts.transport`, or take the fail-closed default): HTTPS only, an explicit trust anchor required, same-origin redirects followed while a downgrade or loop is refused, a 202 Retry-After surfaced but never slept, HTTP Basic or Digest (RFC 7616, SHA-256 / SHA-512-256; MD5 and no-qop refused by default) answered only after the server is authenticated, and the issued certificate chosen by public-key match. `serverkeygen` requests a server-generated key, cleartext or an opaque CMS EnvelopedData, with encryption bound to the CSR's key-identifier attribute over a confidentiality-bearing cipher. `csrattrs` fetches the CA's RFC 9908 attributes policy. For §3.5 channel binding, `simpleenroll` and `simplereenroll` accept a CSR builder `(tls) -> csr` instead of a CSR: it runs after the handshake with that connection's `tlsUnique`, so the request is signed over the session it is sent on, and it re-runs on any new connection a redirect or auth retry opens, recreating the CSR for that session (§3.2.1). A builder alongside Digest authentication is refused, since that header is computed over the body before the handshake. `fullcmc` (§4.3) carries a CMC Full PKI Request and reduces the CA's answer through `pki.cmc.verify` to one terminal outcome, refusing a response that fails to echo the transaction and nonce the request carried, or that covers a key the request never asked for; a certs-only reply to a bound request is refused rather than read as an issuance. Under the verbs sit the transport-agnostic codecs they compose: the RFC 8951 base64 transfer codec (which ignores Content-Transfer-Encoding), the `multipart/mixed` splitter, the certs-only and serverkeygen response validators over CMS, the enroll-attribute builders, and the HTTP response classifier — `transferDecode`/`transferEncode`, `parseCertsOnly`, `splitMultipartMixed`, `parseServerKeygenResponse`, `findIssuedCert`, `classifyResponse`, `paths`, and the builders |
| `pki.scep` | SCEP pkiMessage codec (RFC 8894), the client side of Simple Certificate Enrollment. `build(spec)` assembles a request: a `PKCSReq` or `RenewalReq` wraps a PKCS#10 certification request, whose own proof-of-possession it verifies first; a `CertPoll` wraps an IssuerAndSubject naming the issuing CA and the request subject; and a `GetCert` or `GetCRL` wraps an IssuerAndSerialNumber naming a certificate by its issuer and a positive serial of at most 20 octets. It encrypts the messageData to the CA as the inner EnvelopedData (`pkcsPKIEnvelope`, AES-128-CBC), and signs that under the transaction attributes RFC 8894 §3.2 defines, a messageType, a caller-unique transactionId, and a 16-byte senderNonce. The recipient CA certificate must assert `keyEncipherment`. `build` also issues the CA-side `CertRep` response (§3.3.2): a SUCCESS envelopes a certs-only SignedData of the issued certificate (or the CRL for a GetCRL) to the requester and signs it under the CA key, while a FAILURE (carrying a `failInfo`) or a PENDING omits the pkcsPKIEnvelope and is signed detached, each echoing the request's senderNonce as `recipientNonce`. `parse(bytes, opts?)` reads a `PKCSReq`, `RenewalReq`, `CertPoll`, `GetCert`, `GetCRL`, or `CertRep` back (a messageType outside the RFC 8894 registry is refused): it verifies the outer signature first and refuses a message whose signature does not check, so the transaction attributes it returns come only from a verified signer, never alongside a false verdict. Because a valid signature only proves self-consistency with the embedded certificate, pass `opts.signerCert` to authenticate a CA response against the CA certificate you hold (a public-key mismatch is refused, `signerAuthenticated` reports the result); a caller that omits it gets a crypto-only verdict and must authenticate the surfaced `signerCert` itself. It maps the messageType, pkiStatus, failInfo, and the nonces from their RFC 8894 enumerants and refuses an unknown one; given `recipientKey` it decrypts the `pkcsPKIEnvelope` to the messageData, and given `expectedSenderNonce` it refuses a response whose recipientNonce does not echo it. It reads a CA's CertRep in full. `getCACaps(baseUrl, opts?)`, `getCACert(baseUrl, opts?)`, `getNextCACert(baseUrl, opts)`, `enroll(baseUrl, opts)`, `renew(baseUrl, opts)`, `getCert(baseUrl, opts)`, and `getCrl(baseUrl, opts)` carry that codec to a live CA over HTTP (RFC 8894 §4) through an injectable transport (`pki.transport.https` by default): `getCACaps` reports the CA's advertised capabilities and never lowers the client's algorithm choice on the unauthenticated response, `getCACert` pins the CA certificate to an out-of-band `expectedFingerprint` a returned certificate must match, `getNextCACert` fetches the CA's next (rollover) certificate and verifies the SignedData response against the current CA certificate (`caCertificate`) before returning it, refusing a response signed by any other key, and `enroll` and `renew` POST a PKCSReq or RenewalReq, authenticate the CertRep against the CA certificate and its nonce echo, select the issued certificate by public-key match, throw on a FAILURE with the CA's failInfo, and poll a PENDING response with CertPoll requests until the CA answers or a bounded poll-count and total-wait budget is spent. `getCert` and `getCrl` retrieve an already-issued certificate or a CA CRL by issuer and serial number, authenticated the same way; RFC 8894 §2.6, §2.7, and §3.3.4 recommend these queries only where HTTP certificate-store access (RFC 4387), LDAP, or a CRL distribution point is unavailable, so prefer those where a CA offers them. A PKIOperation goes over HTTP POST by default; `httpMethod: "GET"` carries it base64-encoded in the URL query for a legacy CA that accepts only GET (RFC 8894 §4.1). Every verb reaches an https CA through a forward proxy with `opts.proxy = { url, auth?, tls? }`: a `CONNECT` tunnel is opened to the proxy and the origin TLS is negotiated inside it, so the proxy carries the enrollment without being able to read it or substitute the origin certificate. Basic proxy authentication (RFC 7617) is sent only over an `https://` proxy, where it rides the authenticated TLS-to-proxy channel; an `http://` proxy is tunnel-only, and credentials over a plaintext proxy are refused rather than exposed (see `pki.transport`) — `build`, `parse`, `parseCapabilities`, `getCACaps`, `getCACert`, `getNextCACert`, `enroll`, `renew`, `getCert`, `getCrl` |
| `pki.transport` | The shared, fail-closed `node:https` transport the enrollment clients drive. `pki.transport.https(defaults)` returns a `transport(request) → { status, headers, body, tls }`, where `tls` carries the negotiated `protocol`, `cipher`, raw `peerCertificate`, and one channel binding per TLS version: on TLS 1.2 the RFC 5929 `tlsUnique` that `pki.est.challengePasswordFromTlsUnique` links to an EST enrollment (RFC 7030 §3.5), and on TLS 1.3 the RFC 9266 `tlsExporter` (32 bytes under the label `EXPORTER-Channel-Binding`, zero-length context). Each is `null` on the version that does not define it, and `tlsExporter` is withheld below TLS 1.3 because RFC 9266 §2 also requires the extended master secret of RFC 7627 (obsoleted by RFC 9846), which Node gives no way to confirm. Since `tlsUnique` on the response arrives after the body was sent, a request `body` may instead be a function `(tls) -> bytes` the transport invokes after the handshake and before it writes the body, so the caller builds a channel-bound CSR from `tlsUnique` on the same connection it posts over. It may return a promise, so the CSR can be signed with `pki.csr.sign` while the connection is held open; a callback that throws, rejects, or returns anything other than bytes or a string fails the request closed rather than posting an empty enrollment. This is the toolkit's only socket choke point: an explicit trust anchor (or an opt-in to the system store) is required, `rejectUnauthorized` is always on, TLS is floored at 1.2, the response body is capped while it streams, and a stalled socket times out. The EST, ACME, and CMP clients reuse it verbatim. A `request.proxy = { url, auth?, tls? }` reaches the origin through a forward proxy: a `CONNECT` tunnel is opened to the proxy and the origin TLS is negotiated inside it under the identical origin trust policy, so the proxy cannot read the origin's encrypted session or substitute an authenticated origin certificate. Proxy credentials are sent only over an `https://` proxy, whose own certificate is verified against `proxy.tls`, so they ride an authenticated channel; a plaintext `http://` proxy is tunnel-only and carrying `auth` on one is refused (`proxy-auth-requires-tls`) rather than exposing the credentials to the proxy hop. `auth.scheme` is `"basic"` (RFC 7617), which sends its credential on the first CONNECT, or `"digest"` (RFC 7616), which answers the proxy's `Proxy-Authenticate` challenge on a `407` by hashing the method `CONNECT` over the authority-form target, and answers exactly once, so a proxy repeating its challenge has rejected the credential rather than obtained another attempt. Digest runs the policy the origin verbs apply, refusing MD5 and a challenge carrying no `qop` unless `auth.allowMD5` / `auth.allowLegacyQop` says otherwise. A malformed option, a failed CONNECT, an unverified proxy certificate, or an unaccepted credential fails closed. The SCEP, EST, ACME, and CMP clients each thread it through as `opts.proxy`. If you inject your own transport, return `tls` too: `pki.est.serverkeygen` asserts the negotiated cipher can protect the delivered private key, and a transport that reports no cipher is trusted rather than refused, so omitting the field silently skips that check. `pki.transport.peerChain(request, defaults?)` negotiates TLS and returns that same channel without sending a request, plus `peerChain`: the certificates the endpoint presented as DER, leaf first, walked up from the peer certificate to the one that issues itself. Reading a certificate off an endpoint through the request path would reach the application behind it; this does not. It prepares the connection through the same code the request path uses, so the anchors, the SNI, the version floor, the identity hook, the private-address block and the https-only rule are one policy rather than a second copy of one, and a configured proxy is refused rather than bypassed. A chain it returns is not a validated one: the handshake checked that it builds to a configured anchor and matches the name in the URL, and checked no revocation status and no policy — `https`, `peerChain` |
| `pki.jose` | Flattened JWS (RFC 7515) and JWK thumbprints (RFC 7638). `sign` and `verify` run a Flattened JWS against declarative profiles (ACME outer, EAB inner, keyChange inner) that carry the required and forbidden header rules as data. `base64url` is the strict RFC 4648 §5 codec, rejecting padding, non-alphabet characters, and non-canonical trailing bits. `parseJson` is a bounded reader that refuses duplicate members at any depth. `thumbprint` is the RFC 7638 / 8037 / 9964 canonical digest. The algorithm registry binds each `alg` to its key type (ES/RS/PS/EdDSA/ML-DSA), leaving no code path for `alg:none`, an RS256→HS256 key confusion, or an all-zero ECDSA signature; `assertPublicJwk` refuses a JWK carrying private material, so an exported private key is never published; `verify` holds a JWS's own embedded `jwk` to the same rule (RFC 7515 §4.1.3), so a message carrying a private key in its header is refused rather than returned in the verdict. `opts.key` names the key a message must be signed under and governs: where the profile also permits an embedded header `jwk`, the two must be the same key — compared as RFC 7638 thumbprints, so member order cannot make equal keys differ — and a disagreement is refused rather than resolved in the message's favor. `keySource` reports which key answered, since a signature checked against a key you named is a different claim from one checked against the key the message carried — `sign`, `verify`, `base64url`, `parseJson`, `thumbprint`, `assertPublicJwk` |
| `pki.acme` | ACME (RFC 8555 / 8737 / 8738 / 9773). `client(directoryUrl, opts)` is a stateful client driving a live CA directory over `pki.transport`: `newAccount`, `newOrder`, `newAuthz`, `getOrder`, `getAuthorization`, `getChallenge`, `respondToChallenge`, `finalize`, `pollOrder`, `pollAuthorization`, and `downloadCertificate` walk the issuance flow, with `newAuthz` pre-authorizing a single identifier (§7.4.1) and `downloadCertificate` binding the issued certificate to the order — its key, its identifiers, or both; at least one is required by default and the result reports `boundToKey` / `boundToIdentifiers` — before choosing among alternate chains (`Link rel="alternate"`, §7.4.2, via a `selectChain` predicate bounded by `maxAlternates`). `revokeCert` (account-key or certificate-key signed), `keyChange`, `updateAccount` (change the account contacts, §7.3.2), `listOrders` (the account's orders list, following the §7.1.2.1 `Link rel="next"` pagination bounded by a page cap and confined to the orders origin), `deactivateAccount`, `deactivateAuthorization`, `renewalInfo` (ARI), `renewalWindow` (the RFC 9773 §4.2/4.3 renewal decision), and `scheduleRenewal` (the §4.1 auto-sleeping renewal loop that sleeps until the selected instant and refetches until it is time to renew, stopping at the certificate's notAfter) complete the lifecycle. Every URL is HTTPS only, an explicit trust anchor is required, each request carries a fresh single-use nonce with a bounded badNonce retry (and, when `opts.resignKeys` supplies caller-approved alternative account-key algorithms, a bounded RFC 8555 §6.2 badSignatureAlgorithm re-sign that the CA's advertised list can only filter, never widen, so it cannot force a weaker algorithm), reads are POST-as-GET, polling is bounded and sleeps on a Retry-After via an injectable sleeper capped by a poll count and a total-wait budget, and every response body is size-capped. The transport is injectable via `opts.transport`. Over the message layer it composes resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, the request builders (newAccount with External Account Binding, newOrder with `replaces`, finalize with a CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns and ip identifier validators, and the ARI certID with serial sign-padding preserved — `client`, `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
| `pki.schema.smime` | S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551). `parseSigningCertificate` and `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` and serial); `parseSmimeCapabilities` decodes the ordered capability list; `decodeAttribute` dispatches a CMS attribute by OID, enforcing the single-value rule and deferring on unknown types. A companion decoder for CMS signed attributes rather than an auto-routed format — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
| `pki.cmc` | Build and interpret CMC messages (RFC 5272, obsoleted by RFC 10002). `build(spec, signer)` assembles a Full PKI Request across all three request arms (PKCS#10, CRMF, other) and carries it either way RFC 5272 §3.2 allows: `{ cert, key }` signs it, while `{ mac: { identifier, secret } }` authenticates it under a shared secret for a client that holds no key yet, using the PasswordRecipientInfo that section requires and deriving the key from the identifier followed by the secret. A key that cannot sign for itself proves possession another way: `spec.popChallenge` answers the authority's encrypted challenge (RFC 5272 §6.7), decrypting the proof value, confirming it hashes to the witness the challenge carries, and returning the MAC the authority asked for, with `cmc/pop-failed` when the value is not the one issued. A signed request goes through `pki.cms.sign` under `id-cct-PKIData`. A PKCS#10 (`tcr`) request is verified against its own proof-of-possession before signing, so a request whose self-signature does not verify under its subject public key is refused rather than signed into a message a CA would reject. Body-part identifiers are unique across the whole message and never the reserved 0; a caller's clash is refused rather than renumbered, since a control may already reference it. An Identity Proof V2 witness is computed over the `reqSequence` bytes exactly as emitted (§6.2.1 step 1) rather than a re-serialization, a POP Link Witness is emitted only alongside the POP Link Random control §6.3.1.1 requires beside it, and a renewal carries neither Identification nor Identity Proof in either version. `verify(response, sent)` takes what the CA returned plus the state the client retained and reduces it to one terminal outcome: `issued`, `pending`, `confirm-required`, `pop-required`, or `rejected`. It binds the exchange first — Transaction Identifier, the Sender and Recipient Nonce echo compared in constant time and by full value so a truncation cannot match, and the Data Return echo — with each check applying only if the client sent that half, and, once sent, an absent or differing echo being a refusal, which is the replay defense. `bodyPartIDs` extends the same rule to what the response is about: a status reporting on a body part the request never sent is refused, which the transaction and nonce cannot catch, because a server can echo both correctly while answering about a different message. Several status controls are permitted and the worst governs, so a failure cannot hide behind an earlier success; the absence of any status control is success, per §6.1.2. The carrier's signature must verify (§3.2.1.3.4): a conforming SignedData carries its own signer certificate, so the ordinary build-then-verify flow needs nothing extra and the verdict reports `signatureVerified: true`. Where the signer is found nowhere the posture is fail-closed with a named opt-out — supply `certs` with the responder's certificate, or `allowUnverified: true`, in which case the verdict reports `signatureVerified: false`. Doing neither is refused, the opt-out never excuses a signature that is present and wrong, and a carrier with no signer at all is refused outright. The response's own `cmsSequence` and `otherMsgs` come back raw, since a request whose only arm was the other-message form has no certificate to return and §4.1 puts its answer there. Nothing is trusted: issued certificates are read from the CMS certificate bag (§4.2) and surfaced raw for `pki.path.validate`, and a Publish Trust Anchors control is surfaced with `trusted: false` rather than added to a store — `build`, `verify` |
| `pki.schema.cmc` | Decode CMC messages (RFC 5272 as updated by RFC 6402, the pair obsoleted by RFC 10002 / 10003 / 10004): a Full PKI Request (`PKIData`) or Full PKI Response (`PKIResponse`) riding inside a CMS SignedData, reached by the encapsulated content type (`id-cct-PKIData` / `id-cct-PKIResponse`). Controls are surfaced in wire order with their values raw, so an unrecognized one is data rather than a fault. Tagged requests decode across all three arms. The status verdicts (`CMCStatusInfo` v1 and `CMCStatusInfoV2`) are collected as an ordered list, and the RFC 6402 two-module `OtherStatusInfo` ambiguity — `pendInfo` and `extendedFailInfo` are both untagged SEQUENCEs in the 1988 module, told apart only by their first element — is resolved by inspection and refused when it cannot be told apart. Body-part identity is unique across the whole message rather than per sequence, 0 is reserved as the reference to the enclosing PKIData, and the `reqSequence` bytes are surfaced exactly as they appeared so an Identity Proof witness is computed over the wire bytes. A companion decoder for CMS content rather than an auto-routed format — `parse`, `parsePkiData`, `parsePkiResponse` |
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
| `pki.identity` | Service identity (RFC 9525, which obsoletes RFC 6125). `match(cert, references, opts)` says whether a certificate presents a name the client was trying to reach, which is a separate answer from whether it chains: §1.2 has an application need both, and `pki.path.validate` takes `opts.identity` to run them together and report each. A reference identity is a domain name (DNS-ID), a textual IP address or a 4- or 16-octet `BufferSource` (IP-ID), `{ type: "srv", service: "_imap", value: "example.com" }` (SRV-ID, RFC 4985), or `{ type: "uri", scheme: "https", value: "www.example.com" }` (URI-ID). A record carries its service type and its domain together, so no comparison can pair the service of one reference with the domain of another (§6.5). The subject `commonName` is never read, under any option or fallback, including when the certificate carries no subjectAltName at all (§2). Names are compared label by label under an ASCII-only fold with one trailing dot normalized on both sides; an A-label is compared as the ASCII it is and never decoded, so a reference carrying a character outside ASCII is refused rather than guessed at, this toolkit shipping no IDNA implementation. An IP-ID is compared octet for octet against an `iPAddress` entry, never against a `dNSName` holding the same address as text, and no prefix or mask applies (§6.2, §6.4). Wildcards are on and `opts.wildcards: false` turns them off, which §3 requires stating either way: one wildcard, the whole of the left-most label, reaching exactly one reference label, and a presented name failing those rules is ignored with the search continuing rather than refusing the certificate (§6.3). A wildcard spanning an administrative boundary such as `*.co.uk` is out of scope as it is in §7.1, since no public-suffix list ships here. The verdict names which reference matched, so a caller can use it as the validated identity (§6.6), and lists every entry that was skipped with the reason — `match` |
| `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. A name string that opens as a distinguished name is refused rather than certified as one commonName holding the whole text, so `subject: "CN=Example CA, O=Example"` names the two forms that do work instead of issuing `CN=CN=Example CA\, O=Example`; a common name that contains an `=` further along, such as `Example CA (v=2)`, is unaffected. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, which is what refuses an EC subject key asserting `keyEncipherment` or `dataEncipherment` (RFC 8813 §3), so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985). `sign(spec, key, opts)` builds and signs a `CertificationRequest` from a `spec` of subject (which may be empty), the public key being certified, an optional `extensionRequest` carrying the requested v3 extensions a CA copies into the issued certificate (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
| `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest; a `baseCertificateID` names its issuer as one non-empty directoryName, which sec. 4.2.2 requires in the same words sec. 4.2.3 uses for the AC issuer, and `pki.schema.attrcert.parse` refuses any other shape), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
| `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. A key that cannot sign, which is where an ML-KEM enrollment sits, takes a `POPOPrivKey` proof instead (RFC 4211 sec. 4.2, sec. 4.3): `pop.type` of `keyEncipherment` or `keyAgreement` with `pop.method: 'subsequentMessage'` declares whether an encrypted certificate (`encrCert`) or a challenge (`challengeResp`) completes the proof over the enrolling protocol's next exchange, and `pop.method: 'encryptedKey'` encloses the requester's private key for the CA in a CMS `EnvelopedData` under `id-ct-encKeyWithID`, requiring `pop.privateKey`, `pop.recipients`, the `pop.identifier` sec. 4.2.1 makes mandatory, and `pop.archive: true` because RFC 9810 sec. 5.2.8.3.1 permits the method only where archival is intended. The enclosed key must be the private half of `certTemplate.publicKey`, or the proof demonstrates possession of a key the request never asked to have certified. The `thisMessage` and `dhMAC` alternatives the specification deprecates are refused with their successors named. `pop.method: 'agreeMAC'` completes the four: `pop.key` is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM `PRIVATE KEY` block or a `KeyObject`, and `pop.caCert` the authority's certificate it already holds; the two agree a static secret (RFC 2875 sec. 3, obsoleted by RFC 6955), and the key derived from it MACs the DER `certReq` under `id-dhPop-static-HMAC-SHA1`. Either key may name its group in the PKCS#3 or the X9.42 encoding. `pop.key` must be the private half of `certTemplate.publicKey`, and the template must name both a subject and a public key. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. `verifyPop(messages)` is the receiving side: for each `CertReqMsg` it verifies the `POPOSigningKey` signature over the bytes RFC 4211 sec. 4.1 names — the DER of `poposkInput` when present, of `certReq` when not — through the one path-validation signature engine, and answers `{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }` with the fields re-derived from the message's own bytes. Each verdict reports only what its preimage covers: `publicKey` is the key possession was proven for, and `subject` is the requested name when the `certReq` was signed. A `poposkInput` preimage covers the key and the sender alone, so any subject beside it is unsigned and is withheld with `subjectBound: false`. A `raVerified` proof is an RA's out-of-band assertion and a `keyEncipherment` or `keyAgreement` proof completes over a later exchange, so each is reported unverified with its `method` named instead of guessed. Parsing stays at `pki.schema.crmf.parse` — `build`, `verifyPop` |
| `pki.cmp` | CMP message building, transfer, and verification (RFC 9810). `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` and `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata; a `transactionID` or `senderNonce` the caller omits is filled with 16 fresh random bytes (RFC 9810 §5.1.1, the width a receiver requires under RFC 9483 §3.5), and a `senderNonce` given under 128 bits is refused; a signature-protected `sender` the caller omits is filled with the signer certificate's subject (§3.1); `message.body` is a single-key object naming the arm — request-side `ir`, `cr`, `kur`, `krr` (key recovery, §5.3.7), `ccr` (cross-certification, §5.3.11: a `CertReqMessages` at the normative floor, where the requesting CA keeps its private key so a private-key-transport encryptedKey proof-of-possession is refused, and the optional App. D.6 single-request cardinality is not enforced), `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`, and the registration-authority `nested` wrapper of complete PKIMessages (RFC 9810 §5.1.3.5). Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived), or `opts.kem`, a KEM-based MAC for a client whose key can only establish secrets (RFC 9810 §5.1.3.4): the ciphertext a peer encapsulated to its ML-KEM key is decapsulated, the key is derived under HKDF-SHA256 with a context bound to the transaction identifier of the message that carried the ciphertext, and `pki.cmp.verify` checks it from the shared secret. Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. A `p10cr` request's PKCS#10 proof-of-possession is verified before the message is protected, so one whose self-signature does not verify under its subject public key is refused rather than sent. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a MAC algorithm it does not compute (reported as MAC-protected under that algorithm, never as a failed signature), an omitted keyLength, or a SHA-1 PRF, and returns a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time, or an explicit `opts.time` for historical verification and never the message's self-asserted `messageTime`, before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport, composing `build`, `transfer`, and `verify`. Every response is protection-verified, signer-trusted, and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` and echoed-`recipNonce` chain) before its body is read, with a bounded `pollReq` / `pollRep` loop for a `waiting` status and a `certConf` / `pkiConf` (or implicit) confirmation carrying an explicit `hashAlg` for a signature algorithm that does not convey its hash. It returns a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`. A certification authority can take longer to answer than a process is held open, so a `poll-timeout` carries a `resumeToken` and `resumePoll(token)` continues the same transaction in a later process: the token is plain JSON naming the transaction, the nonce the next request echoes, the polled request and the arm its grant arrives on, and the key the grant must certify. It carries no secret and grants nothing, since the protection material and the endpoint come from the session built to resume with, and the resumed poll verifies, nonce-binds, and key-binds every response. Its fields are what the resumed exchange is held to, so store it where the enrollment's own state is stored: someone who can rewrite it can widen what the resumed session accepts, as far as that session's trust anchors and the authority's protection allow and no further. The same session also drives the other two RFC 9483 operations under that shell: `revoke(request)` sends an `rr` naming a certificate as `{ certificate }` or `{ certDetails }` with an optional CRLReason `reason`, and returns `revoked` / `rejected` / `poll-timeout`; `info(request)` sends a `genm` for one of the four support messages — `{ caCerts }`, `{ rootCaCert }`, `{ certReqTemplate }`, `{ crlUpdate }` — and returns `answered` with the decoded `value` and a `present` flag, since an absent response value is how each says nothing is available. A session revokes its own certificate: the signature over the request is the proof of authorization (§4.2), so the named certificate must be `opts.cert` and a PBMAC1 session is refused. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `openKeyPackage(container, opts)` opens a private key the CA generated centrally and delivered in the `privateKey` field of a granted `CertifiedKeyPair` (RFC 9483 §4.1.6): a CMS `SignedData` over an RFC 5958 `AsymmetricKeyPackage`, sealed in a CMS `EnvelopedData`. `opts.key` (with `opts.cert`) opens the key transport and key agreement containers a signature-protected request selects, `opts.password` the password container a MAC-protected request selects. The authority is authorized before any key material is returned: its certificate must chain to an anchor in `opts.trustAnchors` AND assert the `id-kp-cmKGA` extended key usage, since a certificate with no `extendedKeyUsage` permits every purpose and so authorizes nothing; `opts.authorizedBySharedSecret` states §4.1.6's exemption for an entity that authorizes by the secret that protected its request. Returns `{ keys, kga, trusted }`, where each key is a PKCS#8 `PrivateKeyInfo` ready for `pki.key.import`; `keys` is the whole RFC 5958 `AsymmetricKeyPackage` (`SIZE (1..MAX)`), while a session enrollment requires the sequence of one §4.1.6 profiles it to. Both CMS layers are read as a ContentInfo or as the bare structure, told apart by what the SEQUENCE opens with. `session(opts)` drives the whole exchange under `opts.acceptCentralKeyGeneration`: the request omits `certTemplate.publicKey`, carries no proof of possession, and goes out as `cmp2021`; the session opens the delivered container with the credential it enrolled under and surfaces it as `deliveredKey`, having first checked that the issued certificate certifies the delivered key. Without the option, such a request and such a grant are both refused. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `openKeyPackage`, `session` |
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuer alternative name, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access, which is every extension RFC 5280 §5.2 defines) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The authority key identifier is emitted on every CRL whether or not it is named, as §5.2.1 places it on every conforming issuer, from the issuer certificate's SKI or the issuer key. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign`, `verify`, `isRevoked` |
| `pki.key` | Key-material lifecycle (RFC 5958 / RFC 8018). `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 with AES-CBC-Pad), where `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`. The plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo`, re-validated through `pki.schema.pkcs8.parse`: only PBES2 / PBKDF2 / AES-CBC is accepted (PBES1, PBMAC1, and scrypt are refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), and a malformed parameter set or wrong-length IV is a distinct typed error. Because a MAC-less PBES2-CBC decrypt must not become a padding oracle (RFC 8018 §8), a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` and `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo. The key may come from the platform's WebCrypto or from a separately installed copy of this toolkit, and is exported through whichever holds its material; a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason. Encoding is delegated to WebCrypto, so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters, and an ambiguous RSA or EC import requires `opts.algorithm`. `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards and Montgomery curves, and the FIPS post-quantum ML-DSA and ML-KEM; `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM, with a typed `KeyError` on failure. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt`, `decrypt`, `export`, `import`, `generate`, `publicFromPrivate` |
| `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579, the latter obsoleted by RFC 9879). `build(spec, opts)` assembles a store from the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key, shroudedKey, cert, crl, secret, or nested `safeContents` bags. Keys and certs are validated before wrapping, a key paired with a certificate (the `{ key, cert }` form, or bags sharing a `localKeyId`) must be the private half of that certificate's public key, and `friendlyName` (BMPString) and `localKeyId` are single-value. Integrity is a classic Appendix B HMAC (the default, for maximum interoperability) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`) over SHA-256/384/512, with shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 — which is what OpenSSL and NSS consume, so a file it emits opens in both, cross-checked bidirectionally. The MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity (`opts.integrity.mode: "public-key"`) wraps the AuthenticatedSafe in a CMS SignedData instead of a MAC, signed by any `pki.cms.sign` signer and carrying no MacData (§4); privacy stays independent, so `password` still PBES2-encrypts the bags. Public-key privacy wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData`, never GCM) encrypted to recipient public keys through the `pki.cms.encrypt` recipient model, via per-safe `recipients` or the `opts.recipientCerts` convenience, restricted to certificate recipients (RSA-OAEP, ECDH, X25519, X448, ML-KEM) since a password or KEK recipient could not be reopened by `open`. All four integrity-by-privacy combinations are permitted (§3.1). `open(pfx, password, opts)` reads a store back: it verifies the MAC first, so a wrong password is the MAC verdict rather than a decrypt error, then PBES2-decrypts every privacy safe and shrouded key bag and returns `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` — keys as re-validated PKCS#8 DER, certs, CRLs and secrets as raw DER, all with `friendlyName` and `localKeyId`, nested safes recursively. A MAC-less store is refused unless `opts.allowUnauthenticated`. A public-key-integrity store is verified through its CMS SignedData signature first (`pkcs12/signature-invalid` on failure), with the signer surfaced in `signers` but never trust-chained, which remains the caller's `pki.path.validate` step. A legacy-PBE store's Appendix C 3DES and RC2 bags are decrypted, RC2 through an in-tree RFC 2268 cipher, so an `openssl pkcs12 -legacy` or NSS store opens; the legacy RC4 schemes are refused. An `id-envelopedData` safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` when absent), every recipient-side fault and every post-integrity decrypt failure collapsing to the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`. It reads what OpenSSL and NSS produce. Returns DER or a PEM `PKCS12`, with a typed `Pkcs12Error` on failure. Parsing stays at `pki.schema.pkcs12.parse` — `build`, `verifyMac`, `open` |
| `pki.pkcs11` | PKCS #11 URIs (RFC 7512), the syntax an operator names a token and an object on it with instead of a file path. `parseUri(uri)` reads one into `{ path, vendorPath, query, vendorQuery }`, each a prototype-less record: the path carries `token`, `manufacturer`, `serial`, `model`, the three `library-*` and three `slot-*` attributes, `object`, `type` and `id`, and the query carries `pin-source`, `pin-value`, `module-name` and `module-path`, with a vendor attribute in either component kept under its own name. A vendor query attribute carries a list of its values, which §2.4 permits to repeat; a vendor path attribute carries one value. `id` is a `Buffer`, since the attribute names bytes rather than text, and `slot-id` and `library-version` are numbers. `formatUri(record)` writes the record back, percent-encoding each value to what its own component admits unencoded and encoding `id` whole with uppercase hexadecimal, so two URIs naming one identifier compare equal. Parsing follows the §2.3 / §2.4 ABNF rather than a general URI reader and is fail-closed: a character the component admits only percent-encoded, a truncated or non-hexadecimal escape, a value that is not valid UTF-8, an attribute repeated where the RFC admits it once, a `type` outside the five the RFC enumerates, a URI carrying both `pin-source` and `pin-value`, and a relative `module-path` are each refused with their own `pkcs11/*` code. Syntax only: nothing here loads a PKCS #11 module or talks to a token — `parseUri`, `formatUri` |
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA pairing ML-DSA with a traditional RSA, ECDSA, or EdDSA key (accepted only when both components verify, draft-ietf-lamps-cms-composite-sigs). It builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. The content may also be an async iterable of byte chunks, which signs a large detached payload without holding it in memory: the payload is hashed incrementally under every signer's digest algorithm in one pass, and `verify` accepts the same streamed form as its `content` option. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. A signer descriptor is held to the fields the verb reads, by `sign` and `countersign` alike and by `pki.smime.sign` through them: `{ cert, key, digestAlgoritm: "sha384" }` raises `cms/bad-input` naming the field rather than signing under the default digest, which is what the same misspelling in `opts` already did. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: with signed attributes present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with the primary bytes preserved so it still verifies — attached as the id-countersignature unsigned attribute; `verify` returns each countersignature's verdict under `signers[i].countersignatures` and every unsigned attribute under `signers[i].unsignedAttrs`, surfaced unauthenticated. The CAdES signature timestamp has verbs of its own: `timestampImprint(der, opts)` returns `{ imprint, signerIndex }`, where `imprint` is the `{ hashAlgorithm, hashedMessage }` argument `pki.tsp.request` takes, the hash of a signer's signature value with neither tag nor length (ETSI EN 319 122-1 clause 5.3), `attachTimestamp(der, token, opts)` splices the returned RFC 3161 token in as the signature-time-stamp unsigned attribute and refuses one whose imprint covers other bytes, and `verify` reports a row per attached token under `signers[i].signatureTimeStamps`, each carrying the generation time, the authority, and what the authority's chain established. A row is refused when its time precedes the signature's own `signing-time` or falls after the signing certificate's `notAfter`. `signers[i].cadesBaseline` reports which CAdES baseline requirements the signature meets. The RFC 6211 `CMSAlgorithmProtection` attribute is emitted on request by `sign`, `countersign` and `authenticate` (`algorithmProtection: true`, or `"registry"` for the IANA arc identifier) and always compared where present: a copy of the digest and signature or MAC algorithm identifiers, placed where the signature or the MAC covers them, with a disagreement refused as `cms/algorithm-protection-mismatch` and the result reporting `algorithmProtection` per signer. It matters most on an `AuthenticatedData`, whose MAC input is the authenticated attributes alone, leaving the outer digest and MAC algorithm fields covered by nothing. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is never emitted), key agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key wrap, password (PBKDF2 with the PWRI-KEK of RFC 3211, carried forward by RFC 3370), or the post-quantum ML-KEM (and composite ML-KEM, draft-lamps-pq-composite-kem) KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. The content may also be an async iterable of byte chunks, streamed through the AES-GCM or AES-CBC content cipher so a large plaintext is encrypted without being buffered whole; the ciphertext is assembled before the definite-length encryptedContent is emitted, byte-for-byte the buffered form. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher, EFAIL, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. With `opts.stream: true` the recovered `content` is an async iterable of plaintext chunks instead of a single Buffer, for a payload too large to hold whole; the authenticated modes (AuthEnvelopedData, AuthenticatedData, AES-GCM) verify integrity before the iterable yields, so a forged message throws before any plaintext is exposed, while a CBC EnvelopedData or EncryptedData yields the plaintext as the cipher produces it. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and message-digest) re-tagged to the EXPLICIT SET OF (§9.2), or the content octets directly; `decrypt` recovers the MAC key, recomputes the MAC and independently the message-digest (§9.3), and releases the content only after both pass, with every secret-dependent failure collapsing to the uniform `cms/decrypt-failed`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). `certsOnly(certs, opts?)` builds a certs-only certificate-management message (RFC 8551 §3.8): a degenerate SignedData with an empty signerInfos and no eContent that conveys certificates and CRLs without signing anything, `parseCertsOnly(input, opts?)` reads one back to its raw certificate and CRL DER (a CRL-only message is valid here, unlike the cert-required RFC 5272 form that `pki.est.parseCertsOnly` reads), and `isCertsOnly(input)` recognizes one structurally. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress`, `certsOnly`, `parseCertsOnly`, `isCertsOnly` |
| `pki.smime` | S/MIME message assembly, verification, encryption, and compression over the CMS layer (RFC 8551). `sign(content, signers, opts)` wraps a MIME entity in either form: `multipart/signed`, where the content stays readable in any MUA and a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`, or `application/pkcs7-mime; smime-type=signed-data`, where the whole entity is a base64 CMS SignedData. The signed bytes are the entity's §3.1.1 canonical form with CRLF line endings, and `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies while a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one, as `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat). The `smime-type` is derived from the CMS body rather than the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt`, so it is algorithm-agnostic: any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through. As with `cms.verify`, `verify` returns the per-signer verdict plus the recovered content, and `valid` and `trusted` are separate claims: `valid` says the signature is sound under a certificate the message carried, `trusted` says every signer chained to a root named in `opts.trustAnchors`. Anchoring here is validated for email at both ends of the chain — the signer certificate must carry `emailProtection` (RFC 8550 §4.4.4) and the anchor's own trust metadata must permit that purpose, since a root can be distrusted for email while remaining a good TLS root. `sign` refuses a signer certificate a conforming reader would reject: a `keyUsage` extension without `digitalSignature` or `nonRepudiation` (RFC 8550 §4.4.2), or an `extendedKeyUsage` extension without `emailProtection` or `anyExtendedKeyUsage` (§4.4.4); a certificate carrying neither extension signs. Override either with `requiredEku` / `checkPurpose`. `compress(content, opts)` and `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (§3.6, RFC 3274), a size transform with no integrity or confidentiality (§2.4.5), bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Header protection (RFC 9788): `sign` and `encrypt` take `opts.protectHeaders`, which inlines the caller's `opts.headers` on the Cryptographic Payload root (its Content-Type gaining `hp="clear"` when signed or `hp="cipher"` when encrypted) so the CMS signature or encryption covers them, defeating a transport that rewrites or reads Subject, From, and the rest. `verify` and `decrypt` surface the authenticated inner set as `protectedHeaders` plus `headerProtection { present, mode, fromMismatch, confidential, legacy }`, so a tampered outer header cannot alter it and `fromMismatch` flags an outer From that disagrees. Encryption applies a Header Confidentiality Policy: the default `hcp_baseline` obscures the outer Subject to `[...]` and removes Comments and Keywords, so the real values live only in the ciphertext, and `decrypt` recovers them. Every emitted header routes through a fail-closed injection guard that rejects a CR, LF, or NUL value and a non-ftext name, and a malformed or contradictory `hp` wrap fails closed as `smime/bad-header-protection` rather than silently downgrading. The CMS crypto is unchanged. Inbound legacy RFC 8551 header protection is recognized opt-in: `verify` and `decrypt` with `opts.legacyHeaderProtection` detect a legacy `message/rfc822`-wrapped payload by the RFC 9788 §4.10.1 four-condition identification and surface the inner headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array retaining legally repeated fields such as `Received`. Those never appear in `protectedHeaders` and never set `present: true`. Because a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, this is an explicit heuristic (§4.10.2, "no strong end-to-end guarantees"): a caller keying trust off `present` or `protectedHeaders` is never misled, and only one that explicitly reads `headerProtection.legacy.headers` and cross-checks `legacy.fromMismatch` consumes it. It is off by default, and a nested crypto layer, an inner `hp=`, a non-`message/rfc822` payload, or a duplicate Content-Type reports `legacy: null`. Bidirectionally interoperable with `openssl smime` and `openssl cms`. `buildCertsOnly(certs, opts?)` wraps a `pki.cms.certsOnly` certificate-management message in one `application/pkcs7-mime; smime-type=certs-only; name=smime.p7c` entity, read back by OpenSSL's `pkcs7 -print_certs`. An entity whose Content-Type names one parameter twice, or whose header area names one field twice, is refused rather than resolved by position: RFC 2045 §5.1 and RFC 5322 §3.6 each give one occurrence, so the two readings disagree about which octets the signature covers. One entity is bounded at 1024 body parts, 1024 header lines and 256 Content-Type parameters (`MIME_MAX_PARTS`, `MIME_MAX_HEADERS`, `MIME_MAX_PARAMS`), counted as it is scanned, and the record a verdict is read with holds a copy of the entity rather than a view of the caller's buffer. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress`, `buildCertsOnly` |
| `pki.tsp` | Time-Stamp Protocol (RFC 3161). `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData over `pki.cms.sign` whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` with optional accuracy, nonce, and ordering, plus the §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). The TSA certificate is held to §2.3 before signing (one critical extendedKeyUsage naming timeStamping alone) with the codes `verify` answers, `genTime` keeps its milliseconds as the §2.4.2 fraction of a second, and an accuracy naming no field is refused. `request` and `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq); `response` and `parseResponse` handle the TimeStampResp a TSA returns, either a granted status wrapping a token or a rejection with PKIStatus and failure info, with the §2.4.2 status-to-token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and, with a trust anchor supplied, full certification-path validation of the TSA certificate at the token's `genTime` — judged under the `timeStamping` purpose, so an anchor's own per-purpose trust metadata reaches the decision rather than sitting inert. It returns `{ valid, trusted, genTime, serialNumber, tstInfo, ... }`, where `trusted` is the separate claim that the authority chained to an anchor you named: without one there is nothing to chain to and it is `false`, which is the distinction a timestamp re-read years later turns on — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
| `pki.ocsp` | Online Certificate Status Protocol (RFC 6960), both the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs, with the CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile (obsoleted by RFC 9919) or under SHA-2, plus an optional RFC 9654 nonce (32 to 128 octets, the requester floor that section sets) and an optional requestor signature, under which the signer certificate's subject is the requestorName unless one is stated (RFC 6960 §4.1.2). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER, from the issuing CA directly or a delegated responder, under any `pki.cms.sign` key including the post-quantum ML-DSA and SLH-DSA sets, with `good`, `revoked` (reason and time), or `unknown` per-certificate status, and per-response `singleExtensions` naming the §4.4.4 archive cutoff and the §4.4.2 CRL reference as fields, each read back by name by the parser. The signer refuses what its own lint would grade an error: a pre-encoded singleExtension that RFC 6960 places in requests or in responseExtensions, a CRL entry extension carried at the wrong criticality, and CRL references or entry extensions on a non-issued response, which itself places the §4.4.8 extended revoked definition in responseExtensions without being asked. `buildErrorResponse(status)` produces the unsigned §2.3 error (`tryLater`, `unauthorized`, and the rest). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA, or a CA-issued delegate bearing id-kp-OCSPSigning and id-pkix-ocsp-nocheck and passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency against `thisUpdate` and `nextUpdate`, and the request-nonce echo. It returns `{ status: "good" / "revoked" / "unknown", ... }` and never silently accepts. `verifyRequest(request, opts)` is the responder-side inverse of `buildRequest`: it verifies a client's signed request (RFC 6960 §4.1.1) under the requestor certificate the request carries, trying each embedded certificate since the field is unordered, and reports `signed`, `signatureValid`, every certificate whose key verified the signature and whose subject is the `requestorName` the request states (`signerCerts`, so a key carried under an expired certificate beside its renewal is not hidden behind ordering, while a same-key certificate under another name is not the requestor) with the first one's decoded subject, the full embedded certificate bag (`certs`, including intermediates for path building), and the request's CertIDs; the name check is reported as `requestorNamed` (RFC 6960 §4.1.2) and `valid` requires it, a request no verifying certificate names falls back to listing the verifying ones with `valid: false`, and an unsigned request is reported with `signed: false` rather than refused. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify`, `verifyRequest` |
| `pki.ct` | Certificate Transparency (RFC 6962). `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries, a TLS-presentation-language payload inside the §3.3 double DER wrap, into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature. `reconstructSignedData` rebuilds the exact `digitally-signed` preimage, and `verifySct` verifies an SCT signature against a log's public key, routing an ECDSA signature through the strict DER-conformance gate and verifying through the crypto engine, resolving true or false and throwing a typed error on a structural fault. On the producing side, `encodeSctList` builds the extension value byte for byte as the exact inverse of `parseSctList`, and `signSct` performs a log's signing step. For trust, `parseLogList` ingests the CT log-list JSON into constraint-carrying trusted logs, recomputing each log's id as SHA-256 of its key and refusing a disagreeing id (a swapped key, §3.2) and decoding the state and temporal-interval constraints; `verifySctWithLogList` resolves the log key from an SCT's log id, enforces the state (usable, qualified, and readonly trusted; retired only before retirement; pending and rejected refused) and the temporal-interval window, then delegates the signature check to `verifySct`. `verifyLogListSignature(json, signature, publicKey)` verifies the detached `log_list.sig` over the raw log-list bytes against a caller-pinned signer key (RSASSA-PKCS1-v1.5/SHA-256 and an EC P-256 arm, with forgeable-key defenses failing closed), cross-checked against `openssl dgst`. `fetchLogList(opts)` turns that chain into a live client: it GETs the `log_list.json` and its detached `log_list.sig` over `pki.transport`, verifies the detached signature over the raw fetched bytes against a caller-pinned distributor key before parsing, so an unverified document is never parsed, read, cached, or surfaced, then ingests the same bytes through `parseLogList` and returns the trusted-log set plus the surfaced `version` and `timestamp`. Above the per-SCT layer, The RFC 6962 sec. 4 log client is shipped: `getSth` fetches a signed tree head and verifies it under the caller-pinned log key, returning nothing when it does not verify, since every later check is measured against that root; `sthSignedData` builds the 50-byte sec. 3.5 preimage and `verifySth` checks a head held offline. `getProofByHash` and `getSthConsistency` fetch the audit and consistency proofs and fold them against tree heads a log signed, taking the query sizes from those heads so a size and a head cannot disagree. `getEntries` and `getRoots` fetch what the specification leaves unsigned and say so, with a short entry answer reported as truncation rather than a fault. `addChain` and `addPreChain` submit a chain and verify the SCT the log returns against the chain it was issued for. `verifySctList` renders a certificate-level verdict over every SCT a certificate carries (§3.3), reporting how many distinct trusted logs verified an SCT (so a duplicated SCT cannot inflate the count) and from how many distinct trusted operators against a caller's CT policy (`minScts` and `minOperators`, both defaulting to the RFC floor of one), where a policy shortfall is a verdict and a per-SCT failure is a recorded row rather than a thrown call; `x509CertEntry` reconstructs the precertificate log entry from a final certificate by removing the SCT-list extension from its TBSCertificate as byte surgery, so its embedded SCTs verify end to end (§3.2). The toolkit bakes in no vendor URL or key, TLS trust is explicit with `rejectUnauthorized` always on, each response is size-capped before the trust chain, and the transport is injectable so the whole path is testable offline — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct`, `parseLogList`, `verifySctWithLogList`, `verifySctList`, `x509CertEntry`, `verifyLogListSignature`, `fetchLogList` |
| `pki.merkle` | Merkle trees and their proofs, in both directions (RFC 6962 / RFC 9162). `leafHash`, `nodeHash`, and `emptyRootHash` build the domain-separated (0x00 leaf, 0x01 node) SHA-256 tree hashes. `verifyInclusion` folds an audit proof back to a root, and `verifyConsistency` reconstructs both the old and new root, which is the append-only guarantee; each is constant-time-compared to a trusted checkpoint root. `root` computes the tree head over a set of leaves, `inclusionProof` produces the audit path for one of them, and `consistencyProof` produces the append-only proof between two sizes, each returning what the matching verifier takes. The producing verbs take leaf hashes in the same form the verifiers take them, and the tree size is the leaf array's length rather than a second option that could disagree with it. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `root`, `verifyInclusion`, `verifyConsistency`, `inclusionProof`, `consistencyProof` |
| `pki.tlog` | The C2SP envelope a tiled transparency log speaks, over the hashes `pki.merkle` verifies: a signed note (`c2sp.org/signed-note`), the checkpoint body a log signs into one (`c2sp.org/tlog-checkpoint`), and the tile addressing a static log serves them beside (`c2sp.org/tlog-tiles`). Static-CT, Merkle Tree Certificates and Rekor v2 differ in what they put in the origin line and what they do with the root, not in this layer. A note is text, a blank line, then one signature line per signer, and the signature covers the text including its trailing newline: `parseNote` surfaces `signedBytes` as a view of the input, so a verifier never hashes a re-serialization of what it parsed. A key ID is four bytes and the specification says it identifies without proving, so `verifyNote` tries every supplied key whose ID matches and reports only after all of them; a signature from a key not supplied is ignored, and a note carrying none from a supplied key resolves `verified: false`. A line naming a supplied key by both name and ID that no such key verifies rejects the whole note, so a forged line from a known signer is not masked by a valid one. The derivation is fixed per algorithm and read off the key material rather than declared: Ed25519 hashes the name, a newline, a type byte and the raw 32 bytes, ECDSA over P-256, P-384 or P-521 hashes the SubjectPublicKeyInfo alone, and RSA carries a format tag; an ECDSA signature is checked with the hash its curve calls for. `parseVkey` reads the `name+id+key` text a log publishes, checking the stated ID and type against what the material derives and is. `verifyCheckpoint` takes an expected `origin` and refuses a checkpoint naming another one even where the signature verifies, since a valid signature from one log says nothing about another log's tree. Every Ed25519 key passes the shared Edwards-point gate before it reaches a verify, since Node imports a low-order key without complaint and such a key verifies a forged signature. `parseCheckpoint` reads the origin, the tree size as a `BigInt` and the 32-byte root ready for `pki.merkle.verifyInclusion`, refusing a leading zero in the size, an empty origin or a root that is not 32 bytes; the origin is not validated as a URL, because the specification says a client must not assume it is one. `tilePath` / `entryBundlePath` / `parseTilePath` build and read the paths a log serves, with the index in zero-padded three-digit segments, and `parseTile` / `parseEntryBundle` split what it serves; each of those takes bytes and refuses a string, since decoding a tile as text rewrites every byte above 0x7f. A tiled log serves no proof endpoint, so `inclusionProof` assembles the RFC 6962 audit path from tiles over a `read` the caller supplies, caching tiles for the call, and `tileWidth` says which tile to ask for: a full and a partial tile sit at different paths, so the width has to follow from the verified tree size rather than from the response. Verifying only: nothing signs and nothing fetches — `keyId`, `parseNote`, `verifyNote`, `parseCheckpoint`, `verifyCheckpoint`, `tilePath`, `entryBundlePath`, `checkpointPath`, `parseTilePath`, `parseTile`, `parseEntryBundle`, `tileWidth`, `inclusionProof` |
| `pki.tuf` | The Update Framework metadata layer: the signed documents a repository publishes to say which keys may sign what, and the rules a client applies before believing any of it. This is the trust root a sigstore verifier would otherwise pin by hand. TUF signs the canonical JSON form of a document, so `canonicalJson` is the wire format rather than a convenience: keys sort by code point, no whitespace is emitted, and only backslash and quote are escaped, so a literal newline stays a newline where `JSON.stringify` would write an escape and sign different bytes. A float is refused rather than rounded, because the reference encoder cannot write one. `keyId` derives a key's identifier as the hex SHA-256 of its own canonical form, and `verifySignatures` recomputes it for every key a role names, so a document cannot file one key under another's identifier; a threshold counts one verified signature per distinct identifier, so repetition cannot meet it. `updateRoot` walks the root chain, requiring each new root to be signed by a threshold of the previous root's keys AND a threshold of its own and to carry exactly the next version, which is what makes a key rotation safe and a skipped intermediate impossible. `checkExpiry` is the freeze-attack check, judged at a caller instant rather than the system clock. Verifying only: nothing here fetches — `canonicalJson`, `keyId`, `parseMetadata`, `checkExpiry`, `verifySignatures`, `updateRoot` |
| `pki.possession` | Statement of possession (RFC 9883): how a key that cannot sign gets certified. PKCS#10 proves possession of the key being certified by signing the request with it, so an ML-KEM, X25519 or X448 key could not be certified through a certification request at all. RFC 9883 signs the request with a different key the requester already holds a certificate for, and names that certificate in an attribute. `pki.csr.sign` takes `spec.privateKeyPossessionStatement` and signs with the key given, which is the one case where it accepts a signing key that is not the subject's and a subject key that cannot sign; both stay refused for a request that declares no statement. `includeCertificate: false` emits the compact form the RFC allows, where the CA looks the certificate up. `pki.crmf.build` takes the same structure as `controls.statementOfPossession`, which the RFC gives the attribute's own OID. `pki.possession.verifyRequest` is the CA's side and enforces the two MUSTs: the request's signature is validated with the public key from the signature certificate, and that certificate's certification path is validated, so `opts.trustAnchors` is required and the verb throws without it rather than returning a verdict that skipped a MUST. The two name comparisons the RFC states as SHOULDs are reported and not enforced, because each ends in a question about certificate policy that no library can answer: `subjectMatches` and `subjectAltNamesMatch` carry the comparison and `valid` is `false` with a reason naming the decision. `requestsSignatureCertificate` reports the prohibition on using the attribute to obtain a signature certificate, which `pki.lint.csr` with `profile: "rfc9883"` grades as an error. A statement carrying a certificate its `signer` does not name is refused in both directions — `parse`, `verifyRequest` |
| `pki.altSig` | Alternative signatures (ITU-T X.509 (2019) clause 9.8), the shape widely called Catalyst: one certificate carrying two signatures, so a PKI migrating between algorithm sets does not have to reissue to every relying party at once. A relying party that has not migrated reads the native signature and ignores the extensions; one that has reads the alternative signature instead. `pki.x509.sign` takes `issuer.altKey` beside the issuing key and `extensions.subjectAltPublicKeyInfo` for the subject's alternative key, and emits all three extensions: clause 7.2.2 requires the alternative signature to be generated first, over the structure without it, and the native signature then to cover it, so the ordering is not the caller's to get right. `pki.crl.sign` takes `issuer.altKey` for the same mechanism on a CRL, which clause 7.10.3 gives the same procedure; `subjectAltPublicKeyInfo` is certificate-only. `signedData` builds the bytes that signature covers: the `PreTBSCertificate` of `draft-truskovsky-lamps-pq-hybrid-x509` section 4, which is the `tbsCertificate` without its `signature` field and without the `altSignatureValue` extension, and the `PreTBSCertList` of section 5 for a CRL. Those are the bytes Bouncy Castle emits and checks, so an alternative signature made here verifies there and one made there verifies here. Clause 7.2.2 requires a verifier to rebuild that encoding, and this is the one place the toolkit re-encodes what it parsed: every retained component keeps the bytes it had and only the two SEQUENCE headers whose lengths change are recomputed, so nothing is re-serialized from a decoded model. `verify` checks the signature under the issuer's alternative public key, which `subjectAltPublicKey` reads out of the issuer's own certificate; `SubjectAltPublicKeyInfo` has the same encoding as `SubjectPublicKeyInfo`, so that value is an SPKI and needs no conversion. The algorithm is not derived: the structure states it in `altSignatureAlgorithm`, and that stated algorithm is bound to the key through the engine path validation uses, so a key that does not match it resolves `false` rather than being read under some other algorithm; a structure stating no alternative signature throws instead, there being nothing to check. `pki.lint.certificate` with `profile: "x509-altsig"` grades the clause 9.8.3 pairing and the clause 7.2.2 full set as errors and a critical extension as a warning, criticality being stated only in a NOTE — `signedData`, `verify`, `subjectAltPublicKey` |
| `pki.relatedCert` | Related certificates (RFC 9763): the two structures that let one subject hold a classical certificate and a post-quantum one and have each say the other exists. `requestSignedData` builds the bytes a requester signs to prove it holds the certificate it names, which §3.1 fixes as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime` and nothing else; `locationInfo` travels in the attribute outside those bytes, so a verified proof says nothing about where the certificate can be fetched. `pki.csr.sign` takes the whole attribute as `relatedCertRequest` and encodes those two fields from that same function, so the bytes signed and the bytes emitted cannot drift apart; its `locationInfo` is a `SEQUENCE OF` IA5String URIs rather than one URI, and it keeps the order the requester wrote. `verifyRequest` checks the proof through the engine the certificate path uses, so the algorithm it derives is bound to the key, and returns `{ valid, verified, signerMaySign, signatureAlgorithm }` with a `code` and `reason` when it did not pass. The two checks are reported apart because they fail for different reasons: `verified` is the proof's own signature, and `signerMaySign` is whether the named certificate's keyUsage authorizes that signature at all, which §4.2.1.3 of RFC 5280 decides. A key-establishment certificate passes it, RFC 9763 §3.1 having such a key sign once for proof of possession. The key decides the algorithm, resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, so every algorithm those verbs sign with is reached, including an RSASSA-PSS key whose parameters belong to its identifier; the digest comes from the hash the certificate's signature OID indicates, with `digestAlgorithm` or a DER `signatureAlgorithm` naming either outright. A key whose algorithm fixes its own digest refuses `digestAlgorithm` rather than ignoring it, and a key that cannot sign is refused. A certificate other than the one `certID` names throws rather than returning `false`. `certificateHash` and `matchesCertificate` are the extension side, over the whole related certificate: the algorithm defaults as §4.1 directs, to the hash the related certificate's own signature OID indicates, and a certificate that indicates none throws rather than falling back to one the document does not name. `sha1` is refused. `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the certificate itself computes the digest rather than trusting a caller to name the algorithm it used; supplying both a `hashValue` and a certificate is refused, the two being able to disagree. The extension is emitted non-critical, which §4.1 asks at SHOULD NOT, and a critical one still parses: `pki.lint.certificate` with the `rfc9763` profile grades it, beside the MUST that it appear only in a chain's end-entity certificate — `requestSignedData`, `verifyRequest`, `certificateHash`, `matchesCertificate` |
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor` |
| `pki.shbs` | Stateful hash-based signature verification: HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST SP 800-208 for CNSA 2.0 firmware signing. `verify` checks an HSS signature, where every level must pass, and `verifyLms` a single-tree LMS, over the raw public-key and signature blobs the parsers already surface. Pure public-input SHA-256 and SHAKE256 hashing, a data-driven typecode registry, and bounds-before-slice reads; a malformed blob throws a typed `ShbsError` while a well-formed but wrong signature returns `false`. Verification only by design, since stateful signing needs atomic one-time-key state that belongs in an HSM. `pki.path.validate` verifies an HSS-signed certificate on its own, so a chain reaching one needs no separate call: the signature covers the whole tbsCertificate rather than a digest of it (RFC 9802 §7.1), and a CertificateList naming the algorithm takes the same route. It is proven against the self-signed certificate RFC 9802 Appendix A publishes — `verify`, `verifyLms` |
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180), the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS` and `setupR` establish a sender or recipient context (KEM encapsulation plus the key schedule); the context's `seal` and `open` AEAD-encrypt with a sequence-counter nonce, and `export` derives further secrets; the module-level `seal` and `open` are single-shot wrappers. DHKEM (P-256, P-384, P-521, X25519, X448) in all four modes, proven against the RFC 9180 Appendix A vectors, the post-quantum ML-KEM-512/768/1024 KEMs (IANA 0x0040 to 0x0042) in the base and psk modes, proven against the draft-ietf-hpke-pq Appendix A vectors, and the PQ/T hybrid KEMs MLKEM768-P256 (0x0050), MLKEM1024-P384 (0x0051) and MLKEM768-X25519 (0x647a, X-Wing), each pairing an ML-KEM with a nominal group under the CFRG C2PRI combiner and proven against the draft-irtf-cfrg-concrete-hybrid-kems Appendix B vectors; the two-stage HKDF-SHA256/SHA384/SHA512 and the single-stage SHAKE128/SHAKE256 key schedules, proven against the draft-ietf-hpke-pq Appendix A.7/A.8/A.11/A.12 vectors, by AES-GCM / ChaCha20Poly1305 / export-only. On a single-stage KDF `info`, `psk`, `psk_id` and an export are each capped at 65535 bytes; TurboSHAKE128/256 are not offered while no released OpenSSL carries the XOF. `deriveKeyPair`, `generateKeyPair`, `encap` and `decap` reach a KEM on its own. Pure composition over `node:crypto` — `suites`, `setupS`, `setupR`, `seal`, `open`, `encap`, `decap` |
| `pki.kem` | Composite ML-KEM key establishment (draft-ietf-lamps-pq-composite-kem), a post-quantum ML-KEM hybridized with a traditional RSA-OAEP, ECDH, X25519, or X448 so the established secret holds if either component is later broken. `encapsulate` turns a recipient's composite `SubjectPublicKeyInfo` into a 256-bit shared secret and a ciphertext; `decapsulate` recovers the same secret from the ciphertext and the composite PKCS#8 private key. Each component KEM runs independently and the two secrets are mixed through a SHA3-256 combiner that binds the traditional ciphertext, the traditional public key, and a per-algorithm label. The twelve algorithms pair ML-KEM-768 and ML-KEM-1024 with RSA-OAEP 2048/3072/4096, ECDH over P-256/P-384/P-521 and brainpoolP256r1/P384r1, X25519, and X448, each verified against the draft Appendix G known-answer vectors; a malformed key or ciphertext, an unsupported algorithm, or a component decapsulation failure throws a typed `KemError` — `encapsulate`, `decapsulate` |
| `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature,
the checkpoint read through `pki.tlog` and verified under the key the entry names in `logId.keyId`, so a signed tree root from one pinned log cannot satisfy an entry claiming another; the size and root the proof folds against come from that verified checkpoint rather than from the `inclusionProof` fields no signature covers, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. A Rekor v2 `hashedrekord` v0.0.2 entry is read on either arm: Rekor v2 supports no DSSE entry type, so a DSSE signature is logged as a `hashedrekord` over the hash of its signing preimage, and the entry is bound by the signature it records, the certificate that made it, and that digest. Where the entry states `keyDetails`, that statement is held to the leaf key rather than read in its place. Rekor v2 issues no signed entry timestamp and writes `integratedTime` as zero, so it is ignored and `opts.tsaRoots` pins the timestamp-authority anchors for an RFC 3161 token over the signature bytes; with neither that nor `opts.time` the verdict is `sigstore/no-attested-time`, because a certificate that lives about ten minutes cannot be validated against an unattested instant. `timestampSource` names which of the three dated it, beside `logOrigin`, `treeSize` and `checkpointKeyId` from the verified checkpoint. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
| `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing. Every format `pki.schema.all()` detects has a report and its own named verb, so a PKCS#8 key, a PKCS#12 store, a CRMF request, a CMP message, an EST CSR-attributes response, an RFC 5914 trust anchor list, an OCSP request or response, an RFC 3161 timestamp response and an RFC 5755 attribute certificate each read the same way a certificate does. No report prints private key material: a PKCS#8 and a PKCS#12 name the algorithm and say a private key is present, never its bytes, and a PKCS#12's encrypted safes are named rather than decrypted since these verbs take no password. `asn1(input)` is the structural dump below the reports, shaped after `openssl asn1parse`: one line per value carrying the byte offset, the nesting depth, the header and content lengths, the form, the tag name, and a primitive's rendered content, with an OBJECT IDENTIFIER named from the registry and a time rendered as the bytes on the wire. A conforming input is read by the same strict decoder the parsers use; when that decode refuses the bytes, the report names the refusal and a header walk renders the prefix it can read and the offset where it stopped, so a truncated, trailing-byte, indefinite-length or non-minimal-length file still reads. It withholds no value: where the other verbs here name a key and print none of it, this one prints bytes, private key material included, so a report that must not print a key is `any` or `pkcs8` — `certificate`, `crl`, `csr`, `cms`, `pkcs8`, `pkcs12`, `crmf`, `cmp`, `csrattrs`, `trustanchor`, `ocspRequest`, `ocspResponse`, `tsp`, `attrcert`, `asn1`, `any` |
| `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset. The RFC 5280 profile carries the rows that document's own clauses delegate: §4.2.1.3 states that "appropriate values for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and [RFC4491]", and RFC 8813 §3 is where the elliptic-curve answer now is, so an EC certificate asserting keyEncipherment or dataEncipherment is `lint/rfc5480/ec-key-usage-forbidden` there and under `rfc5480` alone. The RFC 5280 rows themselves: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <file> [--asn1]`, `pki keygen --out <key>`, `pki csr --key <k> --subject <dn>`, `pki issue (--key <k> --subject <dn> \| --csr <req>)`, `pki fetch <https-url>`, `pki lint <file>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>`. `pki <verb> --help` prints that verb's usage on stdout and exits 0. Issuing from a request answers what the request asks for: `--copy-requested-san` writes the requested subjectAltName into the certificate, `--ignore-requested-extensions` issues without it, and a request asking for anything neither accounts for is refused rather than certified short |

### Runnable examples

The tarball carries five programs. After `npm i @blamejs/pki`, run any of them
from the installed package:

```sh
node node_modules/@blamejs/pki/examples/issue-a-certificate.js   # a CA, a leaf, and path validation
node node_modules/@blamejs/pki/examples/read-a-certificate.js    # parse, render, lint, dump one certificate
node node_modules/@blamejs/pki/examples/sign-and-verify-cms.js   # CMS SignedData, attached and detached
node node_modules/@blamejs/pki/examples/post-quantum.js          # ML-DSA, SLH-DSA and ML-KEM through the same verbs
node node_modules/@blamejs/pki/examples/revoke-and-check.js      # a CRL, and the path that stops validating
```

Each is self-contained: it generates what it needs, uses no network, writes
nothing to disk, and opens with the `require("@blamejs/pki")` line a reader
copies. Every line they print is asserted by the test suite, so an example that
stopped demonstrating what it claims fails the build rather than going stale.

### CLI

```sh
pki version                              # the installed @blamejs/pki version
pki oid 1.2.840.113549.1.1.11           # sha256WithRSAEncryption
pki oid sha256                           # 2.16.840.1.101.3.4.2.1
pki parse cert.pem                       # structured JSON summary of a certificate
pki inspect anything.der                 # detects the format and renders it (pki.inspect.any)
pki inspect anything.der --asn1          # the TLV tree, in the shape openssl asn1parse prints
pki keygen --out k.key --pub k.pub       # an ML-DSA-65 key pair; the private key never to stdout
pki keygen --alg Ed25519 --out ed.key --pem
pki csr --key k.key --subject "CN=host.example" --san host.example --out req.der
pki issue --key k.key --subject "CN=ca.example" --ca --days 3650 --out ca.der
pki issue --csr req.der --issuer-cert ca.der --issuer-key k.key --copy-requested-san --out leaf.der
pki issue --csr req.der --issuer-cert ca.der --issuer-key k.key --ignore-requested-extensions --out bare.der
pki fetch https://example.com/ --system  # the chain the endpoint presents; NOT a verification
pki lint cert.pem                        # graded conformance findings; exit 1 on an error
pki lint cert.pem --json --profile cabf-tls
pki lint list.crl                        # detects a CRL or an OCSP response and runs its profile
pki lint status.ors --profile rfc5019    # the RFC 5019 lightweight rows for an OCSP response
pki convert cert.pem --to der > cert.der # transcode between PEM and DER (round-trips)
pki verify leaf.pem --anchor root.pem --time 2026-01-01T00:00:00Z   # RFC 5280 path validation
pki sign msg.txt --cert signer.pem --key signer-key.pem --out msg.p7s   # CMS SignedData (pki.cms.sign)
pki sign msg.txt --cert signer.pem --key signer-key.pem --detached --pem # detached, PEM to stdout
```

Every verb is a thin front-end over the library: `inspect` over `pki.inspect.any`
and `pki.inspect.asn1`, `lint` over `pki.lint`, `keygen` over `pki.key.generate`,
`csr` over `pki.csr.sign`, `issue` over `pki.x509.sign`, `fetch` over
`pki.transport.peerChain`, `convert` over the per-format PEM codecs, `verify`
over `pki.path.validate`, and `sign` over `pki.cms.sign`. The CLI does nothing
the library API cannot. `lint` exits non-zero when any `error` or `fatal` finding
is present, `verify` exits non-zero when the path does not validate, and `sign`
reads a PKCS#8 DER or PEM private key and its certificate and writes a DER (or
`--pem`) SignedData to `--out` or stdout.

`keygen` is the one verb that puts a private key on disk. It writes to the file
`--out` names and never to stdout, refuses to write over a file that exists, and
creates the file with owner-only permissions where the platform enforces a file
mode; on Windows Node does not apply the mode, so the file inherits the
directory's permissions there. `--pub` writes the public half separately, so
sending a public key does not mean handing over the file that holds the private
one. A key path on a command line is visible in the process table to every user
on the machine while the process runs.

`issue` needs both halves of an issuer: `--issuer-cert` without `--issuer-key`,
or the reverse, is refused rather than quietly self-signed. `--csr` verifies the
request's signature before certifying anything from it. `fetch` opens a TLS
handshake and sends no request, so nothing reaches the application behind the
endpoint; it prints what the handshake checked and what it did not, because a
fetched chain is not a validated one and `pki verify` is the verb that validates
a path.

### What's coming

SCEP certificate issuance (the CertRep responder side), and additional
NIST-on-ramp PQC signatures as the OID registry admits them, are on the roadmap
and ride this same core. [ROADMAP.md](ROADMAP.md)
carries the full plan and the current status of each area;
[CHANGELOG.md](CHANGELOG.md) carries what has landed.

## Architecture

Every PKI format is a thin, declarative schema over one shared engine. A parser
declares the ASN.1 structure as data and hands it to `walk`; it never advances a
child cursor, re-checks a tag, or re-rolls PEM handling by hand. Each structural
rule is therefore written once in the engine — bounds-checked positional reads,
optional and context-tagged field ordering, SET-OF ascending order and
uniqueness, arity, and fail-closed typed errors — and no new format can
reintroduce the bug class it prevents. Adding a format is a schema declaration
plus a documentation comment block.

```
┌─ Detect + route ─────────────────────────────────────────────────────────┐
│ pki.schema.parse — inspect the DER root, route to the matching sibling   │
└──────────────────────────────────────────────────────────────────────────┘
                                     │
 ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
 │  x509  │ │  crl   │ │  csr   │ │ pkcs8  │
 └────────┘ └────────┘ └────────┘ └────────┘
 ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
 │  cms   │ │  ocsp  │ │  tsp   │ │  crmf  │
 └────────┘ └────────┘ └────────┘ └────────┘
 ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
 │ pkcs12 │ │  cmp   │ │ smime  │ │attrcert│
 └────────┘ └────────┘ └────────┘ └────────┘
                                     │  routed DER format parsers (siblings)
┌─ Protocols · trust · supply chain  (reached by explicit call) ───────────┐
│ pki.path RFC 5280 · pki.trust anchors · pki.ct SCTs · pki.hpke RFC 9180  │
│ pki.shbs HSS/LMS · pki.merkle RFC 9162 · pki.sigstore npm provenance     │
│ pki.jose · pki.acme · pki.est — compose the layers below directly.       │
└──────────────────────────────────────────────────────────────────────────┘
                                     │  every module composes ↓
┌─ Shared structure ───────────────────────────────────────────────────────┐
│ pki.schema.engine — walk + combinators (positional reads, tag order,     │
│ SET-OF uniqueness, typed errors) · the PKIX sub-schemas · and the        │
│ guard family (guard-*) — one fail-closed choke point per CVE class.      │
└──────────────────────────────────────────────────────────────────────────┘
                                     │  built on ↓
┌─ Foundation ─────────────────────────────────────────────────────────────┐
│ pki.asn1 — strict bounded DER codec · pki.cbor — deterministic CBOR ·    │
│ pki.oid — two-way PQC-seeded registry · pki.errors — PkiError taxonomy   │
│ · pki.C — version-stable LIMITS + scale constants.                       │
└──────────────────────────────────────────────────────────────────────────┘

┌─ Crypto ─────────────────────────────────────────────────────────────────┐
│ pki.webcrypto ─▶ node:crypto — a W3C SubtleCrypto engine: the            │
│ classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation.   │
└──────────────────────────────────────────────────────────────────────────┘
```

**Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
registry (`pki.oid`), the `PkiError` taxonomy (`pki.errors`), and the
version-stable constants (`pki.C`). These have no PKI knowledge; they are the
bytes-and-names layer everything else stands on.

**Shared structure.** The declarative schema engine (`pki.schema.engine`) and the
PKIX sub-schemas it is fed: `AlgorithmIdentifier`, `Name`, `Extension`, the
bounded version reader, and the single coerce-decode-walk parse entry that every
format's `parse` is bound to. Input coercion, the PEM size cap, and the
DER-decode wrapping live here once, so a format cannot diverge on a guard.

**Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, `tsp`,
`attrcert`, `crmf`, `pkcs12`, `cmp`, `smime`, and `csrattrs` are siblings. Each
is a schema declaration composed from the shared pieces, emitting its own typed
`domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
routes to the first sibling whose detector accepts. Where two detectors overlap,
order in the registry is load-bearing and the more specific one sits first, so a
new format is inserted ahead of any more permissive detector.

**Protocols, trust, and supply chain.** Above the format parsers sit the domain
modules reached by explicit call rather than DER routing: `pki.path` (RFC 5280
path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparency
SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
`pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline
npm-provenance verification), `pki.webauthn` (WebAuthn and passkey
verification), `pki.cms` (RFC 5652 SignedData signing and verification),
`pki.tsp` (RFC 3161 timestamping), and the `jose`, `acme`, and `est` enrollment
surfaces. Each composes the shared structure, foundation, and crypto layers
directly. Alongside the schema engine, the fail-closed guard family (`guard-*`)
centralizes each CVE-class defense — detached-buffer re-view, resource caps,
constant-time compares, canonical-DN comparison — as one choke point a format
cannot re-inline.

**Crypto.** `pki.webcrypto` is a W3C `SubtleCrypto` engine over `node:crypto`,
carrying the classical suite plus post-quantum ML-DSA and SLH-DSA signatures and
ML-KEM key generation. Sign and verify resolve algorithms through the same OID
registry the parsers read, so the signing surface and the parsing surface share
one algorithm vocabulary.

## Security posture

- **Zero npm runtime dependencies, nothing vendored.** The cryptography runs on
  Node's built-in `node:crypto`, and the toolkit vendors no third-party code. A
  platform built-in ships zero bytes and stays OpenSSL-interoperable by
  construction. No dependency tree, transitive or vendored, exists to
  compromise or keep current.
- **Fail-closed DER.** The decoder rejects every non-canonical shape — indefinite
  length, non-minimal length or tag encodings, trailing bytes, over-long or
  over-deep input — with a typed `Asn1Error` before it walks the structure. Size
  and depth caps are enforced up front, so adversarial input costs bounded work
  rather than a stack overflow.
- **Fail-closed verification.** Every verify path throws on failure. A default
  that accepts on error is treated as a bug.
- **PQC-first crypto.** Post-quantum ML-DSA and SLH-DSA signatures run in the
  WebCrypto engine (`pki.webcrypto`) alongside the classical set, and ML-KEM
  key generation, encapsulation, and decapsulation carry the CMS KEM recipient
  arm. Every algorithm is named in the OID registry (`pki.oid`), sign and verify
  resolve through that registry, and no default is classical-only where a
  post-quantum option exists.
- **Signed releases.** Release tags are annotated and SSH-signed, and published
  tarballs carry provenance and an SBOM. See
  [SECURITY.md → Verifying release authenticity](SECURITY.md#verifying-release-authenticity).

Report a vulnerability privately — see [SECURITY.md](SECURITY.md). For usage
questions and support channels, see [SUPPORT.md](SUPPORT.md).

## Documentation

The primitive-by-primitive reference lives at [pkijs.com](https://pkijs.com),
generated from the source comment blocks so it cannot drift from the shipped API.

## License

[Apache-2.0](LICENSE). Third-party attribution, currently none since the toolkit
vendors nothing, is tracked in [NOTICE](NOTICE).
