CMP (Certificate Management Protocol, RFC 4210)

The RFC 9810 Certificate Management Protocol producing side. pki.cmp.build assembles a PKIMessage: a PKIHeader (sender / recipient / transaction metadata), a PKIBody carrying one request or transaction (an ir / cr / kur certificate request via pki.crmf.build, a p10cr PKCS#10 via pki.csr.sign, a certConf / pollReq / genm / rr, or a nested wrapper of other PKIMessages an RA forwards), and an optional PKIProtection computed over the message. Protection is a signature under the sender key (any registry algorithm), a PBMAC1 shared-secret MAC, or a KEM-based MAC keyed by a secret the peer encapsulated to the sender's ML-KEM key. The emitted message round-trips through pki.schema.cmp.parse and its protection verifies. pki.cmp.transfer carries a message to a CMP endpoint (RFC 9811), pki.cmp.verify checks an incoming message's protection, and pki.cmp.session drives a full enrollment transaction end to end over these. Parsing lives at pki.schema.cmp.parse.

pki.cmp.build

since 0.3.5 stable
pki.cmp.build(message, opts?) -> Promise<Buffer|string>

Build an RFC 9810 CMP PKIMessage, the producing-side inverse of pki.schema.cmp.parse. message is { header, body }: header carries the sender / recipient GeneralNames plus optional transaction metadata (transactionID, senderNonce, messageTime, freeText, generalInfo, ...). A transactionID or senderNonce the caller omits is filled with 16 fresh random bytes, the width RFC 9810 sec. 5.1.1 recommends and RFC 9483 sec. 3.5 has a receiver require, so every built message is receivable; a senderNonce given under 128 bits is refused (cmp/bad-input), and a recipNonce is never filled. body is a single-key object naming the arm. Request-side: { ir } / { cr } / { kur } / { krr } (key recovery, RFC 9810 sec. 5.3.7) / { ccr } (cross-certification, RFC 9810 sec. 5.3.11: the requesting CA keeps its private key, so a ccr carrying the private-key-transport encryptedKey proof-of-possession is refused; a ccr is a CertReqMessages at the normative floor and the optional App. D.6 single-request cardinality is not enforced); each a CertReqMessages spec delegated to pki.crmf.build; the proof-of-possession key is key on the arm spec), { p10cr } (a PKCS#10 CertificationRequest DER), { certConf }, { pollReq }, { genm }, { rr }. CA/responder-side: { ip } / { cp } / { kup } / { ccp } (a CertRepMessage: caPubs plus response of CertResponse each with a PKIStatusInfo and, under a granting status, a certifiedKeyPair), { rp } (RevRepContent), { genp }, { error } (ErrorMsgContent), { pollRep }, { krp } (KeyRecRepContent), { pkiconf } (NULL). Registration-authority: { nested } (an array of complete PKIMessage DER, each forwarded unchanged and wrapped as NestedMessageContent, RFC 9810 sec. 5.1.3.5; each entry is checked to parse as a PKIMessage before it is wrapped, and an empty array or an entry that does not parse is refused). The message is protected: opts carries exactly A signature-protected header.sender the caller omits is filled with the signer certificate's subject (RFC 9483 sec. 3.1, the value a receiver holds it to), as the transaction fields are filled; an empty-subject signer, which names itself by a subjectAltName entry, requires an explicit sender. The message is protected: opts carries exactly one of { key, cert } (a signature under the sender key over the message, using any registry algorithm: RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or { mac } (a PBMAC1 shared-secret MAC). The protection is computed over the exact DER of the virtual ProtectedPart ::= SEQUENCE { header, body } and self-verified before the message is returned. An embedded p10cr request is checked against its own proof-of-possession first: a PKCS#10 whose self-signature does not verify under its subject public key is refused (cmp/bad-popo) rather than protected and sent. The emitted PKIMessage round-trips byte-identically through pki.schema.cmp.parse.

Options

- `key` (Buffer|CryptoKey) + `cert` (Buffer): signature protection under the sender key; `cert` is
  the signer certificate (its SPKI resolves the algorithm) and is placed in `extraCerts`.
- `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? }): PBMAC1 protection from a shared secret.
- `kem` ({ key, ciphertext, kemAlgorithm, kemContext?, transactionID?, len? }): KEM-based MAC
  protection (RFC 9810 sec. 5.1.3.4), for a sender whose key establishes secrets rather than
  signing. `key` is the KEM private key as a CryptoKey (what `pki.key.generate` returns), a
  PKCS#8 DER Buffer / Uint8Array, or a PEM string. `ciphertext` is what the peer encapsulated to
  it, `kemAlgorithm` names the ML-KEM
  parameter set it was produced under, and the derived key is bound to `transactionID` (this
  message's, unless the ciphertext arrived under another) and any `kemContext`.
- `extraCerts` (array of Buffer): additional certificates to carry in `extraCerts [1]`.
- `pem` (boolean|string): return a PEM `CMP` block instead of DER.
- `pss` (boolean) / `digestAlgorithm` (string): signature-protection algorithm options.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
  var signerCertDer = await pki.x509.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: signerKeyPkcs8 });
  var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) },
    { key: signerKeyPkcs8 });
  var der = await pki.cmp.build(
    { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
      body: { p10cr: csrDer } },
    { key: signerKeyPkcs8, cert: signerCertDer });
  pki.schema.cmp.parse(der).body.arm;   // "p10cr"
}
example();

References

pki.cmp.transfer

since 0.3.19 stable
pki.cmp.transfer(url, message, opts?) -> Promise<{ response, responseBytes, status, contentType, tls }>

POST a DER PKIMessage to a CMP endpoint and return the parsed response PKIMessage, over the shared pki.transport. RFC 9811 transfers every CMP exchange identically, as one HTTP POST of a DER PKIMessage and one response PKIMessage, so a single stateless verb carries ir / cr / kur / p10cr / certConf / pollReq / rr / genm and their responses; the caller builds and protects the message upstream with pki.cmp.build and hands the finished bytes here. message is a DER Buffer/Uint8Array or a PEM CMP string, sent verbatim (the message-layer protection covers these exact bytes; they are never re-encoded). The response is classified fail-closed: HTTP 200 carrying an application/pkixcmp body is parsed and resolved; another 2xx is cmp/unexpected-status (RFC 9811 requires 200); a 3xx is cmp/redirect-not-followed (never auto-followed, sec. 3.1/5); a 4xx/5xx carrying a well-formed CMP error PKIMessage FORWARDS that integrity-protected verdict (sec. 1.2/3.1) with the HTTP status surfaced as data, while a 4xx/5xx that is not a CMP error message (no body, an undecodable body, or a non-error arm) is cmp/http-error. Protection is surfaced, not verified: the client confers no trust, and the caller (or a future pki.cmp.verify) checks the response protection using the raw headerBytes/bodyBytes. By default the transport is https-only and requires an explicit trust anchor; there is no client scheme gate, so an operator who injects an http-capable transport reaches the RFC-9811-permitted plain-HTTP path.

Options

- `transport` -- an injectable `transport(request) -> Promise<{status,headers,body}>` (default
  `pki.transport.https`, which fail-closes on a non-https URL and an unpinned server).
- `tls` -- `{ anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }` for the
  default transport (mutual-TLS `cert`/`key` is common for CMP in addition to message protection).
- `headers` -- extra request headers. The `content-type: application/pkixcmp` is always set and not
  overridable (any casing), and the request-framing headers (`content-length`, `transfer-encoding`) are
  stripped -- the transport computes Content-Length from the exact body, so a caller cannot desync framing.
- `timeout` -- ms (default 30s); `maxResponseBytes` -- streaming cap, tightenable downward only.
- `proxy` -- reach the CA through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).

Example

async function example() {
  var reqDer = await pki.cmp.build(
    { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
      body: { p10cr: csrDer } },
    { key: signerKeyPkcs8, cert: signerCertDer });
  var url = pki.cmp.wellKnownUrl("https://ca.example", { operation: "p10cr" });
  var res = await pki.cmp.transfer(url, reqDer, { transport: transport });
  res.response.body.arm;   // "ip" | "cp" | "error"
}
example();

References

pki.cmp.wellKnownUrl

since 0.3.19 stable
pki.cmp.wellKnownUrl(base, opts?) -> string

Build an RFC 9811 sec. 3.4 CMP request-URI under the /.well-known/cmp prefix (RFC 8615). The four forms: <base>/.well-known/cmp, .../<operation>, .../p/<label>, and .../p/<label>/<operation>. A pure string builder over an authority-rooted well-known path (RFC 8615): https vs http is left to the transport (both forms are accepted, RFC 9811 sec. 3.4), but the base MUST be an http(s) URL. It rejects (cmp/bad-url) an unparseable base, a non-http(s) scheme (file:/data:/a custom scheme has an opaque origin), a base carrying a path / query / fragment (a well-known URI is authority-rooted; a path would capture it), and a label/operation that is empty, a dot-segment, or contains a / or other reserved char (never silently retarget the resource). The operation label is a caller-supplied string, not validated against a profile vocabulary (RFC 9483 is out of scope).

Options

- `label` -- an optional CA/RA name inserted as `.../p/<label>` (a single percent-encoded path segment).
- `operation` -- an optional operation label appended as the final path segment.

Example

pki.cmp.wellKnownUrl("https://ca.example");                          // "https://ca.example/.well-known/cmp"
pki.cmp.wellKnownUrl("https://ca.example", { label: "myca", operation: "cr" });
//                                                                   // ".../.well-known/cmp/p/myca/cr"

References

pki.schema.cmp.parse

since 0.1.19 stable
pki.schema.cmp.parse(input, caps?) -> pkiMessage

Parse a DER Buffer or a PEM string into a structured PKIMessage: { header, headerBytes, body, bodyBytes, protection, extraCerts }. header carries pvno (1..3), sender / recipient (validated GeneralNames, the anonymous NULL-DN form included), and the optional messageTime, protectionAlg, senderKID / recipKID, transactionID, senderNonce / recipNonce, freeText, and generalInfo (recognized id-it types value-checked: implicitConfirm is NULL, confirmWaitTime a GeneralizedTime, certProfile a sequence of UTF8Strings). body is { arm, tag, bytes, decoded? }: request arms (ir / cr / kur / krr / ccr) decode via the CRMF parser; ip / cp / kup / ccp decode to { caPubs, response } (an encrypted certificate's EnvelopedData decodes via the CMS parser; the deprecated EncryptedValue arm and caPubs surface raw; the parser confers no trust); krp decodes to { status, newSigCert, caCerts, keyPairHist }; rr / rp, genm / genp, error, certConf (an empty confirmation is the legal reject-all), and pollReq / pollRep decode structurally; pkiconf decodes to null; every other defined arm surfaces raw with decoded absent: p10cr (feed body.bytes to pki.schema.csr.parse), the challenge-response and announcement arms, and nested (never auto-recursed). certReqId values are BigInt and accept the protocol's -1 sentinel.

Protection is surfaced, not verified: protection carries the raw, octet-aligned BIT STRING (unused bits are rejected at parse, since every RFC 9481 protection algorithm emits whole octets), and the MAC or signature is computed over the DER of the virtual ProtectedPart ::= SEQUENCE { header, body }, which you reconstruct as a DER SEQUENCE wrapping exactly headerBytes || bodyBytes. extraCerts are raw DER certificates and are not covered by protection.

Throws CmpError when the bytes are not a well-formed PKIMessage, and Asn1Error when the underlying DER is malformed. The request-arm interiors (ir / cr / kur / krr / ccr, and an rr entry's certDetails) are walked by the CRMF parser, so a malformed interior throws CrmfError with a crmf/* code; all are PkiError subclasses.

Options

- `maxBytes` / `maxDepth` / `maxItems` (number) -- decode caps for this parse. Each
  defaults to the matching `pki.C.LIMITS` figure and may only be set lower; a value
  above it, or an option outside this set, is refused. A parse that exceeds one is
  refused with the `/too-large` code of this format.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var key = await pki.key.export(pair.privateKey);
  var cert = await pki.x509.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
  var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) }, { key: key });
  var der = await pki.cmp.build(
    { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
      body: { p10cr: csrDer } }, { key: key, cert: cert });
  var m = pki.schema.cmp.parse(der);
  m.body.arm;             // "ir", "ip", "error", ...
  m.header.transactionID; // raw Buffer or null
}
example();

References

pki.schema.cmp.pemDecode

since 0.1.19 stable
pki.schema.cmp.pemDecode(text, label?) -> Buffer

Extract the DER bytes from a PEM block (default label CMP). CMP is wire-DER over HTTP (RFC 9811).

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var key = await pki.key.export(pair.privateKey);
  var cert = await pki.x509.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
  var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) }, { key: key });
  var msg = await pki.cmp.build(
    { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
      body: { p10cr: csrDer } }, { key: key, cert: cert });
  var pemText = pki.schema.cmp.pemEncode(msg);
  var der = pki.schema.cmp.pemDecode(pemText);
}
example();

References

pki.schema.cmp.pemEncode

since 0.1.19 stable
pki.schema.cmp.pemEncode(der, label?) -> string

Wrap DER bytes in a PEM envelope (default label CMP).

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var key = await pki.key.export(pair.privateKey);
  var cert = await pki.x509.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
  var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) }, { key: key });
  var der = await pki.cmp.build(
    { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
      body: { p10cr: csrDer } }, { key: key, cert: cert });
  var pem = pki.schema.cmp.pemEncode(der);
}
example();

References