@blamejs/pki 0.8.49 → 0.8.51

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.
@@ -68,6 +68,32 @@ var _promiseAny = Promise.any;
68
68
  * present on the supported runtime floor. */
69
69
  var _promiseTry = Promise["try"];
70
70
  var _promiseWithResolvers = Promise.withResolvers;
71
+ /** @internal The URL parser, captured for the reason the promise constructor is: what it returns names
72
+ * the host a request is sent to. A replacement installed while a call is in flight answers with a
73
+ * genuine URL of its own, and an endpoint built from that answer reaches a host the caller never
74
+ * wrote while the url the caller passed is still the one the call reports. `canParse` is captured
75
+ * beside it because a module that decides a form with it is deciding from the same binding. */
76
+ var _URL = URL;
77
+ var _urlCanParse = URL.canParse;
78
+ /** @internal And its accessors, because capturing a constructor does not capture the prototype the
79
+ * value it returns reads through. MEASURED: with the constructor captured, a header getter that
80
+ * redefined `URL.prototype.href` had every request go to the host its own getter named while the url
81
+ * the caller passed was still the one the call reported. The parts of a destination are read through
82
+ * these, and the two that build an endpoint are written through the captured setters. */
83
+ var _urlHref = getter(URL.prototype, "href", "URL.prototype");
84
+ var _urlOrigin = getter(URL.prototype, "origin", "URL.prototype");
85
+ var _urlProtocol = getter(URL.prototype, "protocol", "URL.prototype");
86
+ var _urlPathname = getter(URL.prototype, "pathname", "URL.prototype");
87
+ var _urlSearch = getter(URL.prototype, "search", "URL.prototype");
88
+ var _urlHash = getter(URL.prototype, "hash", "URL.prototype");
89
+ var _urlHostname = getter(URL.prototype, "hostname", "URL.prototype");
90
+ var _urlPort = getter(URL.prototype, "port", "URL.prototype");
91
+ var _urlUsername = getter(URL.prototype, "username", "URL.prototype");
92
+ var _urlPassword = getter(URL.prototype, "password", "URL.prototype");
93
+ var _urlSearchParams = getter(URL.prototype, "searchParams", "URL.prototype");
94
+ var _urlSetPathname = setter(URL.prototype, "pathname", "URL.prototype");
95
+ var _urlSetSearch = setter(URL.prototype, "search", "URL.prototype");
96
+ var _urlSetHash = setter(URL.prototype, "hash", "URL.prototype");
71
97
  var _asyncIterator = Symbol.asyncIterator;
72
98
 
73
99
  var _arrForEach = uncurry(Array.prototype.forEach);
@@ -160,6 +186,18 @@ function getter(proto, name, who) {
160
186
  return uncurry(d.get);
161
187
  }
162
188
 
189
+ /** @internal The write half of the same question: a replaced setter can drop the write and leave the
190
+ * value it was given to change unchanged. */
191
+ // @enforced-by behavioral -- reading a descriptor's `set` is not distinguishable from testing one
192
+ function setter(proto, name, who) {
193
+ var d = _getOwnPropertyDescriptor(proto, name);
194
+ if (!d || typeof d.set !== "function") {
195
+ throw new TypeError("guard.intrinsic.setter: this runtime has no intrinsic " + who + "." +
196
+ name + " accessor, so the value's own would have to be invoked instead");
197
+ }
198
+ return uncurry(d.set);
199
+ }
200
+
163
201
  var _taByteLength = getter(_getPrototypeOf(Uint8Array.prototype), "byteLength", "%TypedArray%.prototype");
164
202
  var _dvByteLength = getter(DataView.prototype, "byteLength", "DataView.prototype");
165
203
 
@@ -231,6 +269,23 @@ module.exports = Object.freeze({
231
269
  promiseAny: _promiseAny,
232
270
  promiseTry: _promiseTry,
233
271
  promiseWithResolvers: _promiseWithResolvers,
272
+ setter: setter,
273
+ URL: _URL,
274
+ urlCanParse: _urlCanParse,
275
+ urlHref: _urlHref,
276
+ urlOrigin: _urlOrigin,
277
+ urlProtocol: _urlProtocol,
278
+ urlPathname: _urlPathname,
279
+ urlSearch: _urlSearch,
280
+ urlHash: _urlHash,
281
+ urlHostname: _urlHostname,
282
+ urlPort: _urlPort,
283
+ urlUsername: _urlUsername,
284
+ urlPassword: _urlPassword,
285
+ urlSearchParams: _urlSearchParams,
286
+ urlSetPathname: _urlSetPathname,
287
+ urlSetSearch: _urlSetSearch,
288
+ urlSetHash: _urlSetHash,
234
289
  asyncIterator: _asyncIterator,
235
290
  forEach: _arrForEach,
236
291
  map: _arrMap,
@@ -7,6 +7,13 @@ var crypto = require("crypto");
7
7
  var constants = require("./constants");
8
8
  var intrinsic = require("./guard-intrinsic");
9
9
  var _hasOwn = intrinsic.hasOwn;
10
+ /** @internal The URL parser, taken at load: the origin it reports is what a challenge is bound to.
11
+ * Its accessors are taken with it, since the constructor's capture does not cover the prototype the
12
+ * value reads its parts off and this origin is what a credential is scoped by. */
13
+ var _URL = intrinsic.URL;
14
+ var _urlOrigin = intrinsic.urlOrigin;
15
+ var _urlPathname = intrinsic.urlPathname;
16
+ var _urlSearch = intrinsic.urlSearch;
10
17
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
11
18
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
12
19
  var _fromCharCode = intrinsic.fromCharCode;
@@ -293,7 +300,7 @@ function answer(challenge, params, E) {
293
300
  function _domainEntry(d) {
294
301
  d = String(d == null ? "" : d);
295
302
  if (d.charAt(0) === "/") return { origin: null, full: d };
296
- try { var u = new URL(d); return { origin: u.origin.toLowerCase(), full: (u.pathname || "/") + (u.search || "") }; }
303
+ try { var u = new _URL(d); return { origin: _urlOrigin(u).toLowerCase(), full: (_urlPathname(u) || "/") + (_urlSearch(u) || "") }; }
297
304
  catch (_e) { return null; }
298
305
  }
299
306
 
@@ -79,6 +79,21 @@ var intrinsic = require("./guard-intrinsic");
79
79
  * step rejected proceed, and `peerChain`'s refusal to tunnel through a proxy is one of those. Taken at
80
80
  * load. */
81
81
  var _Promise = intrinsic.Promise;
82
+ /** @internal The URL parser, taken at load: what it returns names the host the socket is opened to,
83
+ * for the request itself and for a proxy it is tunneled through. The conversion it is handed is taken
84
+ * with it, since a replacement answers with a host of its own. */
85
+ var _URL = intrinsic.URL;
86
+ var _stringOf = intrinsic.String;
87
+ /** @internal The parts of that destination are read through the accessors captured at load, not off the
88
+ * prototype the parsed value reads through: a replaced `URL.prototype.hostname` names the host this
89
+ * module then opens a socket to. */
90
+ var _urlProtocol = intrinsic.urlProtocol;
91
+ var _urlHostname = intrinsic.urlHostname;
92
+ var _urlPort = intrinsic.urlPort;
93
+ var _urlPathname = intrinsic.urlPathname;
94
+ var _urlSearch = intrinsic.urlSearch;
95
+ var _urlUsername = intrinsic.urlUsername;
96
+ var _urlPassword = intrinsic.urlPassword;
82
97
  var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
83
98
  var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
84
99
  var frameworkError = require("./framework-error");
@@ -331,7 +346,10 @@ function snapshotProxy(p) {
331
346
  }
332
347
  }
333
348
 
334
- function _validateProxy(request, defaults, E, C, url) {
349
+ /** @internal `originHost`, `statedPort` and `scheme` are the values the request path already read off
350
+ * the parsed origin; they are passed in rather than read again here, so the authority this builds for
351
+ * the CONNECT target is the same authority the socket options were built from. */
352
+ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, originScheme) {
335
353
  var proxy = request.proxy !== undefined ? request.proxy : defaults.proxy;
336
354
  if (proxy === undefined || proxy === null) return null;
337
355
  guard.identifier.assertPlainRecord(proxy, E, C("bad-proxy"), "opts.proxy");
@@ -339,11 +357,12 @@ function _validateProxy(request, defaults, E, C, url) {
339
357
  guard.identifier.assertKnownKeys(proxy, PROXY_KEYS, E, C("bad-proxy"), "opts.proxy has an unknown option ");
340
358
  if (typeof proxy.url !== "string") throw E(C("bad-proxy"), "opts.proxy.url must be a string, got " + guard.text.showValue(proxy.url));
341
359
  var purl;
342
- try { purl = new URL(proxy.url); }
360
+ try { purl = new _URL(proxy.url); }
343
361
  catch (e) { throw E(C("bad-proxy"), "opts.proxy.url did not parse: " + guard.text.showValue(proxy.url), e); }
344
- if (purl.protocol !== "http:" && purl.protocol !== "https:") throw E(C("bad-proxy"), "opts.proxy.url must be an http or https URL, got " + purl.protocol);
345
- if (purl.username || purl.password) throw E(C("bad-proxy"), "opts.proxy.url must not carry credentials in its userinfo; supply opts.proxy.auth over an https proxy");
346
- var secure = purl.protocol === "https:";
362
+ var pscheme = _urlProtocol(purl);
363
+ if (pscheme !== "http:" && pscheme !== "https:") throw E(C("bad-proxy"), "opts.proxy.url must be an http or https URL, got " + pscheme);
364
+ if (_urlUsername(purl) || _urlPassword(purl)) throw E(C("bad-proxy"), "opts.proxy.url must not carry credentials in its userinfo; supply opts.proxy.auth over an https proxy");
365
+ var secure = pscheme === "https:";
347
366
  if (!secure && proxy.tls !== undefined && proxy.tls !== null) throw E(C("bad-proxy"), "opts.proxy.tls applies only to an https proxy; an http proxy connection is plaintext (a supplied tls policy would be silently ignored)");
348
367
 
349
368
  var auth = null;
@@ -396,14 +415,14 @@ function _validateProxy(request, defaults, E, C, url) {
396
415
  proxyTls = { ca: pCa.length ? pCa : undefined, minVersion: pMinVersion, servername: (ptls && ptls.servername) || null };
397
416
  }
398
417
 
399
- var originHost = _stripIpv6Brackets(url.hostname);
400
418
  var originAuthHost = nodeNet.isIP(originHost) === 6 ? "[" + originHost + "]" : originHost;
401
- var phost = _stripIpv6Brackets(purl.hostname);
419
+ var phost = _stripIpv6Brackets(_urlHostname(purl));
402
420
  /** @internal The CONNECT target is the origin's own authority, so an origin URL that states no
403
421
  * port takes the default for the origin's scheme, not the proxy's. Defaulting it to 443 opens
404
422
  * the tunnel to a port an `http:` origin does not listen on. */
405
- var originPort = url.port || (url.protocol === "http:" ? 80 : 443);
406
- return { secure: secure, host: phost, port: purl.port ? +purl.port : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + originPort, auth: auth, proxyTls: proxyTls };
423
+ var originPort = originStatedPort || (originScheme === "http:" ? 80 : 443);
424
+ var pport = _urlPort(purl);
425
+ return { secure: secure, host: phost, port: pport ? +pport : (secure ? 443 : 80), originHost: originHost, originAuthority: originAuthHost + ":" + originPort, auth: auth, proxyTls: proxyTls };
407
426
  }
408
427
 
409
428
  function _basicProxyHeader(auth) {
@@ -484,11 +503,14 @@ function httpsTransport(defaults) {
484
503
  var E = typeof defaults.E === "function" ? defaults.E : defaultE;
485
504
  var prefix = defaults.errPrefix || "transport";
486
505
  function C(name) { return prefix + "/" + name; }
487
- var tlsDefaults = defaults.tls || {};
506
+ /** @internal No prototype where no TLS was bound: an empty object literal inherits from
507
+ * `Object.prototype`, so a value installed there would answer for a setting nobody set, and one of
508
+ * the settings read from this record is the opt-in that turns away an unpinned server. */
509
+ var tlsDefaults = defaults.tls != null ? defaults.tls : intrinsic.create(null);
488
510
 
489
511
  function _prepare(request) {
490
512
  var url;
491
- try { url = new URL(String(request.url)); }
513
+ try { url = new _URL(_stringOf(request.url)); }
492
514
  catch (e) { throw E(C("bad-url"), "the request URL did not parse: " + String(request.url), e); }
493
515
  /** @internal https is the floor, and `request.allowPlaintextHttp` is the one thing that moves
494
516
  * it, per request and never as a default. It exists for the two objects whose integrity does
@@ -499,11 +521,16 @@ function httpsTransport(defaults) {
499
521
  * timeout, and the refusal to follow a redirect, which is what would otherwise turn one
500
522
  * plaintext hop into a downgrade of the next. */
501
523
  var plaintextAllowed = request.allowPlaintextHttp === true;
502
- if (url.protocol !== "https:" && !(plaintextAllowed && url.protocol === "http:")) {
503
- throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + url.protocol);
524
+ /** @internal Read ONCE, through the captured accessor, and every decision below made from that
525
+ * string. The scheme decides whether the request is allowed at all, whether an anchor is
526
+ * required and which port it defaults to, so a prototype whose getter answers each read
527
+ * differently would have those three agree on nothing. */
528
+ var scheme = _urlProtocol(url);
529
+ if (scheme !== "https:" && !(plaintextAllowed && scheme === "http:")) {
530
+ throw E(C("insecure-url"), "transport requires https (RFC 7030 sec. 3.3), got " + scheme);
504
531
  }
505
532
 
506
- var reqTls = request.tls || {};
533
+ var reqTls = request.tls != null ? request.tls : intrinsic.create(null);
507
534
  var anchors = reqTls.anchors !== undefined ? reqTls.anchors : tlsDefaults.anchors;
508
535
  var useSystem = (reqTls.useSystemStore !== undefined ? reqTls.useSystemStore : tlsDefaults.useSystemStore) === true;
509
536
  var hasAnchors = anchors !== undefined && anchors !== null && !(Array.isArray(anchors) && anchors.length === 0);
@@ -511,7 +538,7 @@ function httpsTransport(defaults) {
511
538
  * neither an anchor nor the system store is refused rather than left unpinned. A plaintext hop
512
539
  * negotiates no certificate, so there is nothing for an anchor to pin; what protects the object
513
540
  * fetched over one is its own signature, checked after the fetch by the verb that asked. */
514
- if (url.protocol === "https:" && !hasAnchors && !useSystem) {
541
+ if (scheme === "https:" && !hasAnchors && !useSystem) {
515
542
  throw E(C("no-trust-anchors"), "no explicit trust anchor and useSystemStore not set -- refusing an unpinned server (RFC 7030 sec. 3.6)");
516
543
  }
517
544
 
@@ -534,13 +561,14 @@ function httpsTransport(defaults) {
534
561
  reqHeaders["Content-Length"] = Buffer.byteLength(body);
535
562
  }
536
563
 
537
- var host = _stripIpv6Brackets(url.hostname);
538
- var plaintext = url.protocol === "http:";
564
+ var host = _stripIpv6Brackets(_urlHostname(url));
565
+ var plaintext = scheme === "http:";
566
+ var statedPort = _urlPort(url);
539
567
  var options = {
540
568
  method: request.method || "GET",
541
569
  hostname: host,
542
- port: url.port || (plaintext ? 80 : 443),
543
- path: url.pathname + url.search,
570
+ port: statedPort || (plaintext ? 80 : 443),
571
+ path: _urlPathname(url) + _urlSearch(url),
544
572
  headers: reqHeaders,
545
573
  minVersion: minVersion,
546
574
  rejectUnauthorized: true,
@@ -566,7 +594,7 @@ function httpsTransport(defaults) {
566
594
  };
567
595
  }
568
596
  var proxyInfo;
569
- try { proxyInfo = _validateProxy(request, defaults, E, C, url); }
597
+ try { proxyInfo = _validateProxy(request, defaults, E, C, host, statedPort, scheme); }
570
598
  catch (e) {
571
599
  var typedProxyError = false;
572
600
  try {
package/lib/inspect.js CHANGED
@@ -449,8 +449,9 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
449
449
  }
450
450
  return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
451
451
  },
452
- /** @internal RFC 7633 sec. 6 carries TLS ExtensionType values, so each is named from the registry
453
- * where the toolkit knows it and printed as its number where it does not. */
452
+ /** @internal RFC 7633 sec. 4.1 binds these values to the IANA TLS Extensions registry, so each is
453
+ * named from the registry where the toolkit knows it and printed as its number where it does not.
454
+ * sec. 6 is IANA Considerations and registers the extension's own OID; it carries no value. */
454
455
  tlsFeature: function (decoded, inner) {
455
456
  return (decoded.features || []).map(function (f) {
456
457
  var text = String(f);
package/lib/lint.js CHANGED
@@ -303,9 +303,16 @@ var OID_CPS = oid.byName("cps");
303
303
  /** @internal The extensions RFC 5280 says a conforming CA SHOULD mark non-critical, keyed by OID
304
304
  * with the clause; the MUSTs live in the shared CERT_FIXED_CRITICALITY table. The subjectAltName
305
305
  * and extKeyUsage recommendations are conditional and have their own rules. */
306
+ /** @internal The per-extension citation travels with the finding, so a row here may come from a
307
+ * document other than RFC 5280: RFC 7633 sec. 4 states the same shape of recommendation for the TLS
308
+ * feature extension, and its clause closes by naming the case where critical is what the caller
309
+ * wants, so this grades it rather than refusing it. Adding a row here grades the extension in the
310
+ * DEFAULT set, which a named profile would not; it does not feed `_isUnknownExtension`, so the
311
+ * unknown-critical verdict a critical one draws is unaffected. */
306
312
  var CERT_RECOMMENDED_NON_CRITICAL = Object.create(null);
307
313
  CERT_RECOMMENDED_NON_CRITICAL[oid.byName("issuerAltName")] = "RFC 5280 sec. 4.2.1.7";
308
314
  CERT_RECOMMENDED_NON_CRITICAL[oid.byName("cRLDistributionPoints")] = "RFC 5280 sec. 4.2.1.13";
315
+ CERT_RECOMMENDED_NON_CRITICAL[oid.byName("tlsFeature")] = "RFC 7633 sec. 4";
309
316
  var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
310
317
  function _hasControlChar(str) {
311
318
  for (var i = 0; i < str.length; i++) {
@@ -488,7 +495,7 @@ var RFC5280_RULES = [
488
495
  _criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
489
496
  "RFC 5280 4.2.1.3", "the keyUsage extension should be marked critical"),
490
497
  {
491
- id: "lint/rfc5280/recommended-criticality", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.7 / 4.2.1.13",
498
+ id: "lint/rfc5280/recommended-criticality", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.7 / 4.2.1.13, RFC 7633 sec. 4",
492
499
  message: "an extension a conforming CA should mark non-critical is marked critical",
493
500
  check: function (cert) {
494
501
  var rows = [];
@@ -4056,6 +4063,96 @@ var RFC9763_RULES = [
4056
4063
  },
4057
4064
  ];
4058
4065
 
4066
+ /** @internal RFC 7633. The criticality recommendation lives in the always-on table above, because
4067
+ * sec. 4 states it for every certificate carrying the extension. These rows read the VALUE, which
4068
+ * RFC 7633 leaves open in three ways and another document speaks to in a fourth:
4069
+ *
4070
+ * sec. 4 writes `Features ::= SEQUENCE OF INTEGER` with no range, no ordering rule and no prohibition
4071
+ * on repeats, so none of those is a conformance failure and none is refused by the encoder. What
4072
+ * sec. 4.1 does say is that the values are "TLS extension identifiers ... as specified in the IANA
4073
+ * Transport Layer Security (TLS) Extensions registry", and that registry is 16 bits wide, so a value
4074
+ * outside 0..65535 names nothing in it. That is a `warn`, not an `error`: the finding rests on a
4075
+ * reference to a registry rather than on a syntax rule, and at `error` in a named profile it would
4076
+ * make `pki.x509.sign` refuse to emit the certificate.
4077
+ *
4078
+ * The transparency_info row is RFC 9162 sec. 7.2, read directly: "A certification authority SHOULD
4079
+ * NOT issue any certificate that identifies the transparency_info TLS extension in a TLS feature
4080
+ * extension [RFC7633], because TLS servers are not required to support the transparency_info TLS
4081
+ * extension in order to participate in CT". That is the only clause outside RFC 7633 that constrains
4082
+ * this field.
4083
+ *
4084
+ * There is NO deprecation row for status_request_v2, and the absence is deliberate. The IANA registry
4085
+ * still lists `17 status_request_v2 [RFC 6961]` as Recommended and marks it neither obsolete nor
4086
+ * deprecated; RFC 9846 contains no occurrence of "status_request_v2" and none of "7633", so no
4087
+ * document states a deprecation to cite. What is true is narrower: RFC 9846 obsoletes RFC 6961, which
4088
+ * defined the extension, and the ExtensionType enum at its sec. 4.3 does not list 17 at all, so a
4089
+ * certificate naming feature 17 asks a TLS 1.3 server for something that version does not define
4090
+ * while a TLS 1.2 deployment can still satisfy it. That is reported at `notice`, with the clause that
4091
+ * supports it and nothing more. */
4092
+ var RFC7633_RULES = [
4093
+ {
4094
+ id: "lint/rfc7633/feature-outside-registry-width", severity: "warn", source: "rfc7633", citation: "RFC 7633 4.1",
4095
+ message: "a TLS feature value lies outside the 16-bit width of the IANA TLS ExtensionType registry sec. 4.1 points at, so it names no extension in it",
4096
+ appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
4097
+ check: function (cert, ctx) {
4098
+ var d = ctx.decode("tlsFeature");
4099
+ var features = (d && d.value && d.value.features) || [];
4100
+ var rows = [];
4101
+ for (var i = 0; i < features.length; i++) {
4102
+ var f = features[i];
4103
+ if (f < 0n || f > 65535n) rows.push({ context: { feature: String(f), width: "0..65535" } });
4104
+ }
4105
+ return rows.length ? rows : null;
4106
+ },
4107
+ },
4108
+ {
4109
+ id: "lint/rfc7633/feature-repeated", severity: "notice", source: "rfc7633", citation: "RFC 7633 4",
4110
+ message: "a TLS feature value appears more than once; the syntax permits it and the set it names is the same, so the repeat carries no meaning",
4111
+ appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
4112
+ check: function (cert, ctx) {
4113
+ var d = ctx.decode("tlsFeature");
4114
+ var features = (d && d.value && d.value.features) || [];
4115
+ /** @internal One pass with a set of what has been seen, not a rescan of every preceding value.
4116
+ * This rule is in the default set and a certificate reaching it is unauthenticated, so the
4117
+ * pairwise form is work the input chooses: MEASURED, 20000 distinct features in an 80 KiB
4118
+ * certificate took 355 ms against 5 ms for 50 of them. A set compares the bigint values. */
4119
+ var seen = new Set();
4120
+ var rows = [];
4121
+ for (var i = 0; i < features.length; i++) {
4122
+ if (seen.has(features[i])) rows.push({ context: { feature: String(features[i]) } });
4123
+ else seen.add(features[i]);
4124
+ }
4125
+ return rows.length ? rows : null;
4126
+ },
4127
+ },
4128
+ {
4129
+ id: "lint/rfc7633/transparency-info-feature", severity: "warn", source: "rfc7633", citation: "RFC 9162 7.2",
4130
+ message: "a certification authority should not issue a certificate naming the transparency_info TLS extension (52) in a TLS feature extension, because a TLS server need not support it to participate in certificate transparency",
4131
+ appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
4132
+ check: function (cert, ctx) {
4133
+ var d = ctx.decode("tlsFeature");
4134
+ var features = (d && d.value && d.value.features) || [];
4135
+ for (var i = 0; i < features.length; i++) {
4136
+ if (features[i] === 52n) return { context: { feature: "52", name: "transparency_info" } };
4137
+ }
4138
+ return null;
4139
+ },
4140
+ },
4141
+ {
4142
+ id: "lint/rfc7633/feature-undefined-in-tls13", severity: "notice", source: "rfc7633", citation: "RFC 9846 4.3",
4143
+ message: "a TLS feature value names status_request_v2 (17), which RFC 9846 does not list in its ExtensionType enum and whose defining document RFC 6961 it obsoletes, so a TLS 1.3 server cannot satisfy it",
4144
+ appliesTo: function (cert, ctx) { return !!ctx.raw("tlsFeature"); },
4145
+ check: function (cert, ctx) {
4146
+ var d = ctx.decode("tlsFeature");
4147
+ var features = (d && d.value && d.value.features) || [];
4148
+ for (var i = 0; i < features.length; i++) {
4149
+ if (features[i] === 17n) return { context: { feature: "17", name: "status_request_v2", obsoletedBy: "RFC 9846" } };
4150
+ }
4151
+ return null;
4152
+ },
4153
+ },
4154
+ ];
4155
+
4059
4156
  /** @internal RFC 5280 sec. 5 states the criticality of each CRL extension it profiles, and sec. 5.3
4060
4157
  * does the same for the entry extensions. certificateIssuer is critical (sec. 5.3.3) because a
4061
4158
  * consumer that ignores it attributes an entry to the wrong issuer. */
@@ -6413,6 +6510,7 @@ var PROFILES = Object.assign(Object.create(null), {
6413
6510
  "rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6414
6511
  "rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6415
6512
  "rfc9763": RFC9763_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6513
+ "rfc7633": RFC7633_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6416
6514
  "x509-altsig": X509_ALTSIG_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6417
6515
  "cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6418
6516
  "cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
@@ -6436,7 +6534,7 @@ var PROFILES = Object.assign(Object.create(null), {
6436
6534
  .concat(RFC5280_EXTENSION_SYNTAX_RULES),
6437
6535
  });
6438
6536
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
6439
- .concat(RFC9763_RULES).concat(X509_ALTSIG_RULES)
6537
+ .concat(RFC9763_RULES).concat(RFC7633_RULES).concat(X509_ALTSIG_RULES)
6440
6538
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
6441
6539
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
6442
6540
  * inert under the default set because each one asks whether the caller named the profile. */
@@ -6725,6 +6823,11 @@ function _applyThreshold(report, severity) {
6725
6823
  * id-kp-serverAuth and is not a CA. `"nist-pqc-transition"` and `"cnsa-2.0"` are
6726
6824
  * named the same way: which algorithm policy a certificate is held to is the
6727
6825
  * caller's assertion, so neither is detected and both are inert until named.
6826
+ * `"rfc7633"` adds the rows that read a TLS Feature extension's VALUES: a feature
6827
+ * outside the 16-bit width of the registry sec. 4.1 points at, a repeated feature,
6828
+ * the transparency_info feature RFC 9162 sec. 7.2 asks a CA not to issue, and the
6829
+ * status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum. The
6830
+ * criticality of that extension is graded by default and needs no profile.
6728
6831
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
6729
6832
  * `worst` always reflect the complete, unfiltered result.
6730
6833
  * @example
package/lib/ocsp.js CHANGED
@@ -32,6 +32,8 @@ var _ocspIntrinsic = require("./guard-intrinsic");
32
32
  * the construction to whatever that binding holds, and one settling without awaiting its argument runs
33
33
  * the continuation before the check has finished. */
34
34
  var _Promise = _ocspIntrinsic.Promise;
35
+ /** @internal The URL parser, taken at load: the href it reports is the responder a request is sent to. */
36
+ var _URL = _ocspIntrinsic.URL;
35
37
  var _promiseResolve = _ocspIntrinsic.uncurry(_ocspIntrinsic.promiseResolve);
36
38
  /** @internal A refusal returned as a REJECTED promise is a verdict too: read from the global binding, a
37
39
  * replacement whose `reject` resolves instead turns each `ocsp/bad-input` below into a pass. */
@@ -921,7 +923,7 @@ function httpRequest(der, responderUrl, opts) {
921
923
  * the string the sec. 5 size bound has to be measured on. The appended segment is base64 under
922
924
  * the sec. 2.3 unreserved set, which holds no `.`, so it cannot become a dot segment itself. */
923
925
  var normalized;
924
- try { normalized = new URL(responderUrl).href; }
926
+ try { normalized = new _URL(responderUrl).href; }
925
927
  catch (e) { throw E("ocsp/bad-input", "httpRequest: the responder URL does not parse as a URL: " + guard.text.describeThrown(e), e); }
926
928
  var base = normalized.charAt(normalized.length - 1) === "/"
927
929
  ? normalized : normalized + "/";
@@ -67,6 +67,15 @@ var _reject = intrinsic.uncurry(intrinsic.promiseReject);
67
67
  * whatever the binding holds: a descriptor failure answering `false` through a replaced constructor is
68
68
  * not a refusal, and this module's verdicts are what a caller acts on. */
69
69
  var _Promise = intrinsic.Promise;
70
+ /** @internal The URL parser, taken at load: the host it reports from an access descriptor is the host a
71
+ * revocation fetch would be directed to. Its accessors are taken with it, because the constructor's
72
+ * capture does not cover the prototype the value reads its parts off, and the host read here is the
73
+ * one the address filter is applied to. */
74
+ var _URL = intrinsic.URL;
75
+ var _urlHref = intrinsic.urlHref;
76
+ var _urlProtocol = intrinsic.urlProtocol;
77
+ var _urlHostname = intrinsic.urlHostname;
78
+ var _urlSetHash = intrinsic.urlSetHash;
70
79
  var constants = require("./constants");
71
80
  var validator = require("./validator-all");
72
81
  var cms = require("./schema-cms");
@@ -122,6 +131,7 @@ var OID = intrinsic.assign(intrinsic.create(null), {
122
131
  anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
123
132
  cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
124
133
  subjectKeyIdentifier: oid.byName("subjectKeyIdentifier"),
134
+ tlsFeature: oid.byName("tlsFeature"),
125
135
  authorityKeyIdentifier: oid.byName("authorityKeyIdentifier"),
126
136
  authorityInfoAccess: oid.byName("authorityInfoAccess"),
127
137
  caIssuers: oid.byName("caIssuers"),
@@ -297,6 +307,66 @@ function _isCaCert(cert) {
297
307
  return !!(bc && bc.value && bc.value.cA === true);
298
308
  }
299
309
 
310
+ /** @internal RFC 7633 sec. 4.2.2, the one requirement in that document decidable from certificate
311
+ * bytes: "a certificate that is signed by a Certificate Signing Certificate that contains a TLS
312
+ * feature extension MUST contain a TLS feature extension that offers the same set or a superset of
313
+ * the features advertised in the signing certificate." The clause's own gate is the issuer's key
314
+ * usage, "Certification Authority certificate with the key usage extension value set to
315
+ * keyCertSign", which is a different question from its basicConstraints cA bit, so this asks
316
+ * keyUsage. A subject carrying NO extension under an issuer that advertises fails, because the
317
+ * clause says the subject MUST contain one; an issuer carrying none constrains nothing, so the
318
+ * constraint is one-directional.
319
+ *
320
+ * Set semantics. SEQUENCE OF fixes no order and forbids no repeat, so neither side's order nor its
321
+ * multiplicity may be read as meaning, and the comparison cannot stop at a first match.
322
+ *
323
+ * Returns null when there is nothing to check, so a boundary that was not checked reports no row
324
+ * rather than a passing one.
325
+ *
326
+ * The anchor is not read. `anchorFromCert` keeps the name, key and algorithm and drops the
327
+ * certificate's extensions, so a trust anchor's own advertised features cannot reach this and the
328
+ * rule is applied from the first certificate in the path downward. An anchor that advertises
329
+ * features constrains nothing here, which is stated rather than implied. */
330
+ function tlsFeatureChainCheck(issuerCert, cert) {
331
+ if (!issuerCert) return null;
332
+ var issuerKu;
333
+ try { issuerKu = decodeExt(issuerCert, OID.keyUsage); }
334
+ // allow:swallow-unverified a keyUsage that cannot be read is refused by its own check on that certificate
335
+ catch (_e) { return null; }
336
+ if (!(issuerKu && issuerKu.value && issuerKu.value.keyCertSign === true)) return null;
337
+ var issuerFeatures;
338
+ try { issuerFeatures = decodeExt(issuerCert, OID.tlsFeature); }
339
+ catch (e) { return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-tls-feature") }); }
340
+ if (!issuerFeatures) return null;
341
+ /** @internal The subject's extension is read BEFORE the issuer's set is looked at, because the
342
+ * clause is two requirements and the first one does not mention the set: a certificate signed by a
343
+ * signing certificate that CONTAINS the extension MUST CONTAIN one, and what it contains must then
344
+ * cover the issuer's features. Testing the set first read an issuer whose Features list is EMPTY as
345
+ * nothing to require and returned success for a subject that carried no extension at all, which the
346
+ * MUST CONTAIN half forbids; it also meant a malformed subject value under such an issuer was never
347
+ * decoded. An empty SEQUENCE is a conforming Features, the syntax carrying no SIZE constraint. */
348
+ var offeredExt;
349
+ try { offeredExt = decodeExt(cert, OID.tlsFeature); }
350
+ catch (e) { return guard.verdict.of({ ok: false, code: pathCode(e, "path/bad-tls-feature") }); }
351
+ if (!offeredExt) return guard.verdict.of({ ok: false, code: "path/tls-feature-not-offered" });
352
+ var required = (issuerFeatures.value && issuerFeatures.value.features) || [];
353
+ if (!required.length) return guard.verdict.of({ ok: true });
354
+ var offered = (offeredExt.value && offeredExt.value.features) || [];
355
+ /** @internal Membership through a set rather than a scan of the offered list per required value.
356
+ * Both lists come off untrusted certificates and the syntax bounds neither, so the pairwise form is
357
+ * work an issuer and a subject choose together: MEASURED, 20000 values on each side is 4 x 10^8
358
+ * comparisons, synchronous, on a path whose signatures may already have failed. A set makes it
359
+ * linear in the two lengths, and the values are bigints, which a set compares by value. */
360
+ var offeredSet = new intrinsic.Set();
361
+ for (var j = 0; j < offered.length; j++) intrinsic.setAdd(offeredSet, offered[j]);
362
+ for (var i = 0; i < required.length; i++) {
363
+ if (!intrinsic.setHas(offeredSet, required[i])) {
364
+ return guard.verdict.of({ ok: false, code: "path/tls-feature-not-offered" });
365
+ }
366
+ }
367
+ return guard.verdict.of({ ok: true });
368
+ }
369
+
300
370
  function statefulHbsKeyUsageCheck(cert, isCa) {
301
371
  var ku;
302
372
  try { ku = decodeExt(cert, OID.keyUsage); }
@@ -1549,6 +1619,16 @@ async function _validate(path, opts, run, scope) {
1549
1619
  guard.list.append(checks,{ name: "nameChaining", ok: chainOk === true, code: chainOk === true ? undefined : "path/name-chaining" });
1550
1620
  if (chainOk !== true) failed = true;
1551
1621
 
1622
+ /** @internal In the loop body, so it runs at EVERY issuer-to-subject boundary rather than only at
1623
+ * the target: RFC 7633 sec. 4.2.2 constrains each certificate against the one that signed it. The
1624
+ * issuer is the previous entry, which is `null` for the first certificate because the anchor's
1625
+ * extensions are not part of the path. */
1626
+ var tfRes = tlsFeatureChainCheck(idx > 0 ? certs[idx - 1] : null, cert);
1627
+ if (tfRes) {
1628
+ guard.list.append(checks, { name: "tlsFeatureChain", ok: tfRes.ok, code: tfRes.ok ? undefined : tfRes.code });
1629
+ if (!tfRes.ok) failed = true;
1630
+ }
1631
+
1552
1632
  if (!(selfIssued(cert) && i !== n)) {
1553
1633
  var ncRes;
1554
1634
  try { ncRes = checkNameConstraints(state, cert); }
@@ -3146,16 +3226,20 @@ async function _fetchAiaIssuers(current, aia) {
3146
3226
  if (ad.accessMethod !== OID.caIssuers) continue;
3147
3227
  if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue;
3148
3228
  var u;
3149
- try { u = new URL(ad.accessLocation.value); }
3229
+ try { u = new _URL(ad.accessLocation.value); }
3150
3230
  catch (_e) { continue; }
3151
- if (u.protocol !== "https:") continue;
3152
- if (_isBlockedAiaHost(u.hostname)) continue;
3153
- if (net.isIP(_stripBrackets(u.hostname)) === 0 && !aia.transportGuardsAddresses) continue;
3154
- u.hash = "";
3155
- if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue;
3231
+ if (_urlProtocol(u) !== "https:") continue;
3232
+ /** @internal The host read ONCE and both address tests applied to that one string: the blocklist
3233
+ * and the literal-versus-name question have to be answered about the same host. */
3234
+ var uHost = _urlHostname(u);
3235
+ if (_isBlockedAiaHost(uHost)) continue;
3236
+ if (net.isIP(_stripBrackets(uHost)) === 0 && !aia.transportGuardsAddresses) continue;
3237
+ _urlSetHash(u, "");
3238
+ var uHref = _urlHref(u);
3239
+ if (intrinsic.setHas(aia.fetchedUrls, uHref) || intrinsic.setHas(seenThisCert, uHref)) continue;
3156
3240
  if (seenThisCert.size >= aia.maxPerCert) break;
3157
- intrinsic.setAdd(seenThisCert, u.href);
3158
- guard.list.append(uris, u.href);
3241
+ intrinsic.setAdd(seenThisCert, uHref);
3242
+ guard.list.append(uris, uHref);
3159
3243
  }
3160
3244
  var out = [];
3161
3245
  for (var k = 0; k < uris.length; k++) {
@@ -3348,7 +3432,7 @@ async function build(leaf, opts) {
3348
3432
  * hostname taken from a certificate would be opened on that promise.
3349
3433
  * @guard-via guard.identifier.declaresOwn */
3350
3434
  transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
3351
- tls: opts.tls || {},
3435
+ tls: opts.tls != null ? opts.tls : intrinsic.create(null),
3352
3436
  timeout: opts.aiaTimeout,
3353
3437
  maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "build: opts.maxResponseBytes", constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES, { E: E, code: "path/bad-input", min: 1, max: constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES }),
3354
3438
  maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
package/lib/pki-build.js CHANGED
@@ -851,6 +851,39 @@ function makeBuilder(ctx) {
851
851
  }
852
852
  /** @internal InhibitAnyPolicy (RFC 5280 sec. 4.2.1.14): a bare SkipCerts INTEGER. */
853
853
  function extInhibitAnyPolicy(v) { return b.integer(skipCount(v, "inhibitAnyPolicy")); }
854
+ /** @internal Features (RFC 7633 sec. 4): `SEQUENCE OF INTEGER`, each a TLS ExtensionType value.
855
+ * sec. 4.1 binds those values to the IANA TLS Extensions registry, so the registry names this
856
+ * build knows are accepted beside the numbers, resolved through the one table `C.NAMES.TLS_FEATURE`
857
+ * that `pki.inspect` prints them from rather than a second copy of it. The type states no range and
858
+ * no ordering, so a value outside the registry's 16-bit width and a repeat both encode, and the
859
+ * linter is what grades them: an encoder that dropped or sorted them would hide what it emitted
860
+ * from the rule that reads it. An EMPTY list is refused here although the decoder accepts one on
861
+ * the wire, because an empty Features advertises nothing and makes the extension inert, which is a
862
+ * caller's typo rather than a document to read. */
863
+ function extTlsFeature(list) {
864
+ if (!_isArray(list) || !list.length) {
865
+ throw E("bad-input", "tlsFeature must name at least one TLS ExtensionType value (RFC 7633 sec. 4.1); " +
866
+ "an empty Features advertises nothing, so the extension it would emit is inert");
867
+ }
868
+ var dense = reqDenseArray(list, "tlsFeature");
869
+ return b.sequence(_mapIntrinsic(dense, function (f, i) {
870
+ return b.integer(tlsFeatureValue(f, "tlsFeature entry " + i));
871
+ }));
872
+ }
873
+ /** @internal A registry name resolved off the printing table, so the two directions cannot drift. */
874
+ function tlsFeatureValue(f, who) {
875
+ if (typeof f !== "string") {
876
+ return guard.range.authoredInteger(f, rawErr, ctx.prefix + "/bad-input", who);
877
+ }
878
+ var table = C.NAMES.TLS_FEATURE;
879
+ var keys = intrinsic.keys(table);
880
+ for (var i = 0; i < keys.length; i++) {
881
+ if (table[keys[i]] === f) return intrinsic.BigInt(keys[i]);
882
+ }
883
+ throw E("bad-input", who + " names no TLS ExtensionType this build knows: " + _stringify(f) +
884
+ " (accepted names: " + intrinsic.join(_mapIntrinsic(keys, function (k) { return table[k]; }), ", ") +
885
+ "; any other value is given as its number)");
886
+ }
854
887
  /** @internal PolicyMappings (RFC 5280 sec. 4.2.1.5): SEQUENCE SIZE (1..MAX) OF
855
888
  * { issuerDomainPolicy, subjectDomainPolicy }. The clause forbids mapping either to or from
856
889
  * anyPolicy, so neither side may name it. */
@@ -1282,6 +1315,7 @@ function makeBuilder(ctx) {
1282
1315
  nameConstraints: 1, authorityInfoAccess: 1, cRLDistributionPoints: 1, freshestCRL: 1,
1283
1316
  policyConstraints: 1, inhibitAnyPolicy: 1, policyMappings: 1, policyMappingsCritical: 1,
1284
1317
  issuerAltName: 1, qcStatements: 1, qcStatementsCritical: 1,
1318
+ tlsFeature: 1, tlsFeatureCritical: 1,
1285
1319
  msCertificateTemplate: 1, msEnrollCertType: 1, msApplicationPolicies: 1,
1286
1320
  subjectInfoAccess: 1, subjectDirectoryAttributes: 1,
1287
1321
  };
@@ -1336,6 +1370,11 @@ function makeBuilder(ctx) {
1336
1370
  if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), !!extSpec.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
1337
1371
  if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
1338
1372
  if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), !!extSpec.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
1373
+ /** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
1374
+ * sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
1375
+ * belongs in this table. The criticality knob is here too, because a request carries the
1376
+ * Extension shape whole and the CA decides what it issues. */
1377
+ if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
1339
1378
  if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
1340
1379
  if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
1341
1380
  if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
@@ -1395,6 +1434,7 @@ function makeBuilder(ctx) {
1395
1434
  extNameConstraints: extNameConstraints, extAuthorityInfoAccess: extAuthorityInfoAccess,
1396
1435
  extCrlDistributionPoints: extCrlDistributionPoints, extPolicyConstraints: extPolicyConstraints,
1397
1436
  extInhibitAnyPolicy: extInhibitAnyPolicy, extPolicyMappings: extPolicyMappings,
1437
+ extTlsFeature: extTlsFeature,
1398
1438
  extQcStatements: extQcStatements,
1399
1439
  encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
1400
1440
  requestedExtensions: requestedExtensions,