ACME
pki.jose JWS envelope. This is a MESSAGE LAYER, not an HTTP client: it owns the JWS construction/verification, the resource-object validation (closed status enums, conditional-required fields, immutable arrays), the three RFC 8555 sec. 7.1.6 state machines, the challenge computations (key authorization, http-01, dns-01, tls-alpn-01), the identifier validators (dns / ip / wildcard), and the ARI certID -- over an injectable transport.
Every resource object is validated by a declarative spec table (the JSON analog of the ASN.1 schema engine): one definition per surface drives both validate(obj) and the builders. Unknown fields are tolerated (ignored, never reflected); unknown challenge types are surfaced raw. Where ACME output re-enters the DER world -- the finalize CSR, the downloaded certificate chain, the revokeCert payload, the ARI inputs -- it routes through the shipped pki.schema.csr / pki.schema.x509 parsers, so no new DER detector appears and the format-orchestrator's mutual-exclusion proof is untouched.
pki.acme.assertTransition
pki.acme.assertTransition(kind, from, to) -> void
Assert that a status transition of an ACME resource (kind = "challenge"|"authorization"|"order") from from to to is one of the RFC 8555 sec. 7.1.6 legal edges. A same-status observation is allowed (a server may re-report); any other edge throws acme/bad-transition.
Example
pki.acme.assertTransition("order", "pending", "ready"); // ok
References
- spec RFC 8555
pki.acme.validateProblem
pki.acme.validateProblem(obj) -> obj
Validate an ACME problem document (RFC 7807 + RFC 8555 sec. 6.7): a type in the urn:ietf:params:acme:error: namespace, an optional detail, and subproblems (each itself a problem document, optionally carrying an identifier). A top-level identifier is forbidden (sec. 6.7.1) and throws acme/bad-problem. Returns the object.
Example
pki.acme.validateProblem({ type: "urn:ietf:params:acme:error:malformed" });
References
pki.acme.validate
pki.acme.validate(kind, obj) -> obj
Validate an ACME resource object of a known kind ("directory" | "account" | "order" | "authorization" | "challenge" | "renewalInfo") against its RFC 8555 / RFC 9773 spec: required and conditionally-required fields, closed status enums, URL / RFC 3339 / identifier shapes, and array minimums. Unknown fields are ignored (never reflected). Throws a typed acme/* fault; returns the object.
Example
pki.acme.validate("order", orderObj).status; // -> "pending"
References
pki.acme.identify
pki.acme.identify(obj) -> string
Classify an ACME JSON object into exactly one kind by its discriminating member set -- "jws", "problem", "directory", "order", "authorization", "challenge", "account", "renewalInfo", or "unknown". The discriminators are proven mutually exclusive; a DER structure identifies as "unknown".
Example
pki.acme.identify(orderObj); // -> "order"
References
pki.acme.keyAuthorization
pki.acme.keyAuthorization(token, accountJwk) -> Promise<string>
The RFC 8555 sec. 8.1 key authorization: token || '.' || base64url(SHA-256 JWK thumbprint of the account key). The token is validated (entropy floor + alphabet) first; the thumbprint is the RFC 7638 canonical digest, so changing the account key changes the key authorization.
Example
async function example() {
await pki.acme.keyAuthorization(token, accountJwk); // -> "<token>.<thumbprint>"
}
example();
References
pki.acme.http01
pki.acme.http01(token, accountJwk) -> Promise<{ path, body }>
The http-01 challenge computation (RFC 8555 sec. 8.3): the resource path /.well-known/acme-challenge/<token> and the body (the ASCII key authorization, no trailing newline). Validation reaches TCP port 80 over HTTP.
Example
async function example() {
var c = await pki.acme.http01(token, accountJwk);
c.path; // -> "/.well-known/acme-challenge/<token>"
}
example();
References
- spec RFC 8555
pki.acme.dns01
pki.acme.dns01(token, accountJwk, domain) -> Promise<{ name, value }>
The dns-01 challenge computation (RFC 8555 sec. 8.4): the TXT record name _acme-challenge.<domain> (exactly one leading *. is stripped for a wildcard order) and the value base64url(SHA-256(keyAuthorization)).
Example
async function example() {
var r = await pki.acme.dns01(token, accountJwk, "example.org");
r.name; // -> "_acme-challenge.example.org"
}
example();
References
- spec RFC 8555
pki.acme.tlsAlpn01Extension
pki.acme.tlsAlpn01Extension(token, accountJwk) -> Promise<Buffer>
Build the DER of the critical id-pe-acmeIdentifier extension (RFC 8737 sec. 3): SEQUENCE { extnID 1.3.6.1.5.5.7.1.31, critical TRUE, extnValue OCTET STRING wrapping Authorization ::= OCTET STRING (SIZE 32) of the SHA-256(keyAuthorization) }. Placed in the validation certificate.
Example
async function example() {
var extDer = await pki.acme.tlsAlpn01Extension(token, accountJwk);
}
example();
References
- spec RFC 8737
pki.acme.verifyTlsAlpn01
pki.acme.verifyTlsAlpn01(certDer, token, accountJwk, identifier) -> Promise<void>
Verify a tls-alpn-01 validation certificate (RFC 8737 sec. 3): a CRITICAL id-pe-acmeIdentifier extension whose 32-octet Authorization equals SHA-256(keyAuthorization), AND a SubjectAltName with EXACTLY ONE entry -- a dNSName equal to the dns identifier (case-insensitive) or a single iPAddress for an ip identifier (RFC 8738 sec. 6). Any deviation throws acme/bad-tlsalpn.
Example
async function example() {
await pki.acme.verifyTlsAlpn01(certDer, token, accountJwk, { type: "dns", value: "example.org" });
}
example();
References
pki.acme.postAsGet
pki.acme.postAsGet(opts) -> Promise<object>
Build a POST-as-GET request (RFC 8555 sec. 6.3): a JWS whose payload is the EMPTY octet string (payload: ""), distinct from a POST of an empty object ({}). opts carries { key, alg, nonce, url, kid } (an authenticated read is always kid-signed). Returns the flattened JWS.
Example
async function example() {
await pki.acme.postAsGet({ key, alg: "ES256", nonce, url: orderUrl, kid });
}
example();
References
- spec RFC 8555
pki.acme.newAccount
pki.acme.newAccount(opts) -> Promise<object>
Build a newAccount request (RFC 8555 sec. 7.3): a jwk-signed JWS (a new account has no kid yet) whose payload MAY carry contact (mailto validated fail-closed), termsOfServiceAgreed, onlyReturnExisting, and an externalAccountBinding (an EAB inner JWS from externalAccountBinding). opts = { key, alg, nonce, url, jwk, contact?, termsOfServiceAgreed?, onlyReturnExisting?, externalAccountBinding? }.
Example
async function example() {
await pki.acme.newAccount({ key, alg: "ES256", nonce, url, jwk, termsOfServiceAgreed: true });
}
example();
References
- spec RFC 8555
pki.acme.externalAccountBinding
pki.acme.externalAccountBinding(opts) -> Promise<object>
Build the External Account Binding inner JWS (RFC 8555 sec. 7.3.4): a MAC-only (HS256/HS384/HS512) JWS over the account public JWK, keyed by the CA-issued kid + symmetric macKey (a raw Buffer or an HMAC CryptoKey), url equal to the newAccount URL, NO nonce. opts = { macKey, kid, url, accountJwk, alg? } (alg default HS256). The result is embedded as newAccount's externalAccountBinding.
Example
async function example() {
var eab = await pki.acme.externalAccountBinding({ macKey, kid: "abc123", url, accountJwk });
}
example();
References
- spec RFC 8555
pki.acme.newOrder
pki.acme.newOrder(opts) -> Promise<object>
Build a newOrder request (RFC 8555 sec. 7.4): a kid-signed JWS whose payload carries a non-empty validated identifiers array (each dns/ip, one leading *. wildcard permitted for dns), optional notBefore/notAfter, and an optional RFC 9773 replaces (the ARI certID of the certificate being renewed). opts = { key, alg, nonce, url, kid, identifiers, notBefore?, notAfter?, replaces? }.
Example
async function example() {
await pki.acme.newOrder({ key, alg: "ES256", nonce, url, kid, identifiers: [{ type: "dns", value: "example.org" }] });
}
example();
References
pki.acme.finalize
pki.acme.finalize(opts) -> Promise<object>
Build a finalize request (RFC 8555 sec. 7.4): a kid-signed JWS whose payload csr is the base64url of the DER PKCS#10 (never PEM). The CSR is parsed with pki.schema.csr.parse; its requested identifier set (SAN + CN) MUST equal the order identifiers (acme/csr-identifier-mismatch), and its public key MUST NOT be the account key (acme/key-reuse, sec. 11.1). opts = { key, alg, nonce, url, kid, csr (DER Buffer), identifiers?, accountJwk? }.
Example
async function example() {
await pki.acme.finalize({ key, alg: "ES256", nonce, url, kid, csr: csrDer, identifiers, accountJwk });
}
example();
References
- spec RFC 8555
pki.acme.challengeResponse
pki.acme.challengeResponse(opts) -> Promise<object>
Build a challenge-response POST (RFC 8555 sec. 7.5.1): a kid-signed JWS whose payload is the type-defined response object -- {} for the three registered challenge types (http-01 / dns-01 / tls-alpn-01), which is DISTINCT from a POST-as-GET empty payload. opts = { key, alg, nonce, url, kid, payload? } (payload default {}; pass a custom object for a future challenge type).
Example
async function example() {
await pki.acme.challengeResponse({ key, alg: "ES256", nonce, url: challUrl, kid });
}
example();
References
- spec RFC 8555
pki.acme.deactivate
pki.acme.deactivate(opts) -> Promise<object>
Build a deactivation POST (RFC 8555 sec. 7.3.6 account / sec. 7.5.2 authorization): a kid-signed JWS with the payload {"status":"deactivated"} -- the only client-settable status. opts = { key, alg, nonce, url, kid }.
Example
async function example() {
await pki.acme.deactivate({ key, alg: "ES256", nonce, url: authzUrl, kid });
}
example();
References
- spec RFC 8555
pki.acme.revokeCert
pki.acme.revokeCert(opts) -> Promise<object>
Build a revokeCert request (RFC 8555 sec. 7.6): a JWS whose payload certificate is the base64url of the DER certificate and optional reason is an assigned RFC 5280 CRLReason (0-6, 8-10; 7 is unassigned). Signed EITHER by the account key (kid mode) OR by the certificate key (jwk mode) -- pass exactly one. opts = { key, alg, nonce, url, certificate (DER Buffer), reason?, kid? | jwk? }.
Example
async function example() {
await pki.acme.revokeCert({ key, alg: "ES256", nonce, url, kid, certificate: certDer, reason: 1 });
}
example();
References
pki.acme.keyChange
pki.acme.keyChange(opts) -> Promise<object>
Build a key-change request (RFC 8555 sec. 7.3.5): a nested JWS. The INNER JWS is signed by the NEW account key (embedded jwk, no nonce, url == the keyChange URL) over { account, oldKey }; the OUTER JWS is the account (kid, oldKey) signing that inner object. opts = { key (old private), alg (old), kid (account URL), account (account URL), oldKey (old public JWK), newKey (new private), newJwk (new public JWK), newAlg, nonce, url }.
Example
async function example() {
await pki.acme.keyChange({ key: oldKey, alg: "ES256", kid, account: kid, oldKey: oldJwk, newKey, newJwk, newAlg: "ES256", nonce, url });
}
example();
References
- spec RFC 8555
pki.acme.ariCertId
pki.acme.ariCertId(certDer) -> string
The RFC 9773 sec. 4.1 ARI certificate identifier of a DER certificate: base64url(AKI keyIdentifier) || '.' || base64url(serial content octets). The serial is the raw DER INTEGER content -- its leading 00 sign-padding byte is PRESERVED (dropping it is the documented mass-404 client bug). Throws acme/bad-certid if the certificate lacks an AKI keyIdentifier.
Example
pki.acme.ariCertId(certDer); // -> "<b64u-aki>.<b64u-serial>"
References
- spec RFC 9773
pki.acme.parseAriCertId
pki.acme.parseAriCertId(certId) -> { keyIdentifier, serial }
Parse an ARI certID string (RFC 9773 sec. 4.1) into { keyIdentifier, serial } Buffers. The two dot-joined halves are each strict base64url (padding / non-alphabet rejected); anything but exactly two parts throws acme/bad-certid.
Example
pki.acme.parseAriCertId("<b64u-aki>.<b64u-serial>").serial; // -> Buffer
References
- spec RFC 9773
pki.acme.validateRenewalInfo
pki.acme.validateRenewalInfo(obj) -> obj
Validate an ARI RenewalInfo object (RFC 9773 sec. 4.2): a suggestedWindow with RFC 3339 start and end, end strictly after start (an inverted or zero-width window throws acme/bad-renewal-window -- the client treats it as no response, defusing a renewal stampede), and an optional explanationURL. Returns the object.
Example
pki.acme.validateRenewalInfo({ suggestedWindow: { start: "2026-01-01T00:00:00Z", end: "2026-01-08T00:00:00Z" } });
References
- spec RFC 9773
pki.acme.client
pki.acme.client(directoryUrl, opts) -> client
A stateful RFC 8555 ACME client that drives the live directory flow over the shared pki.transport (inject opts.transport, else a fail-closed pki.transport.https). It composes the shipped message layer (the JWS builders + object validators + state machines) and owns only session state -- the fetched directory, the single-use nonce pool (a fresh anti-replay nonce per JWS, badNonce bounded- retried with the error's Replay-Nonce), and the account URL captured as the kid. opts.accountKey (a private CryptoKey) + opts.accountJwk (its public JWK) + opts.alg sign every request. Every request is https-only (acme/insecure-url); reads are POST-as-GET; a problem+json response is a typed acme/server-problem; a poll sleeps on a bounded Retry-After via an injectable sleeper and is capped by a poll count and a total-wait budget. Returns a client object: directory, newAccount, newOrder, getOrder / getAuthorization / getChallenge, respondToChallenge, finalize, pollOrder / pollAuthorization, downloadCertificate, revokeCert, deactivateAccount / deactivateAuthorization, keyChange, renewalInfo.
Options
- `accountKey` / `accountJwk` / `alg` -- REQUIRED: the account private key, its public JWK, and the JWS alg.
- `transport` -- injectable transport(request) -> {status, headers, body}; default pki.transport.https.
- `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity } for the default transport.
- `timeout` / `maxResponseBytes` / `maxRedirects` -- transport budgets; `maxNonceRetries` -- badNonce retry cap (default 1).
- `maxPolls` / `maxTotalWait` / `sleep` -- poll-loop budgets + an injectable sleeper; `clock` -- an injectable receipt clock (default Date.now) for a Retry-After HTTP-date.
Example
async function example() {
var acme = pki.acme.client("https://acme.example/directory", { accountKey, accountJwk, alg: "ES256", transport });
var acct = await acme.newAccount({ termsOfServiceAgreed: true });
var ord = await acme.newOrder({ identifiers: [{ type: "dns", value: "example.org" }] });
}
example();