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, ...); 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 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