CMC (Certificate Management over CMS, RFC 5272)
verify(response, sent) takes the response a CA returned and the state the client retained from its request, binds the two together (transaction identifier, the Sender/Recipient Nonce echo, the Data Return echo), reads the ordered status verdicts, and reduces them to one terminal outcome: issued, pending, confirm-required, pop-required or rejected. The issued certificates come from the CMS certificate bag, where RFC 5272 sec. 4.2 puts them. Nothing here is trusted: the bag and any Publish Trust Anchors control are surfaced as DATA for the caller to validate through pki.path.validate.pki.cmc.verify
pki.cmc.verify(response, sent?) -> Promise<verdict>
The verdict carries valid, the toolkit-wide alias the other verify verbs use, true only for the issued outcome. Every other outcome, including pending, is valid: false, and which one it was stays in outcome alongside failInfo and pendToken.
Interpret a Full PKI Response into one terminal verdict. response is the DER or a PEM CMS block; an already-parsed message is interpreted too, but only under allowUnverified: true, because both carriers authenticate over BYTES and a parsed object the caller still owns pins none. sent is what the client retained from its request: transactionId, senderNonce, dataReturn, and bodyPartIDs (every identifier the request carried). Each of those is checked only if it was sent, and once sent an absent or differing echo is a refusal: that asymmetry is the replay defense. bodyPartIDs is the same rule applied to what the response is ABOUT: a status naming a body part the request never sent is refused with cmc/body-part-unknown, which the transaction and nonce cannot catch, since a server can echo both correctly while reporting on something else.
Because each half is conditional, a caller who retains nothing runs none of them. A Full PKI Response captured from any earlier exchange with the same CA would then read as this request's answer, correctly signed. That case is refused as cmc/unbound-response unless the caller names it with allowUnbound: true, a separate opt-out from allowUnverified because it answers a separate question: a replayed response is authentic. bodyPartIDs alone does not satisfy it, since identifiers are allocated per message from a small range (RFC 5272 sec. 3.2.2) and the same set recurs across requests. The verdict reports which halves ran as bound.transactionId, bound.senderNonce, bound.dataReturn and bound.bodyPartIDs, with boundToRequest for the roll-up.
The verdict carries the response's own cmsSequence and otherMsgs raw, because a request whose only arm was the other-message form has no certificate to return and RFC 5272 sec. 4.1 puts its answer there instead.
The verdict's outcome is issued, pending, confirm-required, pop-required or rejected. When a response carries several status controls the WORST governs, so a failure cannot be hidden behind a success earlier in the sequence.
The carrier's signature MUST be verified (RFC 5272 sec. 3.2.1.3.4). A conforming SignedData carries its own signer certificate, so the ordinary case needs nothing from the caller: the signature is checked and the verdict reports signatureVerified: true. Where the signer cannot be found (not embedded and not supplied), the posture is fail-closed with a NAMED opt-out rather than a silent one: pass certs with the responder's certificate, or allowUnverified: true and the verdict reports signatureVerified: false. Doing neither is refused, because that is the case where a caller would otherwise assume a check happened. The opt-out covers "could not check", never "checked and it failed": a signature that is present and wrong is always a refusal. A carrier with no SignerInfo at all is not a Full PKI Response and is refused outright.
certs SUPPLEMENTS the certificates the message carries; it does not pin the signer (a SignedData names its own, which is what the signature is checked against) and it establishes no trust. Deciding the signer is acceptable is path validation, which is the caller's.
The other carrier sec. 3.2 permits, AuthenticatedData, is authenticated by its MAC instead: pass recipient with the key material and the MAC is checked through pki.cms.decrypt, so a caller who holds the key gets an authenticated verdict instead of the unauthenticated opt-out.
Nothing is trusted here. certificates is the CMS certificate bag, where RFC 5272 sec. 4.2 puts the issued certificates, surfaced raw for the caller to run through pki.path.validate, and a Publish Trust Anchors control is surfaced as publishTrustAnchors with trusted: false, never added to any store (RFC 5272 sec. 6.15 makes accepting one a four-part manual decision). The CMS signature is likewise the caller's to verify through pki.cms.verify.
Options
- `transactionId` (number|bigint) -- the Transaction Identifier the request sent.
- `senderNonce` (Buffer) -- the Sender Nonce the request sent, echoed back as Recipient Nonce.
- `dataReturn` (Buffer) -- the Data Return payload the request sent, echoed verbatim.
- `certs` (Buffer[]) -- extra certificates for signer lookup, for a message that does not carry
its own signer; the certificates the message carries are searched either way.
- `recipient` (object) -- `{ identifier, secret }` for a response MACed under the shared secret
the client enrolled with, which derives the key the way RFC 5272 sec. 3.2(c) states, or key
material for an AuthenticatedData carrier, in the shape
`pki.cms.decrypt` takes. Its MAC is then checked and the verdict reports
`signatureVerified: true`; the content it authenticates is bound to the content the verdict
was read from, so a MAC over other bytes cannot stand in for it.
- `allowUnverified` (boolean) -- interpret without verifying the carrier; sets `signatureVerified: false`.
- `allowUnbound` (boolean) -- interpret a response nothing ties to a request; sets
`boundToRequest: false`. Needed only when none of `transactionId`, `senderNonce` or
`dataReturn` was retained, and what it accepts is a possible replay.
Example
async function example() {
var b = pki.asn1.build, oid = pki.oid;
var sid = b.sequence([b.sequence([b.set([b.sequence([b.oid(oid.byName("commonName")),
b.utf8("CA")])])]), b.integer(1n)]);
var attrs = b.contextConstructed(0, Buffer.concat([
b.sequence([b.oid(oid.byName("contentType")), b.set([b.oid(oid.byName("id-cct-PKIResponse"))])]),
b.sequence([b.oid(oid.byName("messageDigest")), b.set([b.octetString(Buffer.alloc(32, 3))])])]));
var si = b.sequence([b.integer(1n), sid, b.sequence([b.oid(oid.byName("sha256")), b.nullValue()]),
attrs, b.sequence([b.oid(oid.byName("rsaEncryption")), b.nullValue()]), b.octetString(Buffer.alloc(8, 1))]);
var body = b.sequence([b.sequence([]), b.sequence([]), b.sequence([])]);
var encap = b.sequence([b.oid(oid.byName("id-cct-PKIResponse")), b.explicit(0, b.octetString(body))]);
var sd = b.sequence([b.integer(3n), b.set([]), encap, b.set([si])]);
var der = b.sequence([b.oid(oid.byName("signedData")), b.explicit(0, sd)]);
var v = await pki.cmc.verify(der, { allowUnverified: true, allowUnbound: true });
v.outcome; // "issued" -- no status control means success is assumed
v.signatureVerified; // false -- the opt-out was named, so nothing was checked
v.boundToRequest; // false: nothing ties this response to a request either
}
example();
References
pki.cmc.build
pki.cmc.build(spec, signer, opts?) -> Promise<Buffer|string>
Build and sign an RFC 5272 Full PKI Request. spec.requests is the list of certification requests, each naming exactly one arm: tcr (a PKCS#10 CSR), crm (a CRMF CertReqMsg, or the CertReqMessages pki.crmf.build returns when it carries exactly one) or orm ({ type, value }). spec.controls are additional controls as { type, value }, and signer is the { cert, key } that signs the enclosing CMS SignedData. RFC 5272 sec. 3.2 gives a Full PKI Request a second carrier for a client that has no key to sign with: pass { mac: { identifier, secret } } in place of { cert, key } and the request is an AuthenticatedData whose MAC the shared secret keys, through the PasswordRecipientInfo sec. 3.2(a) requires. The key is derived from the identifier encoded as UTF-8 followed by the secret (sec. 3.2(c)), which is the derivation pki.cmc.verify reads a recipient under, so a caller names what it enrolled with at both ends rather than concatenating by hand. The request also carries the identifier in an Identification control, because a PasswordRecipientInfo names no recipient and that control is what "assists the server in locating the shared-secret" (RFC 5272 sec. 6.2.3); supplying a different name through spec.identityProof.identity is refused rather than chosen between. Since carrying that control also changes how an Identity Proof witness is derived (sec. 6.2.3), a request carrying both derives the witness with the name it carries, and its MAC and its Identity Proof are keyed from one secret rather than two. Naming a signature and a shared secret together is refused, since a request has one carrier. A tcr request is verified against its own proof-of-possession first: a PKCS#10 whose self-signature does not verify under its subject public key is refused (cmc/bad-popo) rather than signed into a message a CA would reject.
Body part identifiers are allocated automatically, unique across the whole message and never 0 (RFC 5272 sec. 3.2.2). A caller may pin one, and a clash is refused, never renumbered, because silently moving an identifier would break any control that already referenced it. For a crm arm the identity is the CertReqMsg's own certReqId, read back out of the supplied message.
spec.identityProof: { secret, identity? } attaches an Identity Proof V2 control whose witness is computed over the reqSequence bytes exactly as they are emitted (sec. 6.2.1 step 1: "encoded exactly as it appears in the Full PKI Request including the sequence type and length"). Supplying identity also emits the Identification control naming the shared secret and, per sec. 6.2.3, derives the MAC key from hash(secret || identity) in place of hash(secret): the two travel together because the control's presence is what changes the derivation. And spec.popLink: { secret } attaches a POP Link Witness V2 together with the POP Link Random control that PL1 requires in the same request. spec.renewal: true marks a renewal, which MUST carry neither Identification nor Identity Proof (sec. 3.2 (a)), so asking for both is refused and never silently dropped.
spec.popChallenge: { challenge, recipient } answers an authority's challenge-response proof of possession, which is how a key that cannot sign for itself proves the client holds it (RFC 5272 sec. 6.7). challenge is the Encrypted POP control's value and recipient is the key material pki.cms.decrypt takes. The proof value is decrypted, hashed under the witness algorithm the challenge names and compared with the witness it carries; a value that does not decrypt or does not match is not the one the authority issued, so the request is refused with cmc/pop-failed and no proof is sent. The answer is a MAC over the request the challenge carried, keyed by that value (truncated to 64 bytes, sec. 6.7) under the algorithm the challenge named, and it names the certification request in THIS request, so a request carrying more than one is refused. The algorithms are the ones RFC 5274 sec. 4.2 requires of a client: SHA-1 or SHA-256 for the witness, HMAC-SHA1 or HMAC-SHA256 for the proof.
A tcr request signed with id-alg-noSignature (RFC 5272 App. C.1) is the request a decrypt-only key produces, and it carries no proof of its own. It is accepted only when this message answers a challenge for that same request and the key that opened the challenge is the request's own subject public key, compared as keys rather than as encodings. A challenge opened by a password or a key-encryption key identifies no key and does not qualify, and its parameters must be present and encoded as NULL. Without such an answer the request is refused with cmc/bad-popo, and a request that does carry a signature is still held to it.
spec.transactionId (number|bigint), spec.senderNonce and spec.dataReturn (bytes) attach the exchange-binding controls (RFC 5272 sec. 6.6 / 6.4), the same three pki.cmc.verify checks the response against. They are named fields, not something to hand-encode into spec.controls, because a request that quietly omits them has no replay defense and neither end can tell: the verifier only enforces the halves the client says it sent. An unrecognized spec field is refused for the same reason: a misspelling would otherwise build and sign a message that simply does not carry what was asked for.
Options
- `pem` (boolean) -- return a PEM `CMS` block instead of DER.
- `popLinkRandomBytes` (number) -- the length of R; default 64 (PL1 SHOULD: >= 512 bits).
Example
async function example() {
var pair = await pki.key.generate("Ed25519");
var key = await pki.key.export(pair.privateKey);
var spki = await pki.key.export(pair.publicKey);
var cert = await pki.x509.sign({ subject: "client", subjectPublicKey: spki,
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
var csr = await pki.csr.sign({ subject: "client", subjectPublicKey: spki }, { key: key });
var req = await pki.cmc.build({ requests: [{ tcr: csr }] }, { cert: cert, key: key });
pki.schema.cmc.parse(req).requests[0].arm; // "tcr"
}
example();
References
pki.schema.cmc.parsePkiData
pki.schema.cmc.parsePkiData(der, caps?) -> parsed
Decode a bare PKIData body (the encapsulated content of a Full PKI Request), without the CMS layer. All FOUR sequences are mandatory though each may be empty, so a message that simply omits a trailing empty one is malformed rather than shorthand. reqSequenceBytes is the raw reqSequence TLV, tag and length included, because the Identity Proof V2 witness is computed over exactly those bytes (RFC 5272 sec. 6.2.1).
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
var b = pki.asn1.build;
var der = b.sequence([b.sequence([]), b.sequence([]), b.sequence([]), b.sequence([])]);
var d = pki.schema.cmc.parsePkiData(der);
d.controls.length; // 0 -- present and empty, which is legal
References
- spec RFC 5272 sec. 3.2.1
pki.schema.cmc.parsePkiResponse
pki.schema.cmc.parsePkiResponse(der, caps?) -> parsed
Decode a bare PKIResponse body. A PKIResponse has three sequences (there is no reqSequence) and all three are mandatory though each may be empty. The status controls are collected in wire order; a response may legitimately carry several, and carrying none means success (RFC 5272 sec. 6.1.2).
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
var b = pki.asn1.build;
var der = b.sequence([b.sequence([]), b.sequence([]), b.sequence([])]);
var r = pki.schema.cmc.parsePkiResponse(der);
r.statuses.length; // 0 -- no status control means success is assumed
References
- spec RFC 5272 sec. 4.2.1
pki.schema.cmc.parse
pki.schema.cmc.parse(input, caps?) -> parsed
Decode a CMC Full PKI Request or Full PKI Response from its CMS carrier. input is DER, a PEM CMS block, or an already-parsed pki.schema.cms object. The CMS layer is peeled, the encapsulated content type selects the body (id-cct-PKIData -> a request, id-cct-PKIResponse -> a response), and any other content type is refused as not-CMC, never guessed at.
The parsed CMS is returned on cms so a caller can verify the signature; this decoder never does: reading a message and trusting it are separate steps, and RFC 5272 sec. 3.2.1.3.4 makes the signature check the caller's obligation.
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
var b = pki.asn1.build;
var body = b.sequence([b.sequence([]), b.sequence([]), b.sequence([]), b.sequence([])]);
var encap = b.sequence([b.oid(pki.oid.byName("id-cct-PKIData")), b.explicit(0, b.octetString(body))]);
var sd = b.sequence([b.integer(3n), b.set([]), encap, b.set([])]);
var der = b.sequence([b.oid("1.2.840.113549.1.7.2"), b.explicit(0, sd)]);
pki.schema.cmc.parse(der).kind; // "pkiData"