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, CertPoll, GetCert, or GetCRL) or a CA-side CertRep response; 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.
Every verb that reaches a CA accepts the same transport bounds, listed on each of them. One of those bounds is a switch: allowCrossOriginRedirect opts in to following a cross-origin redirect on an unsafe method. It is a boolean, and a value other than true, false, null or an absent option is refused scep/bad-input before the first request goes out.
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. A CertPoll (GetCertInitial) query wraps an IssuerAndSubject that names the issuing CA and the request subject; a GetCert or GetCRL query wraps an IssuerAndSerialNumber that names a certificate by its issuer and serial. Every form is enveloped and signed the same way. 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.
A CertRep response is the CA (responder) side: it carries the transaction pkiStatus, echoes the request's senderNonce as recipientNonce, and is signed under the CA's signer. A SUCCESS response envelopes a certs-only SignedData of the issued certificates (or the crls of a GetCRL response) to the requester's recipient certificate; a FAILURE (which carries a failInfo) or a PENDING response omits the pkcsPKIEnvelope and is signed detached (RFC 8894 sec. 3.3.2).
Options
- `messageType` -- "PKCSReq" | "RenewalReq" | "CertPoll" | "GetCert" | "GetCRL" | "CertRep".
- `messageData` -- the PKCS#10 CertificationRequest DER, for a PKCSReq or RenewalReq.
- `requestSubject` -- the request subject Name DER, for a CertPoll.
- `certificate` -- for a GetCert or GetCRL, the certificate DER whose issuer and serial name the target (the convenience form).
- `issuer` / `serialNumber` -- for a GetCert or GetCRL, the issuer Name DER and a positive serial (a BigInt, number, or hex string) naming the target directly. For a CertPoll, `issuer` alone names the issuing CA (defaulting to the recipient's subject) when the request is encrypted to a separate RA.
- `recipient` -- the certificate DER the pkcsPKIEnvelope is encrypted to (an RSA key-transport certificate; a non-RSA recipient is refused, since the envelope uses RSAES-OAEP): the CA or RA certificate for a request, the requester's certificate for a SUCCESS CertRep.
- `signer` -- `{ cert, key }`, the outer SignedData signer (the client for a request, the CA for a CertRep).
- `transactionId` -- a caller-unique PrintableString identifying the transaction (echoes the request's on a CertRep).
- `senderNonce` -- a 16-byte Buffer (generated when omitted).
- `pkiStatus` -- for a CertRep, "SUCCESS" | "FAILURE" | "PENDING".
- `recipientNonce` -- for a CertRep, the 16-byte value echoing the request's senderNonce (required).
- `certificates` / `crls` -- for a SUCCESS CertRep, arrays of the issued certificate DER (or CRL DER) the certs-only SignedData carries.
- `failInfo` / `failInfoText` -- for a FAILURE CertRep, the failInfo (badAlg / badMessageCheck / badRequest / badTime / badCertId) and an optional detail string.
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 message types PKCSReq, RenewalReq, CertRep, CertPoll, GetCert, and GetCRL; a messageType the RFC 8894 registry does not define is refused (scep/bad-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. It also decides whether an envelope using RSAES-PKCS1-v1_5 key transport over a content with no integrity tag is read: that combination is refused for a signer this option has not authenticated.
- `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
pki.scep.parseCapabilities
pki.scep.parseCapabilities(text) -> { AES?, "SHA-256"?, POSTPKIOperation?, ... }
Parse a GetCACaps response body (RFC 8894 sec. 3.5.2): one capability keyword per line, CR/LF or LF delimited. The returned object has a true property for each recognized keyword (AES, DES3, GetNextCACert, POSTPKIOperation, Renewal, SHA-1, SHA-256, SHA-512, SCEPStandard); comparison is case-insensitive and an unknown keyword is ignored (sec. 3.5.2). SCEPStandard implies AES, POSTPKIOperation, and SHA-256 (the sec. 2.9 mandatory profile), which are set on the result. An empty body yields an empty set ("none supported", sec. 3.5.1).
Example
var caps = pki.scep.parseCapabilities("AES\r\nSHA-256\r\nPOSTPKIOperation\r\n");
caps.POSTPKIOperation === true;
References
- spec RFC 8894
pki.scep.getCACaps
pki.scep.getCACaps(baseUrl, opts?) -> Promise<capabilities>
Query a SCEP CA's advertised capabilities (RFC 8894 sec. 3.5.1): a GET ?operation=GetCACaps whose text/plain body is parsed by parseCapabilities. The GetCACaps response is UNAUTHENTICATED (sec. 7.5), so a client MUST NOT downgrade its algorithm choice based on it; this verb only reports what the CA claims. Pass expectSCEPStandard: true (or requireStrongProfile: true) to fail closed when the strong profile is absent, treating the absence as the downgrade signal it is.
Options
- `transport` -- an injected transport (defaults to `pki.transport.https`); the request is a GET with no body.
- `expectSCEPStandard` -- refuse (`scep/weak-profile`) if the response does not advertise `SCEPStandard`. Takes `true`, `false`, `null` or an absent option; any other value is refused `scep/bad-input`.
- `requireStrongProfile` -- refuse unless AES + SHA-256 + POSTPKIOperation are all advertised. Read the same way.
- `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: scepTransport -- an injected transport (omit to use pki.transport.https)
var caps = await pki.scep.getCACaps("http://ca.example/scep", { transport: scepTransport });
caps.AES === true;
}
example();
References
- spec RFC 8894
pki.scep.getCACert
pki.scep.getCACert(baseUrl, opts?) -> Promise<{ caCertificate, certificates }>
Retrieve a SCEP CA's certificate (RFC 8894 sec. 4.2): a GET ?operation=GetCACert returning either a single DER certificate (application/x-x509-ca-cert) or a certs-only CMS chain (application/x-x509-ca-ra-cert). GetCACert is served in the clear; the ONLY authentication is an out-of-band fingerprint (RFC 8894 sec. 2.2). Pass expectedFingerprint (a hex string or Buffer, with fingerprintAlgorithm defaulting to SHA-256) and this refuses (scep/fingerprint-mismatch) unless a returned certificate matches it, returning that certificate as caCertificate. A single-certificate response is unambiguously the CA, so caCertificate holds it. A ca-ra chain lists the CA, any intermediates, and RA certificate(s) in a CMS order that does not identify the CA role, so without a fingerprint caCertificate is null and the caller reads certificates to identify and authenticate the CA itself.
Options
- `expectedFingerprint` -- a hex string or Buffer; a returned certificate must hash to it, or the response is refused.
- `fingerprintAlgorithm` -- the hash for the fingerprint (default "sha256").
- `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: scepCaCertTransport -- an injected transport returning a single CA certificate
var ca = await pki.scep.getCACert("http://ca.example/scep", { transport: scepCaCertTransport });
ca.caCertificate; // the CA certificate DER (a single-certificate response identifies it)
}
example();
References
- spec RFC 8894
pki.scep.getNextCACert
pki.scep.getNextCACert(baseUrl, opts) -> Promise<{ certificates }>
Retrieve a SCEP CA's next (rollover) certificate (RFC 8894 sec. 4.7): a GET ?operation=GetNextCACert returning a SignedData signed by the CURRENT CA signing key (application/x-x509-next-ca-cert), whose content is a degenerate certs-only SignedData carrying the CA certificate(s) to install when the current one expires. The current CA certificate the caller already holds authenticates the response: the outer signature is verified AND pinned to that certificate's key, so a self-consistent CMS signed by any other (embedded) certificate is refused (scep/untrusted-signer) rather than trusted as the rollover certificate. caCertificate is required; an unauthenticated rollover certificate becomes a future trust anchor, so it is refused rather than returned. The verified next certificate(s) are returned in certificates.
Options
- `caCertificate` -- the current SCEP CA certificate DER; the response is authenticated against its key (required).
- `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: caCertDer -- the current CA certificate (from getCACert), scepTransport
var next = await pki.scep.getNextCACert("http://ca.example/scep", { caCertificate: caCertDer, transport: scepTransport });
next.certificates[0]; // the next CA certificate DER, to install when the current one expires
}
example();
References
- spec RFC 8894
pki.scep.enroll
pki.scep.enroll(baseUrl, opts) -> Promise<{ status, certificate?, certificates?, transactionId }>
Enrol for a certificate over SCEP (RFC 8894 sec. 4.3): build a PKCSReq wrapping the caller's PKCS#10 in a pkcsPKIEnvelope encrypted to the CA certificate, HTTP POST it to ?operation=PKIOperation, and read the CertRep. The CA response is authenticated against the CA certificate and its recipientNonce must echo the request's fresh senderNonce. On SUCCESS the issued certificate is selected out of the response by SubjectPublicKeyInfo byte-match against the request; a FAILURE throws scep/enrollment-failed carrying the CA's failInfo. A CA that answers PENDING (manual enrollment, RFC 8894 sec. 2.5) is polled automatically: the client issues CertPoll (GetCertInitial) messages reusing the transactionID, each authenticated against the CA certificate with a fresh nonce it must echo, until the CA answers SUCCESS or FAILURE or a pollCount / maxTotalWait budget is exhausted (scep/poll-exhausted, carrying the transactionId to resume). Pass pollCount: 0 to disable polling and get the single { status: "PENDING", transactionId } return for a caller that drives the retry itself. The content key is transported under RSAES-OAEP, so the CA certificate must be an RSA keyEncipherment certificate that accepts OAEP; the shared-secret authenticator (RFC 8894 sec. 2.4) is the challengePassword the caller placed in the PKCS#10.
Options
- `csr` -- the PKCS#10 CertificationRequest DER to enrol (place a `challengePassword` in it for shared-secret auth).
- `caCert` -- the SCEP CA certificate DER the request is encrypted to.
- `signer` -- `{ cert, key }`, the outer SignedData signer (for a PKCSReq, a self-signed certificate over the request key).
- `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to, to decrypt the CertRep.
- `responderCert` -- the certificate to authenticate the CA's response signature against (defaults to `caCert`).
- `issuerCert` -- the issuing CA certificate whose subject names the CertPoll issuer (defaults to `caCert`; set it when the request is encrypted to a separate RA certificate, RFC 8894 sec. 3.3.3).
- `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded, unique per call).
- `pollCount` -- the maximum number of CertPoll requests on a PENDING response (default 5; `0` disables polling).
- `maxTotalWait` -- the total Retry-After sleep budget in seconds across all polls (default the Retry-After horizon).
- `sleep` -- an injectable sleeper `(ms) -> Promise` for the Retry-After wait (default a real timer).
- `onRetryAfter` -- an optional observer `(delaySeconds) -> void` called before each poll sleep.
- `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
- `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
var out = await pki.scep.enroll("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
signer: { cert: clientCertDer, key: clientKeyPkcs8 },
recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
out.status; // "SUCCESS" (a PENDING request is polled to a terminal status unless pollCount is 0)
}
example();
References
- spec RFC 8894
pki.scep.renew
pki.scep.renew(baseUrl, opts) -> Promise<{ status, certificate?, certificates?, transactionId }>
Renew a certificate over SCEP (RFC 8894 sec. 4.3): identical to enroll but building a RenewalReq, whose outer SignedData is signed with the caller's existing CA-issued certificate rather than a self-signed one. Because the request is signed with the existing key and not the new PKCS#10 key, a RenewalReq does not itself prove possession of the new key (RFC 8894 sec. 7.6); the proof-of-possession over the PKCS#10 is still verified. Options are the same as enroll, with signer being the existing certificate and key.
Example
async function example() {
// requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
var out = await pki.scep.renew("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
signer: { cert: clientCertDer, key: clientKeyPkcs8 },
recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
}
example();
References
- spec RFC 8894
pki.scep.getCert
pki.scep.getCert(baseUrl, opts) -> Promise<{ certificate, certificates, transactionId }>
Retrieve an already-issued certificate over SCEP (RFC 8894 sec. 3.3.4): POST a GetCert query naming the certificate's issuer and serial number, and select it out of the CA's certs-only CertRep by canonical issuer-name and serial match. This is an OPTIONAL fallback: RFC 8894 sec. 3.3.4 advises a client to prefer an HTTP certificate store (RFC 4387) or LDAP and to use this only against a CA that offers no such access. The response is authenticated against the CA certificate and its recipientNonce must echo the request's fresh senderNonce; a FAILURE throws scep/query-failed, a PENDING is refused (scep/unexpected-pending, a query grants or rejects), and a SUCCESS not carrying the requested certificate throws scep/cert-not-found. certificate is the one matched to the requested issuer and serial; certificates is every certificate the authenticated response carried, raw, so a caller relies on certificate for the queried identity.
Options
- `certificate` -- the certificate DER whose issuer and serial name the one to retrieve (the convenience form).
- `issuer` / `serialNumber` -- the issuer Name DER and serial (a BigInt, number, or hex string) naming the certificate, for a caller that did not retain it.
- `caCert` -- the SCEP CA certificate DER the query is encrypted to.
- `signer` -- `{ cert, key }`, the outer SignedData signer.
- `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to, to decrypt the CertRep.
- `responderCert` -- the certificate to authenticate the CA response signature against (defaults to `caCert`).
- `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded).
- `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
- `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: clientCertDer, caCertDer, clientKeyPkcs8, scepTransport
var out = await pki.scep.getCert("http://ca.example/scep", { certificate: clientCertDer, caCert: caCertDer,
signer: { cert: clientCertDer, key: clientKeyPkcs8 },
recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
out.certificate; // the retrieved certificate DER
}
example();
References
- spec RFC 8894
pki.scep.getCrl
pki.scep.getCrl(baseUrl, opts) -> Promise<{ crl, crls, transactionId }>
Retrieve a CRL over SCEP (RFC 8894 sec. 3.3.4): POST a GetCRL query naming the CA by the issuer and serial of a certificate it issued, and read the CRL out of the CA's CRL-only CertRep. This is an OPTIONAL fallback: RFC 8894 sec. 2.7 says a client should compose a GetCRL only if the CA supports neither a CRL distribution point nor HTTP access, and prefer those. The response is authenticated against the CA certificate and its recipientNonce must echo the request's fresh senderNonce; a FAILURE throws scep/query-failed, and a PENDING is refused. The returned CRL is bound to the query: crl is the one whose issuer matches the requested CA, so a response carrying no such CRL throws scep/no-crl and one carrying more than one throws scep/ambiguous-crl. The full response list is on crls, raw for the caller to validate with pki.crl.verify.
Options
- `certificate` -- a certificate DER the CA issued, whose issuer and serial name the CA to query (the convenience form).
- `issuer` / `serialNumber` -- the issuer Name DER and serial (a BigInt, number, or hex string) naming the CA, when the caller did not retain such a certificate.
- `caCert` -- the SCEP CA certificate DER the query is encrypted to.
- `signer` -- `{ cert, key }`, the outer SignedData signer.
- `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to.
- `responderCert` -- the certificate to authenticate the CA response against (defaults to `caCert`).
- `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded).
- `httpMethod` -- `"POST"` (default) or `"GET"`. RFC 8894 sec. 4.1 sends the PKIOperation message over POST; `"GET"` carries it base64-encoded in the URL `message` query for a legacy CA that lacks POST.
- `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` / `proxy` -- transport bounds (`proxy` reaches the CA through a forward HTTP proxy; see pki.transport).
Example
async function example() {
// requires: clientCertDer, caCertDer, clientKeyPkcs8, scepTransport
var out = await pki.scep.getCrl("http://ca.example/scep", { certificate: clientCertDer, caCert: caCertDer,
signer: { cert: clientCertDer, key: clientKeyPkcs8 },
recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
out.crl; // the retrieved CRL DER
}
example();
References
- spec RFC 8894