@blamejs/pki 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/acme.js ADDED
@@ -0,0 +1,1159 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.acme
6
+ * @nav Protocols
7
+ * @title ACME
8
+ * @order 20
9
+ * @slug acme
10
+ *
11
+ * @intro
12
+ * The RFC 8555 ACME message layer (updated by RFC 8737 tls-alpn-01, RFC 8738
13
+ * IP identifiers, and RFC 9773 ARI) -- object validators, request builders,
14
+ * challenge computations, and the ARI certID codec over the `pki.jose` JWS
15
+ * envelope. This is a MESSAGE LAYER, not an HTTP client: it owns the JWS
16
+ * construction/verification, the resource-object validation (closed status
17
+ * enums, conditional-required fields, immutable arrays), the three RFC 8555
18
+ * sec. 7.1.6 state machines, the challenge computations (key authorization,
19
+ * http-01, dns-01, tls-alpn-01), the identifier validators (`dns` / `ip` /
20
+ * wildcard), and the ARI certID -- over an injectable transport.
21
+ *
22
+ * Every resource object is validated by a declarative spec table (the JSON
23
+ * analog of the ASN.1 schema engine): one definition per surface drives both
24
+ * `validate(obj)` and the builders. Unknown fields are tolerated (ignored,
25
+ * never reflected); unknown challenge types are surfaced raw. Where ACME output
26
+ * re-enters the DER world -- the finalize CSR, the downloaded certificate
27
+ * chain, the revokeCert payload, the ARI inputs -- it routes through the shipped
28
+ * `pki.schema.csr` / `pki.schema.x509` parsers, so no new DER detector appears
29
+ * and the format-orchestrator's mutual-exclusion proof is untouched.
30
+ *
31
+ * @card
32
+ * RFC 8555 / 8737 / 8738 / 9773 ACME message layer: object validators, the
33
+ * three state machines, request builders, http-01 / dns-01 / tls-alpn-01
34
+ * challenge computations, and the ARI certID -- over pki.jose, transport-injectable.
35
+ */
36
+
37
+ var jose = require("./jose");
38
+ var asn1 = require("./asn1-der");
39
+ var oid = require("./oid");
40
+ var x509 = require("./schema-x509");
41
+ var csr = require("./schema-csr");
42
+ var pkix = require("./schema-pkix");
43
+ var constants = require("./constants");
44
+ var subtle = require("./webcrypto").webcrypto.subtle;
45
+ var frameworkError = require("./framework-error");
46
+
47
+ var AcmeError = frameworkError.AcmeError;
48
+ function E(code, message, cause) { return new AcmeError(code, message, cause); }
49
+
50
+ // ---- helpers -------------------------------------------------------------
51
+
52
+ function _isObject(v) { return v && typeof v === "object" && !Array.isArray(v); }
53
+ function _isString(v) { return typeof v === "string"; }
54
+ // RFC 3339 date-time (date "T" time with a zone). Beyond the grammar this checks
55
+ // CALENDAR validity -- a syntactically well-formed but impossible instant (month 13,
56
+ // February 30, hour 25, a +25:00 offset) is rejected, so a downstream expiry / window
57
+ // comparison never runs on a value JS Date would silently roll over.
58
+ var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
59
+ function _isRfc3339(v) {
60
+ if (!_isString(v)) return false;
61
+ var m = RFC3339_RE.exec(v);
62
+ if (!m) return false;
63
+ var year = +m[1], month = +m[2], day = +m[3], hour = +m[4], min = +m[5], sec = +m[6];
64
+ if (month < 1 || month > 12) return false;
65
+ // Reject a :60 leap second: Node's Date.parse returns NaN for it, so an expiry /
66
+ // renewal-window comparison on such a value would silently pass (NaN <= x is
67
+ // false). This toolkit only handles instants it can compare, so it fails closed.
68
+ if (hour > 23 || min > 59 || sec > 59) return false;
69
+ var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
70
+ var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
71
+ if (day < 1 || day > daysInMonth[month - 1]) return false;
72
+ if (m[7]) { if (+m[8] > 23 || +m[9] > 59) return false; } // numeric zone offset
73
+ return true;
74
+ }
75
+ // A URL string: an absolute http(s) URI with a real host (RFC 3986). ACME URLs are
76
+ // server-provided endpoints downstream transport will trust, so they are PARSED
77
+ // (not prefix-matched) -- a malformed value like "https://[" or a hostless
78
+ // "http://" is rejected, not accepted by a loose regex.
79
+ function _isUrl(v) {
80
+ // Prefilter the exact authority form with no whitespace BEFORE parsing: new URL()
81
+ // silently repairs " https://.." (trim) and "https:host/.." (insert //), and the
82
+ // ORIGINAL string is what gets copied into a protected `url` field, so a repaired
83
+ // value must be rejected here rather than accepted and mismatched at transport time.
84
+ if (!_isString(v) || !/^https?:\/\/[^\s]+$/.test(v)) return false;
85
+ var u;
86
+ // A parse failure IS the "not a URL" verdict for this boolean predicate -- there
87
+ // is no PkiError here to thread a cause into, so the error is intentionally ignored.
88
+ try { u = new URL(v); } catch (_e) { return false; }
89
+ return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.length > 0;
90
+ }
91
+ // A URI string with any RFC 3986 scheme (mailto:, tel:, http(s):, ...). An account
92
+ // `contact` is a URI, most commonly `mailto:` (RFC 8555 sec. 7.1.2 / RFC 6068), so
93
+ // it must NOT be narrowed to http(s); the strict mailto hygiene lives in the builder.
94
+ function _isUriString(v) { return _isString(v) && /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+$/.test(v); }
95
+
96
+ // ---- resource object specs (RFC 8555 sec. 7.1.x) -------------------------
97
+
98
+ // Each field: { name, type, required?, requiredWhen?(obj), enum?, elemType? }.
99
+ // type: "string" | "url" | "rfc3339" | "boolean" | "object" | "array" | "any".
100
+ // The walker validates presence/shape ONLY; unknown fields are ignored (never
101
+ // reflected). requiredWhen is the conditional-required rule (sec. 7.1.3 expires).
102
+ // The assigned RFC 5280 sec. 5.3.1 CRLReason values (0-6, 8-10). Value 7 is
103
+ // unassigned, so a revokeCert reason of 7 is rejected rather than sent.
104
+ var CRL_REASONS = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10];
105
+
106
+ // The registered challenge types (RFC 8555 sec. 8.3/8.4, RFC 8737). For these the
107
+ // `token` is a required, entropy-bearing base64url value; an unknown future type
108
+ // may define its own response fields, so `token` is only required for these.
109
+ var KNOWN_CHALLENGE_TYPES = { "http-01": 1, "dns-01": 1, "tls-alpn-01": 1 };
110
+
111
+ var STATUS = {
112
+ account: ["valid", "deactivated", "revoked"],
113
+ order: ["pending", "ready", "processing", "valid", "invalid"],
114
+ authorization: ["pending", "valid", "invalid", "deactivated", "expired", "revoked"],
115
+ challenge: ["pending", "processing", "valid", "invalid"],
116
+ };
117
+
118
+ var SPECS = {
119
+ directory: [
120
+ { name: "newNonce", type: "url", required: true },
121
+ { name: "newAccount", type: "url", required: true },
122
+ { name: "newOrder", type: "url", required: true },
123
+ { name: "revokeCert", type: "url", required: true },
124
+ { name: "keyChange", type: "url", required: true },
125
+ { name: "newAuthz", type: "url" },
126
+ { name: "renewalInfo", type: "url" },
127
+ { name: "meta", type: "object" },
128
+ ],
129
+ account: [
130
+ { name: "status", type: "string", required: true, enum: STATUS.account },
131
+ { name: "contact", type: "array", elemType: "contact" },
132
+ { name: "termsOfServiceAgreed", type: "boolean" },
133
+ { name: "externalAccountBinding", type: "object" },
134
+ { name: "orders", type: "url" }, // required per RFC; lenient by default (OQ2)
135
+ ],
136
+ order: [
137
+ { name: "status", type: "string", required: true, enum: STATUS.order },
138
+ { name: "expires", type: "rfc3339", requiredWhen: function (o) { return o.status === "pending" || o.status === "valid"; } },
139
+ { name: "identifiers", type: "array", required: true, minItems: 1, elemType: "orderIdentifier" },
140
+ { name: "notBefore", type: "rfc3339" },
141
+ { name: "notAfter", type: "rfc3339" },
142
+ { name: "error", type: "object" },
143
+ { name: "authorizations", type: "array", required: true, minItems: 1, elemType: "url" },
144
+ { name: "finalize", type: "url", required: true },
145
+ { name: "certificate", type: "url" },
146
+ { name: "replaces", type: "string" },
147
+ ],
148
+ authorization: [
149
+ { name: "identifier", type: "identifier", required: true },
150
+ { name: "status", type: "string", required: true, enum: STATUS.authorization },
151
+ { name: "expires", type: "rfc3339", requiredWhen: function (o) { return o.status === "valid"; } },
152
+ { name: "challenges", type: "array", required: true, minItems: 1, elemType: "challenge" },
153
+ { name: "wildcard", type: "boolean" },
154
+ ],
155
+ challenge: [
156
+ { name: "type", type: "string", required: true },
157
+ { name: "url", type: "url", required: true },
158
+ { name: "status", type: "string", required: true, enum: STATUS.challenge },
159
+ { name: "validated", type: "rfc3339", requiredWhen: function (o) { return o.status === "valid"; } },
160
+ { name: "token", type: "token", requiredWhen: function (o) { return KNOWN_CHALLENGE_TYPES[o.type] === 1; } },
161
+ { name: "error", type: "object" },
162
+ ],
163
+ renewalInfo: [
164
+ { name: "suggestedWindow", type: "object", required: true },
165
+ { name: "explanationURL", type: "url" },
166
+ ],
167
+ };
168
+
169
+ function _checkType(kind, field, value) {
170
+ switch (field.type) {
171
+ case "string": if (!_isString(value)) return "must be a string"; break;
172
+ case "url": if (!_isUrl(value)) return "must be a URL string"; break;
173
+ case "rfc3339": if (!_isRfc3339(value)) return "must be an RFC 3339 date-time"; break;
174
+ case "boolean": if (typeof value !== "boolean") return "must be a boolean"; break;
175
+ case "token": _assertToken(value); break; // >= 22 base64url chars, no padding (throws acme/bad-token)
176
+ case "object": if (!_isObject(value)) return "must be an object"; break;
177
+ case "identifier": _validateIdentifier(value); break;
178
+ case "array":
179
+ if (!Array.isArray(value)) return "must be an array";
180
+ if (field.minItems && value.length < field.minItems) return "must have at least " + field.minItems + " element(s)";
181
+ for (var i = 0; i < value.length; i++) {
182
+ if (field.elemType === "url" && !_isUrl(value[i])) return "element " + i + " must be a URL string";
183
+ if (field.elemType === "contact" && !_isUriString(value[i])) return "element " + i + " must be a URI string";
184
+ if (field.elemType === "identifier") _validateIdentifier(value[i]);
185
+ if (field.elemType === "orderIdentifier") _validateOrderIdentifier(value[i]);
186
+ if (field.elemType === "challenge") _validate("challenge", value[i]);
187
+ if (field.elemType === "object" && !_isObject(value[i])) return "element " + i + " must be an object";
188
+ }
189
+ break;
190
+ default: break; // "any"
191
+ }
192
+ return null;
193
+ }
194
+
195
+ // A kind name kebab-cased for use in an error code (the code shape is strict
196
+ // lowercase-kebab): a camelCase kind like "renewalInfo" must become
197
+ // "renewal-info", never leak "acme/bad-renewalInfo" (which the PkiError code
198
+ // validator rejects, turning a fault into a raw TypeError).
199
+ function _codeSlug(kind) { return kind.replace(/([A-Z])/g, "-$1").toLowerCase(); }
200
+
201
+ // Validate an object against a spec table. Returns the object; throws acme/*.
202
+ function _validate(kind, obj) {
203
+ if (!_isObject(obj)) throw E("acme/bad-" + _codeSlug(kind), "an ACME " + kind + " must be a JSON object");
204
+ var spec = SPECS[kind];
205
+ for (var f = 0; f < spec.length; f++) {
206
+ var field = spec[f];
207
+ var present = Object.prototype.hasOwnProperty.call(obj, field.name);
208
+ var required = field.required || (field.requiredWhen && field.requiredWhen(obj));
209
+ if (!present) {
210
+ if (required) throw E("acme/missing-field", "an ACME " + kind + " is missing the required field " + JSON.stringify(field.name));
211
+ continue;
212
+ }
213
+ if (field.enum && field.enum.indexOf(obj[field.name]) === -1) {
214
+ throw E("acme/bad-status", "the " + kind + " " + field.name + " " + JSON.stringify(obj[field.name]) + " is not a recognized value");
215
+ }
216
+ var err = _checkType(kind, field, obj[field.name]);
217
+ if (err) throw E("acme/bad-" + _codeSlug(kind), "the " + kind + " field " + JSON.stringify(field.name) + " " + err);
218
+ }
219
+ return obj;
220
+ }
221
+
222
+ // ---- identifiers (dns / ip; RFC 8555 sec. 7.1.4 / RFC 8738) --------------
223
+
224
+ // A dns identifier value: lowercase LDH ASCII labels (A-labels; a leading `*.`
225
+ // wildcard is validated by the ORDER path, never here). An ip identifier value:
226
+ // the RFC 5952 (IPv6) / RFC 1123 (IPv4) canonical textual form, byte-identical
227
+ // round-trip. `_validateIdentifier` rejects a value beginning `*.` (that is only
228
+ // legal in an order identifier, checked separately).
229
+ function _validateIdentifier(id) {
230
+ if (!_isObject(id) || !_isString(id.type) || !_isString(id.value)) throw E("acme/bad-identifier", "an identifier must be { type, value } strings");
231
+ if (id.type === "dns") {
232
+ if (id.value.indexOf("*.") === 0) throw E("acme/bad-identifier", "a wildcard *. value is not permitted in an authorization identifier (RFC 8555 sec. 7.1.4)");
233
+ _assertDnsName(id.value);
234
+ } else if (id.type === "ip") {
235
+ _assertIpAddress(id.value);
236
+ }
237
+ // An unrecognized identifier type is surfaced raw (a server may add types).
238
+ return id;
239
+ }
240
+
241
+ // A DNS name (each label lowercase letters/digits/hyphen, not leading/trailing
242
+ // hyphen; an xn-- A-label must be well-formed). Uppercase / non-ASCII rejected
243
+ // (the client sends A-labels only).
244
+ function _assertDnsName(name) {
245
+ if (!_isString(name)) throw E("acme/bad-identifier", "a dns identifier value must be a string");
246
+ if (name.length === 0 || name.length > 253) throw E("acme/bad-identifier", "a dns identifier value must be 1..253 characters");
247
+ var labels = name.split(".");
248
+ for (var i = 0; i < labels.length; i++) {
249
+ var l = labels[i];
250
+ if (l.length > 63) throw E("acme/bad-identifier", "a dns label must be 1..63 characters (RFC 1035 sec. 2.3.4): " + JSON.stringify(l));
251
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(l)) throw E("acme/bad-identifier", "a dns label must be lowercase LDH ASCII (A-label): " + JSON.stringify(l));
252
+ if (l.indexOf("xn--") === 0 && !/^xn--[a-z0-9]+(-[a-z0-9]+)*$/.test(l)) throw E("acme/bad-identifier", "a malformed xn-- A-label: " + JSON.stringify(l));
253
+ }
254
+ }
255
+
256
+ // An IP address in canonical text (RFC 8738 sec. 3): IPv4 dotted-decimal with no
257
+ // leading zeros, or IPv6 in the RFC 5952 sec. 4 compressed lowercase form. The
258
+ // only accepted form is the one that round-trips byte-identically -- an ambiguous
259
+ // value (leading zeros, uppercase hex, an uncompressed run) is rejected, never
260
+ // normalized-and-guessed.
261
+ function _assertIpAddress(value) {
262
+ if (!_isString(value)) throw E("acme/bad-identifier", "an ip identifier value must be a string");
263
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) {
264
+ var parts = value.split(".");
265
+ for (var i = 0; i < 4; i++) {
266
+ if (!/^(0|[1-9]\d{0,2})$/.test(parts[i]) || parseInt(parts[i], 10) > 255) throw E("acme/bad-identifier", "an ip identifier IPv4 octet is out of range or non-canonical: " + JSON.stringify(value));
267
+ }
268
+ return;
269
+ }
270
+ if (value.indexOf(":") !== -1) {
271
+ if (value !== value.toLowerCase()) throw E("acme/bad-identifier", "an IPv6 ip identifier must be lowercase (RFC 5952)");
272
+ var canon = _canonicalizeIpv6(value);
273
+ if (canon === null || canon !== value) throw E("acme/bad-identifier", "an ip identifier must be the RFC 5952 canonical IPv6 form: " + JSON.stringify(value));
274
+ return;
275
+ }
276
+ throw E("acme/bad-identifier", "an ip identifier value must be an IPv4 or IPv6 textual address (RFC 8738 sec. 3)");
277
+ }
278
+
279
+ // Parse an IPv6 address to its 8 groups then re-emit the RFC 5952 canonical form
280
+ // (lowercase, no leading zeros, the longest zero-run compressed with `::` -- the
281
+ // leftmost when tied, never a single 0 group). Returns null on a malformed input.
282
+ function _canonicalizeIpv6(value) {
283
+ var groups;
284
+ if (value.indexOf("::") !== -1) {
285
+ if (value.indexOf("::") !== value.lastIndexOf("::")) return null; // only one ::
286
+ var halves = value.split("::");
287
+ var left = halves[0] ? halves[0].split(":") : [];
288
+ var right = halves[1] ? halves[1].split(":") : [];
289
+ var fill = 8 - left.length - right.length;
290
+ if (fill < 1) return null;
291
+ groups = left.concat(new Array(fill).fill("0")).concat(right);
292
+ } else {
293
+ groups = value.split(":");
294
+ }
295
+ if (groups.length !== 8) return null;
296
+ var nums = [];
297
+ for (var i = 0; i < 8; i++) {
298
+ if (!/^[0-9a-f]{1,4}$/.test(groups[i])) return null;
299
+ nums.push(parseInt(groups[i], 16));
300
+ }
301
+ var hex = nums.map(function (n) { return n.toString(16); });
302
+ // Longest run of zero groups (>= 2) -> "::"; leftmost on a tie.
303
+ var bestStart = -1, bestLen = 0, curStart = -1, curLen = 0;
304
+ for (var g = 0; g < 8; g++) {
305
+ if (nums[g] === 0) { if (curStart === -1) curStart = g; curLen++; if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; } }
306
+ else { curStart = -1; curLen = 0; }
307
+ }
308
+ if (bestLen < 2) return hex.join(":");
309
+ var head = hex.slice(0, bestStart).join(":");
310
+ var tail = hex.slice(bestStart + bestLen).join(":");
311
+ return head + "::" + tail;
312
+ }
313
+
314
+ // ---- state machines (RFC 8555 sec. 7.1.6) --------------------------------
315
+
316
+ // Legal transitions as data. An observed transition outside the set is an
317
+ // illegal server transition the client fails closed on (acme/bad-transition).
318
+ var TRANSITIONS = {
319
+ challenge: { pending: ["processing", "valid", "invalid"], processing: ["processing", "valid", "invalid"] },
320
+ authorization: { pending: ["valid", "invalid"], valid: ["expired", "deactivated", "revoked"] },
321
+ order: { pending: ["ready", "invalid"], ready: ["processing", "valid", "invalid"], processing: ["valid", "invalid"] },
322
+ };
323
+
324
+ /**
325
+ * @primitive pki.acme.assertTransition
326
+ * @signature pki.acme.assertTransition(kind, from, to) -> void
327
+ * @since 0.1.25
328
+ * @status experimental
329
+ * @spec RFC 8555
330
+ * @related pki.acme.validate
331
+ *
332
+ * Assert that a status transition of an ACME resource (`kind` =
333
+ * `"challenge"|"authorization"|"order"`) from `from` to `to` is one of the
334
+ * RFC 8555 sec. 7.1.6 legal edges. A same-status observation is allowed (a
335
+ * server may re-report); any other edge throws `acme/bad-transition`.
336
+ *
337
+ * @example
338
+ * pki.acme.assertTransition("order", "pending", "ready"); // ok
339
+ */
340
+ function assertTransition(kind, from, to) {
341
+ var table = TRANSITIONS[kind];
342
+ if (!table) throw E("acme/bad-input", "unknown resource kind " + JSON.stringify(kind));
343
+ if (from === to) return;
344
+ // The legal edges -- including every non-terminal state's edge to "invalid" -- are
345
+ // the table. A terminal "valid" order/challenge has NO outgoing edge, so a
346
+ // "valid" -> "invalid" regression is rejected (RFC 8555 sec. 7.1.6 makes valid
347
+ // terminal; a client failing closed while polling must not accept it).
348
+ var allowed = table[from];
349
+ if (!allowed || allowed.indexOf(to) === -1) throw E("acme/bad-transition", "illegal " + kind + " transition " + JSON.stringify(from) + " -> " + JSON.stringify(to) + " (RFC 8555 sec. 7.1.6)");
350
+ }
351
+
352
+ // ---- problem documents (RFC 7807 / RFC 8555 sec. 6.7) --------------------
353
+
354
+ var ERROR_NAMESPACE = "urn:ietf:params:acme:error:";
355
+
356
+ /**
357
+ * @primitive pki.acme.validateProblem
358
+ * @signature pki.acme.validateProblem(obj) -> obj
359
+ * @since 0.1.25
360
+ * @status experimental
361
+ * @spec RFC 8555, RFC 7807, RFC 9773
362
+ * @related pki.acme.validate
363
+ *
364
+ * Validate an ACME problem document (RFC 7807 + RFC 8555 sec. 6.7): a `type` in
365
+ * the `urn:ietf:params:acme:error:` namespace, an optional `detail`, and
366
+ * `subproblems` (each itself a problem document, optionally carrying an
367
+ * `identifier`). A top-level `identifier` is forbidden (sec. 6.7.1) and throws
368
+ * `acme/bad-problem`. Returns the object.
369
+ *
370
+ * @example
371
+ * pki.acme.validateProblem({ type: "urn:ietf:params:acme:error:malformed" });
372
+ */
373
+ function validateProblem(obj) {
374
+ if (!_isObject(obj)) throw E("acme/bad-problem", "a problem document must be a JSON object");
375
+ if (Object.prototype.hasOwnProperty.call(obj, "identifier")) throw E("acme/bad-problem", "a top-level problem document must not carry an identifier (RFC 8555 sec. 6.7.1)");
376
+ if (!_isString(obj.type) || obj.type.indexOf(ERROR_NAMESPACE) !== 0) throw E("acme/bad-problem", "an ACME problem type must be in the " + ERROR_NAMESPACE + " namespace (RFC 8555 sec. 6.7)");
377
+ if (Object.prototype.hasOwnProperty.call(obj, "subproblems")) {
378
+ if (!Array.isArray(obj.subproblems)) throw E("acme/bad-problem", "subproblems must be an array");
379
+ for (var i = 0; i < obj.subproblems.length; i++) {
380
+ var sub = obj.subproblems[i];
381
+ if (!_isObject(sub) || !_isString(sub.type) || sub.type.indexOf(ERROR_NAMESPACE) !== 0) throw E("acme/bad-problem", "each subproblem must be an ACME problem document in the error namespace");
382
+ // A subproblem identifier reflects a submitted order identifier, which MAY be a
383
+ // wildcard (a rejectedIdentifier for a *.example.org order), so it is validated
384
+ // with the order-identifier rule, not the stricter authorization one.
385
+ if (Object.prototype.hasOwnProperty.call(sub, "identifier")) _validateOrderIdentifier(sub.identifier);
386
+ }
387
+ }
388
+ return obj;
389
+ }
390
+
391
+ // ---- object validators + identify ----------------------------------------
392
+
393
+ /**
394
+ * @primitive pki.acme.validate
395
+ * @signature pki.acme.validate(kind, obj) -> obj
396
+ * @since 0.1.25
397
+ * @status experimental
398
+ * @spec RFC 8555, RFC 9773
399
+ * @related pki.acme.identify, pki.acme.validateProblem
400
+ *
401
+ * Validate an ACME resource object of a known `kind` (`"directory"` |
402
+ * `"account"` | `"order"` | `"authorization"` | `"challenge"` | `"renewalInfo"`)
403
+ * against its RFC 8555 / RFC 9773 spec: required and conditionally-required
404
+ * fields, closed status enums, URL / RFC 3339 / identifier shapes, and array
405
+ * minimums. Unknown fields are ignored (never reflected). Throws a typed
406
+ * `acme/*` fault; returns the object.
407
+ *
408
+ * @example
409
+ * pki.acme.validate("order", orderObj).status; // -> "pending"
410
+ */
411
+ function validate(kind, obj) {
412
+ if (kind === "problem") return validateProblem(obj);
413
+ // renewalInfo carries an RFC 9773 window sanity check beyond the spec shape, so a
414
+ // generic dispatch (identify -> validate) gets the SAME strictness as a direct
415
+ // validateRenewalInfo call -- an inverted / malformed suggestedWindow is rejected.
416
+ if (kind === "renewalInfo") return validateRenewalInfo(obj);
417
+ if (!SPECS[kind]) throw E("acme/bad-input", "unknown ACME object kind " + JSON.stringify(kind));
418
+ return _validate(kind, obj);
419
+ }
420
+
421
+ /**
422
+ * @primitive pki.acme.identify
423
+ * @signature pki.acme.identify(obj) -> string
424
+ * @since 0.1.25
425
+ * @status experimental
426
+ * @spec RFC 8555, RFC 9773
427
+ * @related pki.acme.validate
428
+ *
429
+ * Classify an ACME JSON object into exactly one kind by its discriminating
430
+ * member set -- `"jws"`, `"problem"`, `"directory"`, `"order"`, `"authorization"`,
431
+ * `"challenge"`, `"account"`, `"renewalInfo"`, or `"unknown"`. The discriminators
432
+ * are proven mutually exclusive; a DER structure identifies as `"unknown"`.
433
+ *
434
+ * @example
435
+ * pki.acme.identify(orderObj); // -> "order"
436
+ */
437
+ function identify(obj) {
438
+ if (!_isObject(obj)) return "unknown";
439
+ var has = function (k) { return Object.prototype.hasOwnProperty.call(obj, k); };
440
+ if (has("protected") && has("signature") && _isString(obj.protected) && _isString(obj.signature)) return "jws";
441
+ if (_isString(obj.type) && obj.type.indexOf(ERROR_NAMESPACE) === 0) return "problem";
442
+ if (has("newNonce") && has("newAccount")) return "directory";
443
+ if (has("suggestedWindow")) return "renewalInfo";
444
+ if (has("finalize") && has("authorizations")) return "order";
445
+ if (has("identifier") && has("challenges")) return "authorization";
446
+ if (has("type") && has("url") && has("token") && !has("identifier")) return "challenge";
447
+ if (has("status") && (has("orders") || has("contact") || has("termsOfServiceAgreed"))) return "account";
448
+ return "unknown";
449
+ }
450
+
451
+ // ---- challenge computations (RFC 8555 sec. 8 / 8737 / 8738) --------------
452
+
453
+ // A challenge token: >= 128 bits of entropy => >= 22 base64url chars, alphabet
454
+ // only, no `=` (RFC 8555 sec. 8, errata 6950). Validated BEFORE any use (also the
455
+ // http-01 reflection-XSS guard).
456
+ var TOKEN_RE = new RegExp("^[A-Za-z0-9_-]{" + constants.LIMITS.ACME_TOKEN_MIN_CHARS + ",}$");
457
+ function _assertToken(token) {
458
+ if (!_isString(token) || !TOKEN_RE.test(token)) throw E("acme/bad-token", "a challenge token must be >= " + constants.LIMITS.ACME_TOKEN_MIN_CHARS + " base64url characters with no padding (RFC 8555 sec. 8)");
459
+ }
460
+
461
+ async function _sha256(bytes) { return Buffer.from(await subtle.digest("SHA-256", bytes)); }
462
+
463
+ /**
464
+ * @primitive pki.acme.keyAuthorization
465
+ * @signature pki.acme.keyAuthorization(token, accountJwk) -> Promise<string>
466
+ * @since 0.1.25
467
+ * @status experimental
468
+ * @spec RFC 8555, RFC 7638
469
+ * @related pki.acme.http01, pki.acme.dns01, pki.acme.tlsAlpn01Extension
470
+ *
471
+ * The RFC 8555 sec. 8.1 key authorization: `token || '.' ||
472
+ * base64url(SHA-256 JWK thumbprint of the account key)`. The token is validated
473
+ * (entropy floor + alphabet) first; the thumbprint is the RFC 7638 canonical
474
+ * digest, so changing the account key changes the key authorization.
475
+ *
476
+ * @example
477
+ * await pki.acme.keyAuthorization(token, accountJwk); // -> "<token>.<thumbprint>"
478
+ */
479
+ async function keyAuthorization(token, accountJwk) {
480
+ _assertToken(token);
481
+ var tp = await jose.thumbprint(accountJwk);
482
+ return token + "." + tp;
483
+ }
484
+
485
+ /**
486
+ * @primitive pki.acme.http01
487
+ * @signature pki.acme.http01(token, accountJwk) -> Promise<{ path, body }>
488
+ * @since 0.1.25
489
+ * @status experimental
490
+ * @spec RFC 8555
491
+ * @related pki.acme.keyAuthorization
492
+ *
493
+ * The http-01 challenge computation (RFC 8555 sec. 8.3): the resource `path`
494
+ * `/.well-known/acme-challenge/<token>` and the `body` (the ASCII key
495
+ * authorization, no trailing newline). Validation reaches TCP port 80 over HTTP.
496
+ *
497
+ * @example
498
+ * var c = await pki.acme.http01(token, accountJwk);
499
+ * c.path; // -> "/.well-known/acme-challenge/<token>"
500
+ */
501
+ async function http01(token, accountJwk) {
502
+ var ka = await keyAuthorization(token, accountJwk);
503
+ return { path: "/.well-known/acme-challenge/" + token, body: ka };
504
+ }
505
+
506
+ /**
507
+ * @primitive pki.acme.dns01
508
+ * @signature pki.acme.dns01(token, accountJwk, domain) -> Promise<{ name, value }>
509
+ * @since 0.1.25
510
+ * @status experimental
511
+ * @spec RFC 8555
512
+ * @related pki.acme.keyAuthorization
513
+ *
514
+ * The dns-01 challenge computation (RFC 8555 sec. 8.4): the TXT record `name`
515
+ * `_acme-challenge.<domain>` (exactly one leading `*.` is stripped for a wildcard
516
+ * order) and the `value` `base64url(SHA-256(keyAuthorization))`.
517
+ *
518
+ * @example
519
+ * var r = await pki.acme.dns01(token, accountJwk, "example.org");
520
+ * r.name; // -> "_acme-challenge.example.org"
521
+ */
522
+ async function dns01(token, accountJwk, domain) {
523
+ if (!_isString(domain)) throw E("acme/bad-identifier", "dns01 requires a domain string");
524
+ var base = domain.indexOf("*.") === 0 ? domain.slice(2) : domain;
525
+ _assertDnsName(base);
526
+ var ka = await keyAuthorization(token, accountJwk);
527
+ return { name: "_acme-challenge." + base, value: jose.base64url.encode(await _sha256(Buffer.from(ka, "ascii"))) };
528
+ }
529
+
530
+ var OID_ACME_IDENTIFIER = oid.byName("acmeIdentifier");
531
+ var OID_SAN = oid.byName("subjectAltName");
532
+ var OID_AKI = oid.byName("authorityKeyIdentifier");
533
+ var OID_CN = oid.byName("commonName");
534
+ var _extNs = pkix.makeNS("acme", AcmeError, oid);
535
+ var _extDecoders = pkix.certExtensionDecoders(_extNs);
536
+ var _extCtx = { E: function (c, m, cause) { return new AcmeError(c, m, cause); }, oid: oid };
537
+
538
+ /**
539
+ * @primitive pki.acme.tlsAlpn01Extension
540
+ * @signature pki.acme.tlsAlpn01Extension(token, accountJwk) -> Promise<Buffer>
541
+ * @since 0.1.25
542
+ * @status experimental
543
+ * @spec RFC 8737
544
+ * @related pki.acme.verifyTlsAlpn01
545
+ *
546
+ * Build the DER of the critical `id-pe-acmeIdentifier` extension (RFC 8737
547
+ * sec. 3): `SEQUENCE { extnID 1.3.6.1.5.5.7.1.31, critical TRUE, extnValue OCTET
548
+ * STRING wrapping Authorization ::= OCTET STRING (SIZE 32) of the
549
+ * SHA-256(keyAuthorization) }`. Placed in the validation certificate.
550
+ *
551
+ * @example
552
+ * var extDer = await pki.acme.tlsAlpn01Extension(token, accountJwk);
553
+ */
554
+ async function tlsAlpn01Extension(token, accountJwk) {
555
+ var ka = await keyAuthorization(token, accountJwk);
556
+ var digest = await _sha256(Buffer.from(ka, "ascii")); // 32 bytes
557
+ var authorization = asn1.build.octetString(digest); // Authorization ::= OCTET STRING (SIZE 32)
558
+ var extnValue = asn1.build.octetString(authorization);
559
+ return asn1.build.sequence([asn1.build.oid(OID_ACME_IDENTIFIER), asn1.build.boolean(true), extnValue]);
560
+ }
561
+
562
+ // The 32-byte Authorization digest inside an acmeIdentifier extnValue, or throw.
563
+ function _readAcmeIdentifier(extnValue) {
564
+ var auth;
565
+ try { auth = asn1.read.octetString(asn1.decode(extnValue)); }
566
+ catch (e) { throw E("acme/bad-tlsalpn", "the acmeIdentifier extnValue is not a well-formed Authorization OCTET STRING", e); }
567
+ if (auth.length !== 32) throw E("acme/bad-tlsalpn", "the acmeIdentifier Authorization must be exactly 32 octets (RFC 8737 sec. 3)");
568
+ return auth;
569
+ }
570
+
571
+ /**
572
+ * @primitive pki.acme.verifyTlsAlpn01
573
+ * @signature pki.acme.verifyTlsAlpn01(certDer, token, accountJwk, identifier) -> Promise<void>
574
+ * @since 0.1.25
575
+ * @status experimental
576
+ * @spec RFC 8737, RFC 8738
577
+ * @related pki.acme.tlsAlpn01Extension
578
+ *
579
+ * Verify a tls-alpn-01 validation certificate (RFC 8737 sec. 3): a CRITICAL
580
+ * `id-pe-acmeIdentifier` extension whose 32-octet Authorization equals
581
+ * SHA-256(keyAuthorization), AND a SubjectAltName with EXACTLY ONE entry -- a
582
+ * dNSName equal to the `dns` identifier (case-insensitive) or a single iPAddress
583
+ * for an `ip` identifier (RFC 8738 sec. 6). Any deviation throws `acme/bad-tlsalpn`.
584
+ *
585
+ * @example
586
+ * await pki.acme.verifyTlsAlpn01(certDer, token, accountJwk, { type: "dns", value: "example.org" });
587
+ */
588
+ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
589
+ var cert = x509.parse(certDer);
590
+ var exts = cert.extensions || [];
591
+ var acmeExt = exts.filter(function (e) { return e.oid === OID_ACME_IDENTIFIER; })[0];
592
+ if (!acmeExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the acmeIdentifier extension (RFC 8737 sec. 3)");
593
+ if (!acmeExt.critical) throw E("acme/bad-tlsalpn", "the acmeIdentifier extension must be critical (RFC 8737 sec. 3)");
594
+ var auth = _readAcmeIdentifier(acmeExt.value);
595
+ var ka = await keyAuthorization(token, accountJwk);
596
+ var expected = await _sha256(Buffer.from(ka, "ascii"));
597
+ if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
598
+ // SAN: exactly one entry, of the identifier's type, equal to its value.
599
+ var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
600
+ if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
601
+ var san = _extDecoders.byOid[OID_SAN](sanExt.value, _extCtx);
602
+ if (!san.names || san.names.length !== 1) throw E("acme/bad-tlsalpn", "the SubjectAltName must carry EXACTLY ONE entry (RFC 8737 sec. 3)");
603
+ var entry = san.names[0];
604
+ if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
605
+ if (identifier.type === "dns") {
606
+ if (entry.tagNumber !== 2) throw E("acme/bad-tlsalpn", "a dns identifier requires a dNSName SAN");
607
+ // Validate the identifier as a base dns name first (rejecting a wildcard *.
608
+ // label or a malformed value), matching the ip branch and the rest of the
609
+ // ACME path -- a tls-alpn-01 identifier must be a concrete, non-wildcard name.
610
+ _assertDnsName(identifier.value);
611
+ if (String(entry.value).toLowerCase() !== identifier.value) throw E("acme/bad-tlsalpn", "the SAN dNSName does not match the identifier");
612
+ } else if (identifier.type === "ip") {
613
+ if (entry.tagNumber !== 7) throw E("acme/bad-tlsalpn", "an ip identifier requires a single iPAddress SAN (RFC 8738 sec. 6)");
614
+ // Reject a NON-canonical identifier (leading-zero octets, uppercase IPv6, an
615
+ // uncompressed run) rather than normalizing-and-guessing it -- the same
616
+ // fail-closed rule the rest of the ACME identifier path applies. After this the
617
+ // identifier value is its own canonical form, so the SAN's canonical text must equal it.
618
+ _assertIpAddress(identifier.value);
619
+ var sanIp = _ipBytesToText(entry.value);
620
+ if (sanIp === null || sanIp !== identifier.value) throw E("acme/bad-tlsalpn", "the iPAddress SAN does not match the ip identifier (RFC 8738 sec. 6)");
621
+ } else {
622
+ throw E("acme/bad-tlsalpn", "unsupported tls-alpn-01 identifier type " + JSON.stringify(identifier.type));
623
+ }
624
+ }
625
+
626
+ // ---- request builders (RFC 8555 sec. 7.x) --------------------------------
627
+
628
+ // The signed outer request is a Flattened JWS under the acme-outer profile: alg
629
+ // + nonce + url + EXACTLY ONE of kid/jwk (jose enforces one-of, nonce, url). The
630
+ // payload is a raw Buffer -- POST-as-GET is an empty Buffer (encodes to ""), a
631
+ // resource POST is the UTF-8 JSON of the payload object. `o` carries the signing
632
+ // key (`key`, a private CryptoKey), its `alg`, the fresh `nonce`, the target
633
+ // `url`, and either `kid` (an account URL) or `jwk` (an embedded public JWK).
634
+ function _outerHeader(o) {
635
+ var h = { alg: o.alg, nonce: o.nonce, url: o.url };
636
+ if (Object.prototype.hasOwnProperty.call(o, "kid")) h.kid = o.kid;
637
+ if (Object.prototype.hasOwnProperty.call(o, "jwk")) h.jwk = o.jwk;
638
+ return h;
639
+ }
640
+ function _payloadBuf(obj) {
641
+ if (obj === undefined) return Buffer.alloc(0); // POST-as-GET
642
+ return Buffer.from(JSON.stringify(obj), "utf8");
643
+ }
644
+ function _signOuter(o, payloadObj) {
645
+ if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
646
+ if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
647
+ return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, jwk: o.jwk, profile: "acme-outer" });
648
+ }
649
+
650
+ /**
651
+ * @primitive pki.acme.postAsGet
652
+ * @signature pki.acme.postAsGet(opts) -> Promise<object>
653
+ * @since 0.1.25
654
+ * @status experimental
655
+ * @spec RFC 8555
656
+ * @related pki.acme.newOrder
657
+ *
658
+ * Build a POST-as-GET request (RFC 8555 sec. 6.3): a JWS whose payload is the
659
+ * EMPTY octet string (`payload: ""`), distinct from a POST of an empty object
660
+ * (`{}`). `opts` carries `{ key, alg, nonce, url, kid }` (an authenticated read
661
+ * is always kid-signed). Returns the flattened JWS.
662
+ *
663
+ * @example
664
+ * await pki.acme.postAsGet({ key, alg: "ES256", nonce, url: orderUrl, kid });
665
+ */
666
+ function postAsGet(o) {
667
+ if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
668
+ // An authenticated read is ALWAYS kid-signed; copy only the kid-mode fields so a
669
+ // leftover jwk (e.g. reused from a newAccount options object) cannot embed a key.
670
+ return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, undefined);
671
+ }
672
+
673
+ // A contact URL (RFC 8555 sec. 7.3): a `mailto:` addr-spec carries no header
674
+ // fields (`?` hfields) and exactly one address (no comma-list) -- fail closed on
675
+ // ambiguity (never send to a guessed recipient). Other URL schemes pass through
676
+ // as opaque strings (the server decides support via unsupportedContact).
677
+ function _assertContacts(contacts) {
678
+ if (!Array.isArray(contacts)) throw E("acme/bad-contact", "contact must be an array of URL strings");
679
+ contacts.forEach(function (c) {
680
+ if (!_isUriString(c)) throw E("acme/bad-contact", "each contact must be a URI string (RFC 8555 sec. 7.1.2)");
681
+ // URI schemes are case-insensitive (RFC 3986), so detect mailto regardless of
682
+ // case -- "MAILTO:a@b?..." must still hit the RFC 6068 header-field guards.
683
+ if (c.slice(0, "mailto:".length).toLowerCase() === "mailto:") {
684
+ var addr = c.slice("mailto:".length);
685
+ if (addr.indexOf("?") !== -1) throw E("acme/bad-contact", "a mailto contact must not carry header fields (RFC 8555 sec. 7.3 / RFC 6068)");
686
+ if (addr.indexOf(",") !== -1) throw E("acme/bad-contact", "a mailto contact must be a single addr-spec, not a comma list (RFC 8555 sec. 7.3)");
687
+ if ((addr.match(/@/g) || []).length !== 1) throw E("acme/bad-contact", "a mailto contact must be exactly one addr-spec");
688
+ }
689
+ });
690
+ }
691
+
692
+ /**
693
+ * @primitive pki.acme.newAccount
694
+ * @signature pki.acme.newAccount(opts) -> Promise<object>
695
+ * @since 0.1.25
696
+ * @status experimental
697
+ * @spec RFC 8555
698
+ * @related pki.acme.externalAccountBinding
699
+ *
700
+ * Build a newAccount request (RFC 8555 sec. 7.3): a jwk-signed JWS (a new account
701
+ * has no kid yet) whose payload MAY carry `contact` (mailto validated fail-closed),
702
+ * `termsOfServiceAgreed`, `onlyReturnExisting`, and an `externalAccountBinding`
703
+ * (an EAB inner JWS from `externalAccountBinding`). `opts` = `{ key, alg, nonce,
704
+ * url, jwk, contact?, termsOfServiceAgreed?, onlyReturnExisting?, externalAccountBinding? }`.
705
+ *
706
+ * @example
707
+ * await pki.acme.newAccount({ key, alg: "ES256", nonce, url, jwk, termsOfServiceAgreed: true });
708
+ */
709
+ function newAccount(o) {
710
+ if (!_isObject(o) || !_isObject(o.jwk)) throw E("acme/bad-input", "newAccount must embed the account public jwk (RFC 8555 sec. 7.3)");
711
+ var payload = {};
712
+ if (o.contact !== undefined) { _assertContacts(o.contact); payload.contact = o.contact; }
713
+ // Require actual booleans -- never coerce a string like "false"/"0", which `!!`
714
+ // would serialize as `true`, silently agreeing to Terms of Service or forcing
715
+ // onlyReturnExisting.
716
+ if (o.termsOfServiceAgreed !== undefined) {
717
+ if (typeof o.termsOfServiceAgreed !== "boolean") throw E("acme/bad-input", "termsOfServiceAgreed must be a boolean");
718
+ payload.termsOfServiceAgreed = o.termsOfServiceAgreed;
719
+ }
720
+ if (o.onlyReturnExisting !== undefined) {
721
+ if (typeof o.onlyReturnExisting !== "boolean") throw E("acme/bad-input", "onlyReturnExisting must be a boolean");
722
+ payload.onlyReturnExisting = o.onlyReturnExisting;
723
+ }
724
+ if (o.externalAccountBinding !== undefined) {
725
+ if (!_isObject(o.externalAccountBinding)) throw E("acme/bad-input", "externalAccountBinding must be an EAB inner JWS object");
726
+ payload.externalAccountBinding = o.externalAccountBinding;
727
+ }
728
+ return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
729
+ }
730
+
731
+ var _HMAC_HASH = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
732
+
733
+ /**
734
+ * @primitive pki.acme.externalAccountBinding
735
+ * @signature pki.acme.externalAccountBinding(opts) -> Promise<object>
736
+ * @since 0.1.25
737
+ * @status experimental
738
+ * @spec RFC 8555
739
+ * @related pki.acme.newAccount
740
+ *
741
+ * Build the External Account Binding inner JWS (RFC 8555 sec. 7.3.4): a MAC-only
742
+ * (`HS256`/`HS384`/`HS512`) JWS over the account public JWK, keyed by the CA-issued
743
+ * `kid` + symmetric `macKey` (a raw `Buffer` or an HMAC `CryptoKey`), `url` equal to
744
+ * the newAccount URL, NO nonce. `opts` = `{ macKey, kid, url, accountJwk, alg? }`
745
+ * (alg default `HS256`). The result is embedded as newAccount's `externalAccountBinding`.
746
+ *
747
+ * @example
748
+ * var eab = await pki.acme.externalAccountBinding({ macKey, kid: "abc123", url, accountJwk });
749
+ */
750
+ async function externalAccountBinding(o) {
751
+ if (!_isObject(o) || !_isObject(o.accountJwk)) throw E("acme/bad-input", "externalAccountBinding requires the account public jwk (sec. 7.3.4)");
752
+ jose.assertPublicJwk(o.accountJwk); // the account JWK is published in the EAB payload -> public-only
753
+ if (!_isString(o.kid)) throw E("acme/bad-input", "externalAccountBinding requires the CA-issued kid");
754
+ var alg = o.alg || "HS256";
755
+ if (!_HMAC_HASH[alg]) throw E("acme/bad-input", "an EAB inner JWS must use an HS* MAC algorithm (sec. 7.3.4), not " + JSON.stringify(alg));
756
+ var key = o.macKey;
757
+ if (Buffer.isBuffer(key)) {
758
+ try { key = await subtle.importKey("raw", key, { name: "HMAC", hash: _HMAC_HASH[alg] }, false, ["sign"]); }
759
+ catch (e) { throw E("acme/bad-input", "the EAB macKey could not be imported as an HMAC key", e); }
760
+ } else if (!key || typeof key !== "object" || key.type !== "secret") {
761
+ // Anything that is neither a raw Buffer nor a secret-key CryptoKey would reach
762
+ // subtle.sign and throw a bare TypeError; fail closed with a typed fault instead.
763
+ throw E("acme/bad-input", "the EAB macKey must be a raw Buffer or an HMAC (secret) CryptoKey");
764
+ }
765
+ return jose.sign({ protected: { alg: alg, kid: o.kid, url: o.url }, payload: _payloadBuf(o.accountJwk), key: key, profile: "eab-inner" });
766
+ }
767
+
768
+ // An order identifier MAY carry a wildcard: EXACTLY ONE leading `*.` label, `dns`
769
+ // only (sec. 7.1.3). An `ip` identifier has no wildcard form. Everything else is
770
+ // the shared _validateIdentifier syntax on the base name.
771
+ function _validateOrderIdentifier(id) {
772
+ if (!_isObject(id) || !_isString(id.type) || !_isString(id.value)) throw E("acme/bad-identifier", "an order identifier must be { type, value } strings");
773
+ if (id.type === "dns") {
774
+ var v = id.value;
775
+ if (v.indexOf("*.") === 0) {
776
+ v = v.slice(2);
777
+ if (v.indexOf("*") !== -1) throw E("acme/bad-identifier", "a wildcard order identifier permits exactly one leading *. label (RFC 8555 sec. 7.1.3)");
778
+ } else if (v.indexOf("*") !== -1) {
779
+ throw E("acme/bad-identifier", "a wildcard must be a single leading *. label (RFC 8555 sec. 7.1.3)");
780
+ }
781
+ _assertDnsName(v);
782
+ } else if (id.type === "ip") {
783
+ if (id.value.indexOf("*") !== -1) throw E("acme/bad-identifier", "an ip identifier has no wildcard form (RFC 8738)");
784
+ _assertIpAddress(id.value);
785
+ }
786
+ return id;
787
+ }
788
+
789
+ /**
790
+ * @primitive pki.acme.newOrder
791
+ * @signature pki.acme.newOrder(opts) -> Promise<object>
792
+ * @since 0.1.25
793
+ * @status experimental
794
+ * @spec RFC 8555, RFC 9773
795
+ * @related pki.acme.finalize, pki.acme.ariCertId
796
+ *
797
+ * Build a newOrder request (RFC 8555 sec. 7.4): a kid-signed JWS whose payload
798
+ * carries a non-empty validated `identifiers` array (each `dns`/`ip`, one leading
799
+ * `*.` wildcard permitted for `dns`), optional `notBefore`/`notAfter`, and an
800
+ * optional RFC 9773 `replaces` (the ARI certID of the certificate being renewed).
801
+ * `opts` = `{ key, alg, nonce, url, kid, identifiers, notBefore?, notAfter?, replaces? }`.
802
+ *
803
+ * @example
804
+ * await pki.acme.newOrder({ key, alg: "ES256", nonce, url, kid, identifiers: [{ type: "dns", value: "example.org" }] });
805
+ */
806
+ function newOrder(o) {
807
+ if (!_isObject(o) || !Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-order", "newOrder requires a non-empty identifiers array (RFC 8555 sec. 7.4)");
808
+ o.identifiers.forEach(_validateOrderIdentifier);
809
+ var payload = { identifiers: o.identifiers };
810
+ if (o.notBefore !== undefined) { if (!_isRfc3339(o.notBefore)) throw E("acme/bad-order", "notBefore must be an RFC 3339 date-time"); payload.notBefore = o.notBefore; }
811
+ if (o.notAfter !== undefined) { if (!_isRfc3339(o.notAfter)) throw E("acme/bad-order", "notAfter must be an RFC 3339 date-time"); payload.notAfter = o.notAfter; }
812
+ if (o.replaces !== undefined) { if (!_isString(o.replaces)) throw E("acme/bad-order", "replaces must be an ARI certID string (RFC 9773 sec. 5)"); payload.replaces = o.replaces; }
813
+ return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload);
814
+ }
815
+
816
+ // The account key's SubjectPublicKeyInfo DER, imported from its public JWK, so a
817
+ // finalize CSR carrying that same key is caught (sec. 11.1). The DER is canonical
818
+ // (node emits canonical SPKI), matched byte-for-byte against the CSR's strict-DER SPKI.
819
+ async function _jwkToSpki(jwk) {
820
+ var importAlg;
821
+ if (jwk.kty === "EC") importAlg = { name: "ECDSA", namedCurve: jwk.crv };
822
+ else if (jwk.kty === "RSA") importAlg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
823
+ else if (jwk.kty === "OKP") importAlg = { name: jwk.crv };
824
+ else if (jwk.kty === "AKP") importAlg = { name: jwk.alg };
825
+ else throw E("acme/bad-key", "unsupported account key type " + JSON.stringify(jwk && jwk.kty));
826
+ var key;
827
+ try { key = await subtle.importKey("jwk", jwk, importAlg, true, []); }
828
+ catch (e) { throw E("acme/bad-key", "the account key JWK could not be imported to derive its SubjectPublicKeyInfo", e); }
829
+ return Buffer.from(await subtle.exportKey("spki", key));
830
+ }
831
+
832
+ // EVERY common name in the subject (a DN may carry more than one CN RDN) -- taking
833
+ // only the first would let a second CN smuggle an identifier past the order-set match.
834
+ function _subjectCommonNames(subject) {
835
+ var names = [];
836
+ if (!subject || !Array.isArray(subject.rdns)) return names;
837
+ subject.rdns.forEach(function (atvs) {
838
+ atvs.forEach(function (atv) {
839
+ if ((atv.name === "commonName" || atv.type === OID_CN) && _isString(atv.value)) names.push(atv.value);
840
+ });
841
+ });
842
+ return names;
843
+ }
844
+
845
+ // An iPAddress SAN octet string (4 = IPv4, 16 = IPv6) to its RFC 8738 canonical
846
+ // text -- the same form an order ip identifier carries -- or null on a bad length.
847
+ function _ipBytesToText(buf) {
848
+ if (!Buffer.isBuffer(buf)) return null;
849
+ if (buf.length === 4) return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
850
+ if (buf.length === 16) {
851
+ var groups = [];
852
+ for (var i = 0; i < 16; i += 2) groups.push(((buf[i] << 8) | buf[i + 1]).toString(16));
853
+ return _canonicalizeIpv6(groups.join(":"));
854
+ }
855
+ return null;
856
+ }
857
+
858
+ // The set of identifiers a CSR requests: the subject CN (counted as dns) plus the
859
+ // SAN dNSName / iPAddress entries in the extensionRequest attribute, keyed
860
+ // "dns:<lower>" / "ip:<canonical>" for an order-insensitive compare.
861
+ function _csrIdentifierSet(parsedCsr) {
862
+ var set = {};
863
+ _subjectCommonNames(parsedCsr.subject).forEach(function (cn) { set["dns:" + cn.toLowerCase()] = true; });
864
+ // PKCS#10 permits duplicate attribute types, so aggregate EVERY extensionRequest
865
+ // attribute and EVERY subjectAltName within each -- taking only the first would let
866
+ // a second extensionRequest smuggle identifiers past the order-set comparison.
867
+ (parsedCsr.attributes || []).forEach(function (a) {
868
+ if (a.type !== oid.byName("extensionRequest") || !Array.isArray(a.extensions)) return;
869
+ a.extensions.forEach(function (e) {
870
+ if (e.oid !== OID_SAN) return;
871
+ var dec = _extDecoders.byOid[OID_SAN](e.value, _extCtx);
872
+ (dec.names || []).forEach(function (n) {
873
+ if (n.tagNumber === 2) set["dns:" + String(n.value).toLowerCase()] = true;
874
+ else if (n.tagNumber === 7) { var t = _ipBytesToText(n.value); if (t) set["ip:" + t] = true; }
875
+ });
876
+ });
877
+ });
878
+ return set;
879
+ }
880
+
881
+ function _orderIdentifierSet(identifiers) {
882
+ var set = {};
883
+ identifiers.forEach(function (id) {
884
+ if (id.type === "dns") set["dns:" + id.value.toLowerCase()] = true;
885
+ else if (id.type === "ip") set["ip:" + id.value] = true;
886
+ });
887
+ return set;
888
+ }
889
+
890
+ function _assertCsrIdentifiers(parsedCsr, identifiers) {
891
+ var have = _csrIdentifierSet(parsedCsr);
892
+ var want = _orderIdentifierSet(identifiers);
893
+ var haveKeys = Object.keys(have), wantKeys = Object.keys(want);
894
+ var mismatch = haveKeys.length !== wantKeys.length ||
895
+ wantKeys.some(function (k) { return !have[k]; }) ||
896
+ haveKeys.some(function (k) { return !want[k]; });
897
+ if (mismatch) {
898
+ throw E("acme/csr-identifier-mismatch", "the finalize CSR identifier set " + JSON.stringify(haveKeys.sort()) +
899
+ " does not equal the order identifiers " + JSON.stringify(wantKeys.sort()) + " (RFC 8555 sec. 7.4)");
900
+ }
901
+ }
902
+
903
+ /**
904
+ * @primitive pki.acme.finalize
905
+ * @signature pki.acme.finalize(opts) -> Promise<object>
906
+ * @since 0.1.25
907
+ * @status experimental
908
+ * @spec RFC 8555
909
+ * @related pki.acme.newOrder
910
+ *
911
+ * Build a finalize request (RFC 8555 sec. 7.4): a kid-signed JWS whose payload
912
+ * `csr` is the base64url of the DER PKCS#10 (never PEM). The CSR is parsed with
913
+ * `pki.schema.csr.parse`; its requested identifier set (SAN + CN) MUST equal the
914
+ * order identifiers (`acme/csr-identifier-mismatch`), and its public key MUST NOT
915
+ * be the account key (`acme/key-reuse`, sec. 11.1). `opts` = `{ key, alg, nonce,
916
+ * url, kid, csr (DER Buffer), identifiers?, accountJwk? }`.
917
+ *
918
+ * @example
919
+ * await pki.acme.finalize({ key, alg: "ES256", nonce, url, kid, csr: csrDer, identifiers, accountJwk });
920
+ */
921
+ async function finalize(o) {
922
+ if (!_isObject(o) || !Buffer.isBuffer(o.csr)) throw E("acme/bad-input", "finalize requires a DER CSR Buffer (opts.csr)");
923
+ var parsed = csr.parse(o.csr); // strict DER; rejects PEM/garbage
924
+ if (o.accountJwk !== undefined) {
925
+ var accountSpki = await _jwkToSpki(o.accountJwk);
926
+ if (parsed.subjectPublicKeyInfo && Buffer.isBuffer(parsed.subjectPublicKeyInfo.bytes) &&
927
+ parsed.subjectPublicKeyInfo.bytes.equals(accountSpki)) {
928
+ throw E("acme/key-reuse", "the finalize CSR public key must not be the account key (RFC 8555 sec. 11.1)");
929
+ }
930
+ }
931
+ if (o.identifiers !== undefined) {
932
+ if (!Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-input", "finalize identifiers must be the non-empty order identifier array");
933
+ o.identifiers.forEach(_validateOrderIdentifier); // caller-supplied -> validate before comparing (no raw TypeError)
934
+ _assertCsrIdentifiers(parsed, o.identifiers);
935
+ }
936
+ var payload = { csr: jose.base64url.encode(o.csr) };
937
+ return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload);
938
+ }
939
+
940
+ /**
941
+ * @primitive pki.acme.challengeResponse
942
+ * @signature pki.acme.challengeResponse(opts) -> Promise<object>
943
+ * @since 0.1.25
944
+ * @status experimental
945
+ * @spec RFC 8555
946
+ * @related pki.acme.http01, pki.acme.dns01
947
+ *
948
+ * Build a challenge-response POST (RFC 8555 sec. 7.5.1): a kid-signed JWS whose
949
+ * payload is the type-defined response object -- `{}` for the three registered
950
+ * challenge types (http-01 / dns-01 / tls-alpn-01), which is DISTINCT from a
951
+ * POST-as-GET empty payload. `opts` = `{ key, alg, nonce, url, kid, payload? }`
952
+ * (payload default `{}`; pass a custom object for a future challenge type).
953
+ *
954
+ * @example
955
+ * await pki.acme.challengeResponse({ key, alg: "ES256", nonce, url: challUrl, kid });
956
+ */
957
+ function challengeResponse(o) {
958
+ if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
959
+ var payload = o.payload !== undefined ? o.payload : {};
960
+ if (!_isObject(payload)) throw E("acme/bad-input", "a challenge response payload must be a JSON object (RFC 8555 sec. 7.5.1)");
961
+ return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload);
962
+ }
963
+
964
+ /**
965
+ * @primitive pki.acme.deactivate
966
+ * @signature pki.acme.deactivate(opts) -> Promise<object>
967
+ * @since 0.1.25
968
+ * @status experimental
969
+ * @spec RFC 8555
970
+ * @related pki.acme.validate
971
+ *
972
+ * Build a deactivation POST (RFC 8555 sec. 7.3.6 account / sec. 7.5.2
973
+ * authorization): a kid-signed JWS with the payload `{"status":"deactivated"}` --
974
+ * the only client-settable status. `opts` = `{ key, alg, nonce, url, kid }`.
975
+ *
976
+ * @example
977
+ * await pki.acme.deactivate({ key, alg: "ES256", nonce, url: authzUrl, kid });
978
+ */
979
+ function deactivate(o) {
980
+ if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
981
+ return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { status: "deactivated" });
982
+ }
983
+
984
+ /**
985
+ * @primitive pki.acme.revokeCert
986
+ * @signature pki.acme.revokeCert(opts) -> Promise<object>
987
+ * @since 0.1.25
988
+ * @status experimental
989
+ * @spec RFC 8555, RFC 5280
990
+ * @related pki.acme.ariCertId
991
+ *
992
+ * Build a revokeCert request (RFC 8555 sec. 7.6): a JWS whose payload `certificate`
993
+ * is the base64url of the DER certificate and optional `reason` is an assigned
994
+ * RFC 5280 CRLReason (0-6, 8-10; 7 is unassigned). Signed EITHER by the account key (`kid` mode) OR by the
995
+ * certificate key (`jwk` mode) -- pass exactly one. `opts` = `{ key, alg, nonce,
996
+ * url, certificate (DER Buffer), reason?, kid? | jwk? }`.
997
+ *
998
+ * @example
999
+ * await pki.acme.revokeCert({ key, alg: "ES256", nonce, url, kid, certificate: certDer, reason: 1 });
1000
+ */
1001
+ function revokeCert(o) {
1002
+ if (!_isObject(o) || !Buffer.isBuffer(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate Buffer (opts.certificate)");
1003
+ x509.parse(o.certificate); // structural validation of the target
1004
+ var hasKid = Object.prototype.hasOwnProperty.call(o, "kid");
1005
+ var hasJwk = Object.prototype.hasOwnProperty.call(o, "jwk");
1006
+ if (hasKid === hasJwk) throw E("acme/bad-input", "revokeCert must be signed with EXACTLY ONE of the account kid or the certificate jwk (RFC 8555 sec. 7.6)");
1007
+ var payload = { certificate: jose.base64url.encode(o.certificate) };
1008
+ if (o.reason !== undefined) {
1009
+ if (typeof o.reason !== "number" || !isFinite(o.reason) || Math.floor(o.reason) !== o.reason || CRL_REASONS.indexOf(o.reason) === -1) {
1010
+ throw E("acme/bad-revocation-reason", "reason must be an assigned RFC 5280 CRLReason (0-6, 8-10; value 7 is unassigned)");
1011
+ }
1012
+ payload.reason = o.reason;
1013
+ }
1014
+ var header = { alg: o.alg, nonce: o.nonce, url: o.url };
1015
+ if (hasKid) header.kid = o.kid; else header.jwk = o.jwk;
1016
+ return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
1017
+ }
1018
+
1019
+ /**
1020
+ * @primitive pki.acme.keyChange
1021
+ * @signature pki.acme.keyChange(opts) -> Promise<object>
1022
+ * @since 0.1.25
1023
+ * @status experimental
1024
+ * @spec RFC 8555
1025
+ * @related pki.acme.newAccount
1026
+ *
1027
+ * Build a key-change request (RFC 8555 sec. 7.3.5): a nested JWS. The INNER JWS is
1028
+ * signed by the NEW account key (embedded `jwk`, no nonce, `url` == the keyChange
1029
+ * URL) over `{ account, oldKey }`; the OUTER JWS is the account (`kid`, `oldKey`)
1030
+ * signing that inner object. `opts` = `{ key (old private), alg (old), kid
1031
+ * (account URL), account (account URL), oldKey (old public JWK), newKey (new
1032
+ * private), newJwk (new public JWK), newAlg, nonce, url }`.
1033
+ *
1034
+ * @example
1035
+ * await pki.acme.keyChange({ key: oldKey, alg: "ES256", kid, account: kid, oldKey: oldJwk, newKey, newJwk, newAlg: "ES256", nonce, url });
1036
+ */
1037
+ async function keyChange(o) {
1038
+ if (!_isObject(o)) throw E("acme/bad-input", "a keyChange options object is required");
1039
+ if (!_isString(o.account)) throw E("acme/bad-input", "keyChange requires the account URL (payload account)");
1040
+ if (!_isObject(o.oldKey)) throw E("acme/bad-input", "keyChange requires the old account public jwk (payload oldKey)");
1041
+ if (!_isObject(o.newJwk)) throw E("acme/bad-input", "keyChange requires the new account public jwk (inner header jwk)");
1042
+ // oldKey is published in the inner payload and newJwk in the inner header -> both public-only.
1043
+ jose.assertPublicJwk(o.oldKey);
1044
+ jose.assertPublicJwk(o.newJwk);
1045
+ var inner = await jose.sign({
1046
+ protected: { alg: o.newAlg, url: o.url, jwk: o.newJwk },
1047
+ payload: _payloadBuf({ account: o.account, oldKey: o.oldKey }),
1048
+ key: o.newKey, jwk: o.newJwk, profile: "keychange-inner",
1049
+ });
1050
+ return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload: _payloadBuf(inner), key: o.key, profile: "acme-outer" });
1051
+ }
1052
+
1053
+ // ---- ARI (RFC 9773 renewal information) ----------------------------------
1054
+
1055
+ /**
1056
+ * @primitive pki.acme.ariCertId
1057
+ * @signature pki.acme.ariCertId(certDer) -> string
1058
+ * @since 0.1.25
1059
+ * @status experimental
1060
+ * @spec RFC 9773
1061
+ * @related pki.acme.parseAriCertId, pki.acme.newOrder
1062
+ *
1063
+ * The RFC 9773 sec. 4.1 ARI certificate identifier of a DER certificate:
1064
+ * `base64url(AKI keyIdentifier) || '.' || base64url(serial content octets)`. The
1065
+ * serial is the raw DER INTEGER content -- its leading `00` sign-padding byte is
1066
+ * PRESERVED (dropping it is the documented mass-404 client bug). Throws
1067
+ * `acme/bad-certid` if the certificate lacks an AKI keyIdentifier.
1068
+ *
1069
+ * @example
1070
+ * pki.acme.ariCertId(certDer); // -> "<b64u-aki>.<b64u-serial>"
1071
+ */
1072
+ function ariCertId(certDer) {
1073
+ if (!Buffer.isBuffer(certDer)) throw E("acme/bad-input", "ariCertId requires a DER certificate Buffer");
1074
+ var cert = x509.parse(certDer);
1075
+ var akiExt = (cert.extensions || []).filter(function (e) { return e.oid === OID_AKI; })[0];
1076
+ if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
1077
+ var aki = _extDecoders.byOid[OID_AKI](akiExt.value, _extCtx);
1078
+ if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
1079
+ var serialBytes = Buffer.from(cert.serialNumberHex, "hex"); // DER INTEGER content; sign-pad preserved
1080
+ return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
1081
+ }
1082
+
1083
+ /**
1084
+ * @primitive pki.acme.parseAriCertId
1085
+ * @signature pki.acme.parseAriCertId(certId) -> { keyIdentifier, serial }
1086
+ * @since 0.1.25
1087
+ * @status experimental
1088
+ * @spec RFC 9773
1089
+ * @related pki.acme.ariCertId
1090
+ *
1091
+ * Parse an ARI certID string (RFC 9773 sec. 4.1) into `{ keyIdentifier, serial }`
1092
+ * Buffers. The two dot-joined halves are each strict base64url (padding /
1093
+ * non-alphabet rejected); anything but exactly two parts throws `acme/bad-certid`.
1094
+ *
1095
+ * @example
1096
+ * pki.acme.parseAriCertId("<b64u-aki>.<b64u-serial>").serial; // -> Buffer
1097
+ */
1098
+ function parseAriCertId(certId) {
1099
+ if (!_isString(certId)) throw E("acme/bad-certid", "an ARI certID must be a string");
1100
+ var parts = certId.split(".");
1101
+ if (parts.length !== 2 || !parts[0] || !parts[1]) throw E("acme/bad-certid", "an ARI certID must be two base64url halves joined by '.' (RFC 9773 sec. 4.1)");
1102
+ var keyIdentifier, serial;
1103
+ try { keyIdentifier = jose.base64url.decode(parts[0]); serial = jose.base64url.decode(parts[1]); }
1104
+ catch (e) { throw E("acme/bad-certid", "an ARI certID half is not strict base64url (RFC 9773 sec. 4.1)", e); }
1105
+ return { keyIdentifier: keyIdentifier, serial: serial };
1106
+ }
1107
+
1108
+ /**
1109
+ * @primitive pki.acme.validateRenewalInfo
1110
+ * @signature pki.acme.validateRenewalInfo(obj) -> obj
1111
+ * @since 0.1.25
1112
+ * @status experimental
1113
+ * @spec RFC 9773
1114
+ * @related pki.acme.validate
1115
+ *
1116
+ * Validate an ARI RenewalInfo object (RFC 9773 sec. 4.2): a `suggestedWindow` with
1117
+ * RFC 3339 `start` and `end`, `end` strictly after `start` (an inverted or
1118
+ * zero-width window throws `acme/bad-renewal-window` -- the client treats it as no
1119
+ * response, defusing a renewal stampede), and an optional `explanationURL`.
1120
+ * Returns the object.
1121
+ *
1122
+ * @example
1123
+ * pki.acme.validateRenewalInfo({ suggestedWindow: { start: "2026-01-01T00:00:00Z", end: "2026-01-08T00:00:00Z" } });
1124
+ */
1125
+ function validateRenewalInfo(obj) {
1126
+ _validate("renewalInfo", obj);
1127
+ var w = obj.suggestedWindow;
1128
+ if (!_isObject(w) || !_isRfc3339(w.start) || !_isRfc3339(w.end)) throw E("acme/bad-renewal-window", "a renewalInfo suggestedWindow must carry RFC 3339 start and end (RFC 9773 sec. 4.2)");
1129
+ if (Date.parse(w.end) <= Date.parse(w.start)) throw E("acme/bad-renewal-window", "the renewal window end must be strictly after start (RFC 9773 sec. 4.2)");
1130
+ return obj;
1131
+ }
1132
+
1133
+ module.exports = {
1134
+ validate: validate,
1135
+ validateProblem: validateProblem,
1136
+ validateRenewalInfo: validateRenewalInfo,
1137
+ identify: identify,
1138
+ assertTransition: assertTransition,
1139
+ keyAuthorization: keyAuthorization,
1140
+ http01: http01,
1141
+ dns01: dns01,
1142
+ tlsAlpn01Extension: tlsAlpn01Extension,
1143
+ verifyTlsAlpn01: verifyTlsAlpn01,
1144
+ // request builders
1145
+ postAsGet: postAsGet,
1146
+ newAccount: newAccount,
1147
+ externalAccountBinding: externalAccountBinding,
1148
+ newOrder: newOrder,
1149
+ finalize: finalize,
1150
+ challengeResponse: challengeResponse,
1151
+ deactivate: deactivate,
1152
+ revokeCert: revokeCert,
1153
+ keyChange: keyChange,
1154
+ // ARI (RFC 9773)
1155
+ ariCertId: ariCertId,
1156
+ parseAriCertId: parseAriCertId,
1157
+ // re-exported for the ACME consumer / test surface
1158
+ jose: jose,
1159
+ };