@blamejs/pki 0.5.30 → 0.5.32

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 (53) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -2
  3. package/lib/acme.js +347 -90
  4. package/lib/asn1-der.js +97 -33
  5. package/lib/attrcert-sign.js +1 -1
  6. package/lib/cmc-build.js +19 -8
  7. package/lib/cmc-verify.js +4 -4
  8. package/lib/cmp-build.js +36 -6
  9. package/lib/cmp-session.js +5 -5
  10. package/lib/cmp-verify.js +86 -16
  11. package/lib/cms-compress.js +2 -2
  12. package/lib/cms-decrypt.js +3 -3
  13. package/lib/cms-encrypt.js +1 -1
  14. package/lib/cms-sign.js +7 -7
  15. package/lib/cms-verify.js +9 -4
  16. package/lib/crl-sign.js +5 -5
  17. package/lib/crmf-sign.js +2 -1
  18. package/lib/csr-sign.js +1 -4
  19. package/lib/ct.js +8 -4
  20. package/lib/est.js +234 -34
  21. package/lib/framework-error.js +20 -6
  22. package/lib/guard-bytes.js +24 -3
  23. package/lib/http-digest.js +69 -16
  24. package/lib/http-retry-after.js +68 -24
  25. package/lib/http-transport.js +60 -6
  26. package/lib/inspect.js +13 -2
  27. package/lib/ip-utils.js +102 -25
  28. package/lib/key.js +2 -2
  29. package/lib/lint.js +1 -1
  30. package/lib/mime.js +27 -4
  31. package/lib/ocsp.js +7 -7
  32. package/lib/oid.js +16 -0
  33. package/lib/path-validate.js +76 -18
  34. package/lib/pbes2.js +2 -2
  35. package/lib/pkcs12-build.js +2 -2
  36. package/lib/pki-build.js +320 -21
  37. package/lib/rfc3339.js +75 -17
  38. package/lib/schema-c509.js +36 -9
  39. package/lib/schema-cmc.js +3 -3
  40. package/lib/schema-pkix.js +174 -25
  41. package/lib/schema-tsp.js +10 -1
  42. package/lib/sign-scheme.js +27 -7
  43. package/lib/sigstore.js +52 -9
  44. package/lib/smime.js +86 -6
  45. package/lib/trust.js +114 -14
  46. package/lib/tsp-sign.js +39 -27
  47. package/lib/validator-tpm.js +2 -2
  48. package/lib/webauthn-mds.js +24 -6
  49. package/lib/webauthn.js +3 -3
  50. package/lib/webcrypto.js +1 -1
  51. package/lib/x509-sign.js +9 -5
  52. package/package.json +1 -1
  53. package/sbom.cdx.json +6 -6
package/lib/est.js CHANGED
@@ -73,6 +73,10 @@ var crmfFmt = require("./schema-crmf");
73
73
  var frameworkError = require("./framework-error");
74
74
  var guard = require("./guard-all");
75
75
  var intrinsic = require("./guard-intrinsic");
76
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
77
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
78
+ var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
79
+ var pkix = require("./schema-pkix"); // shared base64-whitespace strip + JS-\s predicate
76
80
  // Own-key membership through an operation taken at load: written out it reads two replaceable
77
81
  // properties to answer one question, and here it decides which fields of a response are copied.
78
82
  var _hasOwn = intrinsic.hasOwn;
@@ -85,6 +89,14 @@ function E(code, message, cause) { return new EstError(code, message, cause); }
85
89
  var ID_SIGNED_DATA = oid.byName("signedData");
86
90
  var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
87
91
  var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
92
+
93
+ // Charset tables built once from the shared pkix.charTable / pkix.allCharsIn char-scan home.
94
+ var _DIGITS_TABLE = pkix.charTable("0123456789"); // /^[0-9]+$/
95
+ var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // /^[A-Za-z0-9._~-]+$/
96
+
97
+ // Trailing-slash strip (/\/+$/ -> ""): the length after dropping a run of trailing "/" (0x2f); the
98
+ // caller slices to it, so the removed regex read is traded for the slice that replaces it.
99
+ function _stripTrailingSlashEnd(s) { var end = s.length; while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end -= 1; return end; }
88
100
  var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
89
101
  var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
90
102
  var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
@@ -175,7 +187,7 @@ function transferDecode(body) {
175
187
  var s = guard.text.decode(body, cap, EstError, {
176
188
  charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
177
189
  });
178
- var stripped = s.replace(/[\r\n \t]/g, "");
190
+ var stripped = pkix.stripBase64Whitespace(s);
179
191
  // Strict canonical RFC 4648 base64 (RFC 8951 sec. 3.1) via the shared encoding
180
192
  // guard: the alphabet + whole-4-group + canonical round-trip fail a malformed
181
193
  // body closed instead of silently truncating to a shorter, different DER.
@@ -208,11 +220,31 @@ function transferEncode(der) {
208
220
 
209
221
  // ---- the multipart/mixed splitter (/serverkeygen) -----------------------
210
222
 
223
+ // Case-insensitive ASCII prefix test: does `s` begin with the uppercase literal `litUpper`?
224
+ function _ciStartsWith(s, litUpper) {
225
+ if (s.length < litUpper.length) return false;
226
+ for (var k = 0; k < litUpper.length; k++) {
227
+ var c = _charCodeAt(s, k);
228
+ if (c >= 97 && c <= 122) c -= 32; // fold lowercase -> uppercase
229
+ if (c !== _charCodeAt(litUpper, k)) return false;
230
+ }
231
+ return true;
232
+ }
233
+ // /^multipart\/mixed\s*(;|$)/i by character scan: the media type "multipart/mixed" (case-insensitive),
234
+ // then JS \s* whitespace, then a `;` parameter separator or end of the header value.
235
+ var _MULTIPART_MIXED = "MULTIPART/MIXED";
236
+ function _isMultipartMixedPrefix(ct) {
237
+ if (!_ciStartsWith(ct, _MULTIPART_MIXED)) return false;
238
+ var p = _MULTIPART_MIXED.length;
239
+ while (p < ct.length && pkix.isJsWhitespace(_charCodeAt(ct, p))) p++;
240
+ return p === ct.length || _charCodeAt(ct, p) === 0x3b;
241
+ }
242
+
211
243
  // Extract the boundary from a `multipart/mixed; boundary=...` content-type,
212
244
  // tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
213
245
  function _multipartBoundary(contentType) {
214
246
  var ct = String(contentType || "");
215
- if (!/^multipart\/mixed\s*(;|$)/i.test(ct)) return null;
247
+ if (!_isMultipartMixedPrefix(ct)) return null;
216
248
  // Two boundaries name two different splits of the same body; there is no reading
217
249
  // of that header, so it is refused rather than resolved by taking the first.
218
250
  var bp = _ctParam(ct, "boundary");
@@ -223,6 +255,94 @@ function _multipartBoundary(contentType) {
223
255
  return bp.value;
224
256
  }
225
257
 
258
+ // A boundary delimiter at a candidate line start `ls`: `--boundary`, an optional `--` close marker, optional
259
+ // SP/HTAB transport-padding, then a CRLF/LF or end-of-text. Returns { end, close } (end = the index past the
260
+ // trailing line terminator) or null. `(--)?` is taken greedily without backtracking loss: a `-` can be neither
261
+ // padding nor a terminator, so if the close marker is present it is the only possible parse.
262
+ function _matchDelimAt(text, ls, boundary) {
263
+ var n = text.length, q = ls, blen = boundary.length, k;
264
+ if (q + 2 > n || _charCodeAt(text, q) !== 0x2d || _charCodeAt(text, q + 1) !== 0x2d) return null;
265
+ q += 2;
266
+ if (q + blen > n) return null;
267
+ for (k = 0; k < blen; k++) { if (_charCodeAt(text, q + k) !== _charCodeAt(boundary, k)) return null; }
268
+ q += blen;
269
+ var close = false;
270
+ if (q + 2 <= n && _charCodeAt(text, q) === 0x2d && _charCodeAt(text, q + 1) === 0x2d) { close = true; q += 2; }
271
+ while (q < n && (_charCodeAt(text, q) === 0x20 || _charCodeAt(text, q) === 0x09)) q += 1;
272
+ if (q === n) return { end: q, close: close };
273
+ if (_charCodeAt(text, q) === 0x0a) return { end: q + 1, close: close };
274
+ if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
275
+ return null;
276
+ }
277
+ // Every delimiter in `text`, replacing the global /(?:^|\r?\n)--boundary(--)?[ \t]*(?:\r?\n|$)/g scan: a
278
+ // delimiter begins the text or follows a CRLF/LF (which the match CONSUMES), so `at` is the index of that
279
+ // leading terminator (0 at the start) and `bodyStart` is the index past the trailing one. The leftmost match
280
+ // at or after the previous `bodyStart` is taken, so two adjacent delimiters share one terminator and only the
281
+ // first is found -- the same non-overlapping behavior the regex has.
282
+ function _findMultipartMarks(text, boundary) {
283
+ var marks = [], n = text.length, searchStart = 0, p;
284
+ while (searchStart <= n) {
285
+ var found = null, foundAt = -1;
286
+ for (p = searchStart; p <= n; p++) {
287
+ if (p === 0) { var r0 = _matchDelimAt(text, 0, boundary); if (r0) { found = r0; foundAt = 0; break; } }
288
+ var lsN = -1;
289
+ if (_charCodeAt(text, p) === 0x0a) lsN = p + 1;
290
+ else if (_charCodeAt(text, p) === 0x0d && p + 1 < n && _charCodeAt(text, p + 1) === 0x0a) lsN = p + 2;
291
+ if (lsN !== -1) { var r1 = _matchDelimAt(text, lsN, boundary); if (r1) { found = r1; foundAt = p; break; } }
292
+ }
293
+ if (!found) break;
294
+ marks[marks.length] = { at: foundAt, bodyStart: found.end, close: found.close };
295
+ searchStart = found.end === foundAt ? found.end + 1 : found.end;
296
+ }
297
+ return marks;
298
+ }
299
+ // Strip the part-body framing the regexes /^(\r?\n){2}/ and /\r?\n$/ removed: the two leading line terminators
300
+ // (all-or-nothing) that follow the header/body separator, and one trailing terminator.
301
+ function _stripTwoLeadingEol(seg, from) {
302
+ var q = from;
303
+ for (var t = 0; t < 2; t++) {
304
+ if (_charCodeAt(seg, q) === 0x0d && _charCodeAt(seg, q + 1) === 0x0a) q += 2;
305
+ else if (_charCodeAt(seg, q) === 0x0a) q += 1;
306
+ else return from;
307
+ }
308
+ return q;
309
+ }
310
+ function _stripOneTrailingEol(seg, end) {
311
+ if (end >= 1 && _charCodeAt(seg, end - 1) === 0x0a) {
312
+ if (end >= 2 && _charCodeAt(seg, end - 2) === 0x0d) return end - 2;
313
+ return end - 1;
314
+ }
315
+ return end;
316
+ }
317
+ // Unfold folded MIME header lines (/\r?\n(?=[ \t])/g -> ""): a CRLF/LF immediately followed by SP/HTAB is a
318
+ // FOLD, not a header break, so the terminator is dropped while the whitespace stays.
319
+ function _unfoldHeaders(raw) {
320
+ var out = "", last = 0, i = 0, n = raw.length;
321
+ while (i < n) {
322
+ var nlLen = 0;
323
+ if (_charCodeAt(raw, i) === 0x0d && i + 1 < n && _charCodeAt(raw, i + 1) === 0x0a) nlLen = 2;
324
+ else if (_charCodeAt(raw, i) === 0x0a) nlLen = 1;
325
+ if (nlLen > 0) {
326
+ var after = i + nlLen;
327
+ if (after < n && (_charCodeAt(raw, after) === 0x20 || _charCodeAt(raw, after) === 0x09)) { out += _strSlice(raw, last, i); last = after; i = after; continue; }
328
+ }
329
+ i += 1;
330
+ }
331
+ return out + _strSlice(raw, last, n);
332
+ }
333
+ // Split on CRLF/LF (.split(/\r?\n/)) by character scan, preserving JS split semantics (leading/trailing "").
334
+ function _splitCrlf(s) {
335
+ var out = [], last = 0, i = 0, n = s.length;
336
+ while (i < n) {
337
+ var nlLen = 0;
338
+ if (_charCodeAt(s, i) === 0x0d && i + 1 < n && _charCodeAt(s, i + 1) === 0x0a) nlLen = 2;
339
+ else if (_charCodeAt(s, i) === 0x0a) nlLen = 1;
340
+ if (nlLen > 0) { out[out.length] = _strSlice(s, last, i); i += nlLen; last = i; } else i += 1;
341
+ }
342
+ out[out.length] = _strSlice(s, last, n);
343
+ return out;
344
+ }
345
+
226
346
  // Split a multipart/mixed body into its parts, each { headers, contentType,
227
347
  // body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
228
348
  // start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
@@ -234,34 +354,30 @@ function splitMultipartMixed(body, contentType) {
234
354
  var text = guard.text.decode(body, constants.LIMITS.DER_MAX_BYTES * 2, EstError, {
235
355
  charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the multipart body",
236
356
  });
237
- var esc = boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
238
- var delimRe = new RegExp("(?:^|\\r?\\n)--" + esc + "(--)?[ \\t]*(?:\\r?\\n|$)", "g");
239
- var marks = [], m;
240
- while ((m = delimRe.exec(text)) !== null) {
241
- marks.push({ at: m.index, bodyStart: delimRe.lastIndex, close: m[1] === "--" });
242
- if (delimRe.lastIndex === m.index) delimRe.lastIndex += 1; // guard a zero-length match
243
- }
357
+ var marks = _findMultipartMarks(text, boundary);
244
358
  var closeAt = -1;
245
359
  for (var c = 0; c < marks.length; c++) { if (marks[c].close) { closeAt = c; break; } }
246
360
  if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
247
361
  var parts = [];
248
362
  for (var i = 0; i < closeAt; i++) {
249
- var seg = text.slice(marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
363
+ var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
250
364
  var sep = seg.indexOf("\r\n\r\n");
251
365
  if (sep === -1) sep = seg.indexOf("\n\n");
252
366
  if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
253
- var rawHeaders = seg.slice(0, sep);
254
- var partBody = seg.slice(sep).replace(/^(\r?\n){2}/, "").replace(/\r?\n$/, "");
367
+ var rawHeaders = _strSlice(seg, 0, sep);
368
+ var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
255
369
  var headers = {};
256
370
  // Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
257
371
  // FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
258
372
  // continues on the next line must be read whole, not truncated.
259
- rawHeaders.replace(/\r?\n(?=[ \t])/g, "").split(/\r?\n/).forEach(function (line) {
373
+ var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
374
+ for (var li = 0; li < lines.length; li++) {
375
+ var line = lines[li];
260
376
  var col = line.indexOf(":");
261
377
  if (col > 0) headers[line.slice(0, col).trim().toLowerCase()] = line.slice(col + 1).trim();
262
- });
378
+ }
263
379
  var partCt = headers["content-type"] || "";
264
- if (/^multipart\//i.test(partCt)) throw E("est/bad-multipart", "a nested multipart part is not permitted");
380
+ if (_ciStartsWith(partCt, "MULTIPART/")) throw E("est/bad-multipart", "a nested multipart part is not permitted");
265
381
  parts.push({ headers: headers, contentType: partCt, body: partBody });
266
382
  }
267
383
  return parts;
@@ -376,7 +492,7 @@ function _recipientMatches(recipientInfos, opts) {
376
492
  var sn = ias.serialNumber, want;
377
493
  if (typeof sn === "bigint" && sn >= 0n) want = sn;
378
494
  else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
379
- else if (typeof sn === "string" && /^[0-9]+$/.test(sn)) want = BigInt(sn);
495
+ else if (typeof sn === "string" && pkix.allCharsIn(sn, _DIGITS_TABLE)) want = BigInt(sn);
380
496
  else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
381
497
  // An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
382
498
  // an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
@@ -442,12 +558,31 @@ function _ctParam(contentType, name) {
442
558
  return { value: value, duplicated: count > 1 };
443
559
  }
444
560
 
561
+ // A media type `type/subtype` by character scan (RFC 2045 sec. 5.1 restricted token): the first character
562
+ // of each part is alphanumeric, the rest add !#$&^_.+- (case-insensitive), exactly one `/` separates them.
563
+ var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
564
+ var _MT_REST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&^_.+-");
565
+ function _isMediaToken(s, from, to) {
566
+ if (to <= from || !_MT_FIRST[_charCodeAt(s, from)]) return false;
567
+ for (var i = from + 1; i < to; i++) { if (!_MT_REST[_charCodeAt(s, i)]) return false; }
568
+ return true;
569
+ }
570
+ // /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i -> the lowercased media type, or null:
571
+ // trim JS whitespace, require a single `/` splitting two restricted tokens.
572
+ function _mediaType(seg) {
573
+ var a = 0, b = seg.length, slash = -1, i;
574
+ while (a < b && pkix.isJsWhitespace(_charCodeAt(seg, a))) a += 1;
575
+ while (b > a && pkix.isJsWhitespace(_charCodeAt(seg, b - 1))) b -= 1;
576
+ for (i = a; i < b; i++) { if (_charCodeAt(seg, i) === 0x2f) { if (slash !== -1) return null; slash = i; } }
577
+ if (slash === -1 || !_isMediaToken(seg, a, slash) || !_isMediaToken(seg, slash + 1, b)) return null;
578
+ return _toLowerCase(_strSlice(seg, a, b));
579
+ }
580
+
445
581
  function _partMediaType(contentType) {
446
582
  var segs = _splitContentTypeParams(String(contentType || ""));
447
- var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
448
583
  var st = _ctParam(contentType, "smime-type");
449
584
  return {
450
- media: mediaMatch ? mediaMatch[1].toLowerCase() : null,
585
+ media: _mediaType(segs[0]),
451
586
  smimeType: st.value === null ? null : st.value.toLowerCase(),
452
587
  ambiguous: st.duplicated,
453
588
  };
@@ -679,14 +814,15 @@ function classifyResponse(status, headers, body, opts) {
679
814
  function paths(baseUrl, opts) {
680
815
  opts = opts || {};
681
816
  _knownOpts(opts, PATHS_OPTS, "paths");
682
- var prefix = String(baseUrl).replace(/\/+$/, "") + "/.well-known/est";
817
+ var base = String(baseUrl);
818
+ var prefix = base.slice(0, _stripTrailingSlashEnd(base)) + "/.well-known/est";
683
819
  if (opts.label != null) {
684
820
  var label = String(opts.label);
685
821
  // A label is ONE URL path segment: unreserved characters only (RFC 3986),
686
822
  // never a dot-segment or an operation name. Rejecting rather than
687
823
  // percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
688
824
  // silently retargeting the request to a different resource.
689
- if (label === "" || label === "." || label === ".." || !/^[A-Za-z0-9._~-]+$/.test(label) || OPERATIONS.indexOf(label) !== -1) {
825
+ if (label === "" || label === "." || label === ".." || !pkix.allCharsIn(label, _LABEL_TABLE) || OPERATIONS.indexOf(label) !== -1) {
690
826
  throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
691
827
  }
692
828
  prefix += "/" + label;
@@ -707,8 +843,10 @@ function challengePasswordFromTlsUnique(channelBinding) {
707
843
  // The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
708
844
  // full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
709
845
  // "length", { get: function () { return 0; } })` had this refuse bytes that were there.
710
- if (!Buffer.isBuffer(channelBinding) || guard.bytes.lengthOf(channelBinding) === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
711
- var b64 = guard.bytes.view(channelBinding, EstError, "est/bad-input", "the channel-binding bytes").toString("base64");
846
+ if (!guard.bytes.isByteSource(channelBinding)) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
847
+ var _cb = guard.bytes.source(channelBinding, EstError, "est/bad-input", "the channel-binding bytes");
848
+ if (_cb.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
849
+ var b64 = _cb.toString("base64");
712
850
  if (b64.length > 255) throw E("est/tls-unique-too-long", "the base64 tls-unique value exceeds 255 octets (RFC 7030 sec. 3.5)");
713
851
  return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
714
852
  }
@@ -834,9 +972,9 @@ var MAX_TIMEOUT = constants.TIME.seconds(600);
834
972
  // The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
835
973
  // decoded. Any other input is a config-time est/bad-input.
836
974
  function _csrDer(input) {
837
- if (Buffer.isBuffer(input)) return guard.bytes.snapshot(input, EstError, "est/bad-input", "a CSR");
975
+ if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, EstError, "est/bad-input", "a CSR");
838
976
  if (typeof input === "string") return csr.pemDecode(input);
839
- throw E("est/bad-input", "a CSR must be a DER Buffer or a PEM CERTIFICATE REQUEST string");
977
+ throw E("est/bad-input", "a CSR must be a DER BufferSource or a PEM CERTIFICATE REQUEST string");
840
978
  }
841
979
 
842
980
  // Parse + validate the request URL BEFORE any transport is called: a URL that will not
@@ -880,9 +1018,47 @@ function _redirectTarget(current, location, method, opts) {
880
1018
  // which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
881
1019
  // comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
882
1020
  // realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
1021
+ // Blank the CONTENTS of every complete quoted-string (/"(?:[^"\\]|\\.)*"/g -> `""`) by character scan: a
1022
+ // `\` escapes the next character, so `\"` does not close; an unterminated final quote is left as written
1023
+ // (the regex only matches a closed quoted-string).
1024
+ function _blankQuotedStrings(s) {
1025
+ var out = "", last = 0, i = 0, n = s.length;
1026
+ while (i < n) {
1027
+ if (_charCodeAt(s, i) === 0x22) {
1028
+ var j = i + 1, closed = -1;
1029
+ while (j < n) { var c = _charCodeAt(s, j); if (c === 0x5c) { j += 2; continue; } if (c === 0x22) { closed = j; break; } j += 1; }
1030
+ if (closed === -1) break;
1031
+ out += _strSlice(s, last, i) + "\"\"";
1032
+ last = closed + 1; i = closed + 1;
1033
+ } else i += 1;
1034
+ }
1035
+ return out + _strSlice(s, last, n);
1036
+ }
1037
+ // Is `scheme` (our own literal, case-insensitive) offered as a challenge TOKEN in the blanked header?
1038
+ // (?:^|,)\s*SCHEME(?=[\s,]|$): at the start or after a comma, optional whitespace, the scheme, then a
1039
+ // whitespace / comma / end boundary -- never a substring, so `Digest realm="basic"` is not read as Basic.
1040
+ function _schemeOffered(s, scheme) {
1041
+ var n = s.length, slen = scheme.length, p, k;
1042
+ for (p = 0; p <= n; p++) {
1043
+ if (p !== 0 && _charCodeAt(s, p - 1) !== 0x2c) continue;
1044
+ var q = p;
1045
+ while (q < n && pkix.isJsWhitespace(_charCodeAt(s, q))) q += 1;
1046
+ if (q + slen > n) continue;
1047
+ var ok = true;
1048
+ for (k = 0; k < slen; k++) {
1049
+ var x = _charCodeAt(s, q + k), y = _charCodeAt(scheme, k);
1050
+ if (x >= 65 && x <= 90) x += 32;
1051
+ if (y >= 65 && y <= 90) y += 32;
1052
+ if (x !== y) { ok = false; break; }
1053
+ }
1054
+ if (!ok) continue;
1055
+ var after = q + slen;
1056
+ if (after === n || pkix.isJsWhitespace(_charCodeAt(s, after)) || _charCodeAt(s, after) === 0x2c) return true;
1057
+ }
1058
+ return false;
1059
+ }
883
1060
  function _offersScheme(www, scheme) {
884
- var s = String(www || "").replace(/"(?:[^"\\]|\\.)*"/g, "\"\"");
885
- return new RegExp("(?:^|,)\\s*" + scheme + "(?=[\\s,]|$)", "i").test(s);
1061
+ return _schemeOffered(_blankQuotedStrings(String(www || "")), scheme);
886
1062
  }
887
1063
  // The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
888
1064
  // WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
@@ -1573,8 +1749,8 @@ function _collectNestedPaths(list, prefix, out, depth) {
1573
1749
  function _assertAgrees(name, supplied, carried) {
1574
1750
  if (supplied == null) return;
1575
1751
  var same;
1576
- if (Buffer.isBuffer(supplied) || supplied instanceof Uint8Array) {
1577
- same = Buffer.isBuffer(carried) && Buffer.from(supplied).equals(carried);
1752
+ if (guard.bytes.isByteSource(supplied)) {
1753
+ same = Buffer.isBuffer(carried) && guard.bytes.source(supplied, EstError, "est/bad-input", "opts.senderNonce").equals(carried);
1578
1754
  } else {
1579
1755
  // Through the shared authoring guard, not a bare BigInt(): a `number` above
1580
1756
  // Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
@@ -1624,8 +1800,8 @@ function _shallowCopy(o) {
1624
1800
  function _cmcRequestDer(request) {
1625
1801
  // COPIED, not aliased: these bytes are parsed now (for the requested keys) and
1626
1802
  // transmitted later, so sharing the caller's buffer would let the two disagree.
1627
- if (Buffer.isBuffer(request) || request instanceof Uint8Array) {
1628
- return guard.bytes.snapshot(request, EstError, "est/bad-input", "the Full PKI Request");
1803
+ if (guard.bytes.isByteSource(request)) {
1804
+ return guard.bytes.snapshotSource(request, EstError, "est/bad-input", "the Full PKI Request");
1629
1805
  }
1630
1806
  if (typeof request === "string") return cms.pemDecode(request);
1631
1807
  throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
@@ -1991,13 +2167,37 @@ function _serverkeygenEncryptionFromCsr(csrDer) {
1991
2167
  // The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
1992
2168
  // used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
1993
2169
  // transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
2170
+ // The weak-cipher token scan (RFC 3986/2119-free ASCII, by character code): a "word" character is
2171
+ // [A-Za-z0-9_] (regex \w), and a boundary (\b) sits where a word character meets a non-word one or a
2172
+ // string edge. `_subAt`/`_containsSub` test a literal token at / anywhere in an already-uppercased name.
2173
+ function _isWordChar(c) { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95; }
2174
+ function _subAt(s, sub, i) { var m = sub.length; if (i + m > s.length) return false; for (var k = 0; k < m; k++) { if (_charCodeAt(s, i + k) !== _charCodeAt(sub, k)) return false; } return true; }
2175
+ function _containsSub(s, sub) { for (var i = 0; i + sub.length <= s.length; i++) { if (_subAt(s, sub, i)) return true; } return false; }
2176
+ function _wbBefore(s, i) { return i === 0 || !_isWordChar(_charCodeAt(s, i - 1)); }
2177
+ function _wbAfter(s, j) { return j === s.length || !_isWordChar(_charCodeAt(s, j)); }
2178
+ // /NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/ over the uppercased name: the three plain substrings, then
2179
+ // EXP at a word boundary followed by a separator OR a digit run (EXP-RC4-MD5, EXP1024-RC4-SHA -- a bare
2180
+ // \bEXP\b misses EXP1024), then ADH / AECDH each bounded on both sides.
2181
+ function _hasWeakCipherToken(name) {
2182
+ if (_containsSub(name, "NULL") || _containsSub(name, "ANON") || _containsSub(name, "EXPORT")) return true;
2183
+ var i, n = name.length;
2184
+ for (i = 0; i + 3 <= n; i++) {
2185
+ if (_subAt(name, "EXP", i) && _wbBefore(name, i)) {
2186
+ var c = _charCodeAt(name, i + 3);
2187
+ if (c === 45 || c === 95 || (c >= 48 && c <= 57)) return true;
2188
+ }
2189
+ }
2190
+ for (i = 0; i < n; i++) {
2191
+ if (_subAt(name, "ADH", i) && _wbBefore(name, i) && _wbAfter(name, i + 3)) return true;
2192
+ if (_subAt(name, "AECDH", i) && _wbBefore(name, i) && _wbAfter(name, i + 5)) return true;
2193
+ }
2194
+ return false;
2195
+ }
1994
2196
  function _assertConfidentialCipher(res) {
1995
2197
  if (!res || !res.tls || !res.tls.cipher) return;
1996
2198
  var c = res.tls.cipher;
1997
2199
  var name = (String(c.name || "") + " " + String(c.standardName || "")).toUpperCase();
1998
- // EXPORT covers the RFC/IANA name; the OpenSSL short names use an EXP prefix followed by a separator OR a
1999
- // digit run (EXP-RC4-MD5, EXP1024-RC4-SHA), so match EXP before either -- a bare `\bEXP\b` misses EXP1024.
2000
- if (/NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/.test(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
2200
+ if (_hasWeakCipherToken(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
2001
2201
  }
2002
2202
 
2003
2203
  // A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
@@ -2130,7 +2330,7 @@ function serverkeygen(baseUrl, csrInput, opts) {
2130
2330
  var eis = opts.expectedRecipientIssuerSerial;
2131
2331
  if (typeof eis !== "object" || Buffer.isBuffer(eis) || !Buffer.isBuffer(eis.issuer)) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial must be { issuer: Buffer, serialNumber }");
2132
2332
  var s = eis.serialNumber;
2133
- if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && /^[0-9]+$/.test(s)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
2333
+ if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && pkix.allCharsIn(s, _DIGITS_TABLE)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
2134
2334
  }
2135
2335
  // A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
2136
2336
  // key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key
@@ -28,9 +28,23 @@
28
28
  * classes the toolkit throws.
29
29
  */
30
30
 
31
- // The frozen shape of every error code: a `domain/reason` string of
32
- // lowercase alphanumerics and dashes.
33
- var CODE_SHAPE = /^[a-z0-9-]+\/[a-z0-9-]+$/;
31
+ var intrinsic = require("./guard-intrinsic");
32
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
33
+ var _String = intrinsic.String;
34
+ var _defineProperty = intrinsic.defineProperty;
35
+
36
+ // The frozen shape of every error code: a `domain/reason` string -- one or more lowercase alphanumerics or
37
+ // dashes, a single "/", then one or more of the same. Decided by character scan (no regex).
38
+ function _isCodeShape(c) {
39
+ var n = c.length, i, ch, slash = -1;
40
+ if (n < 3) return false; // shortest is "a/b"
41
+ for (i = 0; i < n; i += 1) {
42
+ ch = _charCodeAt(c, i);
43
+ if (ch === 47) { if (slash !== -1) return false; slash = i; } // "/" -- at most one
44
+ else if (!((ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57) || ch === 45)) return false; // a-z 0-9 "-"
45
+ }
46
+ return slash > 0 && slash < n - 1; // one slash, both sides non-empty
47
+ }
34
48
 
35
49
  /**
36
50
  * @primitive pki.errors.PkiError
@@ -59,9 +73,9 @@ class PkiError extends Error {
59
73
  super(message);
60
74
  this.name = "PkiError";
61
75
  var c = code || "pki/invalid";
62
- if (typeof c !== "string" || !CODE_SHAPE.test(c)) {
76
+ if (typeof c !== "string" || !_isCodeShape(c)) {
63
77
  throw new TypeError(
64
- "PkiError code must be a domain/reason string, got " + String(c) +
78
+ "PkiError code must be a domain/reason string, got " + _String(c) +
65
79
  " (PkiError takes (message, code); defineClass subclasses take (code, message))"
66
80
  );
67
81
  }
@@ -114,7 +128,7 @@ function defineClass(name, opts) {
114
128
  }
115
129
  }
116
130
  };
117
- Object.defineProperty(GeneratedError, "name", { value: name, configurable: true });
131
+ _defineProperty(GeneratedError, "name", { value: name, configurable: true });
118
132
  GeneratedError.factory = function (code, message, arg3) {
119
133
  return new GeneratedError(code, message, arg3);
120
134
  };
@@ -41,7 +41,7 @@ var _stringify = intrinsic.stringify;
41
41
  var _forEach = intrinsic.forEach;
42
42
  var _map = intrinsic.map;
43
43
  var _filter = intrinsic.filter;
44
- var _reTest = intrinsic.uncurry(RegExp.prototype.test);
44
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
45
45
  var _setForEach = intrinsic.uncurry(Set.prototype.forEach);
46
46
  var _mapForEach = intrinsic.uncurry(Map.prototype.forEach);
47
47
  var _String = intrinsic.String;
@@ -151,10 +151,26 @@ function _reView(v) {
151
151
  // The two string operations in this table run AT LOAD, in the same statement that reads
152
152
  // `util.types` once and for the same reason. Nothing a caller can replace has run yet, so there is
153
153
  // no live read to remove; capturing them would only move the question one line earlier.
154
+ // A util.types predicate that names a concrete typed-array kind: "is" + one-or-more alphanumerics +
155
+ // "Array" (e.g. "isUint8Array"), decided character by character so no regex match protocol -- which
156
+ // re-dispatches through a replaceable `exec` -- stands between this table and the runtime it reads.
157
+ function _isTypedArrayPredName(name) {
158
+ var n = name.length;
159
+ if (n < 8) return false; // "is" + >=1 middle char + "Array"
160
+ if (_charCodeAt(name, 0) !== 105 || _charCodeAt(name, 1) !== 115) return false; // "is"
161
+ var tail = "Array";
162
+ for (var k = 0; k < 5; k++) { if (_charCodeAt(name, n - 5 + k) !== _charCodeAt(tail, k)) return false; }
163
+ for (var i = 2; i < n - 5; i++) {
164
+ var c = _charCodeAt(name, i);
165
+ if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))) return false;
166
+ }
167
+ return true;
168
+ }
169
+
154
170
  var _CONCRETE_KINDS = _filter(
155
171
  _map(
156
172
  // allow:guard-reads-runtime-live -- module-init, see above
157
- _filter(_objectKeys(util.types), function (name) { return _reTest(/^is[A-Za-z0-9]+Array$/, name); }),
173
+ _filter(_objectKeys(util.types), function (name) { return _isTypedArrayPredName(name); }),
158
174
  // allow:guard-reads-runtime-live -- module-init, see above
159
175
  function (name) { return [util.types[name], globalThis[name.slice(2)]]; }
160
176
  ),
@@ -188,7 +204,12 @@ function _kindName(v) {
188
204
  return typeof v === "function" ? "function" : "value";
189
205
  }
190
206
 
191
- function _article(name) { return _reTest(/^[AEIOU]/i, name) ? "an" : "a"; }
207
+ function _article(name) {
208
+ var c = name.length > 0 ? _charCodeAt(name, 0) : 0;
209
+ var vowel = c === 65 || c === 69 || c === 73 || c === 79 || c === 85 ||
210
+ c === 97 || c === 101 || c === 105 || c === 111 || c === 117; // A E I O U, either case
211
+ return vowel ? "an" : "a";
212
+ }
192
213
 
193
214
  // outputView(input, ErrorClass, code, label) -> a Buffer aliasing a caller's view, for WRITING.
194
215
  //