@blamejs/pki 0.5.31 → 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 (51) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +2 -2
  3. package/lib/acme.js +347 -90
  4. package/lib/asn1-der.js +93 -33
  5. package/lib/cmc-build.js +19 -8
  6. package/lib/cmc-verify.js +4 -4
  7. package/lib/cmp-build.js +36 -6
  8. package/lib/cmp-session.js +5 -5
  9. package/lib/cmp-verify.js +86 -16
  10. package/lib/cms-compress.js +2 -2
  11. package/lib/cms-decrypt.js +3 -3
  12. package/lib/cms-encrypt.js +1 -1
  13. package/lib/cms-sign.js +7 -7
  14. package/lib/cms-verify.js +9 -4
  15. package/lib/crl-sign.js +4 -4
  16. package/lib/crmf-sign.js +2 -1
  17. package/lib/csr-sign.js +1 -4
  18. package/lib/ct.js +8 -4
  19. package/lib/est.js +234 -34
  20. package/lib/framework-error.js +20 -6
  21. package/lib/guard-bytes.js +24 -3
  22. package/lib/http-digest.js +69 -16
  23. package/lib/http-retry-after.js +68 -24
  24. package/lib/http-transport.js +60 -6
  25. package/lib/inspect.js +13 -2
  26. package/lib/key.js +2 -2
  27. package/lib/lint.js +1 -1
  28. package/lib/mime.js +27 -4
  29. package/lib/ocsp.js +7 -7
  30. package/lib/oid.js +16 -0
  31. package/lib/path-validate.js +76 -18
  32. package/lib/pbes2.js +2 -2
  33. package/lib/pkcs12-build.js +2 -2
  34. package/lib/pki-build.js +6 -6
  35. package/lib/rfc3339.js +75 -17
  36. package/lib/schema-c509.js +36 -9
  37. package/lib/schema-cmc.js +3 -3
  38. package/lib/schema-pkix.js +174 -25
  39. package/lib/schema-tsp.js +10 -1
  40. package/lib/sign-scheme.js +1 -1
  41. package/lib/sigstore.js +52 -9
  42. package/lib/smime.js +86 -6
  43. package/lib/trust.js +114 -14
  44. package/lib/tsp-sign.js +39 -27
  45. package/lib/validator-tpm.js +2 -2
  46. package/lib/webauthn-mds.js +24 -6
  47. package/lib/webauthn.js +3 -3
  48. package/lib/webcrypto.js +1 -1
  49. package/lib/x509-sign.js +1 -1
  50. package/package.json +1 -1
  51. package/sbom.cdx.json +6 -6
@@ -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
  //
@@ -19,6 +19,10 @@ var intrinsic = require("./guard-intrinsic");
19
19
  // replaceable properties to answer one thing, and here it decides whether an auth-param repeats --
20
20
  // answering false lets the second copy of a nonce overwrite the first.
21
21
  var _hasOwn = intrinsic.hasOwn;
22
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
23
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
24
+ var _fromCharCode = intrinsic.fromCharCode;
25
+ var pkix = require("./schema-pkix"); // the shared JS-\s code-point predicate (pkix.isJsWhitespace)
22
26
 
23
27
  // Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
24
28
  // "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
@@ -41,7 +45,12 @@ var DEFAULT_CODES = {
41
45
  // is hashed over its exact bytes. KD(secret, data) = H(secret ":" data).
42
46
  function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest("hex"); }
43
47
  function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
44
- function _qstr(v) { return "\"" + String(v).replace(/(["\\])/g, "\\$1") + "\""; }
48
+ // Quote a value: wrap in double quotes, backslash-escaping any " or \ (matching /(["\\])/g -> "\\$1").
49
+ function _qstr(v) {
50
+ var s = String(v), out = "";
51
+ for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 34 || c === 92) out += "\\"; out += _fromCharCode(c); }
52
+ return "\"" + out + "\"";
53
+ }
45
54
 
46
55
  // Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
47
56
  function _commaSplitOutsideQuotes(s) {
@@ -69,7 +78,13 @@ function _firstEqOutsideQuotes(s) {
69
78
  }
70
79
  return -1;
71
80
  }
72
- function _unq(s) { return s.slice(1, -1).replace(/\\(.)/g, "$1"); } // only ever called on a well-formed quoted-string (see _closedQuote)
81
+ // Unquote a well-formed quoted-string (only ever called after _closedQuote): drop the surrounding
82
+ // quotes and each escaping backslash (matching s.slice(1,-1).replace(/\\(.)/g, "$1")).
83
+ function _unq(s) {
84
+ var out = "", last = 1, n = s.length; // skip the opening quote at 0
85
+ for (var i = 1; i < n - 1; i++) { if (_charCodeAt(s, i) === 92) { out += _strSlice(s, last, i); last = i + 1; i += 1; } }
86
+ return out + _strSlice(s, last, n - 1); // trailing content, excluding the closing quote
87
+ }
73
88
  // A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
74
89
  // it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
75
90
  function _closedQuote(s) {
@@ -86,24 +101,62 @@ function _closedQuote(s) {
86
101
  // A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
87
102
  // instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
88
103
  function _hasCtl(s) {
89
- for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
104
+ for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
90
105
  return false;
91
106
  }
92
107
 
93
108
  // Split a WWW-Authenticate header into [{ scheme, paramText }]. A comma-separated segment whose pre-'='
94
109
  // text is "token WS token" (a scheme name before the first auth-param key), or a bare token with no '=',
95
110
  // STARTS a new challenge; a "key=value" segment is a param continuation of the current challenge.
111
+ // The leading token when `pre` is "token WS more" (/^(\S+)\s+(\S[\s\S]*)$/) -- a scheme name before the
112
+ // first auth-param -- else null. \s is the shared JS-\s predicate (RFC 7230 OWS is a subset).
113
+ function _matchTokenRest(pre) {
114
+ var n = pre.length, p = 0;
115
+ while (p < n && !pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1; // ^(\S+)
116
+ if (p === 0) return null;
117
+ var tokEnd = p;
118
+ if (p >= n || !pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // \s+
119
+ while (p < n && pkix.isJsWhitespace(_charCodeAt(pre, p))) p += 1;
120
+ if (p >= n || pkix.isJsWhitespace(_charCodeAt(pre, p))) return null; // (\S[\s\S]*)$ -- at least one non-\s
121
+ return _strSlice(pre, 0, tokEnd);
122
+ }
123
+ // The index past a leading "OWS token OWS" (/^\s*\S+\s+/), or 0 when the string does not begin that way.
124
+ // The caller slices from this index (the slice that replaces the removed .replace).
125
+ function _stripTokenIndex(seg) {
126
+ var n = seg.length, p = 0;
127
+ while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // ^\s*
128
+ var ts = p;
129
+ while (p < n && !pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \S+
130
+ if (p === ts) return 0;
131
+ var ws = p;
132
+ while (p < n && pkix.isJsWhitespace(_charCodeAt(seg, p))) p += 1; // \s+ (required)
133
+ return p === ws ? 0 : p;
134
+ }
135
+ // String.prototype.split(/\s+/): the pieces between \s runs (a leading/trailing run yields a "" element).
136
+ function _splitWhitespace(s) {
137
+ var out = [], i = 0, n = s.length, ps = 0;
138
+ while (i < n) {
139
+ if (pkix.isJsWhitespace(_charCodeAt(s, i))) {
140
+ out[out.length] = _strSlice(s, ps, i);
141
+ while (i < n && pkix.isJsWhitespace(_charCodeAt(s, i))) i += 1;
142
+ ps = i;
143
+ } else i += 1;
144
+ }
145
+ out[out.length] = _strSlice(s, ps);
146
+ return out;
147
+ }
148
+
96
149
  function _splitChallenges(s) {
97
150
  var segs = _commaSplitOutsideQuotes(s);
98
151
  var out = [], cur = null;
99
152
  for (var i = 0; i < segs.length; i++) {
100
- var seg = segs[i].replace(/^\s+|\s+$/g, "");
153
+ var seg = segs[i].trim();
101
154
  if (seg === "") continue;
102
155
  var eq = _firstEqOutsideQuotes(seg);
103
- var pre = (eq < 0 ? seg : seg.slice(0, eq)).replace(/^\s+|\s+$/g, "");
104
- var ws = /^(\S+)\s+(\S[\s\S]*)$/.exec(pre);
156
+ var pre = (eq < 0 ? seg : _strSlice(seg, 0, eq)).trim();
157
+ var scheme = _matchTokenRest(pre);
105
158
  if (eq < 0) { cur = { scheme: seg, paramText: "" }; out.push(cur); } // a bare scheme token (e.g. "Basic")
106
- else if (ws) { cur = { scheme: ws[1], paramText: seg.replace(/^\s*\S+\s+/, "") }; out.push(cur); } // "scheme firstKey=..."
159
+ else if (scheme !== null) { cur = { scheme: scheme, paramText: _strSlice(seg, _stripTokenIndex(seg)) }; out.push(cur); } // "scheme firstKey=..."
107
160
  else if (cur) { cur.paramText = cur.paramText ? cur.paramText + "," + seg : seg; } // "key=value" continuation
108
161
  }
109
162
  return out;
@@ -112,12 +165,12 @@ function _splitChallenges(s) {
112
165
  function _parseParams(paramText, E, code) {
113
166
  var segs = _commaSplitOutsideQuotes(paramText), map = Object.create(null);
114
167
  for (var i = 0; i < segs.length; i++) {
115
- var seg = segs[i].replace(/^\s+|\s+$/g, "");
168
+ var seg = segs[i].trim();
116
169
  if (seg === "") continue;
117
170
  var eq = _firstEqOutsideQuotes(seg);
118
171
  if (eq < 0) throw E(code, "malformed Digest auth-param (no '='): " + JSON.stringify(seg));
119
- var key = seg.slice(0, eq).replace(/^\s+|\s+$/g, "").toLowerCase();
120
- var rawVal = seg.slice(eq + 1).replace(/^\s+|\s+$/g, "");
172
+ var key = _strSlice(seg, 0, eq).trim().toLowerCase();
173
+ var rawVal = _strSlice(seg, eq + 1).trim();
121
174
  var quoted = rawVal.charAt(0) === "\"";
122
175
  if (quoted && !_closedQuote(rawVal)) throw E(code, "an unterminated or trailing-garbage Digest quoted-string (RFC 7230 sec. 3.2.6)");
123
176
  if (_hasOwn(map, key)) throw E(code, "repeated Digest auth-param " + JSON.stringify(key));
@@ -138,7 +191,7 @@ function _validateDigest(paramText, E, code) {
138
191
  var qop = [];
139
192
  if (p.qop) {
140
193
  if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
141
- qop = p.qop.value.split(",").map(function (x) { return x.replace(/^\s+|\s+$/g, "").toLowerCase(); }).filter(Boolean);
194
+ qop = p.qop.value.split(",").map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
142
195
  // A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
143
196
  // absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
144
197
  // cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
@@ -150,8 +203,8 @@ function _validateDigest(paramText, E, code) {
150
203
  // server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
151
204
  // the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
152
205
  if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
153
- var domain = (p.domain && p.domain.value.replace(/^\s+|\s+$/g, "") !== "")
154
- ? p.domain.value.replace(/^\s+|\s+$/g, "").split(/\s+/) : null;
206
+ var domain = (p.domain && p.domain.value.trim() !== "")
207
+ ? _splitWhitespace(p.domain.value.trim()) : null;
155
208
  // charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
156
209
  // other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
157
210
  // rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
@@ -250,7 +303,7 @@ function _octets(s, charset) {
250
303
  // Does the string carry any non-ASCII code unit (> U+007F)? Such a UTF-8 username is sent via `username*`.
251
304
  function _hasNonAscii(s) {
252
305
  s = String(s == null ? "" : s);
253
- for (var i = 0; i < s.length; i++) { if (s.charCodeAt(i) > 0x7F) return true; }
306
+ for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) > 0x7F) return true; }
254
307
  return false;
255
308
  }
256
309
 
@@ -264,7 +317,7 @@ function _pctEncodeUtf8(s) {
264
317
  if ((b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A) ||
265
318
  b === 0x21 || b === 0x23 || b === 0x24 || b === 0x26 || b === 0x2B || b === 0x2D ||
266
319
  b === 0x2E || b === 0x5E || b === 0x5F || b === 0x60 || b === 0x7C || b === 0x7E) {
267
- out += String.fromCharCode(b);
320
+ out += _fromCharCode(b);
268
321
  } else {
269
322
  out += "%" + (b < 0x10 ? "0" : "") + b.toString(16).toUpperCase();
270
323
  }
@@ -323,7 +376,7 @@ function answer(challenge, params, E) {
323
376
  // (a same-origin redirect, an auth-int retry) increments nc; a fresh nonce restarts at 1. The caller supplies
324
377
  // the count for this nonce (default 1); formatted as 8 lowercase hex.
325
378
  var ncNum = (typeof params.nc === "number" && params.nc >= 1) ? Math.floor(params.nc) : 1;
326
- var nc = ("0000000" + ncNum.toString(16)).slice(-8);
379
+ var nc = _strSlice("0000000" + ncNum.toString(16), -8);
327
380
  var response = useQop
328
381
  ? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
329
382
  : KD(alg.hash, HA1, nonce + ":" + HA2);
@@ -19,12 +19,32 @@ var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(
19
19
  // rfc850-date / asctime-date a recipient must still accept. Gating Date.parse on this grammar keeps its
20
20
  // permissiveness (an ISO string like "2026-07-10") from passing as a valid Retry-After header.
21
21
  var MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
22
- var MONTH = "(?:" + MONTHS.join("|") + ")";
23
- var HTTP_DATE_FORMS = [
24
- new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} " + MONTH + " \\d{4} \\d{2}:\\d{2}:\\d{2} GMT$"),
25
- new RegExp("^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-" + MONTH + "-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT$"),
26
- new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) " + MONTH + " [ \\d]\\d \\d{2}:\\d{2}:\\d{2} \\d{4}$"),
27
- ];
22
+ var DOW_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
23
+ var DOW_LONG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
24
+
25
+ // The grammar is scanned character by character (no regex): each form is a fixed layout of a
26
+ // day-of-week name, delimiters, an all-digit field, a three-letter month name, and an HH:MM:SS time.
27
+ function _isDigit(ch) { return ch >= "0" && ch <= "9"; }
28
+ // The two-digit / four-digit integer at `i`, or -1 when those positions are not all digits.
29
+ function _digits2(s, i) { return _isDigit(s.charAt(i)) && _isDigit(s.charAt(i + 1)) ? (s.charCodeAt(i) - 48) * 10 + (s.charCodeAt(i + 1) - 48) : -1; }
30
+ function _digits4(s, i) { for (var k = 0; k < 4; k++) if (!_isDigit(s.charAt(i + k))) return -1; return parseInt(s.slice(i, i + 4), 10); }
31
+ // The length of a name from `list` matching `s` at `i` (the day-of-week alternation), or -1.
32
+ function _matchName(s, i, list) { for (var j = 0; j < list.length; j++) { if (s.slice(i, i + list[j].length) === list[j]) return list[j].length; } return -1; }
33
+ // The three-letter month at `i` when it is a real month name, else null.
34
+ function _monthAt(s, i) { return _matchName(s, i, MONTHS) === -1 ? null : s.slice(i, i + 3); }
35
+ // An "HH:MM:SS" time at `i` -> { hh, mi, ss, next } (next = i + 8), or null.
36
+ function _timeAt(s, i) {
37
+ var hh = _digits2(s, i); if (hh < 0 || s.charAt(i + 2) !== ":") return null;
38
+ var mi = _digits2(s, i + 3); if (mi < 0 || s.charAt(i + 5) !== ":") return null;
39
+ var ss = _digits2(s, i + 6); if (ss < 0) return null;
40
+ return { hh: hh, mi: mi, ss: ss, next: i + 8 };
41
+ }
42
+ // A non-empty run of ASCII digits (matching /^\d+$/, the delay-seconds form).
43
+ function _isAllDigits(s) {
44
+ if (s.length === 0) return false;
45
+ for (var i = 0; i < s.length; i++) if (!_isDigit(s.charAt(i))) return false;
46
+ return true;
47
+ }
28
48
 
29
49
  // Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
30
50
  // year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
@@ -40,29 +60,53 @@ function _rfc850Year(yy, refMs) {
40
60
  return full;
41
61
  }
42
62
 
63
+ // IMF-fixdate (the required form): "Sun, 06 Nov 1994 08:49:37 GMT".
64
+ function _scanImf(s) {
65
+ var p = _matchName(s, 0, DOW_SHORT);
66
+ if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
67
+ var dd = _digits2(s, p); if (dd < 0 || s.charAt(p + 2) !== " ") return null; p += 3;
68
+ var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== " ") return null; p += 4;
69
+ var yy = _digits4(s, p); if (yy < 0 || s.charAt(p + 4) !== " ") return null; p += 5;
70
+ var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
71
+ return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
72
+ }
73
+ // rfc850-date (obsolete, still accepted): "Sunday, 06-Nov-94 08:49:37 GMT" -- a two-digit year expanded
74
+ // relative to the receipt year.
75
+ function _scanRfc850(s, refMs) {
76
+ var p = _matchName(s, 0, DOW_LONG);
77
+ if (p === -1 || s.slice(p, p + 2) !== ", ") return null; p += 2;
78
+ var dd = _digits2(s, p); if (dd < 0 || s.charAt(p + 2) !== "-") return null; p += 3;
79
+ var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== "-") return null; p += 4;
80
+ var yy = _digits2(s, p); if (yy < 0 || s.charAt(p + 2) !== " ") return null; p += 3;
81
+ var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
82
+ return { day: dd, monName: mo, year: _rfc850Year(yy, refMs), hh: t.hh, mi: t.mi, ss: t.ss };
83
+ }
84
+ // asctime-date (obsolete, still accepted): "Sun Nov 6 08:49:37 1994" -- a space-padded one/two-digit day.
85
+ function _scanAsctime(s) {
86
+ var p = _matchName(s, 0, DOW_SHORT);
87
+ if (p === -1 || s.charAt(p) !== " ") return null; p += 1;
88
+ var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== " ") return null; p += 4;
89
+ var c0 = s.charAt(p);
90
+ if (!(c0 === " " || _isDigit(c0)) || !_isDigit(s.charAt(p + 1))) return null;
91
+ var dd = parseInt(s.slice(p, p + 2), 10); if (s.charAt(p + 2) !== " ") return null; p += 3;
92
+ var t = _timeAt(s, p); if (!t || s.charAt(t.next) !== " ") return null;
93
+ var yy = _digits4(s, t.next + 1); if (yy < 0 || t.next + 1 + 4 !== s.length) return null;
94
+ return { day: dd, monName: mo, year: yy, hh: t.hh, mi: t.mi, ss: t.ss };
95
+ }
96
+
43
97
  // Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
44
98
  // extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
45
99
  // no GMT token so delegating would parse it in local time, and the round-trip check rejects an
46
100
  // impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
47
101
  function httpDateMs(s, refMs) {
48
- if (!HTTP_DATE_FORMS.some(function (re) { return re.test(s); })) return NaN;
49
- var day, monName, year;
50
- var asc = /^\w{3} (\w{3}) ([ \d]\d) /.exec(s); // asctime: month then day
51
- if (asc) { monName = asc[1]; day = parseInt(asc[2], 10); year = parseInt(/(\d{4})$/.exec(s)[1], 10); }
52
- else {
53
- var dmy = /(\d{2})[ -](\w{3})[ -](\d{2,4})/.exec(s); // IMF / rfc850: day month year
54
- if (!dmy) return NaN;
55
- day = parseInt(dmy[1], 10); monName = dmy[2]; year = parseInt(dmy[3], 10);
56
- if (year < 100) year = _rfc850Year(year, refMs); // rfc850 two-digit year
57
- }
58
- var t = /(\d{2}):(\d{2}):(\d{2})/.exec(s);
59
- var mon = MONTHS.indexOf(monName);
60
- if (!t || mon < 0) return NaN;
61
- var hh = parseInt(t[1], 10), mi = parseInt(t[2], 10), ss = parseInt(t[3], 10);
62
- var when = Date.UTC(year, mon, day, hh, mi, ss);
102
+ var f = _scanImf(s) || _scanRfc850(s, refMs) || _scanAsctime(s);
103
+ if (!f) return NaN;
104
+ var mon = MONTHS.indexOf(f.monName);
105
+ if (mon < 0) return NaN;
106
+ var when = Date.UTC(f.year, mon, f.day, f.hh, f.mi, f.ss);
63
107
  var d = new Date(when);
64
- if (d.getUTCDate() !== day || d.getUTCMonth() !== mon || d.getUTCFullYear() !== year ||
65
- d.getUTCHours() !== hh || d.getUTCMinutes() !== mi || d.getUTCSeconds() !== ss) return NaN;
108
+ if (d.getUTCDate() !== f.day || d.getUTCMonth() !== mon || d.getUTCFullYear() !== f.year ||
109
+ d.getUTCHours() !== f.hh || d.getUTCMinutes() !== f.mi || d.getUTCSeconds() !== f.ss) return NaN;
66
110
  return when;
67
111
  }
68
112
 
@@ -78,7 +122,7 @@ function parse(value, opts) {
78
122
  opts = opts || {};
79
123
  var raStr = String(value).trim();
80
124
  var out = { retryAfterSeconds: null, retryAfterDate: null };
81
- if (/^\d+$/.test(raStr)) {
125
+ if (_isAllDigits(raStr)) {
82
126
  var n = parseInt(raStr, 10);
83
127
  // opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
84
128
  // ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
@@ -69,14 +69,38 @@ function _budget(value, key, dflt, max, E, code) {
69
69
  // form (e.g. what pki.est.cacerts / parseCertsOnly return), is silently ignored there, so the
70
70
  // connection would fail server authentication despite a valid anchor. Wrap a DER anchor as PEM
71
71
  // (a Buffer already carrying PEM text, or a PEM string, passes through unchanged).
72
+ // The JS regex \s code points representable in a latin1 string (0..255): TAB LF VT FF CR SP NBSP.
73
+ // Built at load so the source stays pure ASCII. The armor scan runs over a latin1 prefix, where these
74
+ // are the only \s characters that can appear, so this is exactly /^\s*/ for that input.
75
+ var _LATIN1_WS = String.fromCharCode(0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, 0xa0);
76
+
77
+ // PEM armor at the START after optional whitespace, decided by character scan (no regex): skip the
78
+ // leading \s run, then require "-----BEGIN ".
79
+ function _looksLikePemArmor(s) {
80
+ var i = 0;
81
+ while (i < s.length && _LATIN1_WS.indexOf(s.charAt(i)) !== -1) i += 1;
82
+ return s.slice(i, i + 11) === "-----BEGIN ";
83
+ }
84
+ // Base64 wrapped to <=64-column lines, each terminated by LF (matching /.{1,64}/g -> "$&\n", which
85
+ // keeps a trailing LF on the final line).
86
+ function _wrap64Lines(s) {
87
+ var out = "";
88
+ for (var i = 0; i < s.length; i += 64) out += s.slice(i, i + 64) + "\n";
89
+ return out;
90
+ }
91
+ // The surrounding "[" "]" of an IPv6 literal removed, else the string unchanged (matching /^\[(.*)\]$/).
92
+ function _stripIpv6Brackets(s) {
93
+ return (s.length >= 2 && s.charAt(0) === "[" && s.charAt(s.length - 1) === "]") ? s.slice(1, s.length - 1) : s;
94
+ }
95
+
72
96
  function _pemifyAnchor(a) {
73
97
  if (typeof a === "string") return a;
74
98
  if (Buffer.isBuffer(a)) {
75
99
  // Detect PEM by its armor at the START (after optional whitespace) only: a DER certificate begins
76
100
  // with the SEQUENCE tag 0x30 and may itself contain the bytes "-----BEGIN" in a subject / extension
77
101
  // value, so an anywhere-search would leave such a DER anchor unwrapped and unusable.
78
- if (/^\s*-----BEGIN /.test(a.toString("latin1", 0, 64))) return a;
79
- var b64 = a.toString("base64").replace(/.{1,64}/g, "$&\n");
102
+ if (_looksLikePemArmor(a.toString("latin1", 0, 64))) return a;
103
+ var b64 = _wrap64Lines(a.toString("base64"));
80
104
  return "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n";
81
105
  }
82
106
  return a;
@@ -179,11 +203,41 @@ var _guardedLookup = _makeGuardedLookup(nodeDns.lookup);
179
203
  // certificate / identity / handshake failure is a server-authentication failure;
180
204
  // anything else is a generic transport error. Every arm threads the raw fault as
181
205
  // `.cause`, so the diagnostic survives.
206
+ // SELF.?SIGNED : "self", an optional single non-line-terminator character, then "signed" (in a lowercased string).
207
+ function _hasSelfSigned(l) {
208
+ for (var i = 0; i + 4 <= l.length; i++) {
209
+ if (l.charCodeAt(i) === 0x73 && l.charCodeAt(i + 1) === 0x65 && l.charCodeAt(i + 2) === 0x6c && l.charCodeAt(i + 3) === 0x66) {
210
+ if (l.indexOf("signed", i + 4) === i + 4) return true;
211
+ var mid = l.charCodeAt(i + 4);
212
+ if (i + 4 < l.length && mid !== 0x0a && mid !== 0x0d && mid !== 0x2028 && mid !== 0x2029 && l.indexOf("signed", i + 5) === i + 5) return true;
213
+ }
214
+ }
215
+ return false;
216
+ }
217
+ // \bWORD\b : `word` (lowercase) bounded on both sides by a non-word character ([a-z0-9_]) or a string edge.
218
+ function _hasWordBounded(l, word) {
219
+ var at = l.indexOf(word);
220
+ while (at !== -1) {
221
+ var before = at === 0 || !_isLowerWordChar(l.charCodeAt(at - 1));
222
+ var afterI = at + word.length;
223
+ var after = afterI === l.length || !_isLowerWordChar(l.charCodeAt(afterI));
224
+ if (before && after) return true;
225
+ at = l.indexOf(word, at + 1);
226
+ }
227
+ return false;
228
+ }
229
+ function _isLowerWordChar(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95; }
182
230
  function _classifyError(e, C) {
183
231
  if (e && e.pkiBlockedAddress) return C("blocked-address");
184
232
  var s = String((e && e.code) || "") + " " + String((e && e.message) || "");
185
- if (/PROTOCOL_VERSION|UNSUPPORTED_PROTOCOL|VERSION_TOO_LOW|WRONG_VERSION|NO_PROTOCOLS_AVAILABLE|INAPPROPRIATE_FALLBACK/i.test(s)) return C("tls-floor");
186
- if (/CERT|SELF.?SIGNED|VERIFY|ALTNAME|HOSTNAME|DEPTH_ZERO|LOCAL_ISSUER|HANDSHAKE|\bSSL\b|\bTLS\b/i.test(s)) return C("server-auth-failed");
233
+ var l = s.toLowerCase();
234
+ // /PROTOCOL_VERSION|UNSUPPORTED_PROTOCOL|VERSION_TOO_LOW|WRONG_VERSION|NO_PROTOCOLS_AVAILABLE|INAPPROPRIATE_FALLBACK/i
235
+ if (l.indexOf("protocol_version") !== -1 || l.indexOf("unsupported_protocol") !== -1 || l.indexOf("version_too_low") !== -1 ||
236
+ l.indexOf("wrong_version") !== -1 || l.indexOf("no_protocols_available") !== -1 || l.indexOf("inappropriate_fallback") !== -1) return C("tls-floor");
237
+ // /CERT|SELF.?SIGNED|VERIFY|ALTNAME|HOSTNAME|DEPTH_ZERO|LOCAL_ISSUER|HANDSHAKE|\bSSL\b|\bTLS\b/i
238
+ if (l.indexOf("cert") !== -1 || l.indexOf("verify") !== -1 || l.indexOf("altname") !== -1 || l.indexOf("hostname") !== -1 ||
239
+ l.indexOf("depth_zero") !== -1 || l.indexOf("local_issuer") !== -1 || l.indexOf("handshake") !== -1 ||
240
+ _hasSelfSigned(l) || _hasWordBounded(l, "ssl") || _hasWordBounded(l, "tls")) return C("server-auth-failed");
187
241
  return C("transport-error");
188
242
  }
189
243
 
@@ -256,7 +310,7 @@ function httpsTransport(defaults) {
256
310
  if (minVersion !== "TLSv1.2" && minVersion !== "TLSv1.3") throw E(C("bad-input"), "tls.minVersion must be 'TLSv1.2' or 'TLSv1.3' (never below the floor), got " + minVersion);
257
311
 
258
312
  var body = request.body;
259
- if (Buffer.isBuffer(body)) body = guard.bytes.view(body, E, C("bad-input"), "the request body");
313
+ if (guard.bytes.isByteSource(body)) body = guard.bytes.source(body, E, C("bad-input"), "the request body");
260
314
 
261
315
  // Frame a body-bearing request with a fixed Content-Length computed from the EXACT body, so it is sent
262
316
  // length-delimited rather than Transfer-Encoding: chunked (node's default when a body is written with no
@@ -273,7 +327,7 @@ function httpsTransport(defaults) {
273
327
 
274
328
  // URL.hostname keeps the square brackets on an IPv6 literal ([2001:db8::1]); node's `hostname`
275
329
  // option and its IP-detection expect the bare address, so strip the brackets once here.
276
- var host = url.hostname.replace(/^\[(.*)\]$/, "$1");
330
+ var host = _stripIpv6Brackets(url.hostname);
277
331
  var options = {
278
332
  method: request.method || "GET",
279
333
  hostname: host,
package/lib/inspect.js CHANGED
@@ -121,7 +121,7 @@ function _serial(cert, indent) {
121
121
  // anything larger renders as a colon-hex block.
122
122
  if (buf.length <= 6) {
123
123
  var n = parseInt(buf.toString("hex") || "0", 16);
124
- return "Serial Number: " + n + " (0x" + (buf.toString("hex").replace(/^0+/, "") || "0") + ")";
124
+ return "Serial Number: " + n + " (0x" + (_stripLeadingZeros(buf.toString("hex")) || "0") + ")";
125
125
  }
126
126
  return "Serial Number:\n" + " ".repeat(indent) + _hexColon(buf, {});
127
127
  }
@@ -905,11 +905,22 @@ _CMS_SHAPE[oid.byName("compressedData")] = function (o) { return _isObj(o.compre
905
905
  // SEQUENCE is a real Certificate/CertificateList -> delegate to the full sub-report
906
906
  // (guarded; a one-line summary on any failure); a CONTEXT-tagged CHOICE alternative
907
907
  // (attribute certificate / other) renders a one-line tag+size summary, never parsed.
908
+ // Leading "0" characters removed, and a single trailing "\n" removed -- both by character scan
909
+ // (no regex): display helpers, matching the /^0+/ and /\n$/ replacements they succeed.
910
+ function _stripLeadingZeros(s) {
911
+ var i = 0;
912
+ while (i < s.length && s.charAt(i) === "0") i += 1;
913
+ return s.slice(i);
914
+ }
915
+ function _stripTrailingNewline(s) {
916
+ return (s.length > 0 && s.charAt(s.length - 1) === "\n") ? s.slice(0, s.length - 1) : s;
917
+ }
918
+
908
919
  function _cmsEmbedded(kind, el, pad) {
909
920
  if (el.tagClass === "universal") {
910
921
  try {
911
922
  var sub = (kind === "CRL") ? crlReport(el.bytes) : certificate(el.bytes);
912
- return pad + kind + ":\n" + sub.replace(/\n$/, "").split("\n").map(function (l) { return pad + " " + l; }).join("\n");
923
+ return pad + kind + ":\n" + _stripTrailingNewline(sub).split("\n").map(function (l) { return pad + " " + l; }).join("\n");
913
924
  } catch (_e) { /* fall through to the summary */ }
914
925
  }
915
926
  return pad + kind + " [" + el.tagClass + " " + el.tagNumber + "] (" + el.bytes.length + " bytes)";
package/lib/key.js CHANGED
@@ -441,7 +441,7 @@ function _inferAlgorithm(detected) {
441
441
  function _importUsages(name, isPublic) {
442
442
  var n = String(name == null ? "" : name).toUpperCase();
443
443
  if (n === "X25519" || n === "X448" || n === "ECDH") return isPublic ? [] : ["deriveBits", "deriveKey"];
444
- if (/^ML-KEM/.test(n)) return isPublic ? ["encapsulateBits"] : ["decapsulateBits"];
444
+ if (n.indexOf("ML-KEM") === 0) return isPublic ? ["encapsulateBits"] : ["decapsulateBits"];
445
445
  if (n === "RSA-OAEP") return isPublic ? ["encrypt"] : ["decrypt"];
446
446
  return isPublic ? ["verify"] : ["sign"]; // signing default (Ed / ECDSA / RSA-* sign / ML-DSA / SLH-DSA)
447
447
  }
@@ -449,7 +449,7 @@ function _importUsages(name, isPublic) {
449
449
  function _generateUsages(name) {
450
450
  var n = String(name == null ? "" : name).toUpperCase();
451
451
  if (n === "X25519" || n === "X448" || n === "ECDH") return ["deriveBits", "deriveKey"];
452
- if (/^ML-KEM/.test(n)) return ["encapsulateBits", "decapsulateBits"];
452
+ if (n.indexOf("ML-KEM") === 0) return ["encapsulateBits", "decapsulateBits"];
453
453
  if (n === "RSA-OAEP") return ["encrypt", "decrypt"];
454
454
  return ["sign", "verify"];
455
455
  }
package/lib/lint.js CHANGED
@@ -97,7 +97,7 @@ function _finding(rule, detail) {
97
97
  // would be the check computed and thrown away. Never throws: _ingest's contract is to return a
98
98
  // finding for bad DATA and reserve throws for a wrong-TYPE argument.
99
99
  function _derivedCert(o) {
100
- if (!o || typeof o !== "object" || Buffer.isBuffer(o) || ArrayBuffer.isView(o) || o instanceof ArrayBuffer) return null;
100
+ if (!o || typeof o !== "object" || guard.bytes.isByteSource(o)) return null;
101
101
  try {
102
102
  var p = guard.parsed.acceptDerived(o, "certificate", x509.parse, _cfg, "lint/bad-input", "the certificate");
103
103
  return guard.parsed.isCert(p) ? p : null;
package/lib/mime.js CHANGED
@@ -36,15 +36,38 @@ function _splitPoint(bytes) {
36
36
  return null;
37
37
  }
38
38
 
39
+ // Line-ending + whitespace helpers, decided character by character (no regex): _toLf collapses any
40
+ // CRLF / bare CR / bare LF to a single LF; _toCrlf then expands every LF to CRLF; _stripLeadingHtab
41
+ // drops a leading run of SP/HTAB. They match the /\r\n/g, /\r/g, /\n/g and /^[ \t]+/ replacements.
42
+ function _toLf(s) {
43
+ var out = "";
44
+ for (var i = 0; i < s.length; i++) {
45
+ var c = s.charAt(i);
46
+ if (c === "\r") { if (s.charAt(i + 1) === "\n") continue; out += "\n"; } // CRLF -> its LF next; bare CR -> LF
47
+ else out += c;
48
+ }
49
+ return out;
50
+ }
51
+ function _toCrlf(s) {
52
+ var lf = _toLf(s), out = "";
53
+ for (var i = 0; i < lf.length; i++) { var c = lf.charAt(i); out += (c === "\n") ? "\r\n" : c; }
54
+ return out;
55
+ }
56
+ function _stripLeadingHtab(s) {
57
+ var i = 0;
58
+ while (i < s.length && (s.charAt(i) === " " || s.charAt(i) === "\t")) i += 1;
59
+ return s.slice(i);
60
+ }
61
+
39
62
  // Split a header block into unfolded logical lines (RFC 5322 folding: a continuation line begins with
40
63
  // SP/HTAB and joins the previous). Returns the raw header strings (each "Name: value").
41
64
  function _unfoldHeaders(headerText) {
42
- var lines = headerText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
65
+ var lines = _toLf(headerText).split("\n");
43
66
  var out = [];
44
67
  for (var i = 0; i < lines.length; i++) {
45
68
  var ln = lines[i];
46
69
  if (ln === "") continue;
47
- if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + ln.replace(/^[ \t]+/, "");
70
+ if ((ln[0] === " " || ln[0] === "\t") && out.length) out[out.length - 1] += " " + _stripLeadingHtab(ln);
48
71
  else out.push(ln);
49
72
  }
50
73
  return out;
@@ -130,7 +153,7 @@ function parse(input, E, code) {
130
153
  // with only the single conventional ": " delimiter space removed. Leading/trailing whitespace inside
131
154
  // the body is preserved, so a consumer that must reproduce the exact authenticated field body (S/MIME
132
155
  // header protection) is not handed a lossily-trimmed value.
133
- headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.replace(/^ /, "") });
156
+ headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.charAt(0) === " " ? body.slice(1) : body });
134
157
  }
135
158
  function header(name) { var l = name.toLowerCase(); for (var j = 0; j < headers.length; j++) if (headers[j].lname === l) return headers[j].value; return null; }
136
159
  var ctv = header("content-type");
@@ -146,7 +169,7 @@ function parse(input, E, code) {
146
169
  // Normalize a text body's line endings to the RFC 8551 sec. 3.1.1 canonical CRLF pair (any bare CR or
147
170
  // LF becomes CRLF; an existing CRLF is preserved).
148
171
  function canonicalizeText(bodyBytes) {
149
- var s = bodyBytes.toString("latin1").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n");
172
+ var s = _toCrlf(bodyBytes.toString("latin1"));
150
173
  return Buffer.from(s, "latin1");
151
174
  }
152
175