CRMF (Certificate Request Message Format, RFC 4211)

The RFC 4211 certificate-request-message producing side. pki.crmf.build assembles a CertReqMessages: one or more CertReqMsg, each a CertRequest (a CertTemplate of the requested certificate fields plus optional controls) paired with a proof of possession. The common proof is a POPOSigningKey signature over the CertRequest, made with the private half of the key being certified (the requester proves possession, exactly as a PKCS#10 CSR does). The message drops into a CMP (RFC 9810) or EST enrollment body. Parsing lives at pki.schema.crmf.parse.

pki.crmf.build

since 0.3.3 stable
pki.crmf.build(spec, key?, opts?) -> Promise<Buffer|string>

Build and DER-encode an RFC 4211 CertReqMessages. spec describes one certificate request message (or pass spec.messages, an array of specs, for a batch): certReqId (an integer, default 0; the RFC 9483 -1 sentinel is allowed), certTemplate (the requested certificate fields: subject, publicKey (the SPKI DER of the key being certified), validity ({ notBefore, notAfter } Dates), extensions (an object of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies / subjectKeyIdentifier, or pre-encoded Extension DER), and an optional version (2)), optional controls and regInfo (an object of regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded AttributeTypeAndValue DER), and an optional pop selector. key (or { key }) is the REQUESTER's private key, the private half of certTemplate.publicKey; the message carries a POPOSigningKey proof of possession signed with it (verified before the message is returned), exactly as a PKCS#10 CSR proves possession. The signature algorithm is resolved from the requested public key (RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). key is optional: omit it for a raVerified proof (opt in with pop: { type: 'raVerified', raVerified: true }). Returns DER, or a PEM block with opts.pem (the label is required). Malformed input throws a typed CrmfError. Certificate-request-message parsing is pki.schema.crmf.parse.

A key that cannot sign proves possession another way (RFC 4211 sec. 4.2, 4.3), which is the case an ML-KEM enrollment is in. Set pop.type to keyEncipherment or keyAgreement and pick a pop.method:

- subsequentMessage with pop.subsequentMessage of encrCert or challengeResp. The proof completes over the enrolling protocol's next exchange (RFC 9810 sec. 5.2.8.3.2 / .3), so this message only declares which one. No key material leaves the requester, and every key type can produce it. - encryptedKey, which sends the requester's private key to the CA inside a CMS EnvelopedData whose content type is id-ct-encKeyWithID. RFC 9810 sec. 5.2.8.3.1 permits it only where archival is intended, so it takes pop.archive: true; pop.privateKey is the PKCS#8 DER, pop.recipients the CMS recipients to envelope it to, and pop.identifier (a string or GeneralName) is required, because sec. 4.2.1 makes it mandatory whenever the purpose is proving possession: without it a decrypting agent cannot tell whose key it holds. pop.contentEncryptionAlgorithm defaults to aes-256-cbc; an AEAD algorithm is refused, since it would produce an AuthEnvelopedData this field cannot carry. - agreeMAC, which agrees a secret with the authority and MACs the request under a key derived from it, so nothing is signed (RFC 4211 sec. 4.3, RFC 2875 sec. 3). pop.key is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM PRIVATE KEY block, or a KeyObject, and pop.caCert is the authority's certificate the requester already holds, as DER or a PEM CERTIFICATE block. Either key may name its group in the PKCS#3 or the X9.42 encoding. pop.key must be the private half of certTemplate.publicKey, which the authority derives its own side of the agreement from, and the template must name both a subject and a public key, which the MAC covers. An elliptic-curve or montgomery pair is refused, as is a key on parameters other than the authority's.

The two alternatives the specification deprecates in the same breath as defining them, thisMessage and dhMAC, are refused with their successors named; pki.schema.crmf.parse still reads both, since a peer may send one. Building an encryptedKey or agreeMAC proof inside pki.cmp.build raises the announced protocol version to cmp2021(3), which RFC 9810 sec. 5.2.8.3 requires.

Options

- `pem` (string) -- return a PEM block with this label instead of DER (e.g. "CERTIFICATE REQUEST MESSAGE").
- `pss` (boolean) -- sign an RSA key with RSASSA-PSS instead of PKCS#1 v1.5.
- `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var signerSpki = await pki.key.export(pair.publicKey);
  var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
  var msg = await pki.crmf.build(
    { certReqId: 0, certTemplate: { subject: "device-42", publicKey: signerSpki } },
    { key: signerKeyPkcs8 });
  pki.schema.crmf.parse(msg).messages[0].certReq.certTemplate.subject.dn;   // "CN=device-42"
}
example();

References

  • spec RFC 4211
  • defends forged-certificate-request (CWE-347)

pki.crmf.buildCertTemplate

since 0.3.5 stable
pki.crmf.buildCertTemplate(template) -> Buffer

Encode a bare RFC 4211 CertTemplate (the requested-certificate fields: subject, publicKey, validity, requested extensions, an optional version 2, issuer) to canonical DER. This is the certTemplate interior of pki.crmf.build, exposed for the RFC 9810 CMP rr (revocation request) body, whose RevDetails.certDetails carries a CertTemplate naming the certificate to revoke. Returns the DER Buffer; a malformed template throws a typed CrmfError.

Example

var tpl = pki.crmf.buildCertTemplate({ serialNumber: 42n, issuer: "CN=CA" });
pki.asn1.decode(tpl).tagNumber === pki.asn1.TAGS.SEQUENCE;   // the CertTemplate SEQUENCE

References

pki.crmf.verifyPop

since 0.5.14 stable
pki.crmf.verifyPop(messages) -> Promise<{ valid, verified, messages: [{ valid, verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }>

Verify the proof of possession on each CertReqMsg in a CertReqMessages. messages is a DER Buffer or a parsed result. A CA or RA that issues without this certifies a key the requester may not hold. One verdict is returned per message, in order, and the top-level verified is true only when every message carried a proof that verified.

For the signature proof the covered bytes are the ones RFC 4211 names: the DER of poposkInput when that field is present, and the DER of certReq when it is absent. Two conformance rules ride with it, both refusals rather than warnings, because each lets a certificate be issued for something nobody signed: poposkInput MUST be omitted exactly when the template carries both subject and public key (its preimage covers the key and the sender, never the subject), and poposkInput.publicKey MUST be exactly the template's public key (sec. 4.1). Verification composes the one path-validation signature engine, with the same algorithm-confusion (RFC 9814 sec. 4) and EdDSA low-order-point gates.

The other proofs are reported, never guessed. raVerified is an RA's out-of-band assertion, so it returns verified: false with method: "raVerified" and a caller who trusts that RA opts in by reading method. keyEncipherment and keyAgreement complete over a later protocol exchange, or need the CA's decryption key, so they return verified: false naming the arm.

Each verdict carries what the verified preimage covers, so a CA issues from what was checked. publicKey is the key possession was proven for. subject is the requested name when the preimage was the certReq, which covers the whole template; when the preimage was poposkInput it covers the key and the sender alone, so any subject in the message is unsigned and is withheld with subjectBound: false. Bind the name by other means before issuing in that case.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var spki = await pki.key.export(pair.publicKey);
  var pkcs8 = await pki.key.export(pair.privateKey);
  var req = await pki.crmf.build({ certReqId: 1n,
    certTemplate: { subject: "device-42", publicKey: spki } }, { key: pkcs8 });   // a bare string is the commonName VALUE
  var r = await pki.crmf.verifyPop(req);
  r.verified;                    // true
  r.messages[0].method;          // "signature"
}
example();

References