Certificate management protocol messages

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, or a certConf / pollReq / genm / rr), and an optional PKIProtection computed over the message. Protection is a signature under the sender key (any registry algorithm) or a PBMAC1 shared-secret MAC. The emitted message round-trips through pki.schema.cmp.parse and its protection verifies. Parsing lives at pki.schema.cmp.parse.

pki.cmp.build

since 0.3.5 experimental
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 } (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 + response of CertResponse each with a PKIStatusInfo and, under a granting status, a certifiedKeyPair), { rp } (RevRepContent), { genp }, { error } (ErrorMsgContent), { pollRep }, { krp } (KeyRecRepContent), { pkiconf } (NULL). The message is protected: opts carries EXACTLY ONE of { key, cert } (a signature under the sender key over the message -- 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. 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.
- `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 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 experimental
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 -- one HTTP POST of a DER PKIMessage, 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; 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.

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 experimental
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