@blamejs/pki
v0.7.34
Published
Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.
Maintainers
Readme
@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.
pkijs.com · Roadmap · Security · Changelog
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 inparse. 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-innode: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 fornpm auditto report on.
Install
npm i @blamejs/pkiRequires Node.js 24.21+ and runs on the shipped runtime, with no build step and no transpilation.
var pki = require("@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 Dates, algorithms and extensions named
through the OID registry, and the exact signed tbsBytes for a downstream
verifier.
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:
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
var der = pki.schema.x509.pemDecode(pem, "CERTIFICATE"); // Buffer of DER bytes
var out = pki.schema.x509.pemEncode(der, "CERTIFICATE"); // 64-column PEM stringDecode 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.
// 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:
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:
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).
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.
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.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 explains the status lifecycle. The
table is an index — the per-function reference, generated from the source
comment blocks, is at 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. 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 |
| 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, 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; 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 Fail-closed — parse, pemDecode, pemEncode |
| 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. 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 Fail-closed — parse, pemDecode, pemEncode |
| pki.schema.csr | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature Fail-closed — parse, 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, 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 Dates, 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 Fail-closed — parse, pemDecode, pemEncode |
| 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 Fail-closed — parse, pemDecode, pemEncode |
| 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 RFC 7627 extended master secret, 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 — https |
| 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. 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). 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): 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, and the certificate-policy tree. 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. Pure and re-entrant — validate, build, crlChecker, ocspChecker |
| 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. 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, and the RFC 6960 OCSP no-check responder marker, 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. 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 |
| 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), 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, or pre-encoded Extension DER) each with its RFC 5755 criticality. 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), 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; 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 ex
