@blamejs/pki 0.5.31 → 0.5.33

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 (52) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -3
  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 +214 -93
  14. package/lib/cms-verify.js +363 -237
  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 +56 -4
  22. package/lib/guard-intrinsic.js +13 -0
  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/key.js +2 -2
  28. package/lib/lint.js +1 -1
  29. package/lib/mime.js +27 -4
  30. package/lib/ocsp.js +7 -7
  31. package/lib/oid.js +16 -0
  32. package/lib/path-validate.js +76 -18
  33. package/lib/pbes2.js +2 -2
  34. package/lib/pkcs12-build.js +2 -2
  35. package/lib/pki-build.js +6 -6
  36. package/lib/rfc3339.js +75 -17
  37. package/lib/schema-c509.js +36 -9
  38. package/lib/schema-cmc.js +3 -3
  39. package/lib/schema-pkix.js +174 -25
  40. package/lib/schema-tsp.js +10 -1
  41. package/lib/sign-scheme.js +1 -1
  42. package/lib/sigstore.js +52 -9
  43. package/lib/smime.js +86 -6
  44. package/lib/trust.js +114 -14
  45. package/lib/tsp-sign.js +39 -27
  46. package/lib/validator-tpm.js +2 -2
  47. package/lib/webauthn-mds.js +24 -6
  48. package/lib/webauthn.js +3 -3
  49. package/lib/webcrypto.js +80 -1
  50. package/lib/x509-sign.js +1 -1
  51. package/package.json +2 -2
  52. package/sbom.cdx.json +6 -6
package/lib/schema-cmc.js CHANGED
@@ -685,7 +685,7 @@ function _finishBody(body, where) {
685
685
  * d.controls.length; // 0 -- present and empty, which is legal
686
686
  */
687
687
  function parsePkiData(input) {
688
- var root = asn1.decode(Buffer.isBuffer(input) ? input : Buffer.from(input));
688
+ var root = asn1.decode(guard.bytes.isByteSource(input) ? guard.bytes.source(input, E, "cmc/bad-input", "the CMC DER") : Buffer.from(input));
689
689
  var body = schema.walk(PKI_DATA, root, NS).result;
690
690
  body.bytes = root.bytes;
691
691
  return _finishBody(body, "pkiData");
@@ -711,7 +711,7 @@ function parsePkiData(input) {
711
711
  * r.statuses.length; // 0 -- no status control means success is assumed
712
712
  */
713
713
  function parsePkiResponse(input) {
714
- var root = asn1.decode(Buffer.isBuffer(input) ? input : Buffer.from(input));
714
+ var root = asn1.decode(guard.bytes.isByteSource(input) ? guard.bytes.source(input, E, "cmc/bad-input", "the CMC DER") : Buffer.from(input));
715
715
  var body = schema.walk(PKI_RESPONSE, root, NS).result;
716
716
  body.bytes = root.bytes;
717
717
  return _finishBody(body, "pkiResponse");
@@ -752,7 +752,7 @@ function parse(input) {
752
752
  // encapContentInfo is the field this function goes on to read, so requiring it
753
753
  // is the same question the code below already asks.
754
754
  var parsedCms = (input && typeof input === "object" && !ArrayBuffer.isView(input) &&
755
- !(input instanceof ArrayBuffer) && input.encapContentInfo != null)
755
+ !guard.bytes.isByteSource(input) && input.encapContentInfo != null)
756
756
  ? input : cms.parse(input);
757
757
  // The CARRIER is part of the identity, not just the encapsulated content type.
758
758
  // RFC 5272 sec. 3.2 / 4.2 admit a SignedData or an AuthenticatedData; other CMS
@@ -42,10 +42,8 @@ var _strIndexOf = intrinsic.stringIndexOf;
42
42
  var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
43
43
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
44
44
  var _charAt = intrinsic.uncurry(String.prototype.charAt);
45
- var _strReplace = intrinsic.uncurry(String.prototype.replace);
45
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
46
46
  var _split = intrinsic.uncurry(String.prototype.split);
47
- var _reExec = intrinsic.uncurry(RegExp.prototype.exec);
48
- var _reTest = intrinsic.uncurry(RegExp.prototype.test);
49
47
  var _create = intrinsic.create;
50
48
  var _isUint8Array = intrinsic.types.isUint8Array;
51
49
  var _bigIntToString = intrinsic.bigIntToString;
@@ -54,12 +52,96 @@ var _weakSet = intrinsic.weakSet;
54
52
  var _WeakMapI = intrinsic.WeakMap;
55
53
  var _arrayFrom = Array.from;
56
54
  var _getUTCFullYear = intrinsic.uncurry(Date.prototype.getUTCFullYear);
55
+
56
+ // The JavaScript regex `\s` class, decided by code point, so the whitespace tests below match
57
+ // exactly what `/\s/` and `/\S/` matched with no caller-replaceable match protocol standing in for
58
+ // them. Set = ECMAScript WhiteSpace + LineTerminator: TAB LF VT FF CR SP, U+00A0, U+1680,
59
+ // U+2000..U+200A, U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF.
60
+ function _isJsWhitespace(c) {
61
+ return c === 0x09 || c === 0x0a || c === 0x0b || c === 0x0c || c === 0x0d || c === 0x20 ||
62
+ c === 0xa0 || c === 0x1680 || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 ||
63
+ c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;
64
+ }
65
+ function _hasWhitespace(s) {
66
+ for (var i = 0; i < s.length; i++) { if (_isJsWhitespace(_charCodeAt(s, i))) return true; }
67
+ return false;
68
+ }
69
+ function _hasNonWhitespace(s) {
70
+ for (var i = 0; i < s.length; i++) { if (!_isJsWhitespace(_charCodeAt(s, i))) return true; }
71
+ return false;
72
+ }
73
+ // Charset validation by character scan (the shared home for the `/^[chars]+$/`-family regexes format
74
+ // modules used to carry): `charTable` builds a code-point membership table from a string of the allowed
75
+ // characters, and `allCharsIn` checks that a NON-EMPTY string is composed only of them (`+`, not `*`).
76
+ // One data-table loop, not a per-charset inlined character-class chain.
77
+ function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
78
+ function _allCharsIn(s, table) {
79
+ if (s.length === 0) return false;
80
+ for (var i = 0; i < s.length; i++) { if (!table[_charCodeAt(s, i)]) return false; }
81
+ return true;
82
+ }
57
83
  // ct is a leaf format module (requires only the codec / oid / constants / error
58
84
  // core, never pkix) so the SCT-list extension VALUE decoder registers here as a
59
85
  // registry row, not a switch. The one-directional edge keeps the layering acyclic.
60
86
  var ct = require("./ct");
61
87
 
62
- var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
88
+ // PEM armor markers, matched literally so no regex -- with its live, replaceable match protocol and
89
+ // the backreference the END repeats -- stands between the parser and the wire.
90
+ var _PEM_BEGIN = "-----BEGIN ";
91
+ var _PEM_END = "-----END ";
92
+ var _PEM_DASHES = "-----";
93
+
94
+ // The next PEM block at or after `from`: "-----BEGIN <label>-----<body>-----END <label>-----" where
95
+ // the label is one or more of [A-Z0-9 ] (RFC 7468 label octets, spaces included) and the END label
96
+ // repeats the BEGIN label exactly (the backreference). Returns { index, label, body, end } -- `index`
97
+ // the BEGIN offset, `end` the offset past the closing dashes -- or null when no such block remains.
98
+ // A "-----BEGIN " with no valid label or no matching END is skipped, mirroring the regex trying each
99
+ // start position; the body is the shortest span to the first matching END (the lazy quantifier).
100
+ function _findPemBlock(text, from) {
101
+ var searchFrom = from;
102
+ for (;;) {
103
+ var bi = _strIndexOf(text, _PEM_BEGIN, searchFrom);
104
+ if (bi === -1) return null;
105
+ var p = bi + _PEM_BEGIN.length, ls = p;
106
+ while (p < text.length) {
107
+ var c = _charCodeAt(text, p);
108
+ if (!((c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 32)) break; // A-Z 0-9 space
109
+ p += 1;
110
+ }
111
+ if (p === ls || _strSlice(text, p, p + 5) !== _PEM_DASHES) { searchFrom = bi + 1; continue; }
112
+ var label = _strSlice(text, ls, p);
113
+ var bodyStart = p + 5;
114
+ var endMarker = _PEM_END + label + _PEM_DASHES;
115
+ var ei = _strIndexOf(text, endMarker, bodyStart);
116
+ if (ei === -1) { searchFrom = bi + 1; continue; }
117
+ return { index: bi, label: label, body: _strSlice(text, bodyStart, ei), end: ei + endMarker.length };
118
+ }
119
+ }
120
+
121
+ // All ASCII CR / LF / TAB / SPACE removed (the base64 body's permitted layout whitespace).
122
+ function _stripB64Whitespace(s) {
123
+ var out = "", runStart = 0, i;
124
+ for (i = 0; i < s.length; i++) {
125
+ var c = _charCodeAt(s, i);
126
+ if (c === 13 || c === 10 || c === 9 || c === 32) {
127
+ if (i > runStart) out += _strSlice(s, runStart, i);
128
+ runStart = i + 1;
129
+ }
130
+ }
131
+ if (i > runStart) out += _strSlice(s, runStart, i);
132
+ return out;
133
+ }
134
+
135
+ // A base64 string wrapped to 64-column lines joined by LF, no trailing LF (matching (.{64})->"$1\n"
136
+ // then a trailing-newline strip; base64 output carries no newline of its own to strip).
137
+ function _wrap64(s) {
138
+ var out = "";
139
+ for (var i = 0; i < s.length; i += 64) {
140
+ if (i > 0) out += "\n";
141
+ out += _strSlice(s, i, i + 64);
142
+ }
143
+ return out;
144
+ }
63
145
 
64
146
  // ---- shared parse-entry ----------------------------------------------
65
147
  // Input handling, PEM unwrapping (with the size cap), DER-decode wrapping, and
@@ -80,10 +162,10 @@ function pemDecode(text, label, PemError) {
80
162
  text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
81
163
  charset: "latin1", tooLarge: "pem/too-large", badInput: "pem/bad-input", label: "PEM input",
82
164
  });
83
- var m = _reExec(PEM_RE, text);
165
+ var m = _findPemBlock(text, 0);
84
166
  if (!m) throw new PemError("pem/no-block", "no PEM block found");
85
- if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m[1]));
86
- var b64 = _strReplace(m[2], /[\r\n\t ]+/g, "");
167
+ if (label && m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
168
+ var b64 = _stripB64Whitespace(m.body);
87
169
  // Strict canonical base64 (RFC 4648 sec. 3.5) via the shared encoding guard.
88
170
  return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
89
171
  }
@@ -99,51 +181,99 @@ function pemDecodeAll(text, label, PemError) {
99
181
  text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
100
182
  charset: "latin1", tooLarge: "pem/too-large", badInput: "pem/bad-input", label: "PEM input",
101
183
  });
102
- var re = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/g;
103
184
  var blocks = [];
104
185
  var lastEnd = 0, m;
105
- while ((m = _reExec(re, text)) !== null) {
106
- if (_reTest(/\S/, _strSlice(text, lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
107
- if (m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m[1]));
108
- var b64 = _strReplace(m[2], /[\r\n\t ]+/g, "");
186
+ while ((m = _findPemBlock(text, lastEnd)) !== null) {
187
+ if (_hasNonWhitespace(_strSlice(text, lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
188
+ if (m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
189
+ var b64 = _stripB64Whitespace(m.body);
109
190
  _push(blocks, guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
110
- lastEnd = re.lastIndex;
191
+ lastEnd = m.end;
111
192
  }
112
193
  if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
113
- if (_reTest(/\S/, _strSlice(text, lastEnd))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted after the PEM chain (RFC 8555 sec. 9.1)");
194
+ if (_hasNonWhitespace(_strSlice(text, lastEnd))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted after the PEM chain (RFC 8555 sec. 9.1)");
114
195
  return blocks;
115
196
  }
116
197
 
117
- // The label must be re-readable by this module's own PEM_RE (uppercase
198
+ // The label must be re-readable by this module's own PEM label scan (uppercase
118
199
  // alphanumeric words separated by single spaces, a subset of the RFC 7468
119
200
  // sec. 3 label grammar). Anything else (lowercase, a leading/trailing space,
120
201
  // an embedded "-----" or newline) would emit armor pemDecode itself rejects
121
202
  // or corrupt the armor grammar: a config-time authoring fault, thrown here.
203
+ // The armor label grammar, decided character by character (no regex protocol on the emit path):
204
+ // one or more A-Z0-9 words joined by single spaces -- so no leading/trailing space and no doubled
205
+ // space, and every other character in [A-Z0-9].
206
+ function _isPemLabel(s) {
207
+ var n = s.length;
208
+ if (n === 0) return false;
209
+ for (var i = 0; i < n; i++) {
210
+ var ch = _charAt(s, i);
211
+ if (ch === " ") {
212
+ if (i === 0 || i === n - 1 || _charAt(s, i - 1) === " ") return false;
213
+ } else if (!((ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9"))) {
214
+ return false;
215
+ }
216
+ }
217
+ return true;
218
+ }
219
+
122
220
  function pemEncode(der, label, PemError) {
123
- if (typeof label !== "string" || !_reTest(/^[A-Z0-9]+( [A-Z0-9]+)*$/, label)) {
221
+ if (typeof label !== "string" || !_isPemLabel(label)) {
124
222
  throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
125
223
  }
126
224
  // Re-view through the byte guard: a string input would silently utf8-armor into
127
225
  // a bogus PEM, and a detached-backed Buffer would armor an empty body; both
128
226
  // fail closed (a Uint8Array / Buffer of real DER re-views cleanly).
129
227
  var buf = guard.bytes.view(der, PemError, "pem/bad-input", "pemEncode DER input");
130
- var b64 = _strReplace(_strReplace(_bufToString(buf, "base64"), /(.{64})/g, "$1\n"), /\n$/, "");
228
+ var b64 = _wrap64(_bufToString(buf, "base64"));
131
229
  return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
132
230
  }
133
231
 
232
+ // Only the base64 alphabet (A-Z a-z 0-9 + / =) kept, every other character dropped -- the lenient
233
+ // filter of the historic signer-certificate path, by character scan.
234
+ function _keepBase64(s) {
235
+ var out = "", runStart = 0, i;
236
+ for (i = 0; i < s.length; i++) {
237
+ var c = _charCodeAt(s, i);
238
+ if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 43 || c === 47 || c === 61)) {
239
+ if (i > runStart) out += _strSlice(s, runStart, i);
240
+ runStart = i + 1;
241
+ }
242
+ }
243
+ if (i > runStart) out += _strSlice(s, runStart, i);
244
+ return out;
245
+ }
246
+
247
+ // The @internal LENIENT PEM decode of a fixed-`label` block (the historic signer-certificate path,
248
+ // distinct from the strict canonical `pemDecode` above): the body of the first
249
+ // "-----BEGIN <label>-----...-----END <label>-----" block with any non-base64 character stripped,
250
+ // decoded through Node's lenient base64. Returns the DER Buffer, or null when no such block exists so
251
+ // the caller keeps its own domain error. NOT for verified input -- pemDecode is the fail-closed door.
252
+ function pemDecodeLenient(text, label) {
253
+ var begin = _PEM_BEGIN + label + _PEM_DASHES;
254
+ var end = _PEM_END + label + _PEM_DASHES;
255
+ var bi = _strIndexOf(text, begin);
256
+ if (bi === -1) return null;
257
+ var bodyStart = bi + begin.length;
258
+ var ei = _strIndexOf(text, end, bodyStart);
259
+ if (ei === -1) return null;
260
+ return _bufferFrom(_keepBase64(_strSlice(text, bodyStart, ei)), "base64");
261
+ }
262
+
134
263
  // Coerce parse input to DER bytes. A PEM string OR a PEM Buffer (a .pem file
135
264
  // read with fs.readFileSync) is unwrapped with opts.pemLabel; a DER Buffer or
136
265
  // Uint8Array is taken as bytes. opts: { pemLabel, PemError, ErrorClass, prefix }.
137
266
  function coerceToDer(input, opts) {
138
267
  if (typeof input === "string") return pemDecode(input, opts.pemLabel, opts.PemError);
139
- if (_isBuffer(input) || _isUint8Array(input)) {
140
- // Route the DER Buffer / Uint8Array through the shared byte guard so a
141
- // detached backing store fails closed here for every format that composes
142
- // this boundary (x509/crl/csr/pkcs8/cms/pkcs12), not per-format.
143
- var buf = guard.bytes.view(input, opts.ErrorClass, opts.prefix + "/bad-input", "parse");
268
+ if (guard.bytes.isByteSource(input)) {
269
+ // Route any DER BufferSource (Buffer, typed array, DataView, or ArrayBuffer) through the shared byte
270
+ // guard so a detached backing store fails closed here for every format that composes this boundary
271
+ // (x509/crl/csr/pkcs8/cms/pkcs12), not per-format, and its byte view is read from the slot -- a
272
+ // cross-realm ArrayBuffer or an offset DataView reaches the decoder exactly as a Buffer does.
273
+ var buf = guard.bytes.source(input, opts.ErrorClass, opts.prefix + "/bad-input", "parse");
144
274
  return _isPemArmor(buf) ? pemDecode(buf, opts.pemLabel, opts.PemError) : buf;
145
275
  }
146
- throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER Buffer or a PEM string");
276
+ throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER BufferSource (Buffer, typed array, DataView, or ArrayBuffer) or a PEM string");
147
277
  }
148
278
 
149
279
  // Does a Buffer carry PEM armor (a .pem read with fs.readFileSync) instead of
@@ -1671,10 +1801,24 @@ function signedEnvelope(ns, tbsSchema, opts) {
1671
1801
  // whitespace, no leading/trailing dot, no empty label, no underscore, LDH labels (1-63 octets, no leading /
1672
1802
  // trailing hyphen), <= 253 octets, an optional leftmost "*" wildcard. Returns a reason string, or null when
1673
1803
  // well-formed. The shared home so the linter AND an identity comparator (case-fold only a valid dNSName) agree.
1804
+ // One LDH label (RFC 1034 preferred syntax), decided character by character: every character an
1805
+ // ASCII letter/digit/hyphen, and a hyphen neither first nor last. The caller has already rejected
1806
+ // any label carrying a dot, whitespace, or underscore, so only the LDH shape remains to check.
1807
+ function _isLdhLabel(s) {
1808
+ var n = s.length;
1809
+ if (n === 0) return false;
1810
+ for (var i = 0; i < n; i++) {
1811
+ var ch = _charAt(s, i);
1812
+ if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9")) continue;
1813
+ if (ch === "-") { if (i === 0 || i === n - 1) return false; } else return false;
1814
+ }
1815
+ return true;
1816
+ }
1817
+
1674
1818
  function dnsNameProblem(s) {
1675
1819
  if (typeof s !== "string" || !s.length) return "empty";
1676
1820
  if (s.length > 253) return "exceeds 253 octets";
1677
- if (_reTest(/\s/, s)) return "whitespace";
1821
+ if (_hasWhitespace(s)) return "whitespace";
1678
1822
  if (_charAt(s, 0) === "." || _charAt(s, s.length - 1) === ".") return "leading/trailing dot";
1679
1823
  if (_strIndexOf(s, "_") !== -1) return "underscore forbidden in dNSName";
1680
1824
  var labels = _split(s, ".");
@@ -1686,7 +1830,7 @@ function dnsNameProblem(s) {
1686
1830
  if (labels.length < 2) return "bare wildcard";
1687
1831
  continue;
1688
1832
  }
1689
- if (!_reTest(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i, label)) return "invalid label syntax";
1833
+ if (!_isLdhLabel(label)) return "invalid label syntax";
1690
1834
  }
1691
1835
  return null;
1692
1836
  }
@@ -1695,7 +1839,12 @@ module.exports = {
1695
1839
  dnsNameProblem: dnsNameProblem,
1696
1840
  pemDecode: pemDecode,
1697
1841
  pemDecodeAll: pemDecodeAll,
1842
+ pemDecodeLenient: pemDecodeLenient,
1698
1843
  pemEncode: pemEncode,
1844
+ isJsWhitespace: _isJsWhitespace, // the shared JS regex \s code-point predicate for char-scan parsers
1845
+ stripBase64Whitespace: _stripB64Whitespace, // shared CR/LF/TAB/SP strip for base64 bodies (PEM, MIME)
1846
+ charTable: _charTable, // build a code-point membership table from a string of allowed characters
1847
+ allCharsIn: _allCharsIn, // is a NON-EMPTY string composed only of table members (the /^[chars]+$/ family)
1699
1848
  coerceToDer: coerceToDer,
1700
1849
  decodeRoot: decodeRoot,
1701
1850
  runParse: runParse,
package/lib/schema-tsp.js CHANGED
@@ -177,7 +177,16 @@ var TST_INFO = schema.seq([
177
177
  // genTime is surfaced as a millisecond-precision Date; genTimeFraction is the
178
178
  // exact fractional-seconds digits (lossless for sub-millisecond precision), or
179
179
  // null when genTime carries no fraction.
180
- genTimeFraction: (function () { var g = /\.(\d+)Z$/.exec(m.fields.genTime.node.content.toString("latin1")); return g ? g[1] : null; })(),
180
+ genTimeFraction: (function () {
181
+ // /\.(\d+)Z$/ : the fractional-seconds digits before a trailing "Z" (the "." and "Z" excluded), or null.
182
+ var s = m.fields.genTime.node.content.toString("latin1"), n = s.length;
183
+ if (n < 3 || s.charAt(n - 1) !== "Z") return null;
184
+ var dot = s.lastIndexOf(".");
185
+ if (dot < 0 || dot > n - 3) return null;
186
+ var frac = s.slice(dot + 1, n - 1);
187
+ for (var fi = 0; fi < frac.length; fi++) { var cc = frac.charCodeAt(fi); if (cc < 48 || cc > 57) return null; }
188
+ return frac;
189
+ })(),
181
190
  accuracy: m.fields.accuracy.present ? m.fields.accuracy.value.result : null,
182
191
  ordering: m.fields.ordering.present ? m.fields.ordering.value : false,
183
192
  nonce: m.fields.nonce.present ? m.fields.nonce.value : null,
@@ -242,7 +242,7 @@ function _normPkcs8(k, label, E) {
242
242
  throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
243
243
  }
244
244
  function _normCompositeKeys(key, comp, E) {
245
- if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) {
245
+ if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) { // allow:byte-source-narrow -- composite-key discrimination: a raw-key BufferSource (incl an ArrayBuffer, which is neither) is correctly classed non-composite and normalized by _normPkcs8
246
246
  throw E("bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
247
247
  }
248
248
  return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
package/lib/sigstore.js CHANGED
@@ -31,6 +31,7 @@ var constants = require("./constants");
31
31
  var asn1 = require("./asn1-der");
32
32
  var guard = require("./guard-all");
33
33
  var x509 = require("./schema-x509");
34
+ var pkix = require("./schema-pkix");
34
35
  var pathValidate = require("./path-validate");
35
36
  var merkle = require("./merkle");
36
37
  var oid = require("./oid");
@@ -43,9 +44,51 @@ function _err(code, message, cause) { return new SigstoreError(code, message, ca
43
44
  var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
44
45
  var SP = Buffer.from(" ", "ascii");
45
46
  var DSSEV1 = Buffer.from("DSSEv1", "ascii");
46
- // The checkpoint signature-line marker is EM DASH (U+2014) + space; built at
47
- // runtime so the source file stays pure ASCII.
48
- var NOTE_SIG = new RegExp("^" + String.fromCharCode(0x2014) + " (\\S+) (\\S+)$");
47
+ // The checkpoint signature-line marker is EM DASH (U+2014) + space; built at runtime so the source
48
+ // file stays pure ASCII. The line is parsed by character scan (no regex): "<EM DASH> <keyId> <sig>",
49
+ // two whitespace-separated non-empty tokens, nothing after.
50
+ var _EM_DASH = String.fromCharCode(0x2014);
51
+
52
+ // The \S token scan uses the shared JS regex \s code-point predicate (pkix.isJsWhitespace) so the
53
+ // note-line grammar stays exactly the /\S/ complement without a second copy of the code-point set.
54
+ function _parseNoteSig(line) {
55
+ var prefix = _EM_DASH + " ";
56
+ if (line.slice(0, prefix.length) !== prefix) return null;
57
+ var p = prefix.length, n = line.length;
58
+ var t1s = p;
59
+ while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
60
+ if (p === t1s) return null; // \S+ requires at least one character
61
+ var keyId = line.slice(t1s, p);
62
+ if (line.charAt(p) !== " ") return null; // the literal single space between the tokens
63
+ p += 1;
64
+ var t2s = p;
65
+ while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
66
+ if (p === t2s) return null;
67
+ var sig = line.slice(t2s, p);
68
+ if (p !== n) return null; // $ -- nothing after the second token
69
+ return { keyId: keyId, sig: sig };
70
+ }
71
+
72
+ // A run of trailing "=" removed (the /=+$/ base64-padding strip).
73
+ function _stripTrailingEquals(s) {
74
+ var i = s.length;
75
+ while (i > 0 && s.charAt(i - 1) === "=") i -= 1;
76
+ return s.slice(0, i);
77
+ }
78
+
79
+ // The recognized bundle media types (draft: the inline ".v0.N+json" form and the
80
+ // "+json;version=0.N" form, N in 1..3) -- an explicit set in place of the anchored alternation regex.
81
+ var _BUNDLE_MEDIA_TYPES = {
82
+ "application/vnd.dev.sigstore.bundle.v0.1+json": 1,
83
+ "application/vnd.dev.sigstore.bundle.v0.2+json": 1,
84
+ "application/vnd.dev.sigstore.bundle.v0.3+json": 1,
85
+ "application/vnd.dev.sigstore.bundle+json;version=0.1": 1,
86
+ "application/vnd.dev.sigstore.bundle+json;version=0.2": 1,
87
+ "application/vnd.dev.sigstore.bundle+json;version=0.3": 1
88
+ };
89
+ function _isBundleMediaType(mt) {
90
+ return typeof mt === "string" && Object.prototype.hasOwnProperty.call(_BUNDLE_MEDIA_TYPES, mt);
91
+ }
49
92
 
50
93
  // Parse untrusted JSON through the bounded, duplicate-member-rejecting guard.
51
94
  function _jsonParse(input, code, label) {
@@ -92,10 +135,10 @@ function pae(payloadType, payloadBytes) {
92
135
  // either, but reject a non-canonical re-encoding (an encoding-malleability guard).
93
136
  function _b64(s, label) {
94
137
  if (typeof s !== "string") throw _err("sigstore/bad-bundle", label + " must be a base64 string");
95
- var urlSafe = /[-_]/.test(s);
138
+ var urlSafe = s.indexOf("-") !== -1 || s.indexOf("_") !== -1;
96
139
  var enc = urlSafe ? "base64url" : "base64";
97
140
  var buf = Buffer.from(s, enc);
98
- if (buf.toString(enc).replace(/=+$/, "") !== s.replace(/=+$/, "")) {
141
+ if (_stripTrailingEquals(buf.toString(enc)) !== _stripTrailingEquals(s)) {
99
142
  throw _err("sigstore/bad-bundle", label + " is not canonical base64");
100
143
  }
101
144
  return buf;
@@ -137,7 +180,7 @@ function parseBundle(input) {
137
180
  // a newer version is recognized-and-deferred, never accepted by an over-broad
138
181
  // match (the inline `.v0.N+json` form and the `+json;version=0.N` form).
139
182
  var mt = obj.mediaType;
140
- if (typeof mt !== "string" || !/^application\/vnd\.dev\.sigstore\.bundle(\.v0\.[123]\+json|\+json;version=0\.[123])$/.test(mt)) {
183
+ if (!_isBundleMediaType(mt)) {
141
184
  throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : mt));
142
185
  }
143
186
  if (!obj.verificationMaterial || typeof obj.verificationMaterial !== "object") {
@@ -265,9 +308,9 @@ function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
265
308
  }
266
309
  var sigBlock = buf.subarray(sep + 2).toString("utf8").split("\n");
267
310
  for (var i = 0; i < sigBlock.length; i++) {
268
- var m = sigBlock[i].match(NOTE_SIG);
269
- if (!m) continue;
270
- var blob = _b64(m[2], "checkpoint signature");
311
+ var note = _parseNoteSig(sigBlock[i]);
312
+ if (!note) continue;
313
+ var blob = _b64(note.sig, "checkpoint signature");
271
314
  if (blob.length < 5) continue;
272
315
  var hint = blob.subarray(0, 4), derSig = blob.subarray(4);
273
316
  var k = _rekorKey(rekorKeys, null, hint, timeMs);
package/lib/smime.js CHANGED
@@ -245,26 +245,95 @@ function _outerPrefix(list) {
245
245
  // non-ASCII protected value round-trips intact -- a latin1 string maps 1:1 to bytes, recovering the emitted UTF-8.
246
246
  function _utf8Header(s) { return Buffer.from(s, "latin1").toString("utf8"); }
247
247
 
248
+ // ---- header parsing by character scan (no regex) ----------------------------------------------
249
+ // ASCII lowercase of a code point (A-Z -> a-z), for the case-insensitive "content-type" literal match.
250
+ function _asciiLower(c) { return (c >= 65 && c <= 90) ? c + 32 : c; }
251
+ var _CONTENT_TYPE = "content-type";
252
+
253
+ // RFC 5322 header unfolding: a CRLF followed by one-or-more SP/HTAB (a folded continuation) collapses
254
+ // to a single space (matching /\r\n[ \t]+/g -> " ").
255
+ function _unfoldHeaderFolds(s) {
256
+ var out = "", i = 0, n = s.length;
257
+ while (i < n) {
258
+ if (s.charCodeAt(i) === 0x0d && i + 1 < n && s.charCodeAt(i + 1) === 0x0a &&
259
+ i + 2 < n && (s.charCodeAt(i + 2) === 0x20 || s.charCodeAt(i + 2) === 0x09)) {
260
+ out += " ";
261
+ i += 2;
262
+ while (i < n && (s.charCodeAt(i) === 0x20 || s.charCodeAt(i) === 0x09)) i += 1;
263
+ } else {
264
+ out += s.charAt(i);
265
+ i += 1;
266
+ }
267
+ }
268
+ return out;
269
+ }
270
+
271
+ // If the line beginning at `start` is a Content-Type field -- ^[ \t]*content-type[ \t]*: -- return the
272
+ // whole line up to (not including) the next CR or LF (the [^\r\n]* tail); else null. Case-insensitive
273
+ // on the literal, matching the /i flag over the ASCII field name.
274
+ function _contentTypeLineAt(s, start) {
275
+ var n = s.length, p = start;
276
+ while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
277
+ if (p + _CONTENT_TYPE.length > n) return null;
278
+ for (var k = 0; k < _CONTENT_TYPE.length; k++) {
279
+ if (_asciiLower(s.charCodeAt(p + k)) !== _CONTENT_TYPE.charCodeAt(k)) return null;
280
+ }
281
+ p += _CONTENT_TYPE.length;
282
+ while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
283
+ if (p >= n || s.charCodeAt(p) !== 0x3a) return null; // ":"
284
+ p += 1;
285
+ while (p < n && s.charCodeAt(p) !== 0x0d && s.charCodeAt(p) !== 0x0a) p += 1;
286
+ return s.slice(start, p);
287
+ }
288
+
289
+ // Every Content-Type field in the header block (the /gim match, /m making ^ match each line start):
290
+ // scan the block start and each position after an LF. Returns the matched lines, or null when none.
291
+ function _contentTypeLines(s) {
292
+ var lines = [], lineStart = 0;
293
+ while (lineStart <= s.length) {
294
+ var line = _contentTypeLineAt(s, lineStart);
295
+ if (line !== null) lines.push(line);
296
+ var nl = s.indexOf("\n", lineStart);
297
+ if (nl === -1) break;
298
+ lineStart = nl + 1;
299
+ }
300
+ return lines.length ? lines : null;
301
+ }
302
+
303
+ // The ^[ \t]*content-type[ \t]*: prefix removed from a matched Content-Type line (the value remains).
304
+ function _contentTypeValue(s) {
305
+ var n = s.length, p = 0;
306
+ while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
307
+ if (p + _CONTENT_TYPE.length > n) return s;
308
+ for (var k = 0; k < _CONTENT_TYPE.length; k++) {
309
+ if (_asciiLower(s.charCodeAt(p + k)) !== _CONTENT_TYPE.charCodeAt(k)) return s;
310
+ }
311
+ p += _CONTENT_TYPE.length;
312
+ while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
313
+ if (p >= n || s.charCodeAt(p) !== 0x3a) return s;
314
+ return s.slice(p + 1);
315
+ }
316
+
248
317
  function _declaresHp(content) {
249
318
  // content is the CANONICAL entity bytes -- the caller (_hpSurface) runs mime.canonicalizeText ONCE so
250
319
  // detection here and the subsequent mime.parse see the identical normalized form (bare CR / LF -> CRLF).
251
320
  // Detecting hp on the canonical form -- the bytes the signature covers -- is what stops a fold/line-ending
252
321
  // transport rewrite from stripping the hp signal from a still-valid signed message.
253
322
  var s = content.toString("latin1");
254
- var m = s.match(/\r\n\r\n/);
255
- var head = (m ? s.slice(0, m.index) : s).replace(/\r\n[ \t]+/g, " ");
323
+ var sep = s.indexOf("\r\n\r\n");
324
+ var head = _unfoldHeaderFolds(sep !== -1 ? s.slice(0, sep) : s);
256
325
  // Detect hp by composing mime.paramCount, the same comment- + quoted-string-aware tokenizer the parser
257
326
  // uses -- over each Content-Type field, so the probe never diverges from mime.parse: a leading-whitespace
258
327
  // or before-colon field name is matched (the parser trims it), while an hp= inside a MIME comment or a
259
328
  // quoted value is not a parameter (so an ordinary Content-Type cannot opt a message into HP processing). A
260
329
  // real hp on ANY Content-Type line is detected; a duplicate Content-Type is rejected downstream.
261
- var cts = head.match(/^[ \t]*content-type[ \t]*:[^\r\n]*/gim);
330
+ var cts = _contentTypeLines(head);
262
331
  if (!cts) return false;
263
332
  for (var i = 0; i < cts.length; i++) {
264
333
  // hasParam (not paramCount): a payload that names the hp attribute without a value ("...; hp") still
265
334
  // CLAIMS header protection -- detect it so _hpSurface fails closed on the malformed declaration rather
266
335
  // than silently downgrading it to unprotected.
267
- if (mime.hasParam(cts[i].replace(/^[ \t]*content-type[ \t]*:/i, ""), "hp")) return true;
336
+ if (mime.hasParam(_contentTypeValue(cts[i]), "hp")) return true;
268
337
  }
269
338
  return false;
270
339
  }
@@ -714,8 +783,19 @@ function _micalgOf(p7Der) {
714
783
  // A fresh multipart boundary that cannot collide with the content (random, dashed, unique per call).
715
784
  function _boundary() { return "----=_pki_smime_" + nodeCrypto.randomBytes(18).toString("hex"); }
716
785
 
786
+ // Base64 wrapped to 64-column lines joined by CRLF, no trailing CRLF (matching (.{64})->"$1\r\n"
787
+ // then a trailing-CRLF strip; base64 output carries no CRLF of its own).
788
+ function _wrap64Crlf(s) {
789
+ var out = "";
790
+ for (var i = 0; i < s.length; i += 64) {
791
+ if (i > 0) out += "\r\n";
792
+ out += s.slice(i, i + 64);
793
+ }
794
+ return out;
795
+ }
796
+
717
797
  function _base64Body(der) {
718
- return Buffer.from(der.toString("base64").replace(/(.{64})/g, "$1\r\n").replace(/\r\n$/, ""), "latin1");
798
+ return Buffer.from(_wrap64Crlf(der.toString("base64")), "latin1");
719
799
  }
720
800
 
721
801
  /**
@@ -1180,7 +1260,7 @@ async function decompress(message, opts) {
1180
1260
  function _decodeCms(ent) {
1181
1261
  // guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
1182
1262
  // MIME line-wrapping whitespace first, and pass _err (not the SmimeError class).
1183
- if (ent.cte === "base64") return guard.encoding.base64(ent.body.toString("latin1").replace(/[\r\n\t ]/g, ""), C.LIMITS.MIME_MAX_BYTES, _err, "smime/bad-mime", "the CMS body");
1263
+ if (ent.cte === "base64") return guard.encoding.base64(pkix.stripBase64Whitespace(ent.body.toString("latin1")), C.LIMITS.MIME_MAX_BYTES, _err, "smime/bad-mime", "the CMS body");
1184
1264
  return ent.body;
1185
1265
  }
1186
1266