Simple Certificate Enrolment Protocol (RFC 8894)
pkcsPKIEnvelope) that encrypts the messageData to the recipient, and an outer SignedData that signs it under a set of authenticated transaction attributes -- messageType, transactionID, pkiStatus, failInfo, and the sender/recipient nonces (RFC 8894 sec. 3). pki.scep.build assembles a request pkiMessage (PKCSReq / RenewalReq); pki.scep.parse verifies the outer signature, reads the transaction attributes BOUND to the verified signer, and (given the recipient key) decrypts the pkcsPKIEnvelope to the messageData.
The transaction attributes are surfaced only from a signature that verified: a pkiMessage whose outer signature does not check is refused, never returned with a false verdict. The pkcsPKIEnvelope is built with AES-128-CBC. This is the message layer, transport-agnostic; a live SCEP CA is driven over pki.transport separately.
pki.scep.build
pki.scep.build(spec) -> Promise<Buffer>
Assemble a SCEP pkiMessage (RFC 8894 sec. 3). A request (PKCSReq / RenewalReq) encrypts the supplied PKCS#10 messageData to the recipient CA certificate as the inner EnvelopedData (pkcsPKIEnvelope, AES-128-CBC), then signs it under the transaction attributes with the caller's signer certificate and key. transactionId is a caller-unique PrintableString; senderNonce is a 16-byte value (a fresh random one is generated when omitted). The recipient CA certificate must assert the keyEncipherment key usage (the EnvelopedData uses RSA key transport). The content key is transported under RSAES-OAEP (SHA-256), the toolkit's key-transport algorithm; the legacy RSAES-PKCS1-v1_5 that older SCEP servers expect is never emitted, so the CA must support OAEP.
For a PKCSReq the caller's signer SHOULD be a self-signed certificate using the same subject name and key as the PKCS#10 request (RFC 8894 sec. 2.3); a RenewalReq is signed with the existing CA-issued certificate instead, which is why this is the caller's choice rather than an enforced match. The proof-of-possession over the PKCS#10 itself is verified regardless.
Options
- `messageType` -- "PKCSReq" | "RenewalReq" (this release's request set).
- `messageData` -- the PKCS#10 CertificationRequest DER to enrol.
- `recipient` -- the CA (or RA) certificate DER the messageData is encrypted to (an RSA key-transport certificate; a non-RSA recipient is refused, since the envelope uses RSAES-OAEP).
- `signer` -- `{ cert, key }`, the outer SignedData signer (the client for a request).
- `transactionId` -- a caller-unique PrintableString identifying the transaction.
- `senderNonce` -- a 16-byte Buffer (generated when omitted).
Example
async function example() {
// requires: csrDer -- the PKCS#10 CertificationRequest DER to enrol
// requires: caCertDer -- the SCEP CA (or RA) certificate the request is encrypted to (keyEncipherment)
// requires: clientCertDer -- the client's own certificate, the outer signer
// requires: clientKeyPkcs8 -- the client's private key matching clientCertDer
var msg = await pki.scep.build({ messageType: "PKCSReq", messageData: csrDer,
recipient: caCertDer, signer: { cert: clientCertDer, key: clientKeyPkcs8 },
transactionId: "txn-0001" });
}
example();
References
- spec RFC 8894
pki.scep.parse
pki.scep.parse(bytes, opts?) -> Promise<verdict>
Disassemble a SCEP pkiMessage (RFC 8894 sec. 3): verify the outer SignedData signature, read the transaction attributes BOUND to the verified signer (never from a separate untrusted parse), and, given the recipient key, decrypt the pkcsPKIEnvelope to recover the messageData. It reads the enrollment message types PKCSReq, RenewalReq, and CertRep; the CertPoll, GetCert, and GetCRL client queries a CA processes are refused (scep/unsupported-message-type). The verdict is { signatureValid, signerAuthenticated, signerCert, messageType, transactionId, senderNonce, recipientNonce, pkiStatus, failInfo, failInfoText, messageData, certificates, crls } (fields null when the message does not carry them). For a decrypted SUCCESS CertRep, certificates and crls hold the issued certificate(s) and any CRL validated out of the certs-only messageData, raw for the caller to path-validate; a GetCRL response carries the CRL and no certificate (RFC 8894 sec. 3.3.4). A malformed message, a missing mandatory attribute, an unknown enumerant, or a nonce mismatch is a typed ScepError.
signatureValid proves only that the message is self-consistent with the certificate it embeds; it does NOT authenticate the signer. A SCEP client MUST authenticate a CA response against the CA certificate it holds: pass opts.signerCert and this refuses a signer whose public key does not match (scep/untrusted-signer) and reports signerAuthenticated: true. A caller that omits it gets a crypto-only verdict (signerAuthenticated: false) and MUST authenticate the surfaced signerCert itself before acting on the transaction state.
Options
- `recipientKey` -- `{ cert, key }` for the recipient, to decrypt the pkcsPKIEnvelope into `messageData`.
- `signerCert` -- the expected signer certificate DER (the CA certificate for a CertRep); the message signer's public key must match it, or the message is refused.
- `expectedSenderNonce` -- a 16-byte Buffer; a message whose `recipientNonce` does not echo it is refused.
Example
async function example() {
// requires: pkiMessage -- a SCEP pkiMessage DER (from pki.scep.build, or a CA's response)
// requires: caCertDer -- the recipient certificate the pkcsPKIEnvelope was encrypted to
// requires: caKeyPkcs8 -- the private key matching caCertDer, to decrypt the messageData
var v = await pki.scep.parse(pkiMessage, { recipientKey: { cert: caCertDer, key: caKeyPkcs8 } });
v.messageType; // "PKCSReq"
v.messageData; // the recovered PKCS#10 DER
}
example();
References
- spec RFC 8894