EST
cacerts, simpleenroll, simplereenroll -- run the thin RFC 7030 client: they compose the codecs below over the shared pki.transport (a caller MAY inject opts.transport; the default is a fail-closed pki.transport.https). This module opens no socket itself: the sole socket choke point is pki.transport, so the verbs stay a thin, fail-closed shell: https-only (est/insecure-url), an explicit trust anchor required (est/no-trust-anchors), same-origin redirects followed but a downgrade / loop refused, a 202 Retry-After SURFACED and never slept, HTTP Basic answered only after the transport authenticated the server, and the issued certificate chosen by public-key match. Under them sit the transport-agnostic codecs, validators, and request builders over the shipped CMS / CSR / PKCS#8 / X.509 parsers: transferDecode / transferEncode are the RFC 8951 sec. 3 base64 transfer codec (RFC 4648, and deliberately blind to any Content-Transfer-Encoding header, per errata 5904/5107); splitMultipartMixed is the /serverkeygen multipart/mixed splitter; parseCertsOnly validates a certs-only Simple PKI Response (RFC 5272 sec. 4.1) over cms.parse output; parseServerKeygenResponse dispatches the two-part key + certificate response with recipient-arm coherence; classifyResponse is the HTTP status / content-type / Retry-After state machine (202 accepted-not-ready surfaces retryAfterSeconds -- never an internal sleep; 204/404 on /csrattrs is a "none available" verdict, not an error). The builders assemble the CSR attributes EST adds -- a channel-binding challengePassword, the out-of-band-key identifiers, SMIMECapabilities, and the RFC 9908 template-priority enroll plan.
Altitude matches the toolkit: structural validation, no crypto verdicts. Certificates come back raw and unordered ("Clients MUST NOT assume the certificates are in any order", RFC 5272 sec. 4.1), so findIssuedCert picks the issued certificate by a public-key match, never a positional guess. The serverkeygen encrypted-key part's EnvelopedData is surfaced structurally (ciphertext raw, decryption external). A /fullcmc response is classified: a 200 may carry either arm RFC 7030 sec. 4.3.2 permits (certs-only or CMC-response), and a 404 or 501 is the distinct not-implemented verdict, meaning this service is absent, not that the transport faulted. Reading the CMC message itself is the CMC module's job. DER-only where DER, fail-closed everywhere.
pki.est.transferDecode
pki.est.transferDecode(body) -> Buffer
Decode an EST payload body (a base64 string or Buffer) to DER. CR/LF/space/tab are stripped anywhere (RFC 8951 sec. 3.1); any other non-alphabet byte fails closed with est/bad-base64. A Content-Transfer-Encoding header is never read (errata 5904/5107). Bounded twice: the raw length before decode and the decoded DER against DER_MAX_BYTES (est/too-large).
Example
var der = pki.asn1.build.sequence([pki.asn1.build.integer(1n)]);
var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
References
pki.est.transferEncode
pki.est.transferEncode(der) -> string
Encode DER as an EST payload body: bare RFC 4648 base64, no line wrapping (senders need not insert whitespace, RFC 8951 sec. 3.1).
Example
var der = pki.asn1.build.sequence([pki.asn1.build.integer(1n)]);
var body = pki.est.transferEncode(der);
References
pki.est.parseCertsOnly
pki.est.parseCertsOnly(der) -> { certificates, crls }
Validate a certs-only CMS Simple PKI Response (RFC 5272 sec. 4.1) over the shipped cms.parse output: a SignedData with no eContent and EMPTY signerInfos, carrying at least one plain X.509 certificate (a context-tagged CertificateChoices alternative is rejected est/bad-certificate-choice). CRLs MAY be present. Certificates come back raw and in as-received order (never sorted, per RFC 5272 sec. 4.1). A non-conformant response throws a typed EstError (est/not-certs-only, est/no-certificates).
Example
async function example() {
var b = pki.asn1.build;
var pair = await pki.key.generate("Ed25519");
var certDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(pair.publicKey),
notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
{ key: await pki.key.export(pair.privateKey) });
// the certs-only Simple PKI Response shape (RFC 7030 sec. 4.1.3): a SignedData
// v1 over id-data with no eContent, the certificates, and an EMPTY signerInfos
var caCertsDer = b.sequence([b.oid("1.2.840.113549.1.7.2"), b.explicit(0, b.sequence([
b.integer(1n), b.set([]), b.sequence([b.oid("1.2.840.113549.1.7.1")]),
b.contextConstructed(0, certDer), b.set([])]))]);
var r = pki.est.parseCertsOnly(caCertsDer);
r.certificates; // -> [Buffer, ...] raw, unordered
}
example();
References
pki.est.classifyResponse
pki.est.classifyResponse(status, headers, body, opts?) -> verdict
Classify an EST HTTP response into a verdict or a typed fault. A 200 requires the operation's exact content-type (est/bad-content-type); a 202 requires a Retry-After (absent -> est/missing-retry-after) -- a delay-seconds value is surfaced as bounded retryAfterSeconds, an HTTP-date as absolute retryAfterDate (epoch ms; retryAfterSeconds too when opts.now is given), and any other value is est/bad-retry-after (never slept on either way); 204/404 on /csrattrs is a none-available verdict (an error on any other operation); 4xx/5xx surface the capped diagnostic on est/http-error.
Options
op: string // the EST operation this response answers
now: number // the response receipt time (epoch ms), to turn an HTTP-date Retry-After into retryAfterSeconds
Example
var v = pki.est.classifyResponse(202, { "retry-after": "120" }, "", { op: "simpleenroll" });
v.retryAfterSeconds; // -> 120
References
pki.est.paths
pki.est.paths(baseUrl, opts?) -> { cacerts, simpleenroll, ... }
Build the RFC 7030 sec. 3.2.2 operation URLs for a base server URL. An OPTIONAL CA label (opts.label) MUST be non-empty, carry no /, and not collide with an operation name, else est/bad-label.
Options
label: string // an OPTIONAL CA label path segment
Example
pki.est.paths("https://ca.example").cacerts;
// -> "https://ca.example/.well-known/est/cacerts"
References
- spec RFC 7030
pki.est.fullcmc
pki.est.fullcmc(baseUrl, request, opts?) -> Promise<verdict | { retry, retryAfterSeconds }>
Enroll through the full CMC message layer: POST a Full PKI Request (from pki.cmc.build, as a DER Buffer or a PEM CMS block) to <baseUrl>/.well-known/est/fullcmc as application/pkcs7-mime; smime-type=CMC-request, base64 per RFC 8951, over the shared pki.transport.
A 200 answers with either smime-type=certs-only (a Simple PKI Response) or smime-type=CMC-response (a Full PKI Response). RFC 7030 sec. 4.3.2 names both, and the label must agree with the bytes. Either way the result is the pki.cmc.verify verdict shape, so a caller reads one outcome (issued / pending / confirm-required / pop-required / rejected) regardless of which arm the server chose.
The exchange binding is read out of the request itself, never taken on the caller's word: whatever Transaction Identifier, Sender Nonce or Data Return the submitted bytes carry is what the response must echo, and transactionId / senderNonce / dataReturn are a cross-check that is refused if it disagrees. A request that carries none of the three leaves nothing for the response to echo, so the answer is refused instead of being accepted as an enrollment result that could be a replay of any earlier exchange. The code is cmc/unbound-response on the CMC-response arm, which the CMC layer interprets, and est/unbound-response on the certs-only arm, which this verb owns. Build the request with a senderNonce (pki.cmc.build) to bind it, or pass allowUnboundResponse: true to accept that it is unbound. The verdict reports which halves ran as bound and boundToRequest.
A 404 **or** a 501 is the distinct est/not-implemented verdict, since support for this verb is optional on both sides (sec. 4.3). A 202 surfaces its Retry-After and does not sleep. A rejection carries a CMC response (sec. 4.3.2 makes it a MUST), which is decoded and attached to a typed est/cmc-failed as err.cmc and err.httpStatus, while a body that cannot be read never masks the HTTP fault it arrived with.
On the certs-only arm the issued certificates are identified by public-key match against the requests that were submitted, the only identification RFC 5272 sec. 4.1 sanctions, since "the certificates are in any order". Every certification request in the message must be answered before the exchange reads as issued: a key wanted by N requests needs N certificates, so a bag that answers only some of them, or none, is a refusal and not a partial success. That arm carries no controls, so it cannot echo a Transaction Identifier, Sender Nonce or Data Return: a request that sent those asked for replay binding it cannot provide (the key match is not one, since an old response for the same key still matches), and it is refused as est/unbound-response, never accepted with silently none of what was asked for. A request that asked for no binding reaches the same refusal on this arm, for the same reason: nothing here can tie the bag to the exchange, so allowUnboundResponse: true is what accepts it. They are surfaced as issuedCertificates (with certificate the first), distinct from certificates, which is the whole returned bag including any chain. Where the requested keys are distinct that list is in request order; where several requests deliberately SHARE one key it is not, and does not claim to be -- the public key is the only identification sec. 4.1 sanctions, so when it is shared nothing in the response says which of those requests a given certificate answers. That arm reports signatureVerified: false: a certs-only body is a degenerate SignedData with no signers by definition, so its security rests on the authenticated TLS channel, not on a signature.
Every EST transport gate holds unchanged, including on a bootstrap enrollment: https-only, an explicit trust anchor required, redirect and size bounds. A Publish Trust Anchors control in the response is SURFACED, never acted on (RFC 5272 sec. 6.15 makes accepting one a manual decision).
Options
- `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `now`, as in pki.est.cacerts.
- `transactionId` / `senderNonce` / `dataReturn`: what the request sent. The values are read out
of the request itself; supplying them here cross-checks that, and a disagreement is refused.
- `responderCerts`: extra certificates for CMC signer lookup, for a response that does not carry
its own signer; the certificates the response carries are searched either way. The carrier's
signature MUST be verified (RFC 5272 sec. 3.2.1.3.4), so a `CMC-response` whose signer is found
nowhere and which does not name the opt-out below is refused.
- `responseRecipient` -- key material for a response carried in AuthenticatedData, in the shape
`pki.cms.decrypt` takes. Its MAC is then checked and the verdict reports
`signatureVerified: true`, so the carrier is not reachable only unauthenticated.
- `allowUnverifiedResponse` -- accept a `CMC-response` whose signer certificate cannot be found,
without checking its signature; the verdict then reports `signatureVerified: false`. For an
unauthenticated bootstrap only, and it never excuses a signature that is present and wrong.
- `allowUnboundResponse` -- accept an answer to a request that carried no Transaction Identifier,
Sender Nonce or Data Return, so nothing ties it to this exchange; the verdict then reports
`boundToRequest: false`. A separate question from the one above, because a replayed response
is authentic, so naming one does not name the other.
- `username` / `password` / `allowCrossOriginRedirect` -- as pki.est.simpleenroll.
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: "device.example", 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: "device.example", subjectPublicKey: spki }, { key: key });
var request = await pki.cmc.build({ requests: [{ tcr: csr }] }, { cert: cert, key: key });
// a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
var r = await pki.est.fullcmc("https://ca.example", request,
{ transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
r.retry && r.retryAfterSeconds; // 60
}
example();
References
pki.est.cacerts
pki.est.cacerts(baseUrl, opts?) -> Promise<{ certificates, crls } | { retry, retryAfterSeconds }>
Fetch a CA's certificates over the wire: GET <baseUrl>/.well-known/est/cacerts through the shared pki.transport (inject opts.transport, else a fail-closed pki.transport.https). Returns the raw, unordered certs-only set ({ certificates, crls }), or { retry: true, retryAfterSeconds } on a 202 (surfaced, never slept). https-only (est/insecure-url); an explicit opts.tls.anchors (or an opts.tls.useSystemStore opt-in) is required (est/no-trust-anchors); the returned CA certificate is not auto-trusted: the caller path-validates it and supplies the accepted anchor on the next call.
Options
- `transport`: an injected transport(request) -> {status, headers, body, tls}; default pki.transport.https.
- `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity }.
- `label` -- an OPTIONAL CA label path segment; `timeout` / `maxResponseBytes` / `maxRedirects` -- budgets.
- `now` -- receipt time (epoch ms) to render a 202 Retry-After HTTP-date as seconds.
- `auth` -- HTTP authentication: `{ scheme: "basic" | "digest", username, password, allowMD5, allowLegacyQop, maxStaleRetries }`.
There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
- `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
Example
async function example() {
// a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
var r = await pki.est.cacerts("https://ca.example",
{ transport: function () { return Promise.resolve({ status: 200, headers: { "content-type": "application/pkcs7-mime" }, body: caCertsDer.toString("base64") }); } });
r.certificates; // -> [Buffer, ...] raw, unordered
}
example();
References
pki.est.simpleenroll
pki.est.simpleenroll(baseUrl, csr, opts?) -> Promise<{ certificate, chain, certificates } | { retry, retryAfterSeconds }>
Enroll for a certificate: POST a PKCS#10 csr (a DER Buffer or a PEM CERTIFICATE REQUEST, e.g. from pki.csr.sign) to <baseUrl>/.well-known/est/simpleenroll as application/pkcs10, over the shared pki.transport. Returns the issued certificate chosen by public-key match against the submitted CSR (certificate), the remaining certificates (chain), and the raw set (certificates); or { retry: true, retryAfterSeconds } on a 202. No returned certificate matching the CSR key fails closed (est/issued-cert-not-found); opts.strict requires exactly the issued certificate. A 401 is answered once with HTTP Basic only when opts.username/password are supplied and the transport already authenticated the server.
Options
- `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `now`, as in pki.est.cacerts.
- `strict`: reject an enroll response that carries more than the single issued certificate.
- `username` / `password` -- HTTP Basic credentials, answered only after server authorization (empty username allowed).
- `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST.
Example
async function example() {
var req = await pki.csr.sign({ subject: "device.example", subjectPublicKey: signerSpki }, { key: signerKeyPkcs8 });
// a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
var r = await pki.est.simpleenroll("https://ca.example", req,
{ transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
r.retry && r.retryAfterSeconds; // 60
}
example();
References
pki.est.simplereenroll
pki.est.simplereenroll(baseUrl, csr, opts?) -> Promise<{ certificate, chain, certificates } | { retry, retryAfterSeconds }>
Renew / rekey a certificate: identical to pki.est.simpleenroll but POSTs to /.well-known/est/simplereenroll and REQUIRES opts.oldCert (the certificate being renewed). Before anything crosses the wire, reenrollGuard enforces that the CSR's Subject and SubjectAltName (names and criticality) are byte-identical to opts.oldCert (RFC 7030 sec. 4.2.2). A mismatch fails closed (est/reenroll-subject-mismatch / est/reenroll-san-mismatch) and the transport is never called. A missing opts.oldCert is est/bad-input.
Options
- `oldCert` -- REQUIRED, the DER certificate being renewed (the re-enroll identity check).
- every option of pki.est.simpleenroll (transport, tls, label, budgets, strict, credentials).
Example
async function example() {
// reenrollGuard enforces the RFC 7030 sec. 4.2.2 identity check before anything is sent
var r = await pki.est.simplereenroll("https://ca.example", renewCsr,
{ oldCert: signerCertDer, transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
r.retry; // true
}
example();
References
- spec RFC 7030
pki.est.serverkeygen
pki.est.serverkeygen(baseUrl, csr, opts?) -> Promise<{ certificates, privateKey } | { certificates, encryptedKey } | { retry, retryAfterSeconds, retryAfterDate }>
Request a SERVER-GENERATED key pair + certificate: POST the CSR (base64 DER, Content-Type: application/pkcs10, identical request encoding to simpleenroll) to <baseUrl>/.well-known/est/serverkeygen. The two-part multipart/mixed response is surfaced as { certificates, privateKey } (a cleartext PKCS#8 PrivateKeyInfo) or { certificates, encryptedKey } (the CMS EnvelopedData the caller decrypts out-of-band with its key-encryption key; the verb never decrypts, so it is not a decryption oracle), or { retry, retryAfterSeconds } on a 202. The certificates are raw and unordered: unlike simpleenroll no leaf is picked, because the CA generated the key so the issued certificate's public key is the generated one, not the throwaway CSR key. A cleartext key is bound to its certificate before it resolves: the delivered private key's public half MUST match exactly one returned certificate (est/key-cert-mismatch on none, est/ambiguous-issued-cert on more than one), so a mis-associated key is refused, never handed back unusable. The encryption requirement + expected recipient are derived from the CSR's own DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute; an opts value that contradicts the CSR is est/bad-input (a cleartext-key downgrade cannot slip past). The delivered key's channel is asserted confidentiality- bearing (a NULL / anonymous / EXPORT cipher is est/weak-cipher). https-only, explicit-anchor, and the whole redirect / auth / budget machinery of simpleenroll apply.
Options
- `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
- every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
Example
async function example() {
var r = await pki.est.serverkeygen("https://ca.example", csrDer,
{ transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
r.retry; // true -- a 202 is surfaced, never slept
}
example();
References
pki.est.csrattrs
pki.est.csrattrs(baseUrl, opts?) -> Promise<{ available: true, attrs, plan } | { available: false, attrs: null }>
Fetch the CA's CSR-attributes policy: GET <baseUrl>/.well-known/est/csrattrs (Accept: application/csrattrs). A 200 body is base64-decoded, parsed as an RFC 9908 CsrAttrs, and returned with a plan (buildEnrollAttributes) the caller applies to its next CSR; the verb never auto-applies attributes to a CSR (single responsibility). A 204 or 404 is { available: false } (a valid "no specific attributes") and not an error; an empty SEQUENCE (30 00) is a complete empty policy (attrs.items empty), distinct from an empty HTTP body (est/empty-body). Server auth is not required for this policy GET but a 401 is tolerated (the shared auth path stays live). https-only + explicit-anchor as elsewhere.
Options
- every transport / tls / label / budget / credential option of the other verbs.
Example
async function example() {
var r = await pki.est.csrattrs("https://ca.example",
{ transport: function () { return Promise.resolve({ status: 404, headers: {}, body: "" }); } });
r.available; // false -- a 404 is "no CSR-attributes policy available"
}
example();