CMC

Interpret an RFC 5272 Full PKI Response. 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

since 0.4.16 experimental
pki.cmc.verify(response, sent?) -> Promise<verdict>

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

since 0.4.16 experimental
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.

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