@blamejs/pki 0.6.14 → 0.6.15

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/lib/acme.js +59 -815
  3. package/lib/asn1-der.js +7 -245
  4. package/lib/attrcert-sign.js +12 -260
  5. package/lib/attrcert-verify.js +1 -7
  6. package/lib/byte-reader.js +0 -8
  7. package/lib/byte-writer.js +0 -11
  8. package/lib/cbor-det.js +0 -54
  9. package/lib/cmc-build.js +15 -357
  10. package/lib/cmc-verify.js +6 -239
  11. package/lib/cmp-build.js +41 -317
  12. package/lib/cmp-session.js +73 -760
  13. package/lib/cmp-verify.js +32 -488
  14. package/lib/cms-compress.js +3 -32
  15. package/lib/cms-decrypt.js +29 -294
  16. package/lib/cms-encrypt.js +14 -234
  17. package/lib/cms-sign.js +29 -324
  18. package/lib/cms-verify.js +9 -500
  19. package/lib/composite-kem.js +2 -102
  20. package/lib/composite-sig.js +1 -77
  21. package/lib/constants.js +0 -249
  22. package/lib/crl-sign.js +9 -237
  23. package/lib/crl-verify.js +1 -10
  24. package/lib/crmf-sign.js +19 -198
  25. package/lib/crmf-verify.js +1 -12
  26. package/lib/csr-sign.js +3 -34
  27. package/lib/csr-verify.js +1 -12
  28. package/lib/ct.js +28 -253
  29. package/lib/edwards-point.js +10 -44
  30. package/lib/est.js +28 -764
  31. package/lib/framework-error.js +4 -182
  32. package/lib/guard-all.js +1 -58
  33. package/lib/guard-async.js +1 -36
  34. package/lib/guard-bytes.js +6 -650
  35. package/lib/guard-compress.js +5 -102
  36. package/lib/guard-crypto.js +1 -40
  37. package/lib/guard-der.js +2 -93
  38. package/lib/guard-encoding.js +2 -60
  39. package/lib/guard-header.js +1 -36
  40. package/lib/guard-identifier.js +7 -531
  41. package/lib/guard-intrinsic.js +1 -150
  42. package/lib/guard-json.js +10 -73
  43. package/lib/guard-limits.js +1 -77
  44. package/lib/guard-list.js +1 -49
  45. package/lib/guard-name.js +3 -209
  46. package/lib/guard-parsed.js +7 -335
  47. package/lib/guard-range.js +1 -71
  48. package/lib/guard-secret.js +1 -102
  49. package/lib/guard-text.js +1 -64
  50. package/lib/guard-time.js +1 -97
  51. package/lib/hpke.js +3 -105
  52. package/lib/http-digest.js +14 -142
  53. package/lib/http-retry-after.js +1 -47
  54. package/lib/http-transport.js +21 -131
  55. package/lib/inspect.js +30 -284
  56. package/lib/ip-utils.js +6 -47
  57. package/lib/jose.js +7 -142
  58. package/lib/key.js +2 -67
  59. package/lib/lint.js +9 -194
  60. package/lib/merkle.js +1 -27
  61. package/lib/mime.js +9 -82
  62. package/lib/ocsp-request-verify.js +1 -14
  63. package/lib/ocsp-verify.js +1 -82
  64. package/lib/ocsp.js +11 -169
  65. package/lib/oid.js +5 -337
  66. package/lib/path-validate.js +103 -1492
  67. package/lib/pbes2.js +2 -89
  68. package/lib/pkcs12-build.js +31 -379
  69. package/lib/pki-build.js +64 -290
  70. package/lib/rc2.js +2 -18
  71. package/lib/rfc3339.js +4 -27
  72. package/lib/scep.js +8 -252
  73. package/lib/schema-all.js +0 -122
  74. package/lib/schema-attrcert.js +4 -146
  75. package/lib/schema-c509.js +153 -802
  76. package/lib/schema-cmc.js +5 -220
  77. package/lib/schema-cmp.js +2 -223
  78. package/lib/schema-cms.js +13 -427
  79. package/lib/schema-crl.js +5 -68
  80. package/lib/schema-crmf.js +6 -167
  81. package/lib/schema-csr.js +1 -54
  82. package/lib/schema-csrattrs.js +2 -87
  83. package/lib/schema-engine.js +0 -152
  84. package/lib/schema-ocsp.js +0 -117
  85. package/lib/schema-pkcs12.js +2 -93
  86. package/lib/schema-pkcs8.js +3 -35
  87. package/lib/schema-pkix.js +24 -655
  88. package/lib/schema-smime.js +0 -54
  89. package/lib/schema-tsp.js +3 -99
  90. package/lib/schema-x509.js +4 -63
  91. package/lib/shbs.js +11 -95
  92. package/lib/sign-scheme.js +2 -97
  93. package/lib/sigstore.js +9 -169
  94. package/lib/sleep.js +2 -8
  95. package/lib/smime.js +15 -408
  96. package/lib/tls-cert-compress.js +0 -62
  97. package/lib/trust.js +19 -185
  98. package/lib/tsp-sign.js +16 -270
  99. package/lib/validator-all.js +1 -30
  100. package/lib/validator-attcert.js +2 -46
  101. package/lib/validator-cose.js +2 -153
  102. package/lib/validator-keydesc.js +1 -37
  103. package/lib/validator-sig.js +1 -46
  104. package/lib/validator-tls.js +1 -88
  105. package/lib/validator-tpm.js +19 -122
  106. package/lib/verify-seam.js +1 -24
  107. package/lib/webauthn-mds.js +3 -400
  108. package/lib/webauthn.js +12 -833
  109. package/lib/webcrypto.js +5 -485
  110. package/lib/x509-sign.js +4 -108
  111. package/package.json +1 -1
  112. package/sbom.cdx.json +6 -6
package/lib/acme.js CHANGED
@@ -51,14 +51,7 @@ var httpTransport = require("./http-transport");
51
51
  var retryAfter = require("./http-retry-after");
52
52
  var guard = require("./guard-all");
53
53
  var intrinsic = require("./guard-intrinsic");
54
- // Membership is asked through an operation taken at load. Written out, the question reads two
55
- // replaceable properties to answer one thing, and either answering wrongly changes whether a field
56
- // counts as present in a document this client is deciding how to act on.
57
54
  var _hasOwn = intrinsic.hasOwn;
58
- // The operations the issued-certificate binding decides with, taken at load. That check runs on bytes
59
- // a server chose, at a moment a caller controls, and it is the only thing standing between an
60
- // enrollment and a certificate for someone else's name -- so what it decides with cannot be a property
61
- // read when it runs. See guard-intrinsic for the whole captured set.
62
55
  var _isBuffer = intrinsic.isBuffer;
63
56
  var _isArrayI = intrinsic.isArray;
64
57
  var _forEach = intrinsic.forEach;
@@ -80,17 +73,15 @@ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
80
73
  var _charAt = intrinsic.uncurry(String.prototype.charAt);
81
74
  var pkgVersion = require("../package.json").version;
82
75
 
83
- // Charset tables + character-scan validators/trimmers replacing acme's rule-#11 regexes. The token / link-token
84
- // tables feed pkix.allCharsIn (a NON-EMPTY string of only these characters); the rest scan by character code.
85
- var _TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"); // ^[A-Za-z0-9_-]
86
- var _LINK_TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+.^_`|~-"); // RFC 7230 tchar
87
- function _isHex1to4(s) { // /^[0-9a-f]{1,4}$/
76
+ var _TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-");
77
+ var _LINK_TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+.^_`|~-");
78
+ function _isHex1to4(s) {
88
79
  var n = s.length;
89
80
  if (n < 1 || n > 4) return false;
90
81
  for (var i = 0; i < n; i++) { var c = _charCodeAt(s, i); if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) return false; }
91
82
  return true;
92
83
  }
93
- function _looksLikeIpv4(s) { // /^\d{1,3}(\.\d{1,3}){3}$/ -- a loose dotted quad (octets are not range-checked)
84
+ function _looksLikeIpv4(s) {
94
85
  var n = s.length, i = 0, octet, d;
95
86
  for (octet = 0; octet < 4; octet++) {
96
87
  if (octet > 0) { if (i >= n || _charCodeAt(s, i) !== 0x2e) return false; i++; }
@@ -99,19 +90,17 @@ function _looksLikeIpv4(s) { // /^\d{1,3}(\.\d{1,3}){3}$/ -- a loose dotted qu
99
90
  }
100
91
  return i === n;
101
92
  }
102
- function _countAtSigns(s) { var c = 0; for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x40) c++; } return c; } // (s.match(/@/g)||[]).length
103
- function _stripLeadingOWS(s, from) { // s.slice(from).replace(/^[ \t]+/, "")
93
+ function _countAtSigns(s) { var c = 0; for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x40) c++; } return c; }
94
+ function _stripLeadingOWS(s, from) {
104
95
  var i = from;
105
96
  while (i < s.length && (_charCodeAt(s, i) === 0x20 || _charCodeAt(s, i) === 0x09)) i++;
106
97
  return _strSlice(s, i);
107
98
  }
108
- function _stripTrailingSlash(s) { // s.replace(/\/+$/, "")
99
+ function _stripTrailingSlash(s) {
109
100
  var end = s.length;
110
101
  while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end--;
111
102
  return _strSlice(s, 0, end);
112
103
  }
113
- // /^(?:[^"\\]|\\.)*$/ : a valid quoted-string interior -- no unescaped '"', every '\' escapes a following
114
- // NON-line-terminator character (the regex '.' matches no line terminator), and no dangling final '\'.
115
104
  function _isValidQuotedInterior(s) {
116
105
  var i = 0, n = s.length;
117
106
  while (i < n) {
@@ -126,11 +115,9 @@ function _isValidQuotedInterior(s) {
126
115
  }
127
116
  return true;
128
117
  }
129
- // Shared character predicates for the scheme / label / URL scans below.
130
118
  function _isAlpha(c) { return (c >= 65 && c <= 90) || (c >= 97 && c <= 122); }
131
119
  function _isAlnumLower(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57); }
132
- function _isSchemeChar(c) { return _isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2b || c === 0x2e || c === 0x2d; } // [A-Za-z0-9+.-]
133
- // /^https?:\/\/[^\s]+$/ : lowercase http(s), "://", then >=1 non-whitespace character to the end.
120
+ function _isSchemeChar(c) { return _isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2b || c === 0x2e || c === 0x2d; }
134
121
  function _isHttpUrlShape(v) {
135
122
  var n = v.length;
136
123
  if (!(_charCodeAt(v, 0) === 0x68 && _charCodeAt(v, 1) === 0x74 && _charCodeAt(v, 2) === 0x74 && _charCodeAt(v, 3) === 0x70)) return false;
@@ -142,7 +129,6 @@ function _isHttpUrlShape(v) {
142
129
  for (; i < n; i++) { if (pkix.isJsWhitespace(_charCodeAt(v, i))) return false; }
143
130
  return true;
144
131
  }
145
- // /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+$/ : an RFC 3986 scheme, ":", then >=1 non-whitespace character to the end.
146
132
  function _isUriShape(v) {
147
133
  var n = v.length;
148
134
  if (n === 0 || !_isAlpha(_charCodeAt(v, 0))) return false;
@@ -154,15 +140,12 @@ function _isUriShape(v) {
154
140
  for (; i < n; i++) { if (pkix.isJsWhitespace(_charCodeAt(v, i))) return false; }
155
141
  return true;
156
142
  }
157
- // /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/ : a DNS label -- first and last characters alphanumeric-lowercase, the
158
- // interior characters additionally allowing '-'.
159
143
  function _isLdhLabel(l) {
160
144
  var n = l.length;
161
145
  if (n === 0 || !_isAlnumLower(_charCodeAt(l, 0)) || !_isAlnumLower(_charCodeAt(l, n - 1))) return false;
162
146
  for (var i = 1; i < n - 1; i++) { var c = _charCodeAt(l, i); if (!(_isAlnumLower(c) || c === 0x2d)) return false; }
163
147
  return true;
164
148
  }
165
- // /^xn--[a-z0-9]+(-[a-z0-9]+)*$/ : an A-label -- "xn--" then hyphen-separated non-empty alphanumeric-lowercase runs.
166
149
  function _isXnLabel(l) {
167
150
  var n = l.length;
168
151
  if (!(n >= 4 && _charCodeAt(l, 0) === 0x78 && _charCodeAt(l, 1) === 0x6e && _charCodeAt(l, 2) === 0x2d && _charCodeAt(l, 3) === 0x2d)) return false;
@@ -177,7 +160,6 @@ function _isXnLabel(l) {
177
160
  }
178
161
  return true;
179
162
  }
180
- // /^(0|[1-9]\d{0,2})$/ : a canonical decimal octet -- a single digit, or 2-3 digits with no leading zero.
181
163
  function _isCanonicalOctet(s) {
182
164
  var n = s.length;
183
165
  if (n === 0 || n > 3) return false;
@@ -186,12 +168,10 @@ function _isCanonicalOctet(s) {
186
168
  for (var i = 1; i < n; i++) { var c = _charCodeAt(s, i); if (!(c >= 48 && c <= 57)) return false; }
187
169
  return true;
188
170
  }
189
- // /[\s\\]/ : contains a whitespace character or a backslash.
190
171
  function _hasWsOrBackslash(s) {
191
172
  for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 0x5c || pkix.isJsWhitespace(c)) return true; }
192
173
  return false;
193
174
  }
194
- // /^[A-Za-z][A-Za-z0-9+.-]*:/ : begins with an RFC 3986 scheme followed by ':' (a prefix test, not anchored at end).
195
175
  function _hasSchemePrefix(uri) {
196
176
  var n = uri.length;
197
177
  if (n === 0 || !_isAlpha(_charCodeAt(uri, 0))) return false;
@@ -199,12 +179,10 @@ function _hasSchemePrefix(uri) {
199
179
  while (i < n && _isSchemeChar(_charCodeAt(uri, i))) i++;
200
180
  return i < n && _charCodeAt(uri, i) === 0x3a;
201
181
  }
202
- // uri.split(/[/?#]/)[0] : the substring up to the first '/', '?', or '#' (the whole string if none).
203
182
  function _uriFirstSegment(uri) {
204
183
  for (var i = 0; i < uri.length; i++) { var c = _charCodeAt(uri, i); if (c === 0x2f || c === 0x3f || c === 0x23) return _strSlice(uri, 0, i); }
205
184
  return uri;
206
185
  }
207
- // Does `s` end with the literal `suffix`? (/suffix$/ for a fixed suffix.)
208
186
  function _endsWith(s, suffix) {
209
187
  var n = s.length, m = suffix.length;
210
188
  if (m > n) return false;
@@ -215,61 +193,27 @@ function _endsWith(s, suffix) {
215
193
  var AcmeError = frameworkError.AcmeError;
216
194
  function E(code, message, cause) { return new AcmeError(code, message, cause); }
217
195
 
218
- // ---- helpers -------------------------------------------------------------
219
196
 
220
197
  function _isObject(v) { return v && typeof v === "object" && !Array.isArray(v); }
221
198
  function _isString(v) { return typeof v === "string"; }
222
- // RFC 3339 date-time validity (grammar + calendar) lives in the shared lib/rfc3339.js primitive so a
223
- // downstream expiry / renewal-window comparison never runs on an impossible instant (month 13, a :60
224
- // leap second, a rolled-over value). pki.ct's log-list window parse composes the same primitive.
225
199
  function _isRfc3339(v) { return rfc3339.isValid(v); }
226
- // A uniform random draw in [0, 1) from the platform CSPRNG (48 bits of entropy). Its only use is spreading
227
- // the ARI renewal instant across the CA's suggested window (RFC 9773 sec. 4.2) so many clients do not
228
- // stampede the same edge: a load-distribution measure, not a secret. Injectable via renewalWindow opts.random.
229
- var _RANDOM_DENOM = Math.pow(2, 48); // 6 bytes -> a uniform fraction in [0, 1)
200
+ var _RANDOM_DENOM = Math.pow(2, 48);
230
201
  function _defaultRandom() { return Buffer.from(webcrypto.getRandomValues(new Uint8Array(6))).readUIntBE(0, 6) / _RANDOM_DENOM; }
231
- // RFC 9773 sec. 4.3.2: the ARI Retry-After bounds the re-poll cadence, so clamp it to [60s, 24h] and a
232
- // hostile (or absent) value can neither hammer the CA nor defer the next check indefinitely. When the CA
233
- // omits Retry-After (permitted), the decision helper still owes the caller a poll interval, so it returns a
234
- // reasonable default in-range (sec. 4.3.2 "the client SHOULD use a reasonable default"), never null.
235
202
  var RENEWAL_RETRY_MIN_SECONDS = 60;
236
203
  var RENEWAL_RETRY_MAX_SECONDS = constants.TIME.days(1) / constants.TIME.seconds(1);
237
204
  var RENEWAL_RETRY_DEFAULT_SECONDS = constants.TIME.hours(6) / constants.TIME.seconds(1);
238
- // A URL string: an absolute http(s) URI with a real host (RFC 3986). ACME URLs are
239
- // server-provided endpoints downstream transport will trust, so they are PARSED
240
- // (not prefix-matched), so a malformed value like "https://[" or a hostless
241
- // "http://" is rejected, not accepted by a loose regex.
242
205
  function _isUrl(v) {
243
- // Prefilter the exact authority form with no whitespace before parsing: new URL()
244
- // silently repairs " https://.." (trim) and "https:host/.." (insert //), and the
245
- // original string is what gets copied into a protected `url` field, so a repaired
246
- // value must be rejected here, never accepted and mismatched at transport time.
247
206
  if (!_isString(v) || !_isHttpUrlShape(v)) return false;
248
207
  var u;
249
- // A parse failure is the "not a URL" verdict for this boolean predicate. There
250
- // is no PkiError here to thread a cause into, so the error is intentionally ignored.
251
208
  try { u = new URL(v); }
252
209
  catch (_e) { return false; }
253
210
  return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.length > 0;
254
211
  }
255
- // A URI string with any RFC 3986 scheme (mailto:, tel:, http(s):, ...). An account
256
- // `contact` is a URI, most commonly `mailto:` (RFC 8555 sec. 7.1.2 / RFC 6068), so
257
- // it must not be narrowed to http(s); the strict mailto hygiene lives in the builder.
258
212
  function _isUriString(v) { return _isString(v) && _isUriShape(v); }
259
213
 
260
- // ---- resource object specs (RFC 8555 sec. 7.1.x) -------------------------
261
214
 
262
- // Each field: { name, type, required?, requiredWhen?(obj), enum?, elemType? }.
263
- // type: "string" | "url" | "rfc3339" | "boolean" | "object" | "array" | "any".
264
- // The walker validates presence and shape only; unknown fields are ignored (never
265
- // reflected). requiredWhen is the conditional-required rule (sec. 7.1.3 expires).
266
- // The assigned RFC 5280 sec. 5.3.1 CRLReason values (0-6, 8-10). Value 7 is
267
- // unassigned, so a revokeCert reason of 7 is rejected, never sent.
268
215
  var CRL_REASONS = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10];
269
216
 
270
- // The registered challenge types (RFC 8555 sec. 8.3/8.4, RFC 8737). For these the
271
- // `token` is a required, entropy-bearing base64url value; an unknown future type
272
- // may define its own response fields, so `token` is only required for these.
273
217
  var KNOWN_CHALLENGE_TYPES = { "http-01": 1, "dns-01": 1, "tls-alpn-01": 1 };
274
218
 
275
219
  var STATUS = {
@@ -295,7 +239,7 @@ var SPECS = {
295
239
  { name: "contact", type: "array", elemType: "contact" },
296
240
  { name: "termsOfServiceAgreed", type: "boolean" },
297
241
  { name: "externalAccountBinding", type: "object" },
298
- { name: "orders", type: "url" }, // required per RFC; lenient by default (OQ2)
242
+ { name: "orders", type: "url" },
299
243
  ],
300
244
  order: [
301
245
  { name: "status", type: "string", required: true, enum: STATUS.order },
@@ -313,9 +257,6 @@ var SPECS = {
313
257
  { name: "identifier", type: "identifier", required: true },
314
258
  { name: "status", type: "string", required: true, enum: STATUS.authorization },
315
259
  { name: "expires", type: "rfc3339", requiredWhen: function (o) { return o.status === "valid"; } },
316
- // challenges is required (the key is present) but MAY be empty for an already-"valid" authorization the CA
317
- // granted out of band (RFC 8555 sec. 7.1.4 / 7.4.1, where no challenge was validated); a pending/other authz still
318
- // needs at least the one challenge the client fulfills.
319
260
  { name: "challenges", type: "array", required: true, minItems: function (o) { return o.status === "valid" ? 0 : 1; }, elemType: "challenge" },
320
261
  { name: "wildcard", type: "boolean" },
321
262
  ],
@@ -332,7 +273,7 @@ var SPECS = {
332
273
  { name: "explanationURL", type: "url" },
333
274
  ],
334
275
  ordersList: [
335
- { name: "orders", type: "array", required: true, elemType: "url" }, // RFC 8555 sec. 7.1.2.1 -- an array of order URLs
276
+ { name: "orders", type: "array", required: true, elemType: "url" },
336
277
  ],
337
278
  };
338
279
 
@@ -342,12 +283,12 @@ function _checkType(kind, field, value, obj) {
342
283
  case "url": if (!_isUrl(value)) return "must be a URL string"; break;
343
284
  case "rfc3339": if (!_isRfc3339(value)) return "must be an RFC 3339 date-time"; break;
344
285
  case "boolean": if (typeof value !== "boolean") return "must be a boolean"; break;
345
- case "token": _assertToken(value); break; // >= 22 base64url chars, no padding (throws acme/bad-token)
286
+ case "token": _assertToken(value); break;
346
287
  case "object": if (!_isObject(value)) return "must be an object"; break;
347
288
  case "identifier": _validateIdentifier(value); break;
348
289
  case "array":
349
290
  if (!Array.isArray(value)) return "must be an array";
350
- var minItems = typeof field.minItems === "function" ? field.minItems(obj) : field.minItems; // conditional (sec. 7.1.4)
291
+ var minItems = typeof field.minItems === "function" ? field.minItems(obj) : field.minItems;
351
292
  if (minItems && value.length < minItems) return "must have at least " + minItems + " element(s)";
352
293
  for (var i = 0; i < value.length; i++) {
353
294
  if (field.elemType === "url" && !_isUrl(value[i])) return "element " + i + " must be a URL string";
@@ -358,22 +299,17 @@ function _checkType(kind, field, value, obj) {
358
299
  if (field.elemType === "object" && !_isObject(value[i])) return "element " + i + " must be an object";
359
300
  }
360
301
  break;
361
- default: break; // "any"
302
+ default: break;
362
303
  }
363
304
  return null;
364
305
  }
365
306
 
366
- // A kind name kebab-cased for use in an error code (the code shape is strict
367
- // lowercase-kebab): a camelCase kind like "renewalInfo" must become
368
- // "renewal-info", never leak "acme/bad-renewalInfo" (which the PkiError code
369
- // validator rejects, turning a fault into a raw TypeError).
370
- function _codeSlug(kind) { // camelCase -> kebab-case: kind.replace(/([A-Z])/g, "-$1").toLowerCase()
307
+ function _codeSlug(kind) {
371
308
  var out = "";
372
309
  for (var i = 0; i < kind.length; i++) { var c = _charCodeAt(kind, i); if (c >= 65 && c <= 90) out += "-"; out += _charAt(kind, i); }
373
310
  return _lower(out);
374
311
  }
375
312
 
376
- // Validate an object against a spec table. Returns the object; throws acme/*.
377
313
  function _validate(kind, obj) {
378
314
  if (!_isObject(obj)) throw E("acme/bad-" + _codeSlug(kind), "an ACME " + kind + " must be a JSON object");
379
315
  var spec = SPECS[kind];
@@ -394,16 +330,10 @@ function _validate(kind, obj) {
394
330
  return obj;
395
331
  }
396
332
 
397
- // ---- identifiers (dns / ip; RFC 8555 sec. 7.1.4 / RFC 8738) --------------
398
333
 
399
- // A dns identifier value: lowercase LDH ASCII labels (A-labels; a leading `*.`
400
- // wildcard is validated by the ORDER path, never here). An ip identifier value:
401
- // the RFC 5952 (IPv6) / RFC 1123 (IPv4) canonical textual form, byte-identical
402
- // round-trip. `_validateIdentifier` rejects a value beginning `*.` (that is only
403
- // legal in an order identifier, checked separately).
404
334
  function _validateIdentifier(id) {
405
335
  if (!_isObject(id)) throw E("acme/bad-identifier", "an identifier must be { type, value } strings");
406
- var type = id.type, value = id.value; // read each field ONCE (a getter-backed / inherited prop is captured here)
336
+ var type = id.type, value = id.value;
407
337
  if (!_isString(type) || !_isString(value)) throw E("acme/bad-identifier", "an identifier must be { type, value } strings");
408
338
  if (type === "dns") {
409
339
  if (value.indexOf("*.") === 0) throw E("acme/bad-identifier", "a wildcard *. value is not permitted in an authorization identifier (RFC 8555 sec. 7.1.4)");
@@ -411,15 +341,9 @@ function _validateIdentifier(id) {
411
341
  } else if (type === "ip") {
412
342
  _assertIpAddress(value);
413
343
  }
414
- // Return a canonical { type, value } built from the single read above; an unrecognized type is surfaced raw
415
- // (a server may add types). Callers serialize this object, not the caller's, so an inherited/getter-backed field
416
- // is not dropped by JSON.stringify and an enumerable extra property is not sent to the CA.
417
344
  return { type: type, value: value };
418
345
  }
419
346
 
420
- // A DNS name (each label lowercase letters/digits/hyphen, not leading/trailing
421
- // hyphen; an xn-- A-label must be well-formed). Uppercase / non-ASCII rejected
422
- // (the client sends A-labels only).
423
347
  function _assertDnsName(name) {
424
348
  if (!_isString(name)) throw E("acme/bad-identifier", "a dns identifier value must be a string");
425
349
  if (name.length === 0 || name.length > 253) throw E("acme/bad-identifier", "a dns identifier value must be 1..253 characters");
@@ -432,11 +356,6 @@ function _assertDnsName(name) {
432
356
  }
433
357
  }
434
358
 
435
- // An IP address in canonical text (RFC 8738 sec. 3): IPv4 dotted-decimal with no
436
- // leading zeros, or IPv6 in the RFC 5952 sec. 4 compressed lowercase form. The
437
- // only accepted form is the one that round-trips byte-identically; an ambiguous
438
- // value (leading zeros, uppercase hex, an uncompressed run) is rejected, never
439
- // normalized-and-guessed.
440
359
  function _assertIpAddress(value) {
441
360
  if (!_isString(value)) throw E("acme/bad-identifier", "an ip identifier value must be a string");
442
361
  if (_looksLikeIpv4(value)) {
@@ -455,13 +374,10 @@ function _assertIpAddress(value) {
455
374
  throw E("acme/bad-identifier", "an ip identifier value must be an IPv4 or IPv6 textual address (RFC 8738 sec. 3)");
456
375
  }
457
376
 
458
- // Parse an IPv6 address to its 8 groups then re-emit the RFC 5952 canonical form
459
- // (lowercase, no leading zeros, the longest zero-run compressed with `::`, the
460
- // leftmost when tied, never a single 0 group). Returns null on a malformed input.
461
377
  function _canonicalizeIpv6(value) {
462
378
  var groups;
463
379
  if (value.indexOf("::") !== -1) {
464
- if (value.indexOf("::") !== value.lastIndexOf("::")) return null; // only one ::
380
+ if (value.indexOf("::") !== value.lastIndexOf("::")) return null;
465
381
  var halves = _split(value, "::");
466
382
  var left = halves[0] ? _split(halves[0], ":") : [];
467
383
  var right = halves[1] ? _split(halves[1], ":") : [];
@@ -480,7 +396,6 @@ function _canonicalizeIpv6(value) {
480
396
  _push(nums, _parseIntI(groups[i], 16));
481
397
  }
482
398
  var hex = _mapI(nums, function (n) { return _numToString(n, 16); });
483
- // Longest run of zero groups (>= 2) -> "::"; leftmost on a tie.
484
399
  var bestStart = -1, bestLen = 0, curStart = -1, curLen = 0;
485
400
  for (var g = 0; g < 8; g++) {
486
401
  if (nums[g] === 0) { if (curStart === -1) curStart = g; curLen++; if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; } }
@@ -492,10 +407,7 @@ function _canonicalizeIpv6(value) {
492
407
  return head + "::" + tail;
493
408
  }
494
409
 
495
- // ---- state machines (RFC 8555 sec. 7.1.6) --------------------------------
496
410
 
497
- // Legal transitions as data. An observed transition outside the set is an
498
- // illegal server transition the client fails closed on (acme/bad-transition).
499
411
  var TRANSITIONS = {
500
412
  challenge: { pending: ["processing", "valid", "invalid"], processing: ["processing", "valid", "invalid"] },
501
413
  authorization: { pending: ["valid", "invalid", "deactivated"], valid: ["expired", "deactivated", "revoked"] },
@@ -522,15 +434,10 @@ function assertTransition(kind, from, to) {
522
434
  var table = TRANSITIONS[kind];
523
435
  if (!table) throw E("acme/bad-input", "unknown resource kind " + JSON.stringify(kind));
524
436
  if (from === to) return;
525
- // The legal edges -- including every non-terminal state's edge to "invalid" -- are
526
- // the table. A terminal "valid" order/challenge has NO outgoing edge, so a
527
- // "valid" -> "invalid" regression is rejected (RFC 8555 sec. 7.1.6 makes valid
528
- // terminal; a client failing closed while polling must not accept it).
529
437
  var allowed = table[from];
530
438
  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)");
531
439
  }
532
440
 
533
- // ---- problem documents (RFC 7807 / RFC 8555 sec. 6.7) --------------------
534
441
 
535
442
  var ERROR_NAMESPACE = "urn:ietf:params:acme:error:";
536
443
 
@@ -560,16 +467,12 @@ function validateProblem(obj) {
560
467
  for (var i = 0; i < obj.subproblems.length; i++) {
561
468
  var sub = obj.subproblems[i];
562
469
  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");
563
- // A subproblem identifier reflects a submitted order identifier, which MAY be a
564
- // wildcard (a rejectedIdentifier for a *.example.org order), so it is validated
565
- // with the order-identifier rule, not the stricter authorization one.
566
470
  if (_hasOwn(sub, "identifier")) _validateOrderIdentifier(sub.identifier);
567
471
  }
568
472
  }
569
473
  return obj;
570
474
  }
571
475
 
572
- // ---- object validators + identify ----------------------------------------
573
476
 
574
477
  /**
575
478
  * @primitive pki.acme.validate
@@ -595,9 +498,6 @@ function validateProblem(obj) {
595
498
  */
596
499
  function validate(kind, obj) {
597
500
  if (kind === "problem") return validateProblem(obj);
598
- // renewalInfo carries an RFC 9773 window sanity check beyond the spec shape, so a
599
- // generic dispatch (identify -> validate) gets the SAME strictness as a direct
600
- // validateRenewalInfo call -- an inverted / malformed suggestedWindow is rejected.
601
501
  if (kind === "renewalInfo") return validateRenewalInfo(obj);
602
502
  if (!SPECS[kind]) throw E("acme/bad-input", "unknown ACME object kind " + JSON.stringify(kind));
603
503
  return _validate(kind, obj);
@@ -637,11 +537,7 @@ function identify(obj) {
637
537
  return "unknown";
638
538
  }
639
539
 
640
- // ---- challenge computations (RFC 8555 sec. 8 / 8737 / 8738) --------------
641
540
 
642
- // A challenge token: >= 128 bits of entropy => >= 22 base64url chars, alphabet
643
- // only, no `=` (RFC 8555 sec. 8, errata 6950). Validated BEFORE any use (also the
644
- // http-01 reflection-XSS guard).
645
541
  function _assertToken(token) {
646
542
  if (!_isString(token) || token.length < constants.LIMITS.ACME_TOKEN_MIN_CHARS || !pkix.allCharsIn(token, _TOKEN_TABLE)) 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)");
647
543
  }
@@ -753,13 +649,12 @@ var _extCtx = { E: function (c, m, cause) { return new AcmeError(c, m, cause); }
753
649
  */
754
650
  async function tlsAlpn01Extension(token, accountJwk) {
755
651
  var ka = await keyAuthorization(token, accountJwk);
756
- var digest = await _sha256(Buffer.from(ka, "ascii")); // 32 bytes
757
- var authorization = asn1.build.octetString(digest); // Authorization ::= OCTET STRING (SIZE 32)
652
+ var digest = await _sha256(Buffer.from(ka, "ascii"));
653
+ var authorization = asn1.build.octetString(digest);
758
654
  var extnValue = asn1.build.octetString(authorization);
759
655
  return asn1.build.sequence([asn1.build.oid(OID_ACME_IDENTIFIER), asn1.build.boolean(true), extnValue]);
760
656
  }
761
657
 
762
- // The 32-byte Authorization digest inside an acmeIdentifier extnValue, or throw.
763
658
  function _readAcmeIdentifier(extnValue) {
764
659
  var auth;
765
660
  try { auth = asn1.read.octetString(asn1.decode(extnValue)); }
@@ -808,7 +703,6 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
808
703
  var ka = await keyAuthorization(token, accountJwk);
809
704
  var expected = await _sha256(Buffer.from(ka, "ascii"));
810
705
  if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
811
- // SAN: exactly one entry, of the identifier's type, equal to its value.
812
706
  var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
813
707
  if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
814
708
  var san = _extDecoders.byOid[OID_SAN](sanExt.value, _extCtx);
@@ -817,17 +711,10 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
817
711
  if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
818
712
  if (identifier.type === "dns") {
819
713
  if (entry.tagNumber !== 2) throw E("acme/bad-tlsalpn", "a dns identifier requires a dNSName SAN");
820
- // Validate the identifier as a base dns name first (rejecting a wildcard *.
821
- // label or a malformed value), matching the ip branch and the rest of the
822
- // ACME path -- a tls-alpn-01 identifier must be a concrete, non-wildcard name.
823
714
  _assertDnsName(identifier.value);
824
715
  if (String(entry.value).toLowerCase() !== identifier.value) throw E("acme/bad-tlsalpn", "the SAN dNSName does not match the identifier");
825
716
  } else if (identifier.type === "ip") {
826
717
  if (entry.tagNumber !== 7) throw E("acme/bad-tlsalpn", "an ip identifier requires a single iPAddress SAN (RFC 8738 sec. 6)");
827
- // Reject a NON-canonical identifier (leading-zero octets, uppercase IPv6, an
828
- // uncompressed run) rather than normalizing-and-guessing it -- the same
829
- // fail-closed rule the rest of the ACME identifier path applies. After this the
830
- // identifier value is its own canonical form, so the SAN's canonical text must equal it.
831
718
  _assertIpAddress(identifier.value);
832
719
  var sanIp = _ipBytesToText(entry.value);
833
720
  if (sanIp === null || sanIp !== identifier.value) throw E("acme/bad-tlsalpn", "the iPAddress SAN does not match the ip identifier (RFC 8738 sec. 6)");
@@ -836,14 +723,7 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
836
723
  }
837
724
  }
838
725
 
839
- // ---- request builders (RFC 8555 sec. 7.x) --------------------------------
840
726
 
841
- // The signed outer request is a Flattened JWS under the acme-outer profile: alg
842
- // + nonce + url + EXACTLY ONE of kid/jwk (jose enforces one-of, nonce, url). The
843
- // payload is a raw Buffer -- POST-as-GET is an empty Buffer (encodes to ""), a
844
- // resource POST is the UTF-8 JSON of the payload object. `o` carries the signing
845
- // key (`key`, a private CryptoKey), its `alg`, the fresh `nonce`, the target
846
- // `url`, and either `kid` (an account URL) or `jwk` (an embedded public JWK).
847
727
  function _outerHeader(o) {
848
728
  var h = { alg: o.alg, nonce: o.nonce, url: o.url };
849
729
  if (_hasOwn(o, "kid")) h.kid = o.kid;
@@ -851,20 +731,13 @@ function _outerHeader(o) {
851
731
  return h;
852
732
  }
853
733
  function _payloadBuf(obj) {
854
- if (obj === undefined) return Buffer.alloc(0); // POST-as-GET
734
+ if (obj === undefined) return Buffer.alloc(0);
855
735
  return Buffer.from(JSON.stringify(obj), "utf8");
856
736
  }
857
- // A verb documented `-> Promise` refuses by REJECTING, never by throwing (guard-async).
858
- //
859
- // The validation stays SYNCHRONOUS: these verbs read a caller's options object -- the key, the
860
- // nonce, the identifiers -- and resolving those before any turn passes is what stops a value being
861
- // swapped between the check and the use. Only the exit changes.
862
737
  var _promised = guard.async.deferred;
863
738
  function _signOuter(o, payloadObj) {
864
739
  if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
865
740
  if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
866
- // The header carries the account jwk in jwk mode and the kid in kid mode, so the public JWK is
867
- // already there whenever there is one to name; jose.sign reads its own opts.jwk in kid mode only.
868
741
  return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, profile: "acme-outer" });
869
742
  }
870
743
 
@@ -887,28 +760,16 @@ function _signOuter(o, payloadObj) {
887
760
  function postAsGet(o) {
888
761
  return _promised(function () {
889
762
  if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
890
- // An authenticated read is ALWAYS kid-signed; copy only the kid-mode fields so a
891
- // leftover jwk (e.g. reused from a newAccount options object) cannot embed a key.
892
763
  return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, undefined);
893
764
  });
894
765
  }
895
766
 
896
- // A contact URL (RFC 8555 sec. 7.3): a `mailto:` addr-spec carries no header
897
- // fields (`?` hfields) and exactly one address (no comma-list) -- fail closed on
898
- // ambiguity (never send to a guessed recipient). Other URL schemes pass through
899
- // as opaque strings (the server decides support via unsupportedContact).
900
767
  function _assertContacts(contacts) {
901
768
  if (!Array.isArray(contacts)) throw E("acme/bad-contact", "contact must be an array of URL strings");
902
- // An own-index loop, NOT forEach: forEach SKIPS a hole in a sparse array (`new Array(1)`), so the hole
903
- // would never be validated and JSON serialization would sign `{"contact":[null]}`. Every index is
904
- // read through an own-property check, so a sparse hole -- or a nullish entry -- is a typed
905
- // acme/bad-contact rather than a silently-signed null.
906
769
  for (var i = 0; i < contacts.length; i++) {
907
770
  if (!_hasOwn(contacts, i)) throw E("acme/bad-contact", "contact[" + i + "] is missing (a sparse array is not allowed)");
908
771
  var c = contacts[i];
909
772
  if (!_isUriString(c)) throw E("acme/bad-contact", "each contact must be a URI string (RFC 8555 sec. 7.1.2)");
910
- // URI schemes are case-insensitive (RFC 3986), so detect mailto regardless of
911
- // case -- "MAILTO:a@b?..." must still hit the RFC 6068 header-field guards.
912
773
  if (c.slice(0, "mailto:".length).toLowerCase() === "mailto:") {
913
774
  var addr = c.slice("mailto:".length);
914
775
  if (addr.indexOf("?") !== -1) throw E("acme/bad-contact", "a mailto contact must not carry header fields (RFC 8555 sec. 7.3 / RFC 6068)");
@@ -940,9 +801,6 @@ function newAccount(o) {
940
801
  if (!_isObject(o) || !_isObject(o.jwk)) throw E("acme/bad-input", "newAccount must embed the account public jwk (RFC 8555 sec. 7.3)");
941
802
  var payload = {};
942
803
  if (o.contact !== undefined) { _assertContacts(o.contact); payload.contact = o.contact; }
943
- // Require actual booleans -- never coerce a string like "false"/"0", which `!!`
944
- // would serialize as `true`, silently agreeing to Terms of Service or forcing
945
- // onlyReturnExisting.
946
804
  if (o.termsOfServiceAgreed !== undefined) {
947
805
  if (typeof o.termsOfServiceAgreed !== "boolean") throw E("acme/bad-input", "termsOfServiceAgreed must be a boolean");
948
806
  payload.termsOfServiceAgreed = o.termsOfServiceAgreed;
@@ -984,7 +842,7 @@ var _HMAC_HASH = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
984
842
  */
985
843
  async function externalAccountBinding(o) {
986
844
  if (!_isObject(o) || !_isObject(o.accountJwk)) throw E("acme/bad-input", "externalAccountBinding requires the account public jwk (sec. 7.3.4)");
987
- jose.assertPublicJwk(o.accountJwk); // the account JWK is published in the EAB payload -> public-only
845
+ jose.assertPublicJwk(o.accountJwk);
988
846
  if (!_isString(o.kid)) throw E("acme/bad-input", "externalAccountBinding requires the CA-issued kid");
989
847
  var alg = o.alg || "HS256";
990
848
  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));
@@ -993,19 +851,14 @@ async function externalAccountBinding(o) {
993
851
  try { key = await subtle.importKey("raw", key, { name: "HMAC", hash: _HMAC_HASH[alg] }, false, ["sign"]); }
994
852
  catch (e) { throw E("acme/bad-input", "the EAB macKey could not be imported as an HMAC key", e); }
995
853
  } else if (!key || typeof key !== "object" || key.type !== "secret") {
996
- // Anything that is neither a raw Buffer nor a secret-key CryptoKey would reach
997
- // subtle.sign and throw a bare TypeError; fail closed with a typed fault instead.
998
854
  throw E("acme/bad-input", "the EAB macKey must be a raw Buffer or an HMAC (secret) CryptoKey");
999
855
  }
1000
856
  return jose.sign({ protected: { alg: alg, kid: o.kid, url: o.url }, payload: _payloadBuf(o.accountJwk), key: key, profile: "eab-inner" });
1001
857
  }
1002
858
 
1003
- // An order identifier MAY carry a wildcard: EXACTLY ONE leading `*.` label, `dns`
1004
- // only (sec. 7.1.3). An `ip` identifier has no wildcard form. Everything else is
1005
- // the shared _validateIdentifier syntax on the base name.
1006
859
  function _validateOrderIdentifier(id) {
1007
860
  if (!_isObject(id)) throw E("acme/bad-identifier", "an order identifier must be { type, value } strings");
1008
- var type = id.type, value = id.value; // read each field ONCE (a getter-backed / inherited prop is captured here)
861
+ var type = id.type, value = id.value;
1009
862
  if (!_isString(type) || !_isString(value)) throw E("acme/bad-identifier", "an order identifier must be { type, value } strings");
1010
863
  if (type === "dns") {
1011
864
  var v = value;
@@ -1020,8 +873,6 @@ function _validateOrderIdentifier(id) {
1020
873
  if (value.indexOf("*") !== -1) throw E("acme/bad-identifier", "an ip identifier has no wildcard form (RFC 8738)");
1021
874
  _assertIpAddress(value);
1022
875
  }
1023
- // Canonical { type, value } from the single read (the ORIGINAL value, wildcard `*.` included) -- see
1024
- // _validateIdentifier: callers serialize THIS, so the wire payload cannot diverge from what was validated.
1025
876
  return { type: type, value: value };
1026
877
  }
1027
878
 
@@ -1045,8 +896,6 @@ function _validateOrderIdentifier(id) {
1045
896
  function newOrder(o) {
1046
897
  return _promised(function () {
1047
898
  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)");
1048
- // Serialize the CANONICAL { type, value } each validator returns, never the caller's objects (which may carry
1049
- // getter-backed / inherited fields JSON.stringify would drop, or extra enumerable fields it would send).
1050
899
  var payload = { identifiers: o.identifiers.map(_validateOrderIdentifier) };
1051
900
  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; }
1052
901
  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; }
@@ -1075,13 +924,9 @@ function newOrder(o) {
1075
924
  */
1076
925
  function newAuthz(o) {
1077
926
  if (!_isObject(o) || !_isObject(o.identifier)) throw E("acme/bad-identifier", "newAuthz requires a single identifier object (RFC 8555 sec. 7.4.1)");
1078
- // Serialize the canonical { type, value } the validator returns, never the caller's object (see _validateIdentifier).
1079
927
  return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { identifier: _validateIdentifier(o.identifier) });
1080
928
  }
1081
929
 
1082
- // The account key's SubjectPublicKeyInfo DER, imported from its public JWK, so a
1083
- // finalize CSR carrying that same key is caught (sec. 11.1). The DER is canonical
1084
- // (node emits canonical SPKI), matched byte-for-byte against the CSR's strict-DER SPKI.
1085
930
  async function _jwkToSpki(jwk) {
1086
931
  var importAlg;
1087
932
  if (jwk.kty === "EC") importAlg = { name: "ECDSA", namedCurve: jwk.crv };
@@ -1095,12 +940,6 @@ async function _jwkToSpki(jwk) {
1095
940
  return Buffer.from(await subtle.exportKey("spki", key));
1096
941
  }
1097
942
 
1098
- // EVERY common name in the subject (a DN may carry more than one CN RDN) -- taking
1099
- // only the first would let a second CN smuggle an identifier past the order-set match.
1100
- // Walked through the captured operations, because this feeds the issued-certificate binding: the set
1101
- // it builds is what says the certificate names what the order asked for, and it is built after the
1102
- // download returns. A replaced traversal could present a name the certificate does not carry, and the
1103
- // comparison would agree that the two sets are equal.
1104
943
  function _subjectCommonNames(subject) {
1105
944
  var names = [];
1106
945
  if (!subject || !_isArrayI(subject.rdns)) return names;
@@ -1113,8 +952,6 @@ function _subjectCommonNames(subject) {
1113
952
  return names;
1114
953
  }
1115
954
 
1116
- // An iPAddress SAN octet string (4 = IPv4, 16 = IPv6) to its RFC 8738 canonical
1117
- // text -- the same form an order ip identifier carries -- or null on a bad length.
1118
955
  function _ipBytesToText(buf) {
1119
956
  if (!_isBuffer(buf)) return null;
1120
957
  if (buf.length === 4) return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
@@ -1126,16 +963,8 @@ function _ipBytesToText(buf) {
1126
963
  return null;
1127
964
  }
1128
965
 
1129
- // The set of identifiers a CSR requests: the subject CN (counted as dns) plus the
1130
- // SAN dNSName / iPAddress entries in the extensionRequest attribute, keyed
1131
- // "dns:<lower>" / "ip:<canonical>" for an order-insensitive compare.
1132
966
  function _csrIdentifierSet(parsedCsr) {
1133
- // Null prototype: membership below is asked as `set[k]`, and against an ordinary object that question
1134
- // reaches Object.prototype, so a name planted there would read as an identifier the request carried.
1135
967
  var set = intrinsic.create(null);
1136
- // PKCS#10 permits duplicate attribute types, so aggregate EVERY extensionRequest
1137
- // attribute and EVERY subjectAltName within each -- taking only the first would let
1138
- // a second extensionRequest smuggle identifiers past the order-set comparison.
1139
968
  _forEach(parsedCsr.attributes || [], function (a) {
1140
969
  if (a.type !== oid.byName("extensionRequest") || !_isArrayI(a.extensions)) return;
1141
970
  _forEach(a.extensions, function (e) {
@@ -1144,40 +973,12 @@ function _csrIdentifierSet(parsedCsr) {
1144
973
  _forEach(dec.names || [], function (n) { _addSanName(set, n, "the finalize CSR"); });
1145
974
  });
1146
975
  });
1147
- // EVERY common name, alternative names present or not -- which is where this side parts from the
1148
- // issued-certificate set, because the two answer different questions. That set asks what a
1149
- // certificate authenticates, and no matcher reads a common name for that. This one asks what the
1150
- // request is ASKING to have certified, and a CA may carry a common name through into the issued
1151
- // certificate, so a CSR naming the order's name in a SAN and an unauthorized one in its subject is
1152
- // a request for a name the order does not cover.
1153
976
  _forEach(_subjectCommonNames(parsedCsr.subject), function (cn) { _addCommonName(set, cn, "the finalize CSR"); });
1154
977
  return set;
1155
978
  }
1156
979
 
1157
- // One subjectAltName entry folded into an identifier set, for the CSR and the issued certificate
1158
- // alike. A dNSName or iPAddress maps to an ACME order identifier; anything else -- rfc822Name,
1159
- // otherName, directoryName, uniformResourceIdentifier, registeredID -- does not, and is REFUSED
1160
- // rather than skipped. Skipping it would leave a name in the certificate that the set-equality
1161
- // comparison never saw, so a certificate additionally naming a mailbox or a URI would compare equal
1162
- // to an order covering neither. A malformed iPAddress is the same case: unmappable, so unbindable.
1163
- // One subject common name folded into an identifier set. A CN holding an IP literal is an IP
1164
- // identifier, not a dns one: CABF TLS BR 7.1.4.2.2 lets a commonName match either a dNSName or an
1165
- // iPAddress SAN, so an IP certificate routinely carries the address in both places while the order
1166
- // carries it once as `ip`. Counting the CN as `dns:` there would make a correct issuance compare as
1167
- // two identifiers against the order's one. A CN that looks like an address but is not the canonical
1168
- // RFC 5952 / RFC 8738 form is neither a usable dns name nor a canonical ip identifier, and it is
1169
- // refused rather than normalized -- the same rule the ip identifier validator applies.
1170
980
  function _addCommonName(set, cn, what) {
1171
981
  var looksIp = _looksLikeIpv4(cn) || intrinsic.stringIndexOf(cn, ":") !== -1;
1172
- // An address in a common name adds no identity of its own. Address matching reads the iPAddress
1173
- // subjectAltName and never falls back to the common name, so a subject naming the ordered address
1174
- // only there cannot authenticate it. What the ordinary IP certificate does -- CABF TLS BR
1175
- // 7.1.4.2.2 permits a common name to match an iPAddress SAN, and issuers write the address in both
1176
- // places -- is repeat an address the alternative names ALREADY carry, and that is a duplicate of an
1177
- // authorized name rather than a new one. So: accepted where the set already holds it, refused
1178
- // otherwise, which covers the address carried only in the subject and the one naming a DIFFERENT
1179
- // address than the alternative names do. The alternative names are folded in before the common
1180
- // names on both sides, so the set is complete by the time this reads it.
1181
982
  if (looksIp) {
1182
983
  var canonical = true;
1183
984
  try { _assertIpAddress(cn); }
@@ -1187,13 +988,7 @@ function _addCommonName(set, cn, what) {
1187
988
  if (canonical && set["ip:" + cn] === true) return;
1188
989
  throw _unmappableName(what, cn, null);
1189
990
  }
1190
- // ASCII case folding, never the Unicode mapping. A DirectoryString common name may hold characters
1191
- // whose Unicode lowercase IS ASCII -- U+212A KELVIN SIGN folds to "k" -- so a full fold turns a name
1192
- // the certificate does not carry as a DNS label into one that compares equal to an order identifier.
1193
991
  var folded = _asciiLower(cn);
1194
- // A single leading `*.` is a wildcard name, which an order identifier carries verbatim (sec. 7.1.3)
1195
- // and a certificate carries as a SAN, so it is validated the way the order validator validates one:
1196
- // strip the one wildcard label, then hold the rest to the A-label rule.
1197
992
  var base = intrinsic.stringIndexOf(folded, "*.") === 0 ? _strSlice(folded, 2) : folded;
1198
993
  if (intrinsic.stringIndexOf(base, "*") !== -1) throw _unmappableName(what, cn, null);
1199
994
  try { _assertDnsName(base); }
@@ -1203,11 +998,6 @@ function _addCommonName(set, cn, what) {
1203
998
  set["dns:" + folded] = true;
1204
999
  }
1205
1000
 
1206
- // A name the comparison cannot express, refused rather than left out. Dropping it is unsafe even
1207
- // though a smaller certificate set can only fail equality: a certificate may carry SEVERAL common
1208
- // names, and dropping the unmappable one while another supplies the match reports the set as bound
1209
- // while the certificate still names something the order never covered -- `CN=example.org,
1210
- // CN=victim.example.`, where the trailing-dot spelling is one a hostname matcher will accept.
1211
1001
  function _unmappableName(what, name, cause) {
1212
1002
  return E("acme/unsupported-identifier-type",
1213
1003
  what + " carries the name " + _stringify(name) + " in its subject, which maps to no ACME order " +
@@ -1215,14 +1005,9 @@ function _unmappableName(what, name, cause) {
1215
1005
  "compared", cause || undefined);
1216
1006
  }
1217
1007
 
1218
- // A-Z to a-z and nothing else, through the toolkit's one definition of that fold. String.prototype
1219
- // .toLowerCase applies the full Unicode mapping, under which several non-ASCII characters fold INTO
1220
- // ASCII, and a name folded that way is one the certificate does not carry.
1221
1008
  var _asciiLower = guard.name.lowerAscii;
1222
1009
 
1223
1010
  function _addSanName(set, n, what) {
1224
- // ASCII case folding on both sides of the comparison, never the Unicode mapping: a character whose
1225
- // Unicode lowercase IS ASCII would otherwise fold into a name the certificate does not carry.
1226
1011
  if (n.tagNumber === 2) { set["dns:" + _asciiLower(_StringI(n.value))] = true; return; }
1227
1012
  if (n.tagNumber === 7) {
1228
1013
  var t = _ipBytesToText(n.value);
@@ -1235,21 +1020,11 @@ function _addSanName(set, n, what) {
1235
1020
  "order identifier (only dNSName and iPAddress do), so the identifier set cannot be compared");
1236
1021
  }
1237
1022
 
1238
- // The order's own identifiers as the same keyed set. Built through the captured operations and with a
1239
- // null prototype for the same two reasons its siblings are: this set is computed AFTER the certificate
1240
- // download returns, so what walks the array and folds each name must not be replaceable in between,
1241
- // and membership is asked as `set[k]`, which against an ordinary object would answer for a name planted
1242
- // on Object.prototype rather than one the order carried.
1243
1023
  function _orderIdentifierSet(identifiers) {
1244
1024
  var set = intrinsic.create(null);
1245
1025
  _forEach(identifiers, function (id) {
1246
1026
  if (id.type === "dns") set["dns:" + _asciiLower(id.value)] = true;
1247
1027
  else if (id.type === "ip") set["ip:" + id.value] = true;
1248
- // A registered identifier type this comparison cannot map to a certificate name is REFUSED, not
1249
- // dropped. Dropping it would leave the comparison answering about the identifiers it happens to
1250
- // understand while reporting a check of the whole set: an order for a dns name and one other type
1251
- // would be satisfied by a certificate naming only the dns name. The ACME identifier registry
1252
- // (RFC 8555 sec. 9.7.7) is open, so this is the shape a new type arrives in.
1253
1028
  else throw E("acme/unsupported-identifier-type",
1254
1029
  "this build cannot bind a certificate to an order identifier of type " + _stringify(id.type) +
1255
1030
  " (only dns and ip map to a certificate name), so it cannot report the identifier set as checked");
@@ -1257,17 +1032,6 @@ function _orderIdentifierSet(identifiers) {
1257
1032
  return set;
1258
1033
  }
1259
1034
 
1260
- // The set of identifiers an ISSUED certificate carries, keyed the same way as the CSR and order sets
1261
- // so the three are comparable. The subjectAltName entries are the names a certificate asserts; the
1262
- // subject common name is consulted ONLY when there is no such entry. Where a SAN is present the
1263
- // common name is not an additional identity -- name matching has read the SAN and ignored the common
1264
- // name for many years, and CABF TLS BR 7.1.4.2.2 requires any common name to appear among the SAN
1265
- // values anyway, so counting it again would make a certificate whose SAN set is exactly the order's
1266
- // compare unequal because of a legacy or organizational common name beside it.
1267
- //
1268
- // A certificate may carry several SAN extensions in a non-conformant encoding, so every one is
1269
- // aggregated rather than the first -- taking one would let a second smuggle a name past the
1270
- // comparison.
1271
1035
  function _certIdentifierSet(parsedCert) {
1272
1036
  var set = intrinsic.create(null);
1273
1037
  _forEach(parsedCert.extensions || [], function (e) {
@@ -1315,7 +1079,7 @@ function _assertCsrIdentifiers(parsedCsr, identifiers) {
1315
1079
  async function finalize(o) {
1316
1080
  if (!_isObject(o) || !guard.bytes.isByteSource(o.csr)) throw E("acme/bad-input", "finalize requires a DER CSR BufferSource (opts.csr)");
1317
1081
  var csrBuf = guard.bytes.snapshotSource(o.csr, E, "acme/bad-input", "the finalize CSR");
1318
- var parsed = csr.parse(csrBuf); // strict DER; rejects PEM/garbage
1082
+ var parsed = csr.parse(csrBuf);
1319
1083
  if (o.accountJwk !== undefined) {
1320
1084
  var accountSpki = await _jwkToSpki(o.accountJwk);
1321
1085
  if (parsed.subjectPublicKeyInfo && Buffer.isBuffer(parsed.subjectPublicKeyInfo.bytes) &&
@@ -1325,10 +1089,9 @@ async function finalize(o) {
1325
1089
  }
1326
1090
  if (o.identifiers !== undefined) {
1327
1091
  if (!Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-input", "finalize identifiers must be the non-empty order identifier array");
1328
- // An own-index loop, NOT forEach: forEach skips a hole in a sparse array, leaving it unvalidated.
1329
1092
  for (var ii = 0; ii < o.identifiers.length; ii++) {
1330
1093
  if (!_hasOwn(o.identifiers, ii)) throw E("acme/bad-input", "finalize identifiers[" + ii + "] is missing (a sparse array is not allowed)");
1331
- _validateOrderIdentifier(o.identifiers[ii]); // caller-supplied -> validate before comparing (no raw TypeError)
1094
+ _validateOrderIdentifier(o.identifiers[ii]);
1332
1095
  }
1333
1096
  _assertCsrIdentifiers(parsed, o.identifiers);
1334
1097
  }
@@ -1416,7 +1179,7 @@ function deactivate(o) {
1416
1179
  function updateAccount(o) {
1417
1180
  return _promised(function () {
1418
1181
  if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
1419
- _assertContacts(o.contact); // RFC 6068 mailto hygiene; an empty array clears all contacts
1182
+ _assertContacts(o.contact);
1420
1183
  return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { contact: o.contact });
1421
1184
  });
1422
1185
  }
@@ -1450,7 +1213,7 @@ function revokeCert(o) {
1450
1213
  return _promised(function () {
1451
1214
  if (!_isObject(o) || !guard.bytes.isByteSource(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate BufferSource (opts.certificate)");
1452
1215
  var certBuf = guard.bytes.snapshotSource(o.certificate, E, "acme/bad-input", "the revokeCert certificate");
1453
- x509.parse(certBuf); // structural validation of the target
1216
+ x509.parse(certBuf);
1454
1217
  var hasKid = _hasOwn(o, "kid");
1455
1218
  var hasJwk = _hasOwn(o, "jwk");
1456
1219
  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)");
@@ -1498,7 +1261,6 @@ async function keyChange(o) {
1498
1261
  if (!_isString(o.account)) throw E("acme/bad-input", "keyChange requires the account URL (payload account)");
1499
1262
  if (!_isObject(o.oldKey)) throw E("acme/bad-input", "keyChange requires the old account public jwk (payload oldKey)");
1500
1263
  if (!_isObject(o.newJwk)) throw E("acme/bad-input", "keyChange requires the new account public jwk (inner header jwk)");
1501
- // oldKey is published in the inner payload and newJwk in the inner header -> both public-only.
1502
1264
  jose.assertPublicJwk(o.oldKey);
1503
1265
  jose.assertPublicJwk(o.newJwk);
1504
1266
  var inner = await jose.sign({
@@ -1509,7 +1271,6 @@ async function keyChange(o) {
1509
1271
  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" });
1510
1272
  }
1511
1273
 
1512
- // ---- ARI (RFC 9773 renewal information) ----------------------------------
1513
1274
 
1514
1275
  /**
1515
1276
  * @primitive pki.acme.ariCertId
@@ -1545,7 +1306,7 @@ function ariCertId(certDer) {
1545
1306
  if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
1546
1307
  var aki = _extDecoders.byOid[OID_AKI](akiExt.value, _extCtx);
1547
1308
  if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
1548
- var serialBytes = Buffer.from(cert.serialNumberHex, "hex"); // DER INTEGER content; sign-pad preserved
1309
+ var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
1549
1310
  return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
1550
1311
  }
1551
1312
 
@@ -1596,101 +1357,43 @@ function validateRenewalInfo(obj) {
1596
1357
  _validate("renewalInfo", obj);
1597
1358
  var w = obj.suggestedWindow;
1598
1359
  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)");
1599
- // The line above already rejected any w.start/w.end that is not a grammar+calendar-valid
1600
- // RFC 3339 string, so neither Date.parse can be NaN here -- the comparison is source-validated.
1601
1360
  // allow:nan-date-comparison-unguarded -- source-validated by the rfc3339.isValid check above.
1602
1361
  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)");
1603
1362
  return obj;
1604
1363
  }
1605
1364
 
1606
- // ---- the thin RFC 8555 client: a stateful session over the shared pki.transport ----
1607
1365
 
1608
1366
  var CLIENT_DEFAULT_TIMEOUT = constants.TIME.seconds(30);
1609
1367
  var CLIENT_MAX_TIMEOUT = constants.TIME.seconds(600);
1610
- // The RFC 8555 sec. 6.1 REQUIRED User-Agent, sent on every request (caller-overridable per request).
1611
1368
  var CLIENT_USER_AGENT = "blamejs-pki/" + pkgVersion + " node/" + process.versions.node;
1612
- // A hard cap on the single-use nonce pool so a server that attaches a Replay-Nonce to every response
1613
- // (permitted, RFC 8555 sec. 6.5) -- including the unauthenticated, poll-scheduled renewalInfo GET --
1614
- // cannot grow it without bound; the oldest (least fresh) nonce is evicted first (CWE-770).
1615
1369
  var CLIENT_MAX_NONCE_POOL = 32;
1616
1370
 
1617
- // Parse + validate a URL BEFORE any request: not-parseable -> acme/bad-url, non-https -> acme/insecure-url
1618
- // (RFC 8555 sec. 6.1 -- HTTPS is REQUIRED; the message-layer _isUrl accepts http, the client does not).
1619
- // The validated string is returned VERBATIM (never url.href): the JWS protected `url` and the account
1620
- // `kid` must be the exact server-provided value (RFC 8555 sec. 6.4/7.3), which URL normalization -- a
1621
- // dropped :443, an added trailing slash -- would otherwise change, breaking the server's match.
1622
1371
  function _clientUrl(urlStr) {
1623
1372
  var s = String(urlStr);
1624
1373
  var url;
1625
1374
  try { url = new URL(s); }
1626
1375
  catch (e) { throw E("acme/bad-url", "the ACME URL did not parse: " + s, e); }
1627
1376
  if (url.protocol !== "https:") throw E("acme/insecure-url", "ACME requires https (RFC 8555 sec. 6.1), got " + url.protocol + " for " + s);
1628
- // An ACME URL never carries userinfo. The transport connects to the host and does NOT put userinfo in the
1629
- // request target, so a signed JWS `url` with userinfo would not match the URL the request is directed to
1630
- // (RFC 8555 sec. 6.4); refuse it rather than sign a credential-bearing URL that steers nothing. Test the RAW
1631
- // authority for a literal "@", not url.username/password: an EMPTY userinfo ("https://@host", "https://:@host")
1632
- // leaves both empty while the verbatim URL still carries the "@" the transport drops.
1633
1377
  var _uAuth = _uriAuthority(s, true);
1634
1378
  if (intrinsic.stringIndexOf(_uAuth ? _uAuth.authority : "", "@") !== -1) throw E("acme/bad-url", "an ACME URL must not contain userinfo: " + JSON.stringify(s));
1635
- // Reject a spelling the transport would REPAIR into a different path -- whitespace (WHATWG percent-
1636
- // encodes or trims it) or a backslash (rewritten to `/`). The transport re-parses the URL to the
1637
- // normalized path while the JWS `url` keeps the original, so the signed and requested URLs would differ.
1638
1379
  if (_hasWsOrBackslash(s)) throw E("acme/bad-url", "an ACME URL must not contain whitespace or a backslash: " + JSON.stringify(s));
1639
- // The path/query round-trip checks below cover only pathname+search; apply the shared structural authority
1640
- // guard too, so a repaired AUTHORITY (a bracket in userinfo -- "a[b@host" -> "a%5Bb@host", a double "@") is
1641
- // rejected on THIS direct path (a directory / Location / caller URL) exactly as the Link-alternate path
1642
- // already rejects it. Otherwise the verbatim URL would be signed (sec. 6.4) while the transport connects to a
1643
- // different authority. (An uppercase host / explicit :443 is NOT a repair here -- it is normalized, not
1644
- // rewritten to a different authority -- and is deliberately honored below, so it is not flagged.)
1645
1380
  if (_uriStructurallyInvalid(s)) throw E("acme/bad-url", "an ACME URL authority is not canonical (the transport would repair it): " + JSON.stringify(s));
1646
- // Reject a HOST the transport would rewrite to a DIFFERENT authority. WHATWG's special-scheme parser coerces an
1647
- // IPv4-address form -- hex ("0x7f.1"), octal ("0177.0.0.1"), decimal ("2130706433"), shorthand ("127.1") -- into
1648
- // a dotted-quad ("127.0.0.1"), so the JWS `url` would be signed over the raw host while the transport connects to
1649
- // a different (often loopback / internal, an SSRF-adjacent) address. Compare the raw host (ASCII-lowercased, the
1650
- // only tolerated normalization) to the parsed hostname: uppercase-host and a default :443 still match, an IPv4
1651
- // coercion or a non-canonical IPv6 literal does not. (path/query/fragment are checked separately below.)
1652
1381
  var _auth = _uriAuthority(s, true);
1653
1382
  if (_auth) {
1654
- var _hp = _auth.authority.slice(_auth.authority.lastIndexOf("@") + 1); // strip any userinfo (does not route the request)
1655
- var _rawHost = _hp.charAt(0) === "[" ? _hp.slice(0, _hp.indexOf("]") + 1) : _hp.split(":")[0]; // strip the port
1383
+ var _hp = _auth.authority.slice(_auth.authority.lastIndexOf("@") + 1);
1384
+ var _rawHost = _hp.charAt(0) === "[" ? _hp.slice(0, _hp.indexOf("]") + 1) : _hp.split(":")[0];
1656
1385
  if (_rawHost.toLowerCase() !== url.hostname) throw E("acme/bad-url", "an ACME URL host is not canonical (the transport would rewrite it): " + JSON.stringify(s));
1657
1386
  }
1658
- // Reject any path the transport would NORMALIZE differently -- literal OR percent-encoded dot-segments
1659
- // (`/..`, `/%2e%2e/`), or an authority-only URL with NO path (`https://ca.example`, where the transport
1660
- // inserts a `/`) -- by comparing the raw path in `s` to the WHATWG-parsed pathname the transport sends as
1661
- // the request target; a mismatch means the signed JWS `url` would keep a path the request never used. The
1662
- // empty raw path is deliberately NOT coerced to `/`: that coercion would mask the authority-only case,
1663
- // whose verbatim `s` (no slash) differs from the `/` the transport requests. (The authority may still
1664
- // normalize -- a default :443 port, an uppercase host -- as that does not change the path; an encoded char
1665
- // like `%41` or a dot inside a segment name round-trips unchanged.)
1666
1387
  var _pathStart = _rawPathStart(s);
1667
1388
  var rawPath = _strSlice(s, _pathStart, _firstDelim(s, _pathStart, 0x3f, 0x23, -1));
1668
1389
  if (rawPath !== url.pathname) throw E("acme/bad-url", "an ACME URL path is not canonical (the transport would normalize it): " + JSON.stringify(s));
1669
- // The query round-trips too (the transport sends pathname+search): an EMPTY query ("...?") -- which WHATWG
1670
- // drops (url.search === "") while the verbatim URL keeps the "?" -- or any query the parser would rewrite
1671
- // signs a JWS `url` differing from the request target. A non-empty canonical query (`?a=b`) round-trips and
1672
- // is honored. (The fragment is rejected below, so the first raw "?" is the query delimiter.)
1673
1390
  var qi = s.indexOf("?");
1674
1391
  var rawQuery = qi === -1 ? "" : s.slice(qi).split("#")[0];
1675
1392
  if (rawQuery !== url.search) throw E("acme/bad-url", "an ACME URL query is not canonical (the transport would normalize it): " + JSON.stringify(s));
1676
- // Reject a fragment: the transport builds the request target from pathname+search only, so a `#frag`
1677
- // would sign a JWS `url` (sec. 6.4) that differs from the URL actually requested, failing at the CA. Test
1678
- // the RAW `#` delimiter, not `url.hash`: an EMPTY fragment ("...alt#") leaves url.hash === "" (falsy) while
1679
- // the verbatim URL still carries the `#`. A literal `#` is always the fragment delimiter (a `#` in a path is
1680
- // %23), so its mere presence is the fragment.
1681
1393
  if (s.indexOf("#") !== -1) throw E("acme/bad-url", "an ACME URL must not contain a fragment: " + JSON.stringify(s));
1682
- // Return the value VERBATIM (the server's exact spelling for the sec. 6.4/7.3 JWS match) -- a conforming
1683
- // CA may emit a form WHATWG would normalize (a default :443 port, an uppercase host); the transport
1684
- // re-parses it for the connection, so such a URL is honored rather than rejected as non-canonical.
1685
1394
  return s;
1686
1395
  }
1687
- // Turn a server `Location` (which MAY be a relative URI-reference -- valid HTTP, common behind a proxy)
1688
- // into the account/order URL: an absolute Location is used verbatim, a relative one is resolved against
1689
- // the request URL, and the result is https-gated. A caller URL uses _clientUrl directly.
1690
1396
  function _resolveLocation(loc, base) {
1691
- // An ABSOLUTE Location is used VERBATIM (its exact spelling -- an uppercase host, an explicit :443 --
1692
- // for the sec. 6.4/7.3 JWS match, via _clientUrl). Only a RELATIVE reference is resolved against the
1693
- // request URL first (the resolved href is then https-gated).
1694
1397
  var isAbsolute = true;
1695
1398
  try { new URL(loc); }
1696
1399
  catch (_e) { isAbsolute = false; }
@@ -1698,45 +1401,28 @@ function _resolveLocation(loc, base) {
1698
1401
  var u;
1699
1402
  try { u = new URL(loc, base); }
1700
1403
  catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
1701
- // A relative reference whose QUERY WHATWG re-encoded on resolution (an RFC 3986 reserved char the special-scheme
1702
- // query set percent-encodes -- an apostrophe -> %27) is non-canonical: the signed+requested URL would differ from
1703
- // the raw advertised query (RFC 8555 sec. 6.4). _clientUrl makes this check for an absolute URL, which a relative
1704
- // reference would otherwise bypass.
1705
1404
  if (_queryRepaired(loc, u)) throw E("acme/bad-url", "a relative Link/Location query is not canonical (resolution re-encoded it): " + JSON.stringify(loc));
1706
1405
  return _clientUrl(u.href);
1707
1406
  }
1708
- // Did resolving `rawRef` re-encode its query (a reserved char like "'" -> %27)? RFC 3986 sec. 6.2.2.2 does NOT make
1709
- // a reserved char equivalent to its percent-escape, so a raw query that differs from the resolved search is a
1710
- // non-canonical reference -- used to reject a repaired target AND to skip a repaired anchor whose context match
1711
- // would otherwise be SPOOFED (a "?x='" anchor byte-matching a base carrying "?x=%27").
1712
1407
  function _queryRepaired(rawRef, resolvedUrl) {
1713
1408
  var qi = rawRef.indexOf("?");
1714
1409
  return qi !== -1 && rawRef.slice(qi).split("#")[0] !== resolvedUrl.search;
1715
1410
  }
1716
- // The default poll sleeper is the shared bounded sleeper (lib/sleep.js): it chunks a delay above Node's
1717
- // 32-bit setTimeout ceiling so a large (parser-bounded, up to a year) Retry-After is honored rather than
1718
- // silently clamped to 1 ms and rapidly re-polled. pki.cmp.session composes the SAME home.
1719
1411
  var _defaultSleep = require("./sleep").sleep;
1720
1412
  function _clientTls(o) {
1721
1413
  var t = o.tls || {};
1722
1414
  return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
1723
1415
  }
1724
- // Split a PEM certificate chain (RFC 8555 sec. 7.4.2 application/pem-certificate-chain) into its DER certs.
1725
1416
  var PEM_CERT_BEGIN = "-----BEGIN CERTIFICATE-----";
1726
1417
  var PEM_CERT_END = "-----END CERTIFICATE-----";
1727
1418
  function _splitPemChain(text) {
1728
1419
  var out = [];
1729
1420
  var lastEnd = 0, from = 0, begin, end;
1730
- // Each block is the shortest BEGIN..END span (the lazy /BEGIN[\s\S]*?END/g scan by literal index).
1731
1421
  while ((begin = intrinsic.stringIndexOf(text, PEM_CERT_BEGIN, from)) !== -1 &&
1732
1422
  (end = intrinsic.stringIndexOf(text, PEM_CERT_END, begin + PEM_CERT_BEGIN.length)) !== -1) {
1733
1423
  var blockEnd = end + PEM_CERT_END.length;
1734
- // application/pem-certificate-chain is ONLY PEM blocks separated by whitespace (RFC 8555 sec. 7.4.2) --
1735
- // no explanatory text before, between, or after; reject any non-whitespace outside a block.
1736
1424
  if (text.slice(lastEnd, begin).trim() !== "") throw E("acme/bad-certificate-chain", "the certificate chain contained non-whitespace text outside a PEM block (RFC 8555 sec. 7.4.2)");
1737
1425
  var der;
1738
- // Decode the armor AND structurally parse it: valid PEM armor can wrap arbitrary base64, so without
1739
- // the strict X.509 parse the download could return non-certificate bytes as the issued certificate.
1740
1426
  try { der = x509.pemDecode(_strSlice(text, begin, blockEnd), "CERTIFICATE"); x509.parse(der); }
1741
1427
  catch (e) { throw E("acme/bad-certificate-chain", "a downloaded certificate did not decode as a valid X.509 certificate (RFC 8555 sec. 7.4.2)", e); }
1742
1428
  out.push(der);
@@ -1748,28 +1434,20 @@ function _splitPemChain(text) {
1748
1434
  return out;
1749
1435
  }
1750
1436
 
1751
- // The default and hard ceilings on the alternate-chain fetch (RFC 8555 sec. 7.4.2). A Link header is an
1752
- // UNTRUSTED server field: cap its length before the walk, and cap how many extra signed POSTs it can drive
1753
- // (CWE-770 fetch amplification), the same posture as maxRedirects.
1754
1437
  var LINK_HEADER_MAX_BYTES = constants.BYTES.kib(8);
1755
1438
  var DEFAULT_MAX_ALTERNATES = 8;
1756
- // Trim only HTTP OWS -- SP (0x20) and HTAB (0x09) -- NOT arbitrary Unicode whitespace. String.prototype.trim
1757
- // would strip an NBSP (obs-text) and other Unicode spaces, letting them masquerade as field whitespace.
1758
- function _trimOWS(s) { // s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "")
1439
+ function _trimOWS(s) {
1759
1440
  var a = 0, b = s.length;
1760
1441
  while (a < b && (_charCodeAt(s, a) === 0x20 || _charCodeAt(s, a) === 0x09)) a++;
1761
1442
  while (b > a && (_charCodeAt(s, b - 1) === 0x20 || _charCodeAt(s, b - 1) === 0x09)) b--;
1762
1443
  return _strSlice(s, a, b);
1763
1444
  }
1764
- // ---- URI / percent / Link character-scan helpers (rule #11: no regex) ----
1765
- var _URIREF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~:/?#[]@!$&'()*+,;=%-"); // RFC 3986 URI-Reference allowed set
1766
- var _UNRES_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // RFC 3986 unreserved
1767
- var _IPVF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~!$&'()*+,;=:-"); // IPvFuture tail chars
1768
- var _HEX_TBL = pkix.charTable("0123456789ABCDEFabcdef"); // hex digit, by table lookup (not an OR-chain)
1445
+ var _URIREF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~:/?#[]@!$&'()*+,;=%-");
1446
+ var _UNRES_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-");
1447
+ var _IPVF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~!$&'()*+,;=:-");
1448
+ var _HEX_TBL = pkix.charTable("0123456789ABCDEFabcdef");
1769
1449
  function _hexVal(c) { if (c >= 48 && c <= 57) return c - 48; if (c >= 65 && c <= 70) return c - 55; return c - 87; }
1770
1450
  function _upperHexChar(c) { return (c >= 97 && c <= 102) ? c - 32 : c; }
1771
- // The RFC 3986 authority front-half shared by every authority check: an optional (or, when schemeRequired,
1772
- // mandatory) scheme, "//", then the authority up to the first "/?#". Returns null when there is no "//".
1773
1451
  function _uriAuthority(uri, schemeRequired) {
1774
1452
  var n = uri.length, i = 0, schemeEnd = -1, j;
1775
1453
  if (n > 0 && _isAlpha(_charCodeAt(uri, 0))) {
@@ -1788,11 +1466,8 @@ function _uriAuthority(uri, schemeRequired) {
1788
1466
  while (authEnd < n) { var c = _charCodeAt(uri, authEnd); if (c === 0x2f || c === 0x3f || c === 0x23) break; authEnd++; }
1789
1467
  return { scheme: schemeEnd === -1 ? "" : _strSlice(uri, 0, schemeEnd + 1), authorityStart: authStart, authorityEnd: authEnd, authority: _strSlice(uri, authStart, authEnd) };
1790
1468
  }
1791
- // The first index at/after `from` whose code is one of the (up to three) delimiters, else the string length.
1792
1469
  function _firstDelim(s, from, d0, d1, d2) { for (var i = from; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === d0 || c === d1 || c === d2) return i; } return s.length; }
1793
- // /^:[0-9]*$/ : ":" then zero or more digits (an empty port ":" passes).
1794
1470
  function _isNumericPort(s) { if (s.length === 0 || _charCodeAt(s, 0) !== 0x3a) return false; for (var i = 1; i < s.length; i++) { var c = _charCodeAt(s, i); if (c < 48 || c > 57) return false; } return true; }
1795
- // /^[vV][0-9A-Fa-f]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$/ : a valid RFC 3986 IPvFuture literal interior.
1796
1471
  function _isIpvFuture(s) {
1797
1472
  var n = s.length, i = 1, h = 0, g = 0;
1798
1473
  if (n === 0 || (_charCodeAt(s, 0) !== 0x76 && _charCodeAt(s, 0) !== 0x56)) return false;
@@ -1802,8 +1477,6 @@ function _isIpvFuture(s) {
1802
1477
  while (i < n) { if (_IPVF_TBL[_charCodeAt(s, i)]) { i++; g++; } else break; }
1803
1478
  return g >= 1 && i === n;
1804
1479
  }
1805
- // The prefix ^(?:scheme:)?//(?:[^/?#@[\]]*@)?\[[^[\]]*\] : optional scheme, "//", optional BRACKET-EXCLUDING
1806
- // userinfo, then an IP-literal host. Returns the index past "]" (the match length from 0) or -1.
1807
1480
  function _matchBracketAuthPrefix(uri) {
1808
1481
  var a = _uriAuthority(uri, false);
1809
1482
  if (!a) return -1;
@@ -1816,8 +1489,6 @@ function _matchBracketAuthPrefix(uri) {
1816
1489
  if (i >= n || _charCodeAt(uri, i) !== 0x5d) return -1;
1817
1490
  return i + 1;
1818
1491
  }
1819
- // The prefix ^(?:scheme:)?//(?:[^/?#@]*@)?\[([^\]]*)\] : optional scheme, "//", optional BRACKET-PERMITTING
1820
- // userinfo, then an IP-literal host. Returns the interior (between "[" and "]") or null.
1821
1492
  function _ipLiteralInterior(uri) {
1822
1493
  var a = _uriAuthority(uri, false);
1823
1494
  if (!a) return null;
@@ -1831,7 +1502,6 @@ function _ipLiteralInterior(uri) {
1831
1502
  if (p >= n) return null;
1832
1503
  return _strSlice(uri, start, p);
1833
1504
  }
1834
- // s.replace(/^[a-z]+:\/\/[^/?#]*/i, "") -> the index where the path begins (0 if there is no scheme+"//").
1835
1505
  function _rawPathStart(s) {
1836
1506
  var n = s.length, i = 0;
1837
1507
  while (i < n && _isAlpha(_charCodeAt(s, i))) i++;
@@ -1841,18 +1511,10 @@ function _rawPathStart(s) {
1841
1511
  while (i < n) { var c = _charCodeAt(s, i); if (c === 0x2f || c === 0x3f || c === 0x23) break; i++; }
1842
1512
  return i;
1843
1513
  }
1844
- // Does `s` contain a character NOT in `table`? (A bespoke false-on-empty scan: !pkix.allCharsIn would wrongly
1845
- // report an empty string as carrying a bad character.)
1846
1514
  function _hasCharOutside(s, table) { for (var i = 0; i < s.length; i++) { if (!table[_charCodeAt(s, i)]) return true; } return false; }
1847
- // /%(?![0-9A-Fa-f]{2})/ : a "%" NOT introducing a valid pct-escape (two hex digits).
1848
1515
  function _hasBadPct(s) { for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x25 && !(_HEX_TBL[_charCodeAt(s, i + 1)] && _HEX_TBL[_charCodeAt(s, i + 2)])) return true; } return false; }
1849
- // /^[A-Za-z][A-Za-z0-9.-]*$/ : an RFC 8288 reg-rel-type (a letter then letters / digits / "." / "-").
1850
1516
  function _isRegRelType(t) { var n = t.length; if (n === 0 || !_isAlpha(_charCodeAt(t, 0))) return false; for (var i = 1; i < n; i++) { var c = _charCodeAt(t, i); if (!(_isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2e || c === 0x2d)) return false; } return true; }
1851
- // Fold every "%2e"/"%2E" in a path segment to "." (that specific dot escape, NOT a general pct-decode); records
1852
- // whether any fold occurred (the /%2e/i prefilter). Returns { decoded, folded }.
1853
1517
  function _foldPct2e(seg) { var out = "", i = 0, n = seg.length, folded = false; while (i < n) { if (_charCodeAt(seg, i) === 0x25 && _charCodeAt(seg, i + 1) === 0x32 && (_charCodeAt(seg, i + 2) === 0x65 || _charCodeAt(seg, i + 2) === 0x45)) { out += "."; folded = true; i += 3; } else { out += _charAt(seg, i); i += 1; } } return { decoded: out, folded: folded }; }
1854
- // inner.replace(/\\(.)/g, "$1") : unescape RFC 7230 quoted-pairs ("\x" -> "x"). "\" + a line terminator is left
1855
- // intact (the regex "." matches no line terminator), as is a dangling final "\".
1856
1518
  function _unescapeQuotedPair(inner) {
1857
1519
  var out = "", i = 0, n = inner.length;
1858
1520
  while (i < n) {
@@ -1864,32 +1526,15 @@ function _unescapeQuotedPair(inner) {
1864
1526
  }
1865
1527
  return out;
1866
1528
  }
1867
- // Is `t` a valid RFC 8288 sec. 3.3 relation-type -- a reg-rel-type (a letter then LDH, compared case-
1868
- // insensitively) OR an ext-rel-type (an absolute URI: a scheme then ':', AND a canonical RFC 3986 reference so
1869
- // a scheme prefix alone is not enough, e.g. "http:%ZZ" is rejected for its malformed pct-escape)? "@" is neither.
1870
1529
  function _validRelType(t) {
1871
- if (_isRegRelType(t)) return true; // reg-rel-type
1872
- // ext-rel-type: a full absolute URI. A relation type is an identifier, never resolved, so a percent-encoded dot
1873
- // (%2e) is a legal char (not a dot-segment); other than that its structure must be a valid absolute URI.
1530
+ if (_isRegRelType(t)) return true;
1874
1531
  if (_hasCharOutside(t, _URIREF_TBL)) return false;
1875
1532
  return _validAbsoluteUri(t);
1876
1533
  }
1877
- // Is `t` a valid RFC 3986 ABSOLUTE URI (a relation-type identifier, never connected to)? A scheme, valid pct-
1878
- // escapes, and a structurally valid authority/path. The characters are already validated (LINK_URI_BADCHAR above);
1879
- // _uriStructurallyInvalid enforces the rest by RFC 3986 GRAMMAR -- brackets only in an authority IP-literal whose
1880
- // content is a valid IPv6/IPvFuture, a non-empty authority, at most one "@", at most one "#", a numeric port. NO
1881
- // URL.canParse on the whole URI: its special-scheme quirks wrongly reject valid identifiers (an IPv4 coercion of a
1882
- // numeric reg-name "1.2.3.4.5", a 16-bit port cap, IPvFuture, an empty reg-name "foo://user@/"); WHATWG is used
1883
- // only inside _badIpLiteral to validate an IPv6 literal, the one place it is correct.
1884
1534
  function _validAbsoluteUri(t) {
1885
1535
  return _hasSchemePrefix(t) && !_hasBadPct(t) && !_uriStructurallyInvalid(t);
1886
1536
  }
1887
- // A URI carries at most ONE "#" (the fragment delimiter); a fragment cannot itself contain "#" (RFC 3986 sec.
1888
- // 3.5), so a second literal "#" is invalid, which WHATWG ACCEPTS by folding the rest into the fragment. A pct-
1889
- // encoded "%23" is a literal char, not counted.
1890
1537
  function _multiFragment(uri) { return uri.split("#").length > 2; }
1891
- // The authority (after "//", optionally after a scheme, up to the next "/?#") carries at most one "@" (userinfo
1892
- // "@" host); a second literal "@" is invalid RFC 3986, which WHATWG ACCEPTS while rewriting the first "@".
1893
1538
  function _authorityMultiAt(uri) {
1894
1539
  var a = _uriAuthority(uri, false);
1895
1540
  if (!a) return false;
@@ -1897,110 +1542,64 @@ function _authorityMultiAt(uri) {
1897
1542
  for (var i = 0; i < a.authority.length; i++) { if (_charCodeAt(a.authority, i) === 0x40) cnt++; }
1898
1543
  return cnt >= 2;
1899
1544
  }
1900
- // A URI whose scheme REQUIRES an authority (or a scheme-relative reference, which inherits one -- always https
1901
- // here) must have a NON-EMPTY authority. A "//" immediately followed by "/", "?", "#", or end is an empty
1902
- // authority WHATWG silently REPAIRS by promoting the next path segment to the host ("///cert/alt" -> host "cert";
1903
- // "////acme.example/p" -> host "acme.example"; "http:///relations" -> host "relations"). A scheme that legitimately
1904
- // allows an empty authority (file:///path, or any non-authority scheme) is NOT flagged.
1905
1545
  function _hasEmptyAuthority(uri) {
1906
1546
  var a = _uriAuthority(uri, false);
1907
1547
  if (!a) return false;
1908
- var scheme = a.scheme === "" ? "" : a.scheme.slice(0, -1).toLowerCase(); // "" == scheme-relative (inherits the https base)
1548
+ var scheme = a.scheme === "" ? "" : a.scheme.slice(0, -1).toLowerCase();
1909
1549
  if (scheme !== "" && scheme !== "http" && scheme !== "https" && scheme !== "ws" && scheme !== "wss" && scheme !== "ftp") return false;
1910
- // The HOST itself (the authority with any userinfo and port stripped) must be non-empty -- not only the char
1911
- // right after "//": "https://user@/x" and "https://:443/x" have an empty host after the userinfo / before the
1912
- // port, which WHATWG rejects (or repairs) for a special scheme.
1913
1550
  var host = a.authority.slice(a.authority.lastIndexOf("@") + 1);
1914
1551
  if (host.charAt(0) !== "[") { var ci = host.indexOf(":"); if (ci !== -1) host = host.slice(0, ci); }
1915
1552
  return host === "";
1916
1553
  }
1917
- // Are any brackets in `uri` confined to a single authority IP-literal (`scheme://[...]`, scheme optional for a
1918
- // network-path reference, RFC 3986 sec. 4.2)? RFC 3986 permits `[`/`]` ONLY there; anywhere else -- a path
1919
- // ("/cert/[alt]") or an opaque scheme part ("urn:[") -- they are invalid, yet WHATWG's lenient parse accepts them.
1920
1554
  function _bracketsOnlyInAuthority(uri) {
1921
1555
  if (uri.indexOf("[") === -1 && uri.indexOf("]") === -1) return true;
1922
- // Optional userinfo may precede the IP-literal host (RFC 3986: authority = [ userinfo "@" ] host [ ":" port ]).
1923
- // A raw "[" / "]" is NOT permitted in userinfo (RFC 3986 sec. 3.2.1/3.2.2 -- the IP-literal host is the ONLY place
1924
- // square brackets are allowed), so the userinfo class excludes them; otherwise "a[b@[::1]" (which WHATWG repairs
1925
- // to "a%5Bb@[::1]") would anchor on the real host and slip.
1926
1556
  var mlen = _matchBracketAuthPrefix(uri);
1927
1557
  if (mlen === -1) return false;
1928
1558
  var rest = _strSlice(uri, mlen);
1929
- if (rest.indexOf("[") !== -1 || rest.indexOf("]") !== -1) return false; // no bracket past the authority
1930
- // After the IP-literal HOST "]", the rest of the authority (up to the next "/?#") may be ONLY an optional numeric
1931
- // ":port". Anything else -- an "@" ("[::1]:80@host" / "[::1]@host", where "[::1]" is a bracketed userinfo, invalid
1932
- // RFC 3986), a non-numeric port, or trailing host text -- means the literal sits in an invalid position, which
1933
- // WHATWG would re-parse to a DIFFERENT host.
1559
+ if (rest.indexOf("[") !== -1 || rest.indexOf("]") !== -1) return false;
1934
1560
  var afterHost = _strSlice(rest, 0, _firstDelim(rest, 0, 0x2f, 0x3f, 0x23));
1935
1561
  return afterHost === "" || _isNumericPort(afterHost);
1936
1562
  }
1937
- // A relative-PATH reference (no scheme, no "//" authority) whose FIRST path segment contains a ":" is a
1938
- // path-noscheme violation (RFC 3986 sec. 3.3 / 4.2: the first segment "cannot contain a colon (':') character",
1939
- // as it would be mistaken for a scheme). ":foo" or "1x:y" is such a case, which WHATWG resolves against the base
1940
- // (same-origin) rather than rejecting. A real scheme ("foo:bar") is handled by the scheme/https gates, not here.
1941
1563
  function _relFirstSegHasColon(uri) {
1942
1564
  if (_hasSchemePrefix(uri) || uri.indexOf("//") === 0) return false;
1943
1565
  return _uriFirstSegment(uri).indexOf(":") !== -1;
1944
1566
  }
1945
- // The authority port (":" after the host, before the next "/?#") must be *DIGIT (RFC 3986 sec. 3.2.3). A
1946
- // non-numeric port ("host:bad") is malformed -- the character + structural checks otherwise miss it, and WHATWG
1947
- // rejects it only on connect (so a fetched target would be skipped rather than failing closed). An empty port
1948
- // (":") and a missing port are fine.
1949
1567
  function _badPort(uri) {
1950
1568
  var a = _uriAuthority(uri, false);
1951
1569
  if (!a) return false;
1952
1570
  var auth = a.authority, n = auth.length, i = 0, at = -1, hostEnd, e;
1953
- for (var u = 0; u < n; u++) { if (_charCodeAt(auth, u) === 0x40) { at = u; break; } } // userinfo up to first "@"
1571
+ for (var u = 0; u < n; u++) { if (_charCodeAt(auth, u) === 0x40) { at = u; break; } }
1954
1572
  if (at !== -1) i = at + 1;
1955
- if (i < n && _charCodeAt(auth, i) === 0x5b) { // IP-literal host "[...]"
1573
+ if (i < n && _charCodeAt(auth, i) === 0x5b) {
1956
1574
  e = i + 1;
1957
1575
  while (e < n && _charCodeAt(auth, e) !== 0x5d) e++;
1958
1576
  if (e < n) hostEnd = e + 1;
1959
1577
  else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; }
1960
- } else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; } // reg-name up to ":"
1961
- if (hostEnd >= n || _charCodeAt(auth, hostEnd) !== 0x3a) return false; // no port
1578
+ } else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; }
1579
+ if (hostEnd >= n || _charCodeAt(auth, hostEnd) !== 0x3a) return false;
1962
1580
  return !_isNumericPort(_strSlice(auth, hostEnd));
1963
1581
  }
1964
- // An IP-literal host "[...]" must CONTAIN a valid IPv6 address or IPvFuture (RFC 3986 sec. 3.2.2) -- "[not-ip]",
1965
- // "[]", "[garbage]" are malformed, which the bracket-placement check alone accepts (it only confirms the brackets
1966
- // are in the authority) and WHATWG rejects only on connect (so a fetched target would be skipped rather than
1967
- // failing closed). IPvFuture is checked by grammar; an IPv6 by WHATWG (which parses it correctly). `URL.canParse`
1968
- // never throws, so no swallow.
1969
1582
  function _badIpLiteral(uri) {
1970
1583
  var interior = _ipLiteralInterior(uri);
1971
1584
  if (interior === null) return false;
1972
- if (_isIpvFuture(interior)) return false; // valid IPvFuture
1585
+ if (_isIpvFuture(interior)) return false;
1973
1586
  return !URL.canParse("http://[" + interior + "]/");
1974
1587
  }
1975
- // The RFC 3986 STRUCTURAL rules a URI-reference must satisfy that WHATWG would otherwise silently REPAIR (accept
1976
- // while rewriting) or reject only on connect: brackets only in an authority IP-literal, a non-empty authority, at
1977
- // most one "@" in the authority, at most one "#", a numeric port, no colon in a relative-path first segment.
1978
- // SHARED by the fetched-target/anchor check (_linkUriInvalid) AND the ext-rel-type check (_validAbsoluteUri) so the
1979
- // two can NEVER diverge -- a new structural rule added here binds both at once.
1980
1588
  function _uriStructurallyInvalid(uri) {
1981
1589
  return !_bracketsOnlyInAuthority(uri) || _hasEmptyAuthority(uri) || _authorityMultiAt(uri) || _multiFragment(uri) || _relFirstSegHasColon(uri) || _badPort(uri) || _badIpLiteral(uri);
1982
1590
  }
1983
- // RFC 3986 sec. 6.2.2 syntax-based normalization for an alternate DEDUP key (the verbatim URL is still what is
1984
- // fetched + signed, sec. 6.4). The WHATWG-normalized href already lowercases scheme+host, drops a default port,
1985
- // and removes dot-segments; on top of that, DECODE a percent-escape of an unreserved char (sec. 6.2.2.2 --
1986
- // "%61" == "a") and UPPERCASE the hex of any other escape (sec. 6.2.2.1), so equivalent spellings collapse to one
1987
- // signed fetch (CWE-770). fromCharCode keeps the source pure-ASCII while matching a runtime byte.
1988
1591
  function _dedupKey(href) {
1989
1592
  var out = "", i = 0, n = href.length;
1990
1593
  while (i < n) {
1991
1594
  if (_charCodeAt(href, i) === 0x25 && _HEX_TBL[_charCodeAt(href, i + 1)] && _HEX_TBL[_charCodeAt(href, i + 2)]) {
1992
1595
  var h1 = _charCodeAt(href, i + 1), h2 = _charCodeAt(href, i + 2), v = (_hexVal(h1) << 4) | _hexVal(h2);
1993
- if (_UNRES_TBL[v]) out += String.fromCharCode(v); // decode an unreserved escape
1994
- else out += "%" + String.fromCharCode(_upperHexChar(h1)) + String.fromCharCode(_upperHexChar(h2)); // else uppercase the hex
1596
+ if (_UNRES_TBL[v]) out += String.fromCharCode(v);
1597
+ else out += "%" + String.fromCharCode(_upperHexChar(h1)) + String.fromCharCode(_upperHexChar(h2));
1995
1598
  i += 3;
1996
1599
  } else { out += _charAt(href, i); i += 1; }
1997
1600
  }
1998
1601
  return out;
1999
1602
  }
2000
- // A path SEGMENT that decodes to "." or ".." via a percent-encoded dot (`%2e`): URL resolution decodes `%2e`
2001
- // to `.` for dot-segment removal, so an encoded `..` silently becomes a path traversal that changes the
2002
- // resolved target. Reject only such an encoded dot-SEGMENT -- an encoded dot elsewhere (a filename `0%2ex` ->
2003
- // `0.x`) is a valid target, and a LITERAL `.`/`..` segment resolves transparently. Path only (before ?/#).
2004
1603
  function _hasEncodedDotSegment(uri) {
2005
1604
  var segs = _split(_strSlice(uri, 0, _firstDelim(uri, 0, 0x3f, 0x23, -1)), "/");
2006
1605
  for (var _si = 0; _si < segs.length; _si++) {
@@ -2010,22 +1609,14 @@ function _hasEncodedDotSegment(uri) {
2010
1609
  }
2011
1610
  return false;
2012
1611
  }
2013
- // Is a Link URI-Reference (a target OR an anchor) NOT a canonical RFC 3986 reference -- i.e. one URL parsing
2014
- // would repair (a disallowed char, a malformed pct-escape, an encoded dot-segment) on resolution?
2015
1612
  function _linkUriInvalid(uri) {
2016
- // A bracket in a resolved target/anchor is valid only in an authority IP-literal (a same-origin IPv6/IPvFuture
2017
- // cert URL is legal); in a path it is invalid and WHATWG would repair it by percent-encoding.
2018
1613
  return _hasCharOutside(uri, _URIREF_TBL) || _hasBadPct(uri) || _hasEncodedDotSegment(uri) || _uriStructurallyInvalid(uri);
2019
1614
  }
2020
- // A control octet forbidden in an HTTP field-value (RFC 9110): any C0 control except HTAB, plus DEL. Checked
2021
- // by code point (not a control-char regex) to keep the source pure ASCII and clear of eslint no-control-regex.
2022
1615
  function _hasCtlOctet(s) {
2023
1616
  for (var _ci = 0; _ci < s.length; _ci++) { var _cc = s.charCodeAt(_ci); if (_cc === 0x7F || (_cc < 0x20 && _cc !== 0x09)) return true; }
2024
1617
  return false;
2025
1618
  }
2026
1619
 
2027
- // Split an RFC 8288 Link field into its comma-separated link-values, respecting <...> and "..." context so
2028
- // a comma inside a URI or a quoted parameter value is not a separator. An unterminated quote fails closed.
2029
1620
  function _splitLinkValues(s) {
2030
1621
  var out = [], start = 0, inAngle = false, inQuote = false;
2031
1622
  for (var i = 0; i < s.length; i++) {
@@ -2040,8 +1631,6 @@ function _splitLinkValues(s) {
2040
1631
  out.push(s.slice(start));
2041
1632
  return out;
2042
1633
  }
2043
- // Split a link-value's parameter tail (everything after `<URI>`) into `;`-separated name=value params,
2044
- // quote-aware; a quoted value is unescaped. Returns [{ name (lower-cased), value }].
2045
1634
  function _splitLinkParams(rest) {
2046
1635
  var parts = [], start = 0, inQuote = false;
2047
1636
  for (var i = 0; i < rest.length; i++) {
@@ -2054,71 +1643,41 @@ function _splitLinkParams(rest) {
2054
1643
  var out = [];
2055
1644
  for (var j = 0; j < parts.length; j++) {
2056
1645
  var p = _trimOWS(parts[j]);
2057
- // `rest` starts with the first `;`, so parts[0] is an expected empty slot before the first parameter. Any
2058
- // OTHER empty part is a `;` with no parameter after it (`;;` or a trailing `;`) -- malformed (RFC 8288).
2059
1646
  if (p === "") { if (j === 0) continue; throw E("acme/bad-link", "an empty Link parameter (a ';' with no parameter) is malformed (RFC 8288): " + JSON.stringify(rest)); }
2060
1647
  var eq = p.indexOf("=");
2061
1648
  var name = (eq === -1 ? p : _trimOWS(p.slice(0, eq))).toLowerCase();
2062
- // A parameter NAME is a token (RFC 8288 / RFC 7230): reject whitespace / specials rather than accept a
2063
- // malformed "bad name=x" as a silently-ignored parameter that masks the value grammar below.
2064
1649
  if (!pkix.allCharsIn(name, _LINK_TOKEN_TABLE)) throw E("acme/bad-link", "a Link parameter name must be a token (RFC 8288 / RFC 7230): " + JSON.stringify(p));
2065
- // A valueless token parameter is VALID: RFC 8288 link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
2066
- // -- the value is OPTIONAL. Keep it as an empty-valued extension param (it is never `rel`, so it is ignored).
2067
- // (An EMPTY part -- a `;` with no token at all, e.g. `;;` -- was already rejected above.)
2068
1650
  if (eq === -1) { out.push({ name: name, value: "", hasValue: false }); continue; }
2069
1651
  var v = _trimOWS(p.slice(eq + 1));
2070
1652
  if (v.length >= 2 && v.charAt(0) === "\"" && v.charAt(v.length - 1) === "\"") {
2071
- var inner = v.slice(1, -1); // a quoted-string may hold a space-separated list (RFC 8288)
1653
+ var inner = v.slice(1, -1);
2072
1654
  if (!_isValidQuotedInterior(inner)) throw E("acme/bad-link", "a quoted Link parameter value is malformed (an unescaped quote or dangling backslash, RFC 7230): " + JSON.stringify(p));
2073
1655
  v = _unescapeQuotedPair(inner);
2074
1656
  } else if (!pkix.allCharsIn(v, _LINK_TOKEN_TABLE)) {
2075
- // An UNQUOTED value is a single NON-EMPTY RFC 7230 token -- no whitespace/specials, and an empty value
2076
- // (`foo=`) is not a token (an empty value must be quoted, `foo=""`). Reject a malformed one rather than
2077
- // split-and-match it as if it were a quoted relation list.
2078
1657
  throw E("acme/bad-link", "an unquoted Link parameter value must be a non-empty token (RFC 8288 / RFC 7230): " + JSON.stringify(p));
2079
1658
  }
2080
1659
  out.push({ name: name, value: v, hasValue: true });
2081
1660
  }
2082
1661
  return out;
2083
1662
  }
2084
- // Does an RFC 8288 rel value (a space-separated relation-type list) contain "alternate" as a WHOLE token,
2085
- // case-insensitively (RFC 8288 sec. 3.3)? Never a substring -- "alternateX" / "xalternate" do not match.
2086
1663
  function _relHas(rel, relName) {
2087
- // The rel value is a SPACE-separated relation-type list (RFC 8288 sec. 3.3) -- split on SP only, NOT a tab
2088
- // or other whitespace, so "next<TAB>index" is a single (non-matching) relation-type, not a false match.
2089
1664
  var toks = String(rel).split(" ");
2090
1665
  for (var i = 0; i < toks.length; i++) { if (toks[i].toLowerCase() === relName) return true; }
2091
1666
  return false;
2092
1667
  }
2093
- // Parse one link-value `"<" URI-Reference ">" *( ";" link-param )` -> { uri, rel } (rel "" if absent);
2094
- // a blank segment (a trailing comma) -> null (skip); anything not starting with <...> -> acme/bad-link.
2095
1668
  function _parseLinkValue(raw) {
2096
1669
  var s = _trimOWS(raw);
2097
1670
  if (s === "") return null;
2098
1671
  var gt = s.indexOf(">");
2099
1672
  if (s.charAt(0) !== "<" || gt === -1) throw E("acme/bad-link", "a Link value must be <URI-Reference> with parameters (RFC 8288): " + JSON.stringify(raw));
2100
- // After the "<URI>" the remainder is *( OWS ";" OWS link-param ) -- so, past any leading whitespace, it is
2101
- // empty or begins with ';'. Any other trailing text (a param not introduced by ';', e.g. "<uri>rel=...") is
2102
- // malformed and MUST NOT be leniently read as a parameter.
2103
1673
  var rest = _stripLeadingOWS(s, gt + 1);
2104
1674
  if (rest !== "" && rest.charAt(0) !== ";") throw E("acme/bad-link", "a Link value's parameters must be introduced by ';' (RFC 8288): " + JSON.stringify(raw));
2105
1675
  var uri = s.slice(1, gt);
2106
- // A URI-Reference carries only RFC 3986 characters; WHATWG URL parsing would silently REPAIR anything else
2107
- // (trim whitespace, percent-encode a brace/control char, rewrite a backslash) when the target is later
2108
- // resolved -- masking a malformed / off-path value. Reject it up front, the same canonicality posture the
2109
- // client applies to every server-provided URL (a relative target skips _clientUrl's raw-vs-parsed check).
2110
1676
  if (_linkUriInvalid(uri)) throw E("acme/bad-link", "a Link URI-Reference contains a character, percent-escape, or encoded dot-segment not permitted by RFC 3986: " + JSON.stringify(raw));
2111
1677
  var params = _splitLinkParams(rest), rel = "", relSeen = false, anchor = null, anchorSeen = false;
2112
1678
  for (var i = 0; i < params.length; i++) {
2113
- // The FIRST occurrence of rel wins; later ones are ignored (RFC 8288 sec. 3.3, a singleton) -- take the first
2114
- // even when its value is empty, so `rel="";rel=alternate` keeps the empty first value (the caller then fails
2115
- // closed on the empty rel, rather than falling through to the later alternate).
2116
1679
  if (params[i].name === "rel" && !relSeen) { rel = params[i].value; relSeen = true; }
2117
1680
  else if (params[i].name === "anchor") {
2118
- // anchor is NOT in the RFC 8288 sec. 3.3 singleton set (rel/media/title/title*/type), so it has no "ignore
2119
- // after the first" rule. A DUPLICATE anchor is ambiguous, and anchor is security-relevant (it overrides the
2120
- // link CONTEXT), so it fails closed rather than silently taking one. A VALUELESS anchor (no `=`) is malformed;
2121
- // an explicit value -- even an empty quoted one (`anchor=""`, resolving to the context) -- is valid.
2122
1681
  if (anchorSeen) throw E("acme/bad-link", "a Link value must not carry more than one anchor parameter (RFC 8288 sec. 3.2, ambiguous context): " + JSON.stringify(raw));
2123
1682
  anchorSeen = true;
2124
1683
  if (!params[i].hasValue) throw E("acme/bad-link", "a Link anchor parameter requires a URI value (RFC 8288 sec. 3.2): " + JSON.stringify(raw));
@@ -2127,34 +1686,16 @@ function _parseLinkValue(raw) {
2127
1686
  }
2128
1687
  return { uri: uri, rel: rel, anchor: anchor, relSeen: relSeen };
2129
1688
  }
2130
- // Parse an RFC 8288 Link response header for the targets of ONE relation type -- the sec. 7.4.2
2131
- // `alternate` certificate chains and the sec. 7.1.2.1 `next` orders page share every byte-cap /
2132
- // control-octet / rel-well-formedness / anchor-context / resolve / https-gate / origin-gate / dedupe
2133
- // rule, and only the matched relation and the singleton rule vary. Read the (case-insensitive,
2134
- // node-lower-cased) `link` field -- a combined string OR an array -- bound its length, split into
2135
- // link-values, keep the `relName`-rel ones, resolve each against `base`, https-gate, origin-gate to
2136
- // `base`'s origin, and dedupe by resolved URL. An UNTRUSTED header: every malformed shape or non-https /
2137
- // off-origin / unresolvable target fails closed as acme/bad-link. opts.singleton (RFC 8288 sec. 3.3)
2138
- // makes >1 DISTINCT target ambiguous -> acme/bad-link, for a singleton relation such as `next`.
2139
1689
  function _parseLinkRelation(headers, base, relName, opts) {
2140
1690
  opts = opts || {};
2141
1691
  var raw = null;
2142
1692
  for (var k in headers) { if (_hasOwn(headers, k) && k.toLowerCase() === "link") { raw = headers[k]; break; } }
2143
1693
  if (raw == null) return [];
2144
- var baseUrl = new URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href; // the request URL
1694
+ var baseUrl = new URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
2145
1695
  var baseKey = _dedupKey(baseHref);
2146
1696
  var fields = Array.isArray(raw) ? raw : [raw], out = [], seen = Object.create(null), distinctCount = 0, totalBytes = 0;
2147
- // The base URL is not a relation OF itself -- a certificate advertised as its own rel="alternate" (RFC 8555
2148
- // sec. 7.4.2), or an orders page advertised as its own rel="next" -- so it is excluded from the followable
2149
- // set below and never causes a redundant re-fetch. It is still COUNTED toward the singleton test, so a
2150
- // self-link plus a distinct target is the two distinct targets RFC 8288 sec. 3.3 makes ambiguous, rather
2151
- // than one target with the self-link silently dropped.
2152
1697
  for (var fi = 0; fi < fields.length; fi++) {
2153
1698
  var field = String(fields[fi]);
2154
- // Cap the AGGREGATE across every Link field (CWE-770): an injected transport / duplicate-Link array can
2155
- // hand back many fields, each under a per-field size, that together are unbounded -- bound the sum so the
2156
- // parse work (and the collected alternate set) cannot amplify regardless of the header's shape. Charge a
2157
- // per-field overhead so a flood of EMPTY fields (each 0 bytes) still counts toward the cap.
2158
1699
  totalBytes += field.length + 1;
2159
1700
  if (totalBytes > LINK_HEADER_MAX_BYTES) throw E("acme/bad-link", "the Link header(s) exceed the " + LINK_HEADER_MAX_BYTES + "-byte aggregate cap (RFC 8288, CWE-770)");
2160
1701
  if (_hasCtlOctet(field)) throw E("acme/bad-link", "a Link header must not contain control octets (RFC 9110 field-value)");
@@ -2162,75 +1703,39 @@ function _parseLinkRelation(headers, base, relName, opts) {
2162
1703
  for (var vi = 0; vi < values.length; vi++) {
2163
1704
  var lv = _parseLinkValue(values[vi]);
2164
1705
  if (lv === null) continue;
2165
- // A rel that is PRESENT but empty (`rel=""`) is a zero-length relation-type list, which is malformed (RFC
2166
- // 8288 sec. 3.3 requires >=1 type). Fail closed rather than silently skip it -- otherwise a malformed value
2167
- // could shadow a later valid one. (A link-value with NO rel at all is simply not one of ours -> skipped.)
2168
1706
  if (lv.relSeen && lv.rel === "") throw E("acme/bad-link", "a Link rel parameter must name at least one relation-type (RFC 8288 sec. 3.3)");
2169
- // The rel value is a space-separated relation-type list with 1*SP separators (RFC 8288 sec. 3.3): multiple
2170
- // spaces BETWEEN types are allowed, but a LEADING or TRAILING space is malformed (so "alternate " is not
2171
- // split-and-matched as containing "alternate", while "alternate index" is a valid two-type list).
2172
1707
  if (lv.rel !== "" && (lv.rel.charAt(0) === " " || lv.rel.charAt(lv.rel.length - 1) === " ")) throw E("acme/bad-link", "a Link rel value has a leading or trailing space (RFC 8288 sec. 3.3): " + JSON.stringify(lv.rel));
2173
- // EVERY relation-type in the list must be well-formed (RFC 8288 sec. 3.3): a bad token (e.g. "@") makes the
2174
- // list malformed, even if another token is "alternate". (Empty tokens are internal 1*SP -- skip them.)
2175
1708
  var relToks = lv.rel === "" ? [] : lv.rel.split(" ");
2176
1709
  for (var ri = 0; ri < relToks.length; ri++) { if (relToks[ri] !== "" && !_validRelType(relToks[ri])) throw E("acme/bad-link", "a Link rel value contains a token that is not a valid relation-type (RFC 8288 sec. 3.3): " + JSON.stringify(lv.rel)); }
2177
1710
  if (!_relHas(lv.rel, relName)) continue;
2178
- // RFC 8288 sec. 3.2: an `anchor` overrides the link's CONTEXT. A certificate alternate's context is the
2179
- // certificate itself (the download URL); a link anchored to another resource is an alternate of THAT
2180
- // resource, not this certificate -- skip it. No anchor, or one resolving to the download URL, is ours.
2181
1711
  if (lv.anchor != null) {
2182
- // Validate the RAW anchor with the same RFC 3986 rules as a target BEFORE resolving: otherwise a
2183
- // repairable anchor (an encoded dot-segment) could traverse to the certificate URL and SPOOF a context
2184
- // match. A non-canonical anchor is not a reliable context -> skip.
2185
1712
  if (_linkUriInvalid(lv.anchor)) continue;
2186
1713
  var au;
2187
1714
  try { au = new URL(lv.anchor, base); } catch (_ae) { continue; }
2188
- // A repaired anchor query ("?x='" re-encoded to "?x=%27") must not byte-match a base carrying "?x=%27":
2189
- // "'" is a reserved sub-delim, not equivalent to its escape (RFC 3986 sec. 6.2.2.2), so a resolution that
2190
- // re-encoded it is a non-canonical anchor -> unreliable context, skip.
2191
1715
  if (_queryRepaired(lv.anchor, au)) continue;
2192
1716
  if (au.href !== baseHref) continue;
2193
1717
  }
2194
1718
  var resolved;
2195
- try { resolved = _resolveLocation(lv.uri, base); } // resolves a relative ref + enforces the https gate
1719
+ try { resolved = _resolveLocation(lv.uri, base); }
2196
1720
  catch (e) {
2197
- // A non-https target is a security-relevant anomaly -> fail the whole header closed. A target that is a
2198
- // valid RFC 3986 reference but not a canonical ACME request URL (literal dot-segments, or a sub-delim
2199
- // WHATWG would re-encode in a special-scheme query) cannot be signed AND requested byte-identically
2200
- // (RFC 8555 sec. 6.4), so it is UNUSABLE. For a MULTI-target relation (alternate) it is skipped and the
2201
- // others are kept (sec. 7.4.2 permits several), rather than discarding every valid alternate over one
2202
- // non-canonical spelling. For a SINGLETON relation (next) there is only the one target, so skipping it
2203
- // would silently END pagination and omit orders -- an unusable singleton fails closed instead.
2204
1721
  if (e && e.code === "acme/insecure-url") throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not an https URL: " + JSON.stringify(lv.uri), e);
2205
1722
  if (opts.singleton) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not a usable ACME request URL: " + _stringify(lv.uri), e);
2206
1723
  continue;
2207
1724
  }
2208
- // Origin-gate: an alternate is fetched with the ACCOUNT-KEY-signed POST-as-GET, so an untrusted (TLS-
2209
- // delivered but unsigned) Link header MUST NOT steer that authenticated request to a different origin
2210
- // (SSRF). Confine alternates to the certificate download's own origin -- the same "possibly compromised
2211
- // directory" threat the cross-origin mTLS-credential strip already defends (RFC 8555 sec. 7.4.2
2212
- // alternates are the CA's own chains, served alongside the certificate).
2213
1725
  var parsed = new URL(resolved);
2214
1726
  if (parsed.origin !== baseOrigin) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not on the request URL's origin (SSRF guard): " + JSON.stringify(lv.uri));
2215
- // Dedup by the RFC 3986 sec. 6.2.2-normalized href (lowercased scheme+host, no default :443, no dot-segments,
2216
- // unreserved pct-escapes decoded), so equivalent spellings ("acme.example" vs "ACME.EXAMPLE:443", "/alt" vs
2217
- // "/%61lt") collapse to one fetch (CWE-770 amplification) -- while the VERBATIM resolved string is what is
2218
- // fetched + signed (RFC 8555 sec. 6.4 exact-URL match).
2219
1727
  var key = _dedupKey(parsed.href);
2220
- if (seen[key]) continue; // an equivalent spelling of this target already counted
1728
+ if (seen[key]) continue;
2221
1729
  seen[key] = true;
2222
- distinctCount += 1; // every distinct matched target, INCLUDING a self-link
2223
- if (key !== baseKey) out.push(resolved); // the base is not followable, but it still counted above
1730
+ distinctCount += 1;
1731
+ if (key !== baseKey) out.push(resolved);
2224
1732
  }
2225
1733
  }
2226
- // RFC 8288 sec. 3.3: `next` is a singleton relation-type, so more than one DISTINCT target is ambiguous.
2227
- // The deduped set already collapses equivalent spellings, so this fires only on genuinely different URLs.
2228
1734
  if (opts.singleton && distinctCount > 1) throw E("acme/bad-link", "the Link header names more than one distinct rel=\"" + relName + "\" target (RFC 8288 sec. 3.3, a singleton relation)");
2229
1735
  return out;
2230
1736
  }
2231
1737
 
2232
1738
  function _parseLinkAlternates(headers, base) { return _parseLinkRelation(headers, base, "alternate", {}); }
2233
- // The single sec. 7.1.2.1 `next` orders-page target resolved against `base`, or null when the header names none.
2234
1739
  function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, base, "next", { singleton: true }); return m.length ? m[0] : null; }
2235
1740
 
2236
1741
  /**
@@ -2269,19 +1774,10 @@ function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, bas
2269
1774
  * var acct = await acme.newAccount({ termsOfServiceAgreed: true });
2270
1775
  * var ord = await acme.newOrder({ identifiers: [{ type: "dns", value: "example.org" }] });
2271
1776
  */
2272
- // The options pki.acme.client reads at construction. A near-miss spelling (a plural, a sibling verb's
2273
- // word, a dropped letter) must be refused with a message naming it, not read as undefined and silently
2274
- // defaulted -- a mistyped `maxRedirects` or `transport` would otherwise change the client's behavior with
2275
- // no error. The per-request options each returned method takes are checked in those methods, not here.
2276
1777
  var _CLIENT_OPTS = {
2277
1778
  accountKey: 1, accountJwk: 1, alg: 1, resignKeys: 1, transport: 1, tls: 1, timeout: 1, maxResponseBytes: 1,
2278
1779
  maxRedirects: 1, maxNonceRetries: 1, maxPolls: 1, maxTotalWait: 1, sleep: 1, clock: 1,
2279
1780
  };
2280
- // The per-request options each client method reads. A near-miss spelling in a method's own bag is
2281
- // refused the same way the constructor refuses one, rather than read as undefined and silently
2282
- // defaulted -- a mistyped `requireBinding` or `reason` would otherwise change the request with no error.
2283
- // finalize accepts `identifiers` but NEVER lets it override the order's own set (RFC 8555 sec. 7.4), so
2284
- // it is recognized here though the binding ignores it.
2285
1781
  var _METHOD_OPTS = {
2286
1782
  finalize: { csr: 1, identifiers: 1 },
2287
1783
  poll: { maxPolls: 1, maxTotalWait: 1, onRetryAfter: 1 },
@@ -2292,19 +1788,11 @@ var _METHOD_OPTS = {
2292
1788
  updateAccount: { contact: 1 },
2293
1789
  listOrders: { maxPages: 1 },
2294
1790
  };
2295
- // Settle a per-method options bag the way the constructor settles its own: guard.identifier.optionsObject
2296
- // snapshots the bag and refuses one a getter rewrites mid-read (a `requireBinding` getter that grows a
2297
- // misspelled key after the check would otherwise slip past assertKnownKeys), THEN reject any unknown key.
2298
- // Returns the settled bag so the method reads the frozen copy, never the live object a getter could rewrite.
2299
1791
  function _gateOpts(bag, whitelist, name) {
2300
1792
  bag = guard.identifier.optionsObject(bag, E, "acme/bad-input", "pki.acme " + name + " options");
2301
1793
  guard.identifier.assertKnownKeys(bag, whitelist, E, "acme/bad-input", "unknown " + name + " option ");
2302
1794
  return bag;
2303
1795
  }
2304
- // Validate + snapshot opts.resignKeys (config-time, fail closed). Absent -> null (the feature is OFF and
2305
- // a badSignatureAlgorithm surfaces to the caller unchanged). Present -> a non-empty array of { alg, key }
2306
- // where alg is a JWS alg string and key is a CryptoKey; each pair is read ONCE into a frozen copy so a
2307
- // caller getter cannot rewrite it between this check and the re-sign, and the caller's order is preserved.
2308
1796
  function _resignKeysOf(raw) {
2309
1797
  if (raw == null) return null;
2310
1798
  if (!_isArrayI(raw) || raw.length === 0) throw E("acme/bad-input", "resignKeys, when set, must be a non-empty array of { alg, key } (RFC 8555 sec. 6.2)");
@@ -2312,18 +1800,12 @@ function _resignKeysOf(raw) {
2312
1800
  for (var i = 0; i < raw.length; i++) {
2313
1801
  var e = raw[i];
2314
1802
  if (!_isObject(e)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
2315
- // Read each field ONCE into the frozen snapshot: a caller getter cannot then answer the validation
2316
- // and the re-sign with different values (the alg checked is the alg signed, the key checked is signed).
2317
1803
  var a = e.alg, k = e.key;
2318
1804
  if (!_isString(a) || !wcEngine.isCryptoKeyLike(k)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
2319
- out[i] = intrinsic.freeze({ alg: a, key: k }); // index-write, not a live-prototype .push
1805
+ out[i] = intrinsic.freeze({ alg: a, key: k });
2320
1806
  }
2321
1807
  return intrinsic.freeze(out);
2322
1808
  }
2323
- // Pick the re-sign entry to use for a badSignatureAlgorithm: the FIRST caller-listed entry whose alg the
2324
- // server advertised as supported. Caller order selects and the server's array only filters, so a bogus
2325
- // badSignatureAlgorithm cannot steer the client to the server's weakest alg (RFC 8555 sec. 10). Returns
2326
- // null when no caller entry matches (the badSignatureAlgorithm then surfaces, fail closed).
2327
1809
  function _chooseResign(resignKeys, serverAlgs) {
2328
1810
  if (!resignKeys || !_isArrayI(serverAlgs)) return null;
2329
1811
  for (var i = 0; i < resignKeys.length; i++) {
@@ -2333,10 +1815,6 @@ function _chooseResign(resignKeys, serverAlgs) {
2333
1815
  }
2334
1816
  return null;
2335
1817
  }
2336
- // A copy of a public JWK with its optional `alg` member set to the alg actually being signed. A
2337
- // WebCrypto-exported RSA JWK carries the key's native alg (e.g. RS256), so embedding it unchanged under a
2338
- // re-sign alg (PS256) would advertise a JWK whose declared alg contradicts the JWS header, which a strict
2339
- // ACME server may reject. Own members are copied through captured intrinsics (no live prototype read).
2340
1818
  function _jwkForAlg(jwk, alg) {
2341
1819
  var out = {};
2342
1820
  var keys = intrinsic.ownKeys(jwk);
@@ -2353,11 +1831,6 @@ function client(directoryUrl, opts) {
2353
1831
  jose.assertPublicJwk(opts.accountJwk);
2354
1832
  var dirUrl = _clientUrl(directoryUrl);
2355
1833
  var accountKey = opts.accountKey, accountJwk = opts.accountJwk, alg = opts.alg;
2356
- // Optional badSignatureAlgorithm re-sign material (RFC 8555 sec. 6.2). Each entry pairs an alternative
2357
- // JWS alg with a CryptoKey for the SAME account key material under that alg (an RSA public JWK is
2358
- // padding-agnostic, so the registered account key still verifies a re-signed request). Snapshotted here
2359
- // so a caller getter cannot rewrite the pairs between validation and use, and ordered by CALLER
2360
- // preference: on a badSignatureAlgorithm the caller's order selects, never the server's (sec. 10).
2361
1834
  var resignKeys = _resignKeysOf(opts.resignKeys);
2362
1835
 
2363
1836
  var transport = opts.transport;
@@ -2377,31 +1850,15 @@ function client(directoryUrl, opts) {
2377
1850
  var maxPolls = guard.limits.cap(opts.maxPolls, "maxPolls", 20, { E: E, code: "acme/bad-input", min: 1, max: 1000 });
2378
1851
  var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "maxTotalWait", retryAfter.MAX_RETRY_AFTER_SECONDS, { E: E, code: "acme/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
2379
1852
  var sleep = typeof opts.sleep === "function" ? opts.sleep : _defaultSleep;
2380
- // The receipt clock for a Retry-After HTTP-date: read FRESH per response (a fixed value would go stale
2381
- // across polls, and a missing one would collapse every HTTP-date to a 1s delay). Injectable for tests.
2382
1853
  var clock = typeof opts.clock === "function" ? opts.clock : function () { return Date.now(); };
2383
1854
 
2384
- // session state
2385
1855
  var dirCache = null;
2386
1856
  var nonces = [];
2387
1857
  var kid = null;
2388
- // The mTLS client credential is bound to the CONFIGURED directory origin: it is presented ONLY to that
2389
- // origin, never to a different one reached via a redirect or advertised by a (possibly compromised)
2390
- // directory. A CA that legitimately spans origins uses JWS auth, not a transport client certificate.
2391
1858
  var trustedOrigin = new URL(dirUrl).origin;
2392
1859
  function _tlsFor(url) {
2393
1860
  var t = budgets.tls;
2394
- // Cross-origin, strip the ORIGIN-SPECIFIC identity: the client certificate/key (credential leak) and the
2395
- // pinned `servername` (SNI, which selects the trusted host's certificate). The caller's
2396
- // `checkServerIdentity` is RETAINED -- it is an ADDITIONAL tightening constraint (a certificate / SPKI
2397
- // pin, or a stricter host check) that node re-evaluates against the actual host, so a pin keeps applying
2398
- // and a host-pinned check fails the wrong host closed; dropping it would accept the cross-origin host
2399
- // under only the default hostname validation, bypassing the pin.
2400
1861
  if (new URL(url).origin !== trustedOrigin) {
2401
- // Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
2402
- // omitted field falls back to the transport's own configured defaults (http-transport resolves
2403
- // reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
2404
- // request-level tls carries no credential, or a transport default would still reach the new origin.
2405
1862
  t = Object.assign({}, t);
2406
1863
  t.cert = null; t.key = null; t.servername = null;
2407
1864
  }
@@ -2413,31 +1870,24 @@ function client(directoryUrl, opts) {
2413
1870
  if (Buffer.isBuffer(b)) return b.toString("utf8");
2414
1871
  if (b == null) return "";
2415
1872
  if (typeof b === "string") return b;
2416
- // A BufferSource (Uint8Array, ArrayBuffer) from an injected transport: read its raw bytes rather than
2417
- // String()-coercing it into its "123,45,..." toString.
2418
1873
  return guard.bytes.snapshotSource(b, E, "acme/bad-response", "the response body").toString("utf8");
2419
1874
  }
2420
- // The RAW bytes for jose.parseJson, so its FATAL UTF-8 validation runs on the wire body -- _bodyText's
2421
- // lossy utf8 decode (replacement chars) would let a malformed RFC 8259 response slip through. A string
2422
- // body (an injected test transport) carries no malformed UTF-8, so encoding it is lossless.
2423
1875
  function _jsonInput(res) {
2424
1876
  var b = res.body;
2425
1877
  if (Buffer.isBuffer(b)) return b;
2426
1878
  if (b == null) return Buffer.alloc(0);
2427
1879
  if (typeof b === "string") return Buffer.from(b, "utf8");
2428
- // A BufferSource (Uint8Array, ArrayBuffer) from an injected transport: normalize its raw bytes so
2429
- // jose.parseJson runs its fatal UTF-8 validation on the wire body, not on its "123,45,..." toString.
2430
1880
  return guard.bytes.snapshotSource(b, E, "acme/bad-response", "the response body");
2431
1881
  }
2432
1882
  function _requireKid() { if (!kid) throw E("acme/no-account", "no account is set -- call newAccount before an authenticated request (RFC 8555 sec. 6.2)"); return kid; }
2433
1883
  function _harvestNonce(v) {
2434
1884
  if (typeof v !== "string" || v === "") return;
2435
1885
  try { jose.base64url.decode(v); }
2436
- catch (_e) { return; } // ignore an invalid Replay-Nonce (RFC 8555 sec. 6.5.1)
2437
- while (nonces.length >= CLIENT_MAX_NONCE_POOL) nonces.shift(); // evict the oldest, bound the pool (CWE-770)
1886
+ catch (_e) { return; }
1887
+ while (nonces.length >= CLIENT_MAX_NONCE_POOL) nonces.shift();
2438
1888
  nonces.push(v);
2439
1889
  }
2440
- function _isProblem(res) { return intrinsic.stringIndexOf(_lower(String(res.headers["content-type"] || "")), "application/problem+json") !== -1; } // ci substring (ASCII content-type)
1890
+ function _isProblem(res) { return intrinsic.stringIndexOf(_lower(String(res.headers["content-type"] || "")), "application/problem+json") !== -1; }
2441
1891
  function _serverProblem(res) {
2442
1892
  var prob = null;
2443
1893
  try { prob = jose.parseJson(_jsonInput(res)); if (_isObject(prob)) validateProblem(prob); }
@@ -2452,31 +1902,19 @@ function client(directoryUrl, opts) {
2452
1902
  catch (e) { throw E("acme/bad-response", "the response body was not valid JSON: " + ((e && e.message) || String(e)), e); }
2453
1903
  }
2454
1904
 
2455
- // One request over the transport; lowercases headers, caps the body, harvests a Replay-Nonce off every
2456
- // FINAL response (validated base64url, RFC 8555 sec. 6.5.1). A redirect on a SAFE method (GET/HEAD --
2457
- // the unauthenticated directory / newNonce / renewalInfo reads) is followed to its https Location, up to
2458
- // the maxRedirects budget; an authenticated POST is bound to its JWS `url` (sec. 6.4), so its redirect
2459
- // is surfaced unchanged for the verb to reject. Every request carries the sec. 6.1 REQUIRED User-Agent.
2460
1905
  function _send(method, url, headers, body) { return _sendFollowing(method, url, headers, body, 0); }
2461
1906
  function _sendFollowing(method, url, headers, body, redirects) {
2462
1907
  var reqHeaders = Object.assign({ "user-agent": CLIENT_USER_AGENT }, headers || {});
2463
- // _tlsFor binds the mTLS credential to the trusted origin, so EVERY request (this one, a redirect
2464
- // target, or a directory-advertised URL on another host) presents the client certificate only there.
2465
1908
  return transport({ method: method, url: url, headers: reqHeaders, body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
2466
1909
  res = res || {};
2467
1910
  var h = {};
2468
1911
  Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
2469
- // Measure an injected string body as UTF-8 -- the exact bytes _jsonInput re-encodes it to before the
2470
- // decoder sees it -- so a non-ASCII body (e.g. multi-byte emoji, ~half the byte count under latin1)
2471
- // cannot slip past the cap and reach the JSON parser.
2472
1912
  var blen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8")
2473
1913
  : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, E, "acme/bad-response", "the response body"))));
2474
1914
  if (blen > budgets.maxResponseBytes) throw E("acme/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
2475
1915
  if ((method === "GET" || method === "HEAD") && res.status >= 300 && res.status < 400) {
2476
1916
  if (redirects >= budgets.maxRedirects) throw E("acme/too-many-redirects", "the redirect budget of " + budgets.maxRedirects + " was exceeded");
2477
1917
  if (!_isString(h["location"])) throw E("acme/bad-redirect", "a " + res.status + " redirect carried no Location header (RFC 7231 sec. 6.4)");
2478
- // A Location may be relative (common behind a reverse proxy) -- resolve it against the request URL
2479
- // before the https gate, so `/v2/directory` follows rather than failing the URL parse.
2480
1918
  var next;
2481
1919
  try { next = new URL(h["location"], url).href; }
2482
1920
  catch (e) { throw E("acme/bad-redirect", "the redirect Location did not resolve to a valid URL", e); }
@@ -2500,29 +1938,19 @@ function client(directoryUrl, opts) {
2500
1938
  function _resource(name) {
2501
1939
  return _getDirectory().then(function (dir) {
2502
1940
  if (!_isString(dir[name])) throw E("acme/resource-unavailable", "the directory does not advertise the '" + name + "' resource (RFC 8555 sec. 7.1.1)");
2503
- return _clientUrl(dir[name]); // the client is the sole https gate -- a directory advertising an http resource fails closed (RFC 8555 sec. 6.1)
1941
+ return _clientUrl(dir[name]);
2504
1942
  });
2505
1943
  }
2506
- // Take the FRESHEST pooled nonce (LIFO): every response's Replay-Nonce is appended, so on a badNonce
2507
- // the error response's fresh nonce is the last harvested and is the one the retry consumes (RFC 8555
2508
- // sec. 6.5) -- never a staler pooled nonce (e.g. one an unauthenticated read left behind).
2509
1944
  function _takeNonce() {
2510
1945
  if (nonces.length) return Promise.resolve(nonces.pop());
2511
1946
  var before = nonces.length;
2512
1947
  return _resource("newNonce").then(function (nn) { return _send("HEAD", nn, {}, null); }).then(function (res) {
2513
- // newNonce must succeed (RFC 8555 sec. 7.2: 200/204). An ERROR response (a 4xx/5xx rate limit) also
2514
- // carries a Replay-Nonce, but using it would silently proceed past the server's error -- discard the
2515
- // harvested nonce and surface the problem instead.
2516
1948
  if (res.status !== 200 && res.status !== 204) { nonces.length = before; throw _serverProblem(res); }
2517
1949
  if (!nonces.length) throw E("acme/no-nonce", "the server did not provide a usable Replay-Nonce (RFC 8555 sec. 7.2)");
2518
1950
  return nonces.pop();
2519
1951
  });
2520
1952
  }
2521
1953
 
2522
- // POST a JWS built by makeJws(nonce) as application/jose+json, and on a badNonce error retry (bounded)
2523
- // with the error response's fresh nonce (RFC 8555 sec. 6.5 -- _takeNonce's LIFO pool surfaces it).
2524
- // `accept` is the media type the request advertises (default application/json). This is the single
2525
- // home of the badNonce retry, so every signed POST -- account-key or certificate-key -- inherits it.
2526
1954
  function _postJws(url, makeJws, accept, okStatuses, allowResign) {
2527
1955
  var ok = okStatuses || [200];
2528
1956
  var attempt = 0;
@@ -2531,20 +1959,12 @@ function client(directoryUrl, opts) {
2531
1959
  return _takeNonce().then(function (nonce) { return makeJws(nonce, override); }).then(function (jws) {
2532
1960
  return _send("POST", url, { "content-type": "application/jose+json", "accept": accept || "application/json" }, JSON.stringify(jws));
2533
1961
  }).then(function (res) {
2534
- // An application/problem+json body is an ERROR regardless of the HTTP status -- a CA / proxy
2535
- // returning a problem with an erroneous 2xx must NOT be accepted as success (RFC 7807 / RFC 8555).
2536
1962
  if (res.status >= 300 || _isProblem(res)) {
2537
1963
  if (_isProblem(res)) {
2538
1964
  var prob = null;
2539
1965
  try { prob = jose.parseJson(_jsonInput(res)); }
2540
1966
  catch (_e) { /* allow:swallow-unverified a non-JSON problem body is not a badNonce; prob stays null, falling through to the typed acme/server-problem throw */ }
2541
- // A badNonce retry keeps the CURRENT signing override (a re-sign is not undone by a following
2542
- // badNonce), so it re-signs with the same alg the request already switched to.
2543
1967
  if (prob && typeof prob.type === "string" && _endsWith(prob.type, ":badNonce") && attempt < maxNonceRetries) { attempt++; return once(override); }
2544
- // A badSignatureAlgorithm (RFC 8555 sec. 6.2): if the caller supplied resignKeys and one of its
2545
- // algs is in the server's advertised `algorithms`, re-sign ONCE with that caller-preferred key.
2546
- // The caller's order selects (never the server's), so a bogus badSignatureAlgorithm cannot force
2547
- // a downgrade (sec. 10); no match, or no resignKeys, falls through to the fail-closed throw.
2548
1968
  if (allowResign && resignKeys && prob && typeof prob.type === "string" && _endsWith(prob.type, ":badSignatureAlgorithm") && resignAttempt < 1) {
2549
1969
  var pick = _chooseResign(resignKeys, prob.algorithms);
2550
1970
  if (pick) { resignAttempt++; return once(pick); }
@@ -2552,43 +1972,28 @@ function client(directoryUrl, opts) {
2552
1972
  }
2553
1973
  throw _serverProblem(res);
2554
1974
  }
2555
- // Enforce the PER-OPERATION success status (RFC 8555): only newAccount/newOrder may return 201
2556
- // (created); every other verb requires 200, so a verb never acts on an incomplete result -- e.g.
2557
- // keyChange rotating the session key or revokeCert reporting success on a 201/202.
2558
1975
  if (ok.indexOf(res.status) === -1) throw E("acme/unexpected-status", "an ACME POST returned an unexpected status " + res.status + " (expected " + ok.join("/") + ", RFC 8555)");
2559
1976
  return res;
2560
1977
  });
2561
1978
  }
2562
1979
  return once();
2563
1980
  }
2564
- // Build an account-key JWS (kid or jwk mode) with a fresh nonce and POST it through the retrying _postJws.
2565
1981
  function _post(url, builder, extra, mode, accept, okStatuses) {
2566
1982
  return _postJws(url, function (nonce, override) {
2567
- // The client-owned JWS session fields (signing key, alg, single-use nonce, protected url) are applied
2568
- // AFTER the caller's payload options, so a payload carrying key/alg/nonce/url cannot override them and
2569
- // desynchronize the protected header from the actual request (or reuse a nonce). On a badSignatureAlgorithm
2570
- // re-sign `override` carries the caller's chosen { alg, key } (the SAME account material under another alg);
2571
- // the embedded jwk stays accountJwk, which still verifies it (an RSA public JWK is padding-agnostic).
2572
1983
  var base = Object.assign({}, extra || {}, { key: override ? override.key : accountKey, alg: override ? override.alg : alg, nonce: nonce, url: url });
2573
- // On a re-sign the embedded jwk's declared alg must follow the alg being signed, not the account
2574
- // key's native one, so the published JWK and the JWS header agree (jwk mode / newAccount).
2575
1984
  if (mode === "jwk") base.jwk = override ? _jwkForAlg(accountJwk, override.alg) : accountJwk; else base.kid = _requireKid();
2576
1985
  return builder(base);
2577
1986
  }, accept, okStatuses, true);
2578
1987
  }
2579
1988
  function _postAsGet(url) { return _post(url, postAsGet, null, "kid"); }
2580
1989
 
2581
- // ---- verbs (internal _-prefixed to avoid shadowing the module builders) ----
2582
1990
  function _directory() { return _getDirectory(); }
2583
1991
 
2584
1992
  function _newAccount(payloadOpts) {
2585
1993
  return _resource("newAccount").then(function (url) {
2586
- // newAccount is 201 (created) for a new account or 200 for an existing one (RFC 8555 sec. 7.3).
2587
1994
  return _post(url, newAccount, payloadOpts || {}, "jwk", undefined, [200, 201]).then(function (res) {
2588
1995
  var loc = res.headers["location"];
2589
1996
  if (!_isString(loc)) throw E("acme/no-account-url", "newAccount did not return an account URL in a Location header (RFC 8555 sec. 7.3)");
2590
- // Validate the account object BEFORE committing the kid, so a malformed account (no status,
2591
- // an unknown status) fails closed rather than enabling authenticated operations under a bad kid.
2592
1997
  var account = validate("account", _json(res));
2593
1998
  kid = _resolveLocation(loc, url);
2594
1999
  return { account: account, url: kid };
@@ -2597,7 +2002,6 @@ function client(directoryUrl, opts) {
2597
2002
  }
2598
2003
  function _newOrder(payloadOpts) {
2599
2004
  return _resource("newOrder").then(function (url) {
2600
- // newOrder returns 201 Created ONLY (RFC 8555 sec. 7.4) -- a 200 is a non-conforming server / proxy.
2601
2005
  return _post(url, newOrder, payloadOpts || {}, "kid", undefined, [201]).then(function (res) {
2602
2006
  var loc = res.headers["location"];
2603
2007
  if (!_isString(loc)) throw E("acme/no-order-url", "newOrder did not return an order URL in a Location header (RFC 8555 sec. 7.4)");
@@ -2607,39 +2011,19 @@ function client(directoryUrl, opts) {
2607
2011
  }
2608
2012
  function _newAuthz(identifier) {
2609
2013
  return Promise.resolve().then(function () {
2610
- // Reject a wildcard / bad AUTHORIZATION identifier BEFORE any network (sec. 7.4.1); the builder re-checks. Bind
2611
- // to the CANONICAL { type, value } (a single validated read), so the wire body, the sent identifier, and the
2612
- // returned-authz comparison below all use the same values -- never a getter that could re-read differently.
2613
2014
  var canon = _validateIdentifier(identifier);
2614
2015
  return _resource("newAuthz").then(function (url) {
2615
- // 201 (Created) is the norm (RFC 8555 sec. 7.4.1), but a CA that returns an already-existing authorization
2616
- // (the out-of-band valid case) MAY answer 200 (OK) -- the authz was not newly created -- just as newAccount
2617
- // accepts 200 for an existing account. Accept both; the Location + object are validated below regardless.
2618
2016
  return _post(url, newAuthz, { identifier: canon }, "kid", undefined, [200, 201]).then(function (res) {
2619
2017
  var loc = res.headers["location"];
2620
2018
  if (!_isString(loc)) throw E("acme/no-authorization-url", "newAuthz did not return an authorization URL in a Location header (RFC 8555 sec. 7.4.1)");
2621
2019
  var authz = validate("authorization", _json(res));
2622
- // Bind the returned authorization to the SUBMITTED identifier (sec. 7.4.1): a server returning an authz
2623
- // for a different identifier is not silently accepted. The submitted identifier is always non-wildcard
2624
- // (_validateIdentifier rejects a leading *.), so an authz the CA marks wildcard:true (sec. 7.1.4 -- it
2625
- // authorizes *.<value>, a BROADER grant than requested) is a mismatch, not the authorization asked for.
2626
2020
  if (!authz.identifier || authz.identifier.type !== canon.type || authz.identifier.value !== canon.value || authz.wildcard === true) throw E("acme/identifier-mismatch", "the returned authorization does not match the requested identifier (RFC 8555 sec. 7.4.1)");
2627
- // A pre-authorization's status "MUST be 'pending' unless the server has out-of-band information about the
2628
- // client's authorization status" (sec. 7.4.1): the normal case is a fresh "pending" authz the caller then
2629
- // fulfills, but the CA MAY instead return an already-"valid" authz (the identifier is already authorized for
2630
- // this account -- no challenge to answer). Both are usable; only a terminal failed state ("invalid"/
2631
- // "deactivated"/"expired"/"revoked") is not the authorization the pre-auth flow can proceed with.
2632
2021
  if (authz.status !== "pending" && authz.status !== "valid") throw E("acme/unexpected-authorization-status", "newAuthz returned an authorization that is neither pending nor already valid (status " + JSON.stringify(authz.status) + "); RFC 8555 sec. 7.4.1");
2633
2022
  return { authorization: authz, url: _resolveLocation(loc, url) };
2634
2023
  });
2635
2024
  });
2636
2025
  });
2637
2026
  }
2638
- // A bad URL leaves as a REJECTION, like every other verb on this client. It is not only a caller
2639
- // typo that reaches here: the message layer deliberately accepts http as well as https, so an
2640
- // order whose `authorizations` array carries an http URL validates, and the very next step --
2641
- // getAuthorization(order.authorizations[0]) -- threw out of the middle of an async ACME loop
2642
- // rather than rejecting into the handler already wrapped around it.
2643
2027
  function _getOrder(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("order", _json(res)); }); }); }
2644
2028
  function _getAuthorization(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("authorization", _json(res)); }); }); }
2645
2029
  function _getChallenge(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("challenge", _json(res)); }); }); }
@@ -2650,19 +2034,10 @@ function client(directoryUrl, opts) {
2650
2034
  o = _gateOpts(o, _METHOD_OPTS.finalize, "finalize");
2651
2035
  if (!_isObject(order) || !_isString(order.finalize)) throw E("acme/bad-input", "finalize requires the order object with its finalize URL");
2652
2036
  if (!Array.isArray(order.identifiers)) throw E("acme/bad-input", "finalize requires the order's identifiers to enforce the RFC 8555 sec. 7.4 CSR-set match");
2653
- // The sec. 7.4 CSR-vs-order check is ALWAYS bound to the order's OWN authoritative identifiers (the
2654
- // client holds the validated order) -- never a caller-supplied set, which could otherwise loosen the
2655
- // check so a CSR matching the replacement passes locally only to be rejected by the CA.
2656
2037
  return _post(_clientUrl(order.finalize), finalize, { csr: o.csr, identifiers: order.identifiers, accountJwk: accountJwk }, "kid").then(function (res) { return validate("order", _json(res)); });
2657
2038
  }
2658
2039
 
2659
- // Poll a resource by POST-as-GET until it reaches a terminal state, honoring Retry-After (surfaced +
2660
- // slept via the injectable sleep), bounded by maxPolls + maxTotalWait; each observed pair runs
2661
- // assertTransition. A terminal `invalid` surfaces the object (the caller inspects .status).
2662
2040
  function _poll(kind, url, budget) {
2663
- // A per-call poll override is validated through the SAME bounds as the constructor budget, so an
2664
- // Infinity / NaN / over-ceiling value cannot turn a non-terminal resource into an unbounded loop.
2665
- // Surface a bad override (or url) as a REJECTION, not a sync throw, so this async verb is uniform.
2666
2041
  var capPolls, capWait;
2667
2042
  try {
2668
2043
  budget = _gateOpts(budget, _METHOD_OPTS.poll, "poll");
@@ -2670,9 +2045,6 @@ function client(directoryUrl, opts) {
2670
2045
  capWait = budget.maxTotalWait != null ? guard.limits.cap(budget.maxTotalWait, "maxTotalWait", maxTotalWait, { E: E, code: "acme/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS }) : maxTotalWait;
2671
2046
  url = _clientUrl(url);
2672
2047
  } catch (e) { return Promise.reject(e); }
2673
- // An order poll terminates on every STABLE state (RFC 8555 sec. 7.1.6): `ready` (all authorizations
2674
- // met -- the signal to finalize) and `valid` / `invalid`, not just valid/invalid -- otherwise a poll
2675
- // run before finalize spins on the stable `ready` until the budget is exhausted.
2676
2048
  var terminal = kind === "order" ? { ready: true, valid: true, invalid: true } : { valid: true, invalid: true, deactivated: true, expired: true, revoked: true };
2677
2049
  var last = null, count = 0, waited = 0;
2678
2050
  function step() {
@@ -2697,13 +2069,7 @@ function client(directoryUrl, opts) {
2697
2069
  function _pollOrder(url, budget) { return _poll("order", url, budget); }
2698
2070
  function _pollAuthorization(url, budget) { return _poll("authorization", url, budget); }
2699
2071
 
2700
- // Read + strictly validate a downloaded chain response into { certificate, chain, certificates }. The caller
2701
- // routes through _post (okStatuses [200]), so a non-200 has already thrown _serverProblem before this runs.
2702
2072
  function _readCertChain(res) {
2703
- // The certificate representation is bound to application/pem-certificate-chain (RFC 8555 sec. 7.4.2):
2704
- // a 200 with a wrong media type (a proxy error, a misrouted response) fails closed even if its body
2705
- // happens to contain parseable PEM. Match the media-type TOKEN exactly (before any ;-parameters), so
2706
- // a lookalike like application/pem-certificate-chain-evil does not slip through a substring test.
2707
2073
  var ctToken = String(res.headers["content-type"] || "").split(";")[0].trim().toLowerCase();
2708
2074
  if (ctToken !== "application/pem-certificate-chain") throw E("acme/bad-certificate-chain", "the certificate download returned an unexpected media type " + JSON.stringify(ctToken) + " (expected application/pem-certificate-chain, RFC 8555 sec. 7.4.2)");
2709
2075
  var chain = _splitPemChain(_bodyText(res));
@@ -2711,29 +2077,18 @@ function client(directoryUrl, opts) {
2711
2077
  }
2712
2078
  function _fetchCertChain(url) { return _post(url, postAsGet, null, "kid", "application/pem-certificate-chain").then(_readCertChain); }
2713
2079
 
2714
- // The material that says which certificate this download is allowed to be, reduced ONCE at the
2715
- // door: the caller's options object is read here and never again, so a value that changes between
2716
- // the check and the use cannot be what the check ran on. Returns { spki, ids, require }.
2717
2080
  function _bindingSpec(opts) {
2718
2081
  var spki = opts.expectedSpki, ids = opts.identifiers, req = opts.requireBinding;
2719
2082
  if (spki !== undefined) {
2720
2083
  if (!guard.bytes.isByteSource(spki)) throw E("acme/bad-input", "downloadCertificate opts.expectedSpki must be a DER SubjectPublicKeyInfo BufferSource");
2721
- // SNAPSHOT: the comparison happens after a network round trip, so a retained reference would let
2722
- // the caller rewrite the key the certificate is checked against once the request had gone out --
2723
- // rejecting the right certificate, or accepting one minted for the replacement bytes.
2724
2084
  spki = guard.bytes.snapshotSource(spki, AcmeError, "acme/bad-input", "downloadCertificate expectedSpki");
2725
2085
  }
2726
2086
  if (ids !== undefined) {
2727
2087
  if (!_isArrayI(ids) || ids.length === 0) throw E("acme/bad-input", "downloadCertificate opts.identifiers must be the non-empty order identifier array");
2728
- // Reduced to this module's OWN validated copy at the door: the array and its entries are the
2729
- // caller's, and the comparison below happens after a network round trip.
2730
2088
  ids = _mapI(ids, _validateOrderIdentifier);
2731
2089
  }
2732
2090
  if (req !== undefined && typeof req !== "boolean") throw E("acme/bad-input", "downloadCertificate opts.requireBinding must be a boolean");
2733
2091
  var require_ = req === undefined ? true : req;
2734
- // Required by default, because a download nobody checked is the one an operator installs. The
2735
- // waiver is explicit and it waives only the REQUIREMENT to supply material: anything supplied is
2736
- // still checked below, so the waiver cannot become the place a mismatch hides.
2737
2092
  if (require_ && spki === undefined && ids === undefined) {
2738
2093
  throw E("acme/binding-required",
2739
2094
  "downloadCertificate needs the material that binds the issued certificate to this order -- " +
@@ -2744,12 +2099,6 @@ function client(directoryUrl, opts) {
2744
2099
  return { spki: spki, ids: ids, require: require_ };
2745
2100
  }
2746
2101
 
2747
- // Bind the certificate that came back to the request that was made. RFC 8555 sec. 7.4.2 says nothing
2748
- // about what the certificate resource may return, so nothing on the wire stops a CA -- or anything
2749
- // that can answer that URL -- from handing back a certificate for another key or another name. The
2750
- // OUTBOUND direction already refuses a CSR whose identifier set is not the order's (sec. 7.4); this
2751
- // is the same relation read back off the issued certificate. The sibling enrollment client makes the
2752
- // same check: pki.est picks the issued certificate by public-key match against the submitted CSR.
2753
2102
  function _assertCertBinding(leafDer, spec) {
2754
2103
  var leaf;
2755
2104
  try { leaf = x509.parse(leafDer); }
@@ -2777,7 +2126,6 @@ function client(directoryUrl, opts) {
2777
2126
  }
2778
2127
 
2779
2128
  function _downloadCertificate(url, opts) {
2780
- // Prelude in a resolved-promise .then so a bad opt / URL is a REJECTION, uniform for a .catch caller.
2781
2129
  return Promise.resolve().then(function () {
2782
2130
  opts = _gateOpts(opts, _METHOD_OPTS.download, "downloadCertificate");
2783
2131
  var select = opts.selectChain;
@@ -2787,36 +2135,18 @@ function client(directoryUrl, opts) {
2787
2135
  var dlUrl = _clientUrl(url);
2788
2136
  return _post(dlUrl, postAsGet, null, "kid", "application/pem-certificate-chain").then(function (res) {
2789
2137
  var primary = _readCertChain(res);
2790
- // The Link header is UNTRUSTED. Parse it now but DEFER any malformed-header error: a malformed Link is
2791
- // fatal only if the alternates are actually needed (the primary itself is rejected below). So a broken
2792
- // or hostile Link can never deny a valid primary download -- whether the caller is selecting or not.
2793
2138
  var alternates = [], linkError = null;
2794
2139
  try { alternates = _parseLinkAlternates(res.headers, dlUrl); }
2795
2140
  catch (e) { linkError = e; }
2796
- // Bind the primary leaf NOW, before a rejected primary can drive alternate fetches: an
2797
- // unbindable certificate fails on the response that carried it rather than after up to
2798
- // maxAlternates further signed POSTs.
2799
2141
  _assertCertBinding(primary.certificate, binding);
2800
- // The same assertion again on whatever is actually returned. Every candidate must carry the
2801
- // primary's own end-entity certificate (sec. 7.4.2, enforced below), so today this can only
2802
- // re-confirm what the line above settled; it is here because `result` is the one door every
2803
- // return passes through, and a candidate that stopped sharing that leaf would still be bound.
2804
2142
  function result(cand) {
2805
2143
  _assertCertBinding(cand.certificate, binding);
2806
2144
  return { certificate: cand.certificate, chain: cand.chain, certificates: cand.certificates, alternates: alternates,
2807
2145
  boundToKey: binding.spki !== undefined, boundToIdentifiers: binding.ids !== undefined };
2808
2146
  }
2809
2147
  if (!select) return result(primary);
2810
- // Selection: consider the PRIMARY first, then each alternate in header order, bounded by maxAlt
2811
- // (CWE-770 fetch amplification). selectChain MAY be async -- await its result (Promise.resolve wraps a
2812
- // sync return too) so a returned Promise is never treated as always-truthy. The first truthy wins. An
2813
- // alternate whose end-entity certificate differs from the primary's violates sec. 7.4.2 ("starting with
2814
- // the same end-entity certificate") -- fail closed rather than accept a substituted leaf. A predicate
2815
- // throw / rejection propagates unswallowed. Exhausting the budget with alternates left unfetched is
2816
- // acme/too-many-alternates; fetching every alternate with no match is acme/no-matching-chain.
2817
2148
  return Promise.resolve(select(primary)).then(function (primaryMatch) {
2818
2149
  if (primaryMatch) return result(primary);
2819
- // The primary was rejected, so the alternates ARE needed now -- a malformed Link finally fails closed.
2820
2150
  if (linkError) throw linkError;
2821
2151
  var leaf = primary.certificate, toFetch = alternates.slice(0, maxAlt), overBudget = alternates.length > maxAlt, idx = 0;
2822
2152
  function next() {
@@ -2836,23 +2166,15 @@ function client(directoryUrl, opts) {
2836
2166
  }
2837
2167
 
2838
2168
  function _revokeCert(o) {
2839
- // Bad input surfaces as a REJECTION -- _promised turns a throw into one -- so this async verb stays
2840
- // uniform for a .catch caller rather than throwing synchronously.
2841
2169
  return _promised(function () {
2842
2170
  o = _gateOpts(o, _METHOD_OPTS.revoke, "revokeCert");
2843
2171
  if (!guard.bytes.isByteSource(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate BufferSource (opts.certificate)");
2844
- // Certificate-key mode (RFC 8555 sec. 7.6) needs the COMPLETE triple certKey + certJwk + certAlg: an
2845
- // incomplete set must NOT fall through to the account-key path (wrong credential), and certAlg must be
2846
- // explicit -- inheriting the account alg would sign a different-family certificate key under the wrong
2847
- // JWS algorithm (an ES256 account revoking with an RSA certificate key), which JOSE then rejects.
2848
2172
  var certKeyMode = o.certKey != null || _isObject(o.certJwk) || o.certAlg != null;
2849
2173
  if (certKeyMode && !(o.certKey != null && _isObject(o.certJwk) && _isString(o.certAlg))) throw E("acme/bad-input", "revokeCert certificate-key mode requires certKey, certJwk, AND certAlg together, or none (RFC 8555 sec. 7.6)");
2850
2174
  var extra = { certificate: o.certificate };
2851
2175
  if (o.reason !== undefined) extra.reason = o.reason;
2852
2176
  return _resource("revokeCert").then(function (url) {
2853
2177
  if (certKeyMode) {
2854
- // sign with the certificate key (jwk mode) rather than the account (RFC 8555 sec. 7.6), through
2855
- // the same _postJws so it inherits the badNonce bounded retry the kid path has.
2856
2178
  return _postJws(url, function (nonce) {
2857
2179
  return revokeCert(Object.assign({}, extra, { key: o.certKey, alg: o.certAlg, nonce: nonce, url: url, jwk: o.certJwk }));
2858
2180
  }).then(function () { return true; });
@@ -2865,19 +2187,10 @@ function client(directoryUrl, opts) {
2865
2187
  function _deactivateAccount() { return _promised(function () { return _post(_requireKid(), deactivate, null, "kid").then(function (res) { return validate("account", _json(res)); }); }); }
2866
2188
  function _deactivateAuthorization(url) { return _promised(function () { return _post(_clientUrl(url), deactivate, null, "kid").then(function (res) { return validate("authorization", _json(res)); }); }); }
2867
2189
 
2868
- // updateAccount (RFC 8555 sec. 7.3.2): a kid-signed POST to the account URL updating the caller's
2869
- // contact set. The options bag is snapshotted FIRST (a getter that grows a key after the check cannot
2870
- // slip past), then the fields the server MUST ignore are refused with guidance -- a caller that sends
2871
- // one has silently not done what they asked, so the client will not emit a payload the CA discards.
2872
2190
  function _updateAccount(o) {
2873
2191
  return _promised(function () {
2874
2192
  o = guard.identifier.optionsObject(o, E, "acme/bad-input", "pki.acme updateAccount options");
2875
2193
  if (o.status !== undefined) throw E("acme/bad-input", "updateAccount cannot change the account status; use deactivateAccount (RFC 8555 sec. 7.3.6 is the only client-settable status)");
2876
- // RFC 8555 sec. 7.3.2 says the server MUST IGNORE a termsOfServiceAgreed update to the account, so
2877
- // sending it is a silently-dropped no-op; this client refuses it at the door instead. termsOfServiceAgreed
2878
- // is set at account creation (sec. 7.3.1 newAccount), and a CA whose terms have changed answers with a
2879
- // 403 userActionRequired problem (sec. 7.3.3) that directs a HUMAN to the terms URL -- it is not re-agreed
2880
- // through an account update.
2881
2194
  if (o.termsOfServiceAgreed !== undefined) throw E("acme/bad-input", "the server MUST ignore a termsOfServiceAgreed update to the account (RFC 8555 sec. 7.3.2); it is set at account creation, and a changed-terms CA answers with a userActionRequired problem (sec. 7.3.3) directing a human to the terms URL");
2882
2195
  if (o.orders !== undefined) throw E("acme/bad-input", "the account orders URL is server-assigned and cannot be updated (RFC 8555 sec. 7.3.2)");
2883
2196
  if (o.externalAccountBinding !== undefined) throw E("acme/bad-input", "externalAccountBinding is not updateable by the client (RFC 8555 sec. 7.1.2 / 7.3.4)");
@@ -2887,27 +2200,20 @@ function client(directoryUrl, opts) {
2887
2200
  });
2888
2201
  }
2889
2202
 
2890
- // listOrders (RFC 8555 sec. 7.1.2.1): a POST-as-GET to the account's orders URL, aggregating the
2891
- // sec. 7.1.2.1 `Link: rel="next"` pages. The `next` header is untrusted (TLS-delivered, unsigned) and
2892
- // steers the next account-key-signed POST-as-GET, so it is https-gated, confined to the request URL's
2893
- // origin (SSRF), deduped against a visited-page loop, and bounded by maxPages (CWE-770 amplification).
2894
2203
  function _listOrders(ordersUrl, o) {
2895
2204
  return _promised(function () {
2896
2205
  o = _gateOpts(o, _METHOD_OPTS.listOrders, "listOrders");
2897
2206
  var maxPages = guard.limits.cap(o.maxPages, "maxPages", 50, { E: E, code: "acme/bad-input", min: 1 });
2898
2207
  var out = [], seen = intrinsic.create(null), pages = 0, truncated = false;
2899
2208
  function fetchPage(pageUrl) {
2900
- seen[_dedupKey(pageUrl)] = true; // pageUrl is a canonical _clientUrl output, so the dedup key is consistent
2209
+ seen[_dedupKey(pageUrl)] = true;
2901
2210
  return _postAsGet(pageUrl).then(function (res) {
2902
2211
  pages += 1;
2903
2212
  var body = validate("ordersList", _json(res));
2904
2213
  for (var i = 0; i < body.orders.length; i++) _push(out, body.orders[i]);
2905
- var next = _parseLinkNext(res.headers, pageUrl); // resolved against this page, https + origin-gated, singleton
2214
+ var next = _parseLinkNext(res.headers, pageUrl);
2906
2215
  if (next == null) return;
2907
- var nextUrl = _clientUrl(next); // re-affirm the https gate on the followed target
2908
- // The loop guard runs BEFORE the cap decision: a next to an already-visited page names nothing new,
2909
- // so the list is COMPLETE (not truncated). Only a next to an unseen page that the cap forbids is a
2910
- // truncation -- a cycle landing exactly on the cap must not be reported as one.
2216
+ var nextUrl = _clientUrl(next);
2911
2217
  if (seen[_dedupKey(nextUrl)]) return;
2912
2218
  if (pages >= maxPages) { truncated = true; return; }
2913
2219
  return fetchPage(nextUrl);
@@ -2924,15 +2230,7 @@ function client(directoryUrl, opts) {
2924
2230
  var account = _requireKid();
2925
2231
  return _resource("keyChange").then(function (url) {
2926
2232
  return _post(url, keyChange, { account: account, oldKey: accountJwk, newKey: o.newKey, newJwk: o.newJwk, newAlg: o.newAlg }, "kid").then(function (res) {
2927
- // RFC 8555 sec. 7.3.5: a 200 means the server has COMMITTED the rollover (the status is gated to 200
2928
- // by _post; a problem+json is already rejected). Rotate the session key FIRST so the client stays in
2929
- // sync -- a bodyless or malformed account body must NOT leave the client on the old key while the
2930
- // server holds the new one. The account object is optional + informational: parse it best-effort.
2931
2233
  accountKey = o.newKey; accountJwk = o.newJwk; alg = o.newAlg;
2932
- // The resignKeys alternatives are CryptoKeys for the OLD account key material, so they no longer
2933
- // match the rotated account key. Drop them: re-signing with a stale-key alternative under the new
2934
- // kid would be rejected by the CA. With them cleared, a later badSignatureAlgorithm surfaces (the
2935
- // conformant default) rather than retrying with obsolete key material.
2936
2234
  resignKeys = null;
2937
2235
  var acct = null;
2938
2236
  if (_bodyText(res).trim() !== "") {
@@ -2944,29 +2242,20 @@ function client(directoryUrl, opts) {
2944
2242
  });
2945
2243
  }
2946
2244
 
2947
- // ARI RenewalInfo (RFC 9773 sec. 4.1) -- the SOLE UNAUTHENTICATED GET (no JWS, no nonce).
2948
2245
  function _renewalInfo(certDer, clockFn, retryAfterCapSeconds) {
2949
2246
  return _promised(function () { return _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds); });
2950
2247
  }
2951
2248
  function _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds) {
2952
2249
  if (!guard.bytes.isByteSource(certDer)) throw E("acme/bad-input", "renewalInfo requires a DER certificate BufferSource");
2953
- var _rawClk = typeof clockFn === "function" ? clockFn : clock; // renewalWindow may pass a per-call clock
2954
- // The clock MUST return finite epoch ms: a NaN / Infinity would make the expiry comparison below silently
2955
- // false and bypass the RFC 9773 sec. 4.3 gate, so the validating wrapper fails closed on a non-finite value.
2250
+ var _rawClk = typeof clockFn === "function" ? clockFn : clock;
2956
2251
  function clk() { var t = _rawClk(); if (typeof t !== "number" || !isFinite(t)) throw E("acme/bad-input", "the renewalInfo clock returned a non-finite value"); return t; }
2957
- // RFC 9773 sec. 4.3: a client MUST NOT check a certificate's RenewalInfo after it has expired. Gate BEFORE the
2958
- // unauthenticated GET -- the same expiry test renewalWindow applies -- so the raw verb cannot query a dead cert.
2959
2252
  var notAfterMs = guard.time.instantOf(x509.parse(certDer).validity.notAfter);
2960
2253
  // allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN instant).
2961
2254
  if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate is already past its notAfter; a client MUST NOT check RenewalInfo after it has expired (RFC 9773 sec. 4.3)");
2962
2255
  var certId = ariCertId(certDer);
2963
2256
  return _resource("renewalInfo").then(function (base) {
2964
- // Re-check expiry immediately BEFORE the GET: the directory fetch above (uncached) can take long enough that a
2965
- // certificate close to notAfter crosses it in between, and RFC 9773 sec. 4.3 forbids the query after expiry.
2966
2257
  // allow:nan-date-comparison-unguarded -- notAfterMs is a codec-parsed cert date; clk() is validated finite.
2967
2258
  if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate expired before the RenewalInfo request could be issued; a client MUST NOT check RenewalInfo after expiry (RFC 9773 sec. 4.3)");
2968
- // Append the certID to the PATH, before any query string (RFC 9773): a directory renewalInfo URL may
2969
- // carry a query, so string concatenation would push the certID into the query rather than the path.
2970
2259
  var u = new URL(base);
2971
2260
  u.pathname = _stripTrailingSlash(u.pathname) + "/" + certId;
2972
2261
  var url = _clientUrl(u.href);
@@ -2976,9 +2265,6 @@ function client(directoryUrl, opts) {
2976
2265
  var ra = res.headers["retry-after"];
2977
2266
  var retryAfterSeconds = null;
2978
2267
  if (typeof ra === "string" && ra.trim() !== "") {
2979
- // renewalWindow passes a cap: the Retry-After is ADVISORY there and is clamped anyway, so a huge or
2980
- // unparseable value must clamp / drop to null (renewalWindow then defaults) rather than discard the
2981
- // validated window. The raw renewalInfo verb (no cap) stays strict -- a bad Retry-After fails closed.
2982
2268
  var raOpts = { now: clk(), E: E, code: "acme/bad-retry-after" };
2983
2269
  if (typeof retryAfterCapSeconds === "number") { raOpts.cap = retryAfterCapSeconds; raOpts.lenient = true; }
2984
2270
  retryAfterSeconds = retryAfter.parse(ra, raOpts).retryAfterSeconds;
@@ -2988,56 +2274,26 @@ function client(directoryUrl, opts) {
2988
2274
  });
2989
2275
  }
2990
2276
 
2991
- // ARI renewal-window decision (RFC 9773 sec. 4.2 / 4.3) -- a pure, timer-less helper that composes the
2992
- // unauthenticated renewalInfo GET, picks a uniform-random instant in the CA's suggested window, and
2993
- // returns the decision as DATA. It never sleeps or schedules: an auto-renewing daemon is an explicit
2994
- // future opt-in, not a hidden timer inside the thin client.
2995
2277
  function _renewalWindow(certDer, o) {
2996
2278
  return Promise.resolve().then(function () {
2997
2279
  o = _gateOpts(o, _METHOD_OPTS.renewalWindow, "renewalWindow");
2998
2280
  if (!guard.bytes.isByteSource(certDer)) throw E("acme/bad-input", "renewalWindow requires a DER certificate BufferSource");
2999
2281
  if (o.random !== undefined && typeof o.random !== "function") throw E("acme/bad-input", "renewalWindow opts.random must be a function returning a number in [0, 1]");
3000
2282
  if (o.clock !== undefined && typeof o.clock !== "function") throw E("acme/bad-input", "renewalWindow opts.clock must be a function returning epoch milliseconds");
3001
- // replaced is the sec. 4.3 "MUST NOT act on a replaced certificate" signal: require a real boolean so a
3002
- // truthy non-boolean (a caller storing replacement state as a timestamp/object) cannot fail OPEN past
3003
- // the gate below (=== true would let it through) and issue the RenewalInfo GET the RFC forbids.
3004
2283
  if (o.replaced !== undefined && typeof o.replaced !== "boolean") throw E("acme/bad-input", "renewalWindow opts.replaced must be a boolean");
3005
- // opts.previous is a prior renewalWindow RESULT the caller stored: RFC 9773 sec. 4.2 says the client SHOULD
3006
- // store the selected time and reuse it, so refreshing ARI (a later call after retryAfterSeconds) keeps the
3007
- // SAME renewal instant while the CA's window is unchanged rather than re-randomizing and jittering it.
3008
2284
  if (o.previous !== undefined && !_isObject(o.previous)) throw E("acme/bad-input", "renewalWindow opts.previous must be a prior renewalWindow result object");
3009
- // A per-call clock (documented opt) overrides the client's receipt clock for the WHOLE decision -- the
3010
- // expiry gate, the renew-now comparison, and the RenewalInfo Retry-After parse -- so a caller can ask
3011
- // "should I renew as of time T?" deterministically without reconfiguring the client. Every READING must
3012
- // be a finite epoch ms: a NaN / Infinity would make each relational comparison silently false and bypass
3013
- // the expiry gate, so the validating wrapper fails closed on a non-finite value.
3014
2285
  var _clk = typeof o.clock === "function" ? o.clock : clock;
3015
2286
  function clk() { var t = _clk(); if (typeof t !== "number" || !isFinite(t)) throw E("acme/bad-input", "the renewalWindow clock returned a non-finite value"); return t; }
3016
- // RFC 9773 sec. 4.3: a client MUST NOT act on renewal info for a certificate it can no longer use --
3017
- // one already past its notAfter (nothing to renew) or one the caller knows has been replaced. Gate
3018
- // BEFORE the fetch so a decommissioned certificate never even reaches the CA.
3019
2287
  var notAfter = x509.parse(certDer).validity.notAfter;
3020
2288
  var now = clk();
3021
- // Exclusive of notAfter: X.509 validity is inclusive of notAfter (a cert is still valid AT it), matching
3022
- // path-validate's `t > notAfter` expiry test -- only a cert strictly past notAfter has nothing to renew.
3023
2289
  // allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN
3024
- // instant) and the window start/end below are rfc3339-validated by validateRenewalInfo; neither is NaN.
3025
2290
  if (now > guard.time.instantOf(notAfter)) throw E("acme/certificate-expired", "the certificate is already past its notAfter; there is nothing to renew (RFC 9773 sec. 4.3)");
3026
2291
  if (o.replaced === true) throw E("acme/certificate-replaced", "the caller asserts this certificate has already been replaced (RFC 9773 sec. 4.3)");
3027
2292
  return _renewalInfo(certDer, clk, RENEWAL_RETRY_MAX_SECONDS).then(function (ri) {
3028
2293
  var w = ri.renewalInfo.suggestedWindow;
3029
- // start/end are grammar+calendar-validated RFC 3339 by validateRenewalInfo (an inverted window has
3030
- // already thrown acme/bad-renewal-window), so neither Date.parse is NaN and end > start here.
3031
2294
  var startMs = Date.parse(w.start), endMs = Date.parse(w.end);
3032
- // Bound the window by the certificate's own expiry: a renewal instant AFTER notAfter is useless (you
3033
- // must renew BEFORE the cert dies), so a CA suggestedWindow that extends past notAfter is clamped to it
3034
- // -- selection stays uniform within the still-valid sub-window. If the whole window is past notAfter,
3035
- // effStart collapses to effEnd (renew at the last valid instant).
3036
2295
  var notAfterMs = guard.time.instantOf(notAfter);
3037
2296
  var effEnd = Math.min(endMs, notAfterMs), effStart = Math.min(startMs, effEnd);
3038
- // RFC 9773 sec. 4.2: if the caller passed a prior result (opts.previous) whose suggestedWindow is UNCHANGED,
3039
- // REUSE its selectedTime (clamped to the still-valid, notAfter-bounded window) instead of drawing again, so
3040
- // repeated ARI refreshes converge on one stable renewal instant. A changed window (or no prior) draws fresh.
3041
2297
  var pw = o.previous && _isObject(o.previous.suggestedWindow) ? o.previous.suggestedWindow : null;
3042
2298
  var selectedMs;
3043
2299
  if (pw && pw.start === w.start && pw.end === w.end && _isString(o.previous.selectedTime)) {
@@ -3045,12 +2301,8 @@ function client(directoryUrl, opts) {
3045
2301
  if (!isFinite(prevMs)) throw E("acme/bad-input", "renewalWindow opts.previous.selectedTime must be an RFC 3339 date-time");
3046
2302
  selectedMs = Math.max(effStart, Math.min(effEnd, prevMs));
3047
2303
  } else {
3048
- // Validate the draw's TYPE, never coerce: Number(null)/Number(false) -> 0, Number("0.5") -> 0.5 would
3049
- // silently accept a non-number callback return. Require an actual number in [0, 1].
3050
2304
  var draw = o.random ? o.random() : _defaultRandom();
3051
2305
  if (typeof draw !== "number" || !(draw >= 0 && draw <= 1)) throw E("acme/bad-input", "renewalWindow opts.random must return a number in [0, 1]");
3052
- // select a uniform-random instant across the (validity-bounded) window to spread renewals; if it is
3053
- // already in the past, the caller should renew immediately.
3054
2306
  selectedMs = Math.round(effStart + draw * (effEnd - effStart));
3055
2307
  }
3056
2308
  var retryAfterSeconds = ri.retryAfterSeconds == null ? RENEWAL_RETRY_DEFAULT_SECONDS :
@@ -3058,11 +2310,6 @@ function client(directoryUrl, opts) {
3058
2310
  return {
3059
2311
  suggestedWindow: w,
3060
2312
  selectedTime: new Date(selectedMs).toISOString(),
3061
- // Decide renew-now against a FRESH clock read: the RenewalInfo GET may itself span the selected
3062
- // instant, so the pre-fetch `now` (used above only for the expiry gate) could be stale here. Also
3063
- // renew now when the SELECTED instant lands at or past notAfter -- whether the whole window opened
3064
- // after expiry or a straddling window's draw hit the clamp endpoint, there is no margin left, so
3065
- // waiting for the (clamped) time would leave the cert to expire unrenewed.
3066
2313
  renewNow: selectedMs <= clk() || selectedMs >= notAfterMs,
3067
2314
  retryAfterSeconds: retryAfterSeconds,
3068
2315
  explanationURL: _isString(ri.renewalInfo.explanationURL) ? ri.renewalInfo.explanationURL : null,
@@ -3107,7 +2354,6 @@ module.exports = {
3107
2354
  dns01: dns01,
3108
2355
  tlsAlpn01Extension: tlsAlpn01Extension,
3109
2356
  verifyTlsAlpn01: verifyTlsAlpn01,
3110
- // request builders
3111
2357
  postAsGet: postAsGet,
3112
2358
  newAccount: newAccount,
3113
2359
  externalAccountBinding: externalAccountBinding,
@@ -3119,9 +2365,7 @@ module.exports = {
3119
2365
  updateAccount: updateAccount,
3120
2366
  revokeCert: revokeCert,
3121
2367
  keyChange: keyChange,
3122
- // ARI (RFC 9773)
3123
2368
  ariCertId: ariCertId,
3124
2369
  parseAriCertId: parseAriCertId,
3125
- // re-exported for the ACME consumer / test surface
3126
2370
  jose: jose,
3127
2371
  };