@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/pki-build.js CHANGED
@@ -25,16 +25,279 @@ var _bufferFrom = intrinsic.bufferFrom;
25
25
  var _bufferEquals = intrinsic.bufferEquals;
26
26
  var oid = require("./oid");
27
27
  var ipUtils = require("./ip-utils");
28
+ // packIpLiteral decides a GeneralName form (a bare string -> iPAddress or not), so its binding is taken at
29
+ // load: calling it through the exports object would be a live property read, and code that reassigned that
30
+ // property could steer the form decision without touching a regex.
31
+ var _packIpLiteral = ipUtils.packIpLiteral;
28
32
  var nodeCrypto = require("crypto");
29
33
  // The key-material comparison, taken at load like every other operation a decision here rests on.
30
34
  var _keyEquals = intrinsic.uncurry(nodeCrypto.KeyObject.prototype.equals);
35
+ // The bare-string GeneralName classifier (_classifyBareGeneralName) decides which name FORM a SAN string
36
+ // becomes. It uses no regular expression: a regex is matched through a live, replaceable protocol
37
+ // (RegExp.prototype.exec / test, String.prototype.match / split consult the regex's own `exec` and the
38
+ // `[Symbol.match]` / `[Symbol.split]` hooks, and RegExp.prototype.compile can mutate a pattern in place),
39
+ // so co-resident code could steer a form decision without our seeing it -- and the structural URI/email
40
+ // rules a regex cannot state cleanly are what kept drawing edge-case escapes. Instead every form is
41
+ // decided by scanning the string one character code at a time through these captured string primitives,
42
+ // which read the value directly and have no per-character dispatch to replace.
43
+ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
44
+ var _stringIndexOf = intrinsic.stringIndexOf;
45
+ var _stringLastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
46
+ var _sliceStr = intrinsic.uncurry(String.prototype.slice);
47
+ var _String = intrinsic.String;
48
+ var _objectKeys = intrinsic.keys;
49
+ var _isBufferChk = intrinsic.isBuffer;
50
+ var _stringify = intrinsic.stringify;
31
51
 
32
- var b = asn1.build;
52
+ // --- bare-string GeneralName form detection, by character-code scan (no regex) ---
53
+ // Each predicate answers "is this string syntactically THIS name form", reading the string one code unit
54
+ // at a time; a caller cannot steer the answer by replacing a prototype method. The classifier below tries
55
+ // the forms in a fixed precedence and refuses anything none of them accepts (the object form is the escape).
56
+ function _isAlphaCode(c) { return (c >= 65 && c <= 90) || (c >= 97 && c <= 122); } // A-Z a-z
57
+ function _isDigitCode(c) { return c >= 48 && c <= 57; } // 0-9
58
+ function _isHexCode(c) { return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70); }
59
+
60
+ // RFC 3986 sec. 2.3 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~".
61
+ function _isUnreserved(c) { return _isAlphaCode(c) || _isDigitCode(c) || c === 45 || c === 46 || c === 95 || c === 126; }
62
+ // RFC 3986 sec. 2.2 sub-delims: ! $ & ' ( ) * + , ; =
63
+ function _isSubDelim(c) {
64
+ switch (c) {
65
+ case 33: case 36: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 59: case 61: return true;
66
+ default: return false;
67
+ }
68
+ }
69
+ // A userinfo byte: unreserved / sub-delim / ":" (percent-encoding handled separately). No "@".
70
+ function _isUserinfoByte(c) { return _isUnreserved(c) || _isSubDelim(c) || c === 58; }
71
+ // A path/query/fragment byte: unreserved / sub-delim / a non-bracket gen-delim (":" "/" "?" "#" "@").
72
+ // "%" is handled separately (it must open a "%HEXHEX" triplet); the IP-literal brackets "[" "]" are
73
+ // excluded, so an IPv6-literal host is refused by the shorthand and uses the object form.
74
+ function _isUriByte(c) {
75
+ return _isUnreserved(c) || _isSubDelim(c) || c === 58 || c === 47 || c === 63 || c === 35 || c === 64;
76
+ }
77
+
78
+ // An RFC 5322 sec. 3.2.3 "atext" byte: ALPHA / DIGIT and a fixed set of specials, used for an unquoted
79
+ // email local part. "." is deliberately excluded so the dot-atom structure is enforced by the scan.
80
+ function _isAtextByte(c) {
81
+ if (_isAlphaCode(c) || _isDigitCode(c)) return true;
82
+ switch (c) {
83
+ case 33: case 35: case 36: case 37: case 38: case 39: // ! # $ % & '
84
+ case 42: case 43: case 45: case 47: case 61: case 63: // * + - / = ?
85
+ case 94: case 95: case 96: case 123: case 124: case 125: case 126: // ^ _ ` { | } ~
86
+ return true;
87
+ default: return false;
88
+ }
89
+ }
90
+
91
+ // IPv4 SHAPE: one or more digit groups joined by single dots (only digits and dots, no leading/trailing/
92
+ // doubled dot, at least one dot). This is the "looks like an address" test, not a valid-address test --
93
+ // packIpLiteral decides validity; a shape match that is not a valid address is refused as ambiguous.
94
+ function _looksLikeIPv4Shape(s) {
95
+ var n = s.length, i, c, sawDot = false, atomEmpty = true;
96
+ if (n === 0) return false;
97
+ for (i = 0; i < n; i += 1) {
98
+ c = _charCodeAt(s, i);
99
+ if (c === 46) { if (atomEmpty) return false; sawDot = true; atomEmpty = true; } // "."
100
+ else if (_isDigitCode(c)) { atomEmpty = false; }
101
+ else return false;
102
+ }
103
+ return sawDot && !atomEmpty;
104
+ }
105
+
106
+ // Every byte in [from,to) is an allowed component byte (per `byteOk`) or opens a "%HEXHEX" triplet.
107
+ function _componentBytesOk(s, from, to, byteOk) {
108
+ var i = from, c;
109
+ while (i < to) {
110
+ c = _charCodeAt(s, i);
111
+ if (c === 37) { // "%" -> %HEXHEX
112
+ if (i + 2 >= to || !_isHexCode(_charCodeAt(s, i + 1)) || !_isHexCode(_charCodeAt(s, i + 2))) return false;
113
+ i += 3;
114
+ } else if (byteOk(c)) { i += 1; }
115
+ else return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ // The URI host in [from,to): RFC 5280 sec. 4.2.1.6 requires a URI SAN authority to carry a fully-qualified
121
+ // domain name or IP host, stricter than an RFC 3986 reg-name. So the host is either a valid IPv4 literal,
122
+ // or an FQDN of strict-LDH labels (the email-domain grammar: no underscore, no hyphen at a label edge) with
123
+ // at least one dot -- a single-label host such as "localhost" is not fully qualified and uses the object
124
+ // form, matching the email domain. An IPv4-SHAPED host that is not a valid address is refused as ambiguous
125
+ // (the same rule the top-level classifier uses), and an IPv6-literal host -- needing the excluded "[" "]"
126
+ // -- uses the object form.
127
+ function _validUriHost(s, from, to) {
128
+ if (to <= from) return false; // empty host
129
+ var host = _sliceStr(s, from, to), len = host.length;
130
+ if (_looksLikeIPv4Shape(host)) return _packIpLiteral(host) !== null; // IPv4 (first-match-wins)
131
+ var end = _charCodeAt(host, len - 1) === 46 ? len - 1 : len; // ignore an absolute-root "."
132
+ if (end > 253) return false; // RFC 1035 name length (root dot excluded)
133
+ var firstDot = _stringIndexOf(host, ".", 0);
134
+ if (firstDot === -1 || firstDot >= end) return false; // multi-label FQDN ("localhost" is not)
135
+ return _labelsValid(host, 0, len, _isHostLabelRange, true); // strict-LDH FQDN, root "." allowed
136
+ }
137
+
138
+ // The authority in [from,to) is "[ userinfo "@" ] host [ ":" port ]" (RFC 3986 sec. 3.2): at most one "@"
139
+ // (userinfo and reg-name each exclude it), a host validated by _validUriHost (RFC 5280 FQDN or IPv4), and
140
+ // an all-digit -- possibly empty -- port after the host's single ":" (a reg-name has no ":"). The userinfo
141
+ // bytes are validated against the RFC 3986 userinfo class.
142
+ function _validUriAuthority(s, from, to) {
143
+ var i, c, at = -1;
144
+ for (i = from; i < to; i += 1) {
145
+ c = _charCodeAt(s, i);
146
+ if (c === 64) { if (at !== -1) return false; at = i; } // "@" -- at most one
147
+ }
148
+ var hostStart = at === -1 ? from : at + 1;
149
+ if (at !== -1 && !_componentBytesOk(s, from, at, _isUserinfoByte)) return false; // userinfo
150
+ var colon = -1;
151
+ for (i = hostStart; i < to; i += 1) { if (_charCodeAt(s, i) === 58) { colon = i; break; } } // ":" -> port
152
+ var hostEnd = colon === -1 ? to : colon;
153
+ if (!_validUriHost(s, hostStart, hostEnd)) return false; // FQDN or IPv4 host
154
+ if (colon !== -1) {
155
+ for (i = colon + 1; i < to; i += 1) { if (!_isDigitCode(_charCodeAt(s, i))) return false; } // port = *DIGIT
156
+ }
157
+ return true;
158
+ }
159
+
160
+ // scheme "://" authority [ path ] [ "?" query ] [ "#" fragment ], parsed by RFC 3986 component. The
161
+ // authority is validated by _validUriAuthority; the remainder is path/query/fragment bytes with at most
162
+ // one "#" (its single fragment delimiter). A structurally malformed URL is refused; the object form is
163
+ // the escape for one the shorthand does not accept.
164
+ function _looksLikeUri(s) {
165
+ var n = s.length, i, c;
166
+ if (n === 0 || !_isAlphaCode(_charCodeAt(s, 0))) return false; // scheme starts ALPHA
167
+ i = 1;
168
+ while (i < n) {
169
+ c = _charCodeAt(s, i);
170
+ if (_isAlphaCode(c) || _isDigitCode(c) || c === 43 || c === 45 || c === 46) i += 1; // + - . in scheme
171
+ else break;
172
+ }
173
+ if (i + 3 > n || _charCodeAt(s, i) !== 58 || _charCodeAt(s, i + 1) !== 47 || _charCodeAt(s, i + 2) !== 47) return false;
174
+ i += 3;
175
+ var authStart = i, authEnd = n;
176
+ for (; i < n; i += 1) { c = _charCodeAt(s, i); if (c === 47 || c === 63 || c === 35) { authEnd = i; break; } }
177
+ if (authEnd === authStart) return false; // empty authority
178
+ if (!_validUriAuthority(s, authStart, authEnd)) return false;
179
+ var sawHash = false; // path / query / fragment
180
+ i = authEnd;
181
+ while (i < n) {
182
+ c = _charCodeAt(s, i);
183
+ if (c === 37) { // "%" -> %HEXHEX
184
+ if (i + 2 >= n || !_isHexCode(_charCodeAt(s, i + 1)) || !_isHexCode(_charCodeAt(s, i + 2))) return false;
185
+ i += 3;
186
+ continue;
187
+ }
188
+ if (c === 35) { if (sawHash) return false; sawHash = true; } // one "#" fragment delimiter
189
+ if (!_isUriByte(c)) return false;
190
+ i += 1;
191
+ }
192
+ return true;
193
+ }
194
+
195
+ // A single dNSName label over [from,to): 1-63 bytes, LDH plus a LEADING or EMBEDDED underscore, no hyphen
196
+ // at either edge, and no underscore at the trailing edge. The underscore tolerance is deliberate and
197
+ // leading/embedded only: the real forms are _acme-challenge, _dmarc, _25._tcp (a label-initial "_"); a
198
+ // label ending in "_" has no such use. An email DOMAIN is stricter still -- see _isHostLabelRange.
199
+ function _isDnsLabelRange(s, from, to) {
200
+ var len = to - from, i, c;
201
+ if (len < 1 || len > 63) return false;
202
+ for (i = from; i < to; i += 1) {
203
+ c = _charCodeAt(s, i);
204
+ if (_isAlphaCode(c) || _isDigitCode(c)) continue; // A-Z a-z 0-9 anywhere
205
+ if (i === to - 1) return false; // last byte must be alnum
206
+ if (c === 95) continue; // "_" leading or embedded
207
+ if (c === 45 && i !== from) continue; // "-" embedded (not leading)
208
+ return false;
209
+ }
210
+ return true;
211
+ }
212
+
213
+ // A single RFC 5321 sec. 2.3.5 hostname label over [from,to): the strict letter-digit-hyphen grammar with
214
+ // no hyphen at either edge and NO underscore. This is the email-domain label rule (a mail domain is a real
215
+ // hostname), stricter than the underscore-tolerant dNSName label above.
216
+ function _isHostLabelRange(s, from, to) {
217
+ var len = to - from, i, c, edge;
218
+ if (len < 1 || len > 63) return false;
219
+ for (i = from; i < to; i += 1) {
220
+ c = _charCodeAt(s, i);
221
+ edge = (i === from || i === to - 1);
222
+ if (_isAlphaCode(c) || _isDigitCode(c)) continue; // A-Z a-z 0-9
223
+ if (c === 45 && !edge) continue; // "-" not at an edge
224
+ return false;
225
+ }
226
+ return true;
227
+ }
228
+
229
+ // Every dot-separated label in [from,to) satisfies `labelFn`. Scans with indexOf; no split allocation. When
230
+ // `allowRootDot` is set, a single trailing "." (the absolute-FQDN root) is stripped before validating, so
231
+ // "example.com." checks as "example.com"; an interior empty label (a doubled or leading dot) still fails.
232
+ function _labelsValid(s, from, to, labelFn, allowRootDot) {
233
+ if (allowRootDot && to - from >= 1 && _charCodeAt(s, to - 1) === 46) to -= 1; // an absolute-root "."
234
+ if (to <= from) return false; // nothing (a lone ".")
235
+ var start = from, dot;
236
+ for (;;) {
237
+ dot = _stringIndexOf(s, ".", start);
238
+ var end = (dot === -1 || dot >= to) ? to : dot;
239
+ if (!labelFn(s, start, end)) return false;
240
+ if (end === to) return true;
241
+ start = end + 1;
242
+ }
243
+ }
33
244
 
34
- // A well-formed dotted-decimal OID: a first arc 0-2 followed by one or more non-leading-zero arcs. Used to
35
- // accept a raw OID string (an unregistered KeyPurposeId / policy OID) where a registered name would go; the
36
- // encoder (encodeOidContent) remains the authoritative validator of the arc-value bounds.
37
- var DOTTED_OID_RE = /^[0-2](?:\.(?:0|[1-9]\d*))+$/;
245
+ // A dNSName: at most 253 bytes, an optional single leading "*." wildcard, then dot-separated dNSName labels
246
+ // (underscore-tolerant), with an optional trailing absolute-root ".".
247
+ function _looksLikeDnsName(s) {
248
+ var n = s.length, start = 0;
249
+ if (n < 1) return false;
250
+ var end = _charCodeAt(s, n - 1) === 46 ? n - 1 : n; // exclude an absolute-root "."
251
+ if (end < 1 || end > 253) return false; // RFC 1035 name length (root dot excluded)
252
+ if (end >= 2 && _charCodeAt(s, 0) === 42 && _charCodeAt(s, 1) === 46) start = 2; // "*."
253
+ if (start >= end) return false;
254
+ return _labelsValid(s, start, n, _isDnsLabelRange, true);
255
+ }
256
+
257
+ // An rfc822Name: an unquoted RFC 5321 dot-atom local part (atext runs joined by single dots), one "@",
258
+ // and a dotted hostname domain (at least one dot -- an FQDN, no wildcard, strict LDH labels, no underscore).
259
+ function _looksLikeEmail(s) {
260
+ if (s.length > 254) return false; // the whole mailbox <= 254 octets (RFC 5321 sec. 4.5.3.1.3)
261
+ var at = _stringIndexOf(s, "@", 0);
262
+ if (at < 1 || at > 64 || _stringLastIndexOf(s, "@") !== at) return false; // one "@", a 1-64 octet local part (RFC 5321 sec. 4.5.3.1.1)
263
+ var i, c, atomEmpty = true;
264
+ for (i = 0; i < at; i += 1) { // local part = dot-atom
265
+ c = _charCodeAt(s, i);
266
+ if (c === 46) { if (atomEmpty) return false; atomEmpty = true; } // no leading/doubled dot
267
+ else if (_isAtextByte(c)) { atomEmpty = false; }
268
+ else return false;
269
+ }
270
+ if (atomEmpty) return false; // no trailing dot / empty
271
+ var dStart = at + 1, dLen = s.length - dStart;
272
+ if (dLen < 1 || dLen > 253 || _stringIndexOf(s, ".", dStart) === -1) return false; // domain must be dotted
273
+ return _labelsValid(s, dStart, s.length, _isHostLabelRange, false); // strict LDH, no underscore or root dot
274
+ }
275
+
276
+ // A well-formed dotted-decimal OID SHAPE (no regex, by the same character scan the name forms use): a first
277
+ // arc 0-2, then one or more arcs each introduced by "." and written without a leading zero ("0" alone, or a
278
+ // non-zero digit followed by more digits). Used to accept a raw OID string (an unregistered KeyPurposeId /
279
+ // policy OID) where a registered name would go; the encoder (encodeOidContent) remains the authoritative
280
+ // validator of the arc-value bounds.
281
+ function _isDottedOid(s) {
282
+ var n = s.length, i, c, d0, arcs = 0;
283
+ if (n < 3) return false; // shortest is "X.Y"
284
+ c = _charCodeAt(s, 0);
285
+ if (c < 48 || c > 50) return false; // first arc is a single digit 0-2
286
+ i = 1;
287
+ while (i < n) {
288
+ if (_charCodeAt(s, i) !== 46) return false; // every following arc is introduced by "."
289
+ i += 1;
290
+ if (i >= n) return false; // a trailing "." has no arc
291
+ d0 = _charCodeAt(s, i);
292
+ if (d0 === 48) { i += 1; } // "0" is a complete arc; a leading zero is refused
293
+ else if (d0 >= 49 && d0 <= 57) { i += 1; while (i < n && _isDigitCode(_charCodeAt(s, i))) i += 1; }
294
+ else { return false; } // an arc must start with a digit
295
+ arcs += 1;
296
+ }
297
+ return arcs >= 1;
298
+ }
299
+
300
+ var b = asn1.build;
38
301
 
39
302
  // KeyUsage named-bit positions (RFC 5280 sec. 4.2.1.3); contentCommitment is the RFC 5280 rename of the
40
303
  // X.509 nonRepudiation bit (1).
@@ -92,7 +355,7 @@ function makeBuilder(ctx) {
92
355
  // A DN spec -> RDNSequence DER. A string is shorthand for a single commonName RDN; a Buffer is raw
93
356
  // pre-encoded Name DER (validated through the parser). An empty array yields an empty RDNSequence.
94
357
  function encodeName(spec) {
95
- if (Buffer.isBuffer(spec)) { assertValidNameDer(spec); return spec; }
358
+ if (guard.bytes.isByteSource(spec)) { var _nd = guard.bytes.snapshotSource(spec, ErrorClass, ctx.prefix + "/bad-name", "raw Name DER"); assertValidNameDer(_nd); return _nd; }
96
359
  if (typeof spec === "string") spec = [{ commonName: spec }];
97
360
  if (!_isArray(spec)) throw E("bad-name", "a name must be a string, an array of RDNs, or raw Name DER");
98
361
  return b.sequence(spec.map(encodeRdn));
@@ -112,15 +375,51 @@ function makeBuilder(ctx) {
112
375
 
113
376
  // ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) ----
114
377
  function ia5Content(s) {
115
- s = String(s);
378
+ // Captured String / charCodeAt / bufferFrom: this converts a GeneralName value (a classified bare
379
+ // string or an object-form value) to the emitted IA5String bytes, so co-resident code replacing any of
380
+ // them after load could otherwise change which host the certificate names from the one that was checked.
381
+ s = _String(s);
116
382
  for (var i = 0; i < s.length; i++) {
117
- if (s.charCodeAt(i) > 0x7F) throw E("bad-input", "value requires 7-bit ASCII (IA5String): " + JSON.stringify(s));
383
+ if (_charCodeAt(s, i) > 0x7F) throw E("bad-input", "value requires 7-bit ASCII (IA5String): " + _stringify(s));
118
384
  }
119
- return Buffer.from(s, "latin1");
385
+ return _bufferFrom(s, "latin1");
386
+ }
387
+ // A bare-string GeneralName shorthand: classify a string into exactly one form, FAIL-CLOSED. RFC 5280
388
+ // sec. 4.2.1.6 GeneralName is a CHOICE, so an input that does not UNAMBIGUOUSLY match one form throws
389
+ // rather than being guessed -- the explicit object form ({ dNSName: s } etc.) is always the escape. The
390
+ // per-form syntax is decided by the character-scan predicates above (no regex). Precedence resolves the
391
+ // overlaps of the later tests: an IP literal is also LDH-with-dots (so it is tested first), and a
392
+ // scheme://authority URL can carry userinfo that reads like an email (so URI is tested before rfc822Name).
393
+ // Form detection only -- IA5String (7-bit) enforcement stays at ia5Content, so a non-ASCII value still
394
+ // throws at encode.
395
+ function _classifyBareGeneralName(s) {
396
+ if (_packIpLiteral(s) !== null) return { iPAddress: s };
397
+ // A string of only digit groups separated by dots is an IPv4 ATTEMPT; if packIpLiteral rejected it,
398
+ // it is a MALFORMED address, not a dNSName. It reads two ways (an invalid IP and a syntactically
399
+ // valid all-numeric name), so it is ambiguous and refused rather than silently encoded as a name the
400
+ // caller did not intend -- a genuine all-numeric name uses the explicit { dNSName: ... } form.
401
+ if (_looksLikeIPv4Shape(s)) {
402
+ throw E("bad-input", "the bare GeneralName string " + _stringify(s) + " looks like an IPv4 address but is not a valid one; pass { iPAddress: ... } for an address or { dNSName: ... } for a name");
403
+ }
404
+ if (_looksLikeUri(s)) return { uniformResourceIdentifier: s };
405
+ if (_looksLikeEmail(s)) return { rfc822Name: s };
406
+ if (_looksLikeDnsName(s)) return { dNSName: s };
407
+ var _qs = _stringify(s);
408
+ throw E("bad-input", "cannot classify the bare GeneralName string " + _qs +
409
+ " as a dNSName, rfc822Name, iPAddress, or URI; pass an explicit form object, e.g. { dNSName: " + _qs + " }");
120
410
  }
121
411
  function encodeGeneralName(entry) {
122
- if (!entry || typeof entry !== "object" || Buffer.isBuffer(entry)) throw E("bad-input", "a GeneralName must be an object with exactly one name form");
123
- var keys = Object.keys(entry);
412
+ // A bare non-empty string is shorthand -- normalized to its single-key object form, fail-closed,
413
+ // before the object checks below; an empty string is refused here (never classified).
414
+ if (typeof entry === "string") {
415
+ if (entry === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
416
+ entry = _classifyBareGeneralName(entry);
417
+ }
418
+ if (!entry || typeof entry !== "object" || _isBufferChk(entry)) throw E("bad-input", "a GeneralName must be an object with exactly one name form");
419
+ // Captured Object.keys / property read: this reads which form and value the certificate will emit for a
420
+ // GeneralName (a freshly classified bare string, or a caller object), so a replaced Object.keys must not
421
+ // be able to relabel the classified result into a different, attacker-chosen form after validation.
422
+ var keys = _objectKeys(entry);
124
423
  if (keys.length !== 1) throw E("bad-input", "a GeneralName entry must have exactly one form, got " + keys.length);
125
424
  var k = keys[0], v = entry[k];
126
425
  if (v == null || v === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
@@ -134,10 +433,10 @@ function makeBuilder(ctx) {
134
433
  // octets by hand (a GeneralName iPAddress is a bare host address, RFC 5280 sec. 4.2.1.6).
135
434
  var ipBuf = v;
136
435
  if (typeof v === "string") {
137
- ipBuf = ipUtils.packIpLiteral(v);
138
- if (ipBuf === null) throw E("bad-input", "iPAddress string is not a valid IPv4 or IPv6 literal: " + JSON.stringify(v));
436
+ ipBuf = _packIpLiteral(v);
437
+ if (ipBuf === null) throw E("bad-input", "iPAddress string is not a valid IPv4 or IPv6 literal: " + _stringify(v));
139
438
  }
140
- if (!Buffer.isBuffer(ipBuf) || (ipBuf.length !== 4 && ipBuf.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer or an IPv4/IPv6 string");
439
+ if (!_isBufferChk(ipBuf) || (ipBuf.length !== 4 && ipBuf.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer or an IPv4/IPv6 string");
141
440
  return b.contextPrimitive(7, ipBuf);
142
441
  case "directoryName": return b.explicit(4, encodeName(v)); // Name is a CHOICE -> the context tag is EXPLICIT
143
442
  case "otherName":
@@ -187,8 +486,8 @@ function makeBuilder(ctx) {
187
486
  function _resolveOid(n, label) {
188
487
  var dotted = O(n);
189
488
  if (dotted != null) return dotted;
190
- if (typeof n === "string" && DOTTED_OID_RE.test(n)) {
191
- // The regex fixes the general shape; b.oid's encoder is the authoritative X.660 arc-bounds check
489
+ if (typeof n === "string" && _isDottedOid(n)) {
490
+ // The shape scan fixes the general form; b.oid's encoder is the authoritative X.660 arc-bounds check
192
491
  // (a first arc 0/1 caps the second at 39; an arc must DER-encode). A failure there is this producer's
193
492
  // bad-input, not a leaked oid/* code, so every shared-builder consumer keeps its own error contract.
194
493
  try { b.oid(n); }
@@ -261,14 +560,14 @@ function makeBuilder(ctx) {
261
560
  return nodeCrypto.createHash("sha1").update(keyBytes).digest();
262
561
  }
263
562
  function skiKeyId(val, spkiDer) {
264
- if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, ErrorClass, ctx.prefix + "/bad-input", "subjectKeyIdentifier");
563
+ if (guard.bytes.isByteSource(val)) return guard.bytes.snapshotSource(val, ErrorClass, ctx.prefix + "/bad-input", "subjectKeyIdentifier");
265
564
  if (val === true) return spkiKeyId(spkiDer);
266
- throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
565
+ throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a BufferSource key id");
267
566
  }
268
567
 
269
568
  // ---- embedded-input validators ----
270
569
  function reqDer(v, what) {
271
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, ErrorClass, ctx.prefix + "/bad-input", what);
570
+ if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, ErrorClass, ctx.prefix + "/bad-input", what);
272
571
  throw E("bad-input", what + " must be a DER Buffer");
273
572
  }
274
573
  // Full validation of an embedded SubjectPublicKeyInfo, walked through the parser the decoder uses.
@@ -364,8 +663,8 @@ function makeBuilder(ctx) {
364
663
  } else if (typeof serial === "bigint") { v = serial; }
365
664
  else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw E("bad-serial", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer for a value above 2^53-1)"); v = BigInt(serial); }
366
665
  else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw E("bad-serial", "serialNumber string must be a decimal or 0x-hex integer", e); } }
367
- else if (Buffer.isBuffer(serial)) { v = serial.length ? BigInt("0x" + serial.toString("hex")) : 0n; }
368
- else { throw E("bad-serial", "serialNumber must be a BigInt, integer, hex string, or Buffer"); }
666
+ else if (guard.bytes.isByteSource(serial)) { var _sb = guard.bytes.source(serial, ErrorClass, ctx.prefix + "/bad-serial", "serialNumber"); v = _sb.length ? BigInt("0x" + _sb.toString("hex")) : 0n; }
667
+ else { throw E("bad-serial", "serialNumber must be a BigInt, integer, hex string, or BufferSource"); }
369
668
  if (v <= 0n) throw E("bad-serial", "serialNumber must be a positive integer (RFC 5280 sec. 4.1.2.2)");
370
669
  var tlv = b.integer(v);
371
670
  if (asn1.decode(tlv).content.length > 20) throw E("bad-serial", "serialNumber must not exceed 20 octets (RFC 5280 sec. 4.1.2.2)");
package/lib/rfc3339.js CHANGED
@@ -13,23 +13,72 @@
13
13
  * domain fault code.
14
14
  */
15
15
 
16
- // date "T" time with a zone (Z or a numeric offset); an optional fractional second.
17
- var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
16
+ // Exactly `count` ASCII digits at `start`, as an integer, or null on any non-digit or short read.
17
+ // The grammar is decided character by character (no regex, whose match protocol re-dispatches through
18
+ // a replaceable `exec`); `\d` in the original is ASCII [0-9] only, matched here as 48..57.
19
+ function _digitsAt(v, start, count) {
20
+ if (start + count > v.length) return null;
21
+ var val = 0;
22
+ for (var i = 0; i < count; i++) {
23
+ var c = v.charCodeAt(start + i);
24
+ if (c < 48 || c > 57) return null;
25
+ val = val * 10 + (c - 48);
26
+ }
27
+ return val;
28
+ }
29
+
30
+ // RFC 3339 date-time scanned into its components, or null on any grammar violation:
31
+ // YYYY "-" MM "-" DD [Tt] HH ":" MM ":" SS, an optional "." + one-or-more fraction digits, then a
32
+ // zone -- [Zz] or ([+-]) OFFHH ":" OFFMM -- with nothing after. Calendar validity is checked by the
33
+ // caller. `offset` carries the numeric-offset hours/minutes when present, else null.
34
+ function _scanDateTime(v) {
35
+ var n = v.length, p = 0;
36
+ var year = _digitsAt(v, p, 4); if (year === null) return null; p += 4;
37
+ if (v.charAt(p) !== "-") return null; p += 1;
38
+ var month = _digitsAt(v, p, 2); if (month === null) return null; p += 2;
39
+ if (v.charAt(p) !== "-") return null; p += 1;
40
+ var day = _digitsAt(v, p, 2); if (day === null) return null; p += 2;
41
+ var t = v.charAt(p); if (t !== "T" && t !== "t") return null; p += 1;
42
+ var hour = _digitsAt(v, p, 2); if (hour === null) return null; p += 2;
43
+ if (v.charAt(p) !== ":") return null; p += 1;
44
+ var min = _digitsAt(v, p, 2); if (min === null) return null; p += 2;
45
+ if (v.charAt(p) !== ":") return null; p += 1;
46
+ var sec = _digitsAt(v, p, 2); if (sec === null) return null; p += 2;
47
+ if (v.charAt(p) === ".") { // optional fractional second: "." + one or more digits
48
+ p += 1;
49
+ var fracStart = p;
50
+ while (p < n) { var fc = v.charCodeAt(p); if (fc < 48 || fc > 57) break; p += 1; }
51
+ if (p === fracStart) return null;
52
+ }
53
+ var z = v.charAt(p), offset = null;
54
+ if (z === "Z" || z === "z") {
55
+ p += 1;
56
+ } else if (z === "+" || z === "-") {
57
+ p += 1;
58
+ var offHour = _digitsAt(v, p, 2); if (offHour === null) return null; p += 2;
59
+ if (v.charAt(p) !== ":") return null; p += 1;
60
+ var offMin = _digitsAt(v, p, 2); if (offMin === null) return null; p += 2;
61
+ offset = { hour: offHour, min: offMin };
62
+ } else {
63
+ return null; // a zone is required
64
+ }
65
+ if (p !== n) return null; // no trailing characters
66
+ return { year: year, month: month, day: day, hour: hour, min: min, sec: sec, offset: offset };
67
+ }
18
68
 
19
69
  // isValid(v) -> boolean: v is a syntactically well-formed AND calendar-valid RFC 3339 date-time string.
20
70
  function isValid(v) {
21
71
  if (typeof v !== "string") return false;
22
- var m = RFC3339_RE.exec(v);
23
- if (!m) return false;
24
- var year = +m[1], month = +m[2], day = +m[3], hour = +m[4], min = +m[5], sec = +m[6];
25
- if (month < 1 || month > 12) return false;
72
+ var t = _scanDateTime(v);
73
+ if (!t) return false;
74
+ if (t.month < 1 || t.month > 12) return false;
26
75
  // Reject a :60 leap second (Node's Date.parse returns NaN for it, so a comparison on such a value
27
76
  // would silently pass) and any hour/minute/second out of range.
28
- if (hour > 23 || min > 59 || sec > 59) return false;
29
- var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
77
+ if (t.hour > 23 || t.min > 59 || t.sec > 59) return false;
78
+ var leap = (t.year % 4 === 0 && t.year % 100 !== 0) || t.year % 400 === 0;
30
79
  var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
31
- if (day < 1 || day > daysInMonth[month - 1]) return false;
32
- if (m[7]) { if (+m[8] > 23 || +m[9] > 59) return false; } // a numeric zone offset must itself be valid
80
+ if (t.day < 1 || t.day > daysInMonth[t.month - 1]) return false;
81
+ if (t.offset) { if (t.offset.hour > 23 || t.offset.min > 59) return false; } // a numeric zone offset must itself be valid
33
82
  return true;
34
83
  }
35
84
 
@@ -44,20 +93,29 @@ function parse(v, E, code, label) {
44
93
  // RFC 3339 sec. 5.6 `full-date`: a calendar date with no time component. A distinct production from
45
94
  // the date-time above, and deliberately a separate pair of functions instead of a flag: a consumer
46
95
  // that expects one and is handed the other has a malformed value, not a permissible variant.
47
- var FULL_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
96
+ // RFC 3339 full-date scanned into its components (YYYY "-" MM "-" DD, nothing after), or null.
97
+ function _scanFullDate(v) {
98
+ var p = 0;
99
+ var year = _digitsAt(v, p, 4); if (year === null) return null; p += 4;
100
+ if (v.charAt(p) !== "-") return null; p += 1;
101
+ var month = _digitsAt(v, p, 2); if (month === null) return null; p += 2;
102
+ if (v.charAt(p) !== "-") return null; p += 1;
103
+ var day = _digitsAt(v, p, 2); if (day === null) return null; p += 2;
104
+ if (p !== v.length) return null;
105
+ return { year: year, month: month, day: day };
106
+ }
48
107
 
49
108
  // isValidDate(v) -> boolean: v is a syntactically well-formed AND calendar-valid RFC 3339 full-date.
50
109
  // Calendar validity matters as much as shape: "2021-02-30" parses as a Date in JS by rolling over
51
110
  // into March, so a shape-only check would silently accept a day that does not exist.
52
111
  function isValidDate(v) {
53
112
  if (typeof v !== "string") return false;
54
- var m = FULL_DATE_RE.exec(v);
55
- if (!m) return false;
56
- var year = +m[1], month = +m[2], day = +m[3];
57
- if (month < 1 || month > 12) return false;
58
- var leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
113
+ var t = _scanFullDate(v);
114
+ if (!t) return false;
115
+ if (t.month < 1 || t.month > 12) return false;
116
+ var leap = (t.year % 4 === 0 && t.year % 100 !== 0) || t.year % 400 === 0;
59
117
  var daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
60
- return day >= 1 && day <= daysInMonth[month - 1];
118
+ return t.day >= 1 && t.day <= daysInMonth[t.month - 1];
61
119
  }
62
120
 
63
121
  // parseDate(v, E, code, label) -> Date at UTC midnight. Anchored explicitly at UTC ("T00:00:00Z")
@@ -263,16 +263,37 @@ function _specialText(node) {
263
263
  // name one certificate. A natively signed certificate is exempt: sec. 3.1.4 says bytes and tag 48
264
264
  // there "do not correspond to any predefined text string encoding and may also be used for other attribute
265
265
  // types", so no canonical text spelling exists to hold one to.
266
- var _HEX_OPTIMIZED = /^(?:[0-9a-f]{2})+$/;
267
- var _EUI64_TEXT = /^(?:[0-9A-F]{2}-){7}[0-9A-F]{2}$/;
266
+ // Even-length lowercase hex ("HHHH..."), decided by character so no regex match protocol -- a live,
267
+ // replaceable exec -- stands between the canonicalization gate and the value it inspects.
268
+ function _isEvenHexLower(s) {
269
+ var n = s.length;
270
+ if (n < 2 || (n & 1) !== 0) return false;
271
+ for (var i = 0; i < n; i++) {
272
+ var c = s.charCodeAt(i);
273
+ if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) return false; // 0-9 a-f
274
+ }
275
+ return true;
276
+ }
277
+ // EUI-64 text form: eight uppercase hex pairs joined by single hyphens ("HH-HH-...-HH", 23 chars).
278
+ function _isEui64Text(s) {
279
+ if (s.length !== 23) return false;
280
+ for (var i = 0; i < 23; i++) {
281
+ if ((i % 3) === 2) { if (s.charCodeAt(i) !== 45) return false; } // "-" at 2,5,...,20
282
+ else {
283
+ var c = s.charCodeAt(i);
284
+ if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 70))) return false; // 0-9 A-F
285
+ }
286
+ }
287
+ return true;
288
+ }
268
289
  function _assertCanonicalSpecialText(node, isNative, E) {
269
290
  if (isNative) return;
270
291
  if (node.majorType === 3) {
271
292
  var t = cbor.read.textString(node);
272
- if (t.length >= 2 && _HEX_OPTIMIZED.test(t)) {
293
+ if (t.length >= 2 && _isEvenHexLower(t)) {
273
294
  throw _err(E, "a text attribute value of even-length hex characters must be encoded as a CBOR byte string (draft sec. 3.1.4)");
274
295
  }
275
- if (_EUI64_TEXT.test(t)) {
296
+ if (_isEui64Text(t)) {
276
297
  throw _err(E, "a text attribute value in EUI-64 form must be encoded as a CBOR tag-48 MAC address (draft sec. 3.1.4)");
277
298
  }
278
299
  return;
@@ -1973,7 +1994,7 @@ function _reconstructDer(r, sigNode) {
1973
1994
  // The signature is re-wrapped as a DER ECDSA-Sig-Value from the fixed-width r||s, so only an ECDSA
1974
1995
  // signature algorithm is in the type-3 reconstruction covered set (an RSA/EdDSA signature is raw bytes,
1975
1996
  // not r||s, so it is rejected). A wrong-length r||s surfaces the caller's typed code.
1976
- if (!/^ecdsa/i.test(r.signatureAlgorithm.name || "")) {
1997
+ if (String(r.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) {
1977
1998
  throw _err("c509/non-invertible", "type-3 signature reconstruction covers only ECDSA; got " + r.signatureAlgorithm.name);
1978
1999
  }
1979
2000
  // The fixed-width r||s must split at a real curve field width: P-256/384/521 = 64/96/132 bytes
@@ -2143,7 +2164,7 @@ function _encAlgorithm(alg, toInt, key) {
2143
2164
  function _encSpecialText(rdn) {
2144
2165
  if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
2145
2166
  var v = String(rdn.value);
2146
- if (v.length >= 2 && _HEX_OPTIMIZED.test(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
2167
+ if (v.length >= 2 && _isEvenHexLower(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
2147
2168
  return cbor.build.textString(v);
2148
2169
  }
2149
2170
  // A Name -> CBOR: null (issuer only) | a bare SpecialText single utf8 commonName | an array of RDN pairs.
@@ -2293,9 +2314,15 @@ function _encodeC509Array(r) {
2293
2314
  // value (draft-20 sec. 3.2.3): an FF-FE-in-the-middle EUI-64 collapses to its 6-byte EUI-48, else the
2294
2315
  // 8-byte EUI-64 verbatim. A non-MAC commonName returns null (encoded as text). The exact inverse of
2295
2316
  // _macToEui64String, so the reconstruction rebuilds the identical DER commonName string.
2317
+ // The "-" characters removed from a string, by character scan (no regex replace protocol).
2318
+ function _stripHyphens(s) {
2319
+ var out = "";
2320
+ for (var i = 0; i < s.length; i++) { var ch = s.charAt(i); if (ch !== "-") out += ch; }
2321
+ return out;
2322
+ }
2296
2323
  function _euiFromCn(value) {
2297
- if (!/^[0-9A-F]{2}(-[0-9A-F]{2}){7}$/.test(value)) return null;
2298
- var bytes = Buffer.from(value.replace(/-/g, ""), "hex");
2324
+ if (!_isEui64Text(value)) return null;
2325
+ var bytes = Buffer.from(_stripHyphens(value), "hex");
2299
2326
  if (bytes[3] === 0xff && bytes[4] === 0xfe) return Buffer.concat([bytes.subarray(0, 3), bytes.subarray(5)]);
2300
2327
  return bytes;
2301
2328
  }
@@ -2415,7 +2442,7 @@ _derToType3 = function (input, opts) {
2415
2442
  // (A v3 certificate with the extensions field OMITTED is fully supported: sec. 3.1.10 encodes
2416
2443
  // an omitted 'extensions' field as an empty CBOR array.)
2417
2444
  if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
2418
- if (!/^ecdsa/i.test(c.signatureAlgorithm.name || "")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
2445
+ if (String(c.signatureAlgorithm.name || "").toLowerCase().indexOf("ecdsa") !== 0) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
2419
2446
  if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
2420
2447
  var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
2421
2448
  var curve = oid.name(curveOid);
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