@blamejs/pki 0.8.50 → 0.8.52

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.
package/lib/est.js CHANGED
@@ -85,6 +85,15 @@ var _Promise = intrinsic.Promise;
85
85
  * is handed is taken with it, since a replacement answers with a host of its own. */
86
86
  var _URL = intrinsic.URL;
87
87
  var _stringOf = intrinsic.String;
88
+ /** @internal And the accessors its result is read through: capturing the constructor does not capture
89
+ * the prototype the value reads its parts off, and each part here decides something, the scheme
90
+ * whether the request is allowed, the origin whether credentials may still be sent after a redirect. */
91
+ var _urlHref = intrinsic.urlHref;
92
+ var _urlOrigin = intrinsic.urlOrigin;
93
+ var _urlProtocol = intrinsic.urlProtocol;
94
+ var _urlPathname = intrinsic.urlPathname;
95
+ var _urlSearch = intrinsic.urlSearch;
96
+ var _urlHash = intrinsic.urlHash;
88
97
  var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
89
98
  var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
90
99
  var pkix = require("./schema-pkix");
@@ -810,7 +819,8 @@ function _parseUrl(urlStr) {
810
819
  var url;
811
820
  try { url = new _URL(_stringOf(urlStr)); }
812
821
  catch (e) { throw E("est/bad-url", "the EST server URL did not parse: " + String(urlStr), e); }
813
- if (url.protocol !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + url.protocol + " for " + urlStr);
822
+ var scheme = _urlProtocol(url);
823
+ if (scheme !== "https:") throw E("est/insecure-url", "EST requires https (RFC 7030 sec. 3.3), got " + scheme + " for " + urlStr);
814
824
  return url;
815
825
  }
816
826
 
@@ -823,11 +833,12 @@ function _tlsForRequest(opts) {
823
833
  function _redirectTarget(current, location, method, opts) {
824
834
  if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
825
835
  var resolved;
826
- try { resolved = new _URL(_stringOf(location), current.href); }
836
+ try { resolved = new _URL(_stringOf(location), _urlHref(current)); }
827
837
  catch (e) { throw E("est/bad-url", "a redirect Location did not parse: " + location, e); }
828
- if (resolved.protocol !== "https:") throw E("est/insecure-redirect", "a redirect to a non-https URL is refused (RFC 7030 sec. 3.2.1): " + resolved.protocol);
838
+ var resolvedScheme = _urlProtocol(resolved);
839
+ if (resolvedScheme !== "https:") throw E("est/insecure-redirect", "a redirect to a non-https URL is refused (RFC 7030 sec. 3.2.1): " + resolvedScheme);
829
840
  var safeMethod = method === "GET" || method === "HEAD";
830
- if (resolved.origin !== current.origin && !safeMethod && !opts.allowCrossOriginRedirect) {
841
+ if (_urlOrigin(resolved) !== _urlOrigin(current) && !safeMethod && !opts.allowCrossOriginRedirect) {
831
842
  throw E("est/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect (RFC 7030 sec. 3.2.1)");
832
843
  }
833
844
  return resolved;
@@ -890,10 +901,10 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
890
901
  var staleRetries = 0;
891
902
  var authSpaces = 0;
892
903
  var lastDigestNonce = null;
893
- var initialOrigin = url.origin;
904
+ var initialOrigin = _urlOrigin(url);
894
905
  function _tlsFor(u) {
895
906
  var t = budgets.tls;
896
- if (t && u.origin !== initialOrigin) {
907
+ if (t && _urlOrigin(u) !== initialOrigin) {
897
908
  t = Object.assign({}, t);
898
909
  t.cert = null; t.key = null; t.servername = null;
899
910
  }
@@ -905,20 +916,26 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
905
916
  var digestSent = false;
906
917
  function _headersFor(u) {
907
918
  digestSent = false;
908
- if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
919
+ /** @internal The origin and the request target are each read ONCE and both the protection-space
920
+ * test and the answer are built from those locals: the credential is bound to the target it is
921
+ * computed over, so a part answering two reads differently would answer for one target and be
922
+ * sent to another. */
923
+ var uOrigin = _urlOrigin(u);
924
+ var uTarget = _urlPathname(u) + _urlSearch(u);
925
+ if (uOrigin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
909
926
  var hh = guard.identifier.ownOptions(headers);
910
927
  if (digestChallenge) {
911
- if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
928
+ if (!httpDigest.inProtectionSpace(digestChallenge, uOrigin, uTarget)) return headers;
912
929
  var nkey = digestChallenge.nonce;
913
930
  digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
914
- hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: u.pathname + u.search, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
931
+ hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: uTarget, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
915
932
  digestSent = true;
916
933
  } else { hh.authorization = authValue; }
917
934
  return hh;
918
935
  }
919
936
  function step() {
920
937
  var hopBody = (typeof body === "function" && budgets.onAttempt) ? budgets.onAttempt() : body;
921
- return guard.async.invoked(transport, [{ method: method, url: url.href, headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "est/bad-input", "opts.transport").then(function (res) {
938
+ return guard.async.invoked(transport, [{ method: method, url: _urlHref(url), headers: _headersFor(url), body: hopBody, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "est/bad-input", "opts.transport").then(function (res) {
922
939
  res = res || {};
923
940
  var blen = _bodyLen(res);
924
941
  if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
@@ -939,7 +956,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
939
956
  return step();
940
957
  }
941
958
  if (status === 401) {
942
- if (url.origin !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
959
+ if (_urlOrigin(url) !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
943
960
  var www = String(h["www-authenticate"] || "");
944
961
  if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
945
962
  var scheme = _authScheme(opts);
@@ -951,8 +968,8 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
951
968
  selPol.preferStale = digestSent;
952
969
  selPol.priorNonce = lastDigestNonce;
953
970
  selPol.priorRealm = digestChallenge && digestChallenge.realm;
954
- selPol.requestOrigin = url.origin;
955
- selPol.requestTarget = url.pathname + url.search;
971
+ selPol.requestOrigin = _urlOrigin(url);
972
+ selPol.requestTarget = _urlPathname(url) + _urlSearch(url);
956
973
  var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
957
974
  if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
958
975
  if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
@@ -988,7 +1005,7 @@ function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt,
988
1005
  var base;
989
1006
  try { base = new _URL(_stringOf(baseUrl)); }
990
1007
  catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + guard.text.showValue(baseUrl), e); }
991
- if (base.search || base.hash) throw E("est/bad-url", "the EST base URL must not carry a query or fragment component (RFC 7030 sec. 3.2.2)");
1008
+ if (_urlSearch(base) || _urlHash(base)) throw E("est/bad-url", "the EST base URL must not carry a query or fragment component (RFC 7030 sec. 3.2.2)");
992
1009
  var url = _parseUrl(paths(baseUrl, { label: opts.label })[op]);
993
1010
  var transport = checked !== undefined ? checked : _assertTransport(opts);
994
1011
  if (!transport) {
@@ -85,6 +85,7 @@ var _urlOrigin = getter(URL.prototype, "origin", "URL.prototype");
85
85
  var _urlProtocol = getter(URL.prototype, "protocol", "URL.prototype");
86
86
  var _urlPathname = getter(URL.prototype, "pathname", "URL.prototype");
87
87
  var _urlSearch = getter(URL.prototype, "search", "URL.prototype");
88
+ var _urlHash = getter(URL.prototype, "hash", "URL.prototype");
88
89
  var _urlHostname = getter(URL.prototype, "hostname", "URL.prototype");
89
90
  var _urlPort = getter(URL.prototype, "port", "URL.prototype");
90
91
  var _urlUsername = getter(URL.prototype, "username", "URL.prototype");
@@ -92,6 +93,7 @@ var _urlPassword = getter(URL.prototype, "password", "URL.prototype");
92
93
  var _urlSearchParams = getter(URL.prototype, "searchParams", "URL.prototype");
93
94
  var _urlSetPathname = setter(URL.prototype, "pathname", "URL.prototype");
94
95
  var _urlSetSearch = setter(URL.prototype, "search", "URL.prototype");
96
+ var _urlSetHash = setter(URL.prototype, "hash", "URL.prototype");
95
97
  var _asyncIterator = Symbol.asyncIterator;
96
98
 
97
99
  var _arrForEach = uncurry(Array.prototype.forEach);
@@ -275,6 +277,7 @@ module.exports = Object.freeze({
275
277
  urlProtocol: _urlProtocol,
276
278
  urlPathname: _urlPathname,
277
279
  urlSearch: _urlSearch,
280
+ urlHash: _urlHash,
278
281
  urlHostname: _urlHostname,
279
282
  urlPort: _urlPort,
280
283
  urlUsername: _urlUsername,
@@ -282,6 +285,7 @@ module.exports = Object.freeze({
282
285
  urlSearchParams: _urlSearchParams,
283
286
  urlSetPathname: _urlSetPathname,
284
287
  urlSetSearch: _urlSetSearch,
288
+ urlSetHash: _urlSetHash,
285
289
  asyncIterator: _asyncIterator,
286
290
  forEach: _arrForEach,
287
291
  map: _arrMap,
package/lib/guard-time.js CHANGED
@@ -19,17 +19,112 @@ var _dateGetUTCMinutes = require("./guard-intrinsic").uncurry(Date.prototype.get
19
19
  var _dateGetUTCSeconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCSeconds);
20
20
  var _dateGetUTCMilliseconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMilliseconds);
21
21
  var _dateSetUTCFullYear = require("./guard-intrinsic").uncurry(Date.prototype.setUTCFullYear);
22
+ var _dateToISOString = require("./guard-intrinsic").uncurry(Date.prototype.toISOString);
23
+ var _charCodeAt = require("./guard-intrinsic").uncurry(String.prototype.charCodeAt);
24
+ var _Number = require("./guard-intrinsic").Number;
25
+ var constants = require("./constants");
26
+ /** @internal RFC 3161 clause 2.4.2 states an Accuracy's third field in microseconds, which no
27
+ * `C.TIME` scale names because no PKI structure carries a microsecond duration anywhere else. */
28
+ var MICROS_PER_MILLISECOND = 1000;
22
29
  var _dateUTC = Date.UTC;
23
30
  var _floor = Math.floor;
24
31
  var _ceil = Math.ceil;
25
32
  /** @internal A DER GeneralizedTime carries a four-digit year and a UTCTime a narrower one, so a Date
26
33
  * outside 0000..9999 has no PKI encoding. */
27
34
  var DER_YEAR_MIN = 0, DER_YEAR_MAX = 9999;
35
+ /** @internal The widest accuracy a date comparison can use, the DER year range in seconds, and the
36
+ * instant a wider one saturates to: the last instant a DER time can carry, which is past every
37
+ * certificate date and, unlike an arbitrary ceiling, is a `Date` a verdict can render. Declared
38
+ * after the range it derives from, since a `var` read above its own initializer is `undefined`. */
39
+ var MAX_ACCURACY_SECONDS = constants.TIME.days((DER_YEAR_MAX + 1) * 366) / constants.TIME.seconds(1);
40
+ var MAX_INSTANT = _dateUTC(DER_YEAR_MAX, 11, 31, 23, 59, 59, 999);
28
41
 
29
42
  // @enforced-by guard-shape-reinlined
30
43
  // @guard-shape \.getTime\s*\(
31
44
  function instantOf(value) { return _dateGetTime(value); }
32
45
 
46
+ /** The instant an error message or a verdict names, rendered through the operation captured at load.
47
+ * A replaced `toISOString` chooses the time a refusal reports, which is what an operator reads when
48
+ * deciding whether the refusal was right, so the rendering is taken from the same place every other
49
+ * read of a Date is.
50
+ *
51
+ * @enforced-by behavioral -- the live-read budget each module declares is what holds a call-time
52
+ * `toISOString` out of a converted file; a `@guard-shape` here would name the read in every module
53
+ * that renders a date and the budget already answers for those.
54
+ */
55
+ function isoOf(value) { return _dateToISOString(value); }
56
+
57
+ /** The instant a DER GeneralizedTime names, rounded UP to the next whole millisecond where its
58
+ * fractional seconds carry a digit a `Date` cannot hold. `fraction` is the fraction's digit string;
59
+ * the first three digits ARE the milliseconds, so a nonzero digit after them puts the real instant
60
+ * strictly between `value` and `value + 1ms`.
61
+ *
62
+ * An upper bound is decided against this rather than against the Date, because the Date truncates:
63
+ * a time stamped at `...000000.000001Z` reads as `...000000.000Z`, which compares equal to a bound
64
+ * of `...000000Z` and so passes an "after the bound" test it is on the wrong side of. The two
65
+ * consumers are the timestamp verifier's own check of the authority's certificate window and the
66
+ * signature-timestamp row's check of the signing certificate's expiry.
67
+ *
68
+ * @enforced-by behavioral -- a truncating comparison has no rename-proof shape; the RED vector is a
69
+ * token minted with a sub-millisecond fraction one tick past a certificate's notAfter.
70
+ */
71
+ function ceilInstantOf(value, fraction) { return latestInstantOf(value, fraction, null); }
72
+
73
+ /** @internal The part of a DER GeneralizedTime's fractional seconds a `Date` cannot hold, in whole
74
+ * microseconds. The first three digits ARE the milliseconds; the next three are microseconds, and
75
+ * anything finer than a microsecond counts as one, since a bound is only a bound if it is not
76
+ * exceeded. Read digit by digit through the captured accessor: a fraction is caller bytes. */
77
+ function _fractionMicros(fraction) {
78
+ if (typeof fraction !== "string" || fraction.length <= 3) return 0;
79
+ var micros = 0;
80
+ var finer = false;
81
+ for (var i = 3; i < fraction.length; i++) {
82
+ var c = _charCodeAt(fraction, i);
83
+ if (c < 48 || c > 57) break;
84
+ var digit = c - 48;
85
+ if (i === 3) micros += digit * 100;
86
+ else if (i === 4) micros += digit * 10;
87
+ else if (i === 5) micros += digit;
88
+ else if (digit > 0) finer = true;
89
+ }
90
+ return finer ? micros + 1 : micros;
91
+ }
92
+
93
+ /** The LATEST instant a timestamp could have been created at: its `genTime` ceiling plus the
94
+ * accuracy the token states. RFC 3161 clause 2.4.2 defines accuracy as the bound on the difference
95
+ * between the stated time and the real one, so a token saying it is accurate to two seconds places
96
+ * its creation anywhere in a four-second window, and an upper bound that reads only `genTime`
97
+ * concludes something the token does not establish. `accuracy` is the parsed `{ seconds, millis,
98
+ * micros }` or null; `seconds` arrives as a BigInt, the other two as numbers, and a micros
99
+ * remainder rounds up because a bound is only a bound if it is not exceeded.
100
+ *
101
+ * @enforced-by behavioral -- a bound that ignores a field has no rename-proof shape; the RED vector
102
+ * is a token whose genTime precedes a certificate's notAfter while its accuracy window does not.
103
+ */
104
+ function latestInstantOf(value, fraction, accuracy) {
105
+ var at = instantOf(value);
106
+ var ms = 0;
107
+ /** @internal The sub-millisecond parts are summed and rounded ONCE. Rounding the genTime fraction
108
+ * and the accuracy up separately overstates the bound by a millisecond when both carry one, which
109
+ * refuses a token whose real window ends before the instant it is compared against. */
110
+ var micros = _fractionMicros(fraction);
111
+ if (accuracy !== null && typeof accuracy === "object") {
112
+ /** @internal An Accuracy's seconds field is an arbitrary-size INTEGER, so it is bounded BEFORE
113
+ * it is scaled: a token claiming more than the DER time range can hold is saturated to the
114
+ * widest instant rather than scaled into a value the scale helper refuses. The refusal would
115
+ * escape the row this builds and take the whole verify call down, where what the token has
116
+ * established is nothing about which side of any certificate date it falls on, which is the
117
+ * answer a saturated bound gives. */
118
+ if (accuracy.seconds > MAX_ACCURACY_SECONDS) return MAX_INSTANT;
119
+ if (typeof accuracy.seconds === "bigint") ms += constants.TIME.seconds(_Number(accuracy.seconds));
120
+ else if (typeof accuracy.seconds === "number") ms += constants.TIME.seconds(accuracy.seconds);
121
+ if (typeof accuracy.millis === "number") ms += constants.TIME.milliseconds(accuracy.millis);
122
+ if (typeof accuracy.micros === "number") micros += accuracy.micros;
123
+ }
124
+ if (micros > 0) ms += _ceil(micros / MICROS_PER_MILLISECOND);
125
+ return ms > 0 ? at + ms : at;
126
+ }
127
+
33
128
  // @enforced-by nan-date-comparison-unguarded
34
129
  function assertValid(value, E, code, label) {
35
130
  if (!util.types.isDate(value) || _isNaN(instantOf(value))) {
@@ -120,6 +215,9 @@ module.exports = _freeze({
120
215
  assertEncodable: assertEncodable,
121
216
  addMonths: addMonths,
122
217
  instantOf: instantOf,
218
+ isoOf: isoOf,
219
+ ceilInstantOf: ceilInstantOf,
220
+ latestInstantOf: latestInstantOf,
123
221
  isDate: isDate,
124
222
  toDate: toDate,
125
223
  within: within,
@@ -7,8 +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. */
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. */
11
13
  var _URL = intrinsic.URL;
14
+ var _urlOrigin = intrinsic.urlOrigin;
15
+ var _urlPathname = intrinsic.urlPathname;
16
+ var _urlSearch = intrinsic.urlSearch;
12
17
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
13
18
  var _strSlice = intrinsic.uncurry(String.prototype.slice);
14
19
  var _fromCharCode = intrinsic.fromCharCode;
@@ -295,7 +300,7 @@ function answer(challenge, params, E) {
295
300
  function _domainEntry(d) {
296
301
  d = String(d == null ? "" : d);
297
302
  if (d.charAt(0) === "/") return { origin: null, full: d };
298
- 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) || "") }; }
299
304
  catch (_e) { return null; }
300
305
  }
301
306
 
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
@@ -68,8 +68,14 @@ var _reject = intrinsic.uncurry(intrinsic.promiseReject);
68
68
  * not a refusal, and this module's verdicts are what a caller acts on. */
69
69
  var _Promise = intrinsic.Promise;
70
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. */
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. */
72
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;
73
79
  var constants = require("./constants");
74
80
  var validator = require("./validator-all");
75
81
  var cms = require("./schema-cms");
@@ -125,6 +131,7 @@ var OID = intrinsic.assign(intrinsic.create(null), {
125
131
  anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
126
132
  cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
127
133
  subjectKeyIdentifier: oid.byName("subjectKeyIdentifier"),
134
+ tlsFeature: oid.byName("tlsFeature"),
128
135
  authorityKeyIdentifier: oid.byName("authorityKeyIdentifier"),
129
136
  authorityInfoAccess: oid.byName("authorityInfoAccess"),
130
137
  caIssuers: oid.byName("caIssuers"),
@@ -300,6 +307,66 @@ function _isCaCert(cert) {
300
307
  return !!(bc && bc.value && bc.value.cA === true);
301
308
  }
302
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
+
303
370
  function statefulHbsKeyUsageCheck(cert, isCa) {
304
371
  var ku;
305
372
  try { ku = decodeExt(cert, OID.keyUsage); }
@@ -1552,6 +1619,16 @@ async function _validate(path, opts, run, scope) {
1552
1619
  guard.list.append(checks,{ name: "nameChaining", ok: chainOk === true, code: chainOk === true ? undefined : "path/name-chaining" });
1553
1620
  if (chainOk !== true) failed = true;
1554
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
+
1555
1632
  if (!(selfIssued(cert) && i !== n)) {
1556
1633
  var ncRes;
1557
1634
  try { ncRes = checkNameConstraints(state, cert); }
@@ -3151,14 +3228,18 @@ async function _fetchAiaIssuers(current, aia) {
3151
3228
  var u;
3152
3229
  try { u = new _URL(ad.accessLocation.value); }
3153
3230
  catch (_e) { continue; }
3154
- if (u.protocol !== "https:") continue;
3155
- if (_isBlockedAiaHost(u.hostname)) continue;
3156
- if (net.isIP(_stripBrackets(u.hostname)) === 0 && !aia.transportGuardsAddresses) continue;
3157
- u.hash = "";
3158
- 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;
3159
3240
  if (seenThisCert.size >= aia.maxPerCert) break;
3160
- intrinsic.setAdd(seenThisCert, u.href);
3161
- guard.list.append(uris, u.href);
3241
+ intrinsic.setAdd(seenThisCert, uHref);
3242
+ guard.list.append(uris, uHref);
3162
3243
  }
3163
3244
  var out = [];
3164
3245
  for (var k = 0; k < uris.length; k++) {