EST

Enrollment over Secure Transport (RFC 7030, updated by RFC 8951 and RFC 9908). The network verbs -- 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 -- 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) -- 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). /fullcmc is recognized and rejected with a precise est/fullcmc-not-supported (deferred to the CMC format module). DER-only where DER, fail-closed everywhere.

pki.est.transferDecode

since 0.1.24 experimental
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 roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));

References

pki.est.transferEncode

since 0.1.24 experimental
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 body = pki.est.transferEncode(der);

References

pki.est.parseCertsOnly

since 0.1.24 experimental
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 -- RFC 5272 sec. 4.1). A non-conformant response throws a typed EstError (est/not-certs-only, est/no-certificates).

Example

var r = pki.est.parseCertsOnly(caCertsDer);
r.certificates;   // -> [Buffer, ...] raw, unordered

References

pki.est.classifyResponse

since 0.1.24 experimental
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

since 0.1.24 experimental
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

pki.est.cacerts

since 0.3.16 experimental
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}; 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.

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

since 0.3.16 experimental
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 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

since 0.3.16 experimental
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