@blamejs/pki 0.8.51 → 0.8.53

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/constants.js CHANGED
@@ -293,6 +293,21 @@ var NAMES = Object.assign(Object.create(null), {
293
293
  * this length: an OCSP CertID's issuer hashes, a timestamp's messageImprint. The XOFs (SHAKE) have
294
294
  * no single length and are absent. */
295
295
  DIGEST_OCTETS: { sha1: 20, sha256: 32, sha384: 48, sha512: 64, "sha3-256": 32, "sha3-512": 64 },
296
+ /** @internal The digests a verify path refuses by default and admits only under an explicit
297
+ * opt-in, by the OID registry's name, with the WebCrypto hash name each maps to. A digest is weak
298
+ * once a collision is reachable, which breaks the binding of every structure that carries "the
299
+ * hash of X": a signature's message digest, a timestamp's messageImprint, an ESS certificate
300
+ * identifier. Membership is defined once here, so a path that refuses one refuses all of them and
301
+ * a path that admits them under an opt-in admits the same set. A digest no path will compute at
302
+ * all is not listed; it is absent from DIGEST_HASH instead. */
303
+ WEAK_DIGESTS: { sha1: "SHA-1" },
304
+ /** @internal The digests an RFC 3161 message imprint may name in this build, by the OID registry's
305
+ * name, with the node name each computes under. One table for the whole timestamp round trip: a
306
+ * verb that TAKES an imprint, a verb that attaches a token over one, and the verifier that reads
307
+ * it all answer from this, so none of them can produce an imprint another would refuse. SHA-1 is
308
+ * listed because archived tokens carry it and the verifier admits it under an explicit opt-in;
309
+ * WEAK_DIGESTS is what decides that, not absence from here. */
310
+ TIMESTAMP_IMPRINT_DIGESTS: { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "sha512" },
296
311
  });
297
312
 
298
313
  /** @internal Each inner table is looked up by a name a certificate supplies, so it answers from
package/lib/ct.js CHANGED
@@ -2177,7 +2177,7 @@ function _b64Array(obj, name, code, label, exactLen, maxCount) {
2177
2177
  * @opts headers OPTIONAL extra request headers (the request-framing headers are stripped; the verb owns the GET method).
2178
2178
  * @opts timeout OPTIONAL ms budget, default 30s (cap-validated).
2179
2179
  * @opts maxResponseBytes OPTIONAL per-GET size cap, default 4 MiB, tightenable DOWNWARD only.
2180
- * @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET.
2180
+ * @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET. A value other than `true`, `false`, `null` or an absent option is refused `ct/bad-input` before the request goes out.
2181
2181
  * @example
2182
2182
  * // a live distributor uses the default pki.transport.https; here an injected transport returns the pair
2183
2183
  * var r = await pki.ct.fetchLogList({ url: "https://ct.example/log_list.json", signerKey: googleSignerSpki,
@@ -2241,7 +2241,8 @@ async function fetchLogList(opts) {
2241
2241
  var req = { headers: headers, tls: fetchTls, timeout: timeout, maxResponseBytes: maxResponseBytes };
2242
2242
  /** @internal Read before the request goes out, so the option that decides the check is the one the call
2243
2243
  * was made with rather than whatever it says once the response is back. */
2244
- var requireJsonContentType = opts.requireJsonContentType === true;
2244
+ var requireJsonContentType = guard.identifier.booleanOption(opts.requireJsonContentType, _ctErr,
2245
+ "ct/bad-input", "fetchLogList opts.requireJsonContentType");
2245
2246
  var jsonRes = await _fetchBody(transport, jsonUrl, req, "CT log-list JSON");
2246
2247
  if (requireJsonContentType && jsonRes.contentType !== "application/json") throw _ctErr("ct/bad-content-type", "the CT log-list JSON GET returned content-type " + JSON.stringify(jsonRes.contentType || null) + " (opts.requireJsonContentType is set)");
2247
2248
  var sigRes = await _fetchBody(transport, sigUrl, req, "CT log-list signature");
package/lib/guard-der.js CHANGED
@@ -96,6 +96,31 @@ function paramsAbsentOrNull(params) {
96
96
  return params === null || params === undefined || isNull(params);
97
97
  }
98
98
 
99
+ /** Whether two decoded `AlgorithmIdentifier` records name the same algorithm with the same
100
+ * parameters, comparing the parameters AS THEY ARE: absent equals absent, and an explicit DER NULL
101
+ * is a different identifier from one with no parameters field.
102
+ *
103
+ * This is the comparison for an identifier whose parameters carry meaning. A digest identifier has
104
+ * the opposite rule, because RFC 3370 clause 2.1 and RFC 5754 clause 2 require an implementation to
105
+ * accept both spellings of the same digest, and `schema-cms.sameDigestAlgorithm` grants that. A
106
+ * signature identifier does not: RFC 4055 requires the `*WithRSAEncryption` identifiers to carry a
107
+ * NULL, RFC 5758 and RFC 8410 require the `ecdsaWith*` and the Edwards and post-quantum ones to omit
108
+ * the field, and RSASSA-PSS carries its hash, mask generator, salt length and trailer field there
109
+ * under RFC 4056 clause 2.2. Collapsing the two spellings for one of those is admitting a different
110
+ * algorithm, which is why the grant does not travel.
111
+ *
112
+ * @enforced-by behavioral -- an equality that is too permissive has no rename-proof shape; the RED
113
+ * vector is an identifier pair differing only in an explicit NULL, which must compare unequal here
114
+ * and equal through the digest helper.
115
+ */
116
+ function sameAlgorithmExactly(a, c) {
117
+ if (!a || !c || a.oid !== c.oid) return false;
118
+ var ap = a.parameters === undefined ? null : a.parameters;
119
+ var cp = c.parameters === undefined ? null : c.parameters;
120
+ if (ap === null || cp === null) return ap === cp;
121
+ return _isBuffer(ap) && _isBuffer(cp) && _sizeOf(ap) === _sizeOf(cp) && _compare(ap, cp) === 0;
122
+ }
123
+
99
124
  // @enforced-by behavioral -- see paramsAbsentOrNull.
100
125
  function assertParams(params, shape, E, code, label) {
101
126
  var ok = shape === "null" ? isNull(params) : (shape === "absent"
@@ -109,4 +134,5 @@ function assertParams(params, shape, E, code, label) {
109
134
  module.exports = _freeze({
110
135
  element: element, tlv: tlv,
111
136
  isNull: isNull, paramsAbsentOrNull: paramsAbsentOrNull, assertParams: assertParams,
137
+ sameAlgorithmExactly: sameAlgorithmExactly,
112
138
  });
@@ -600,6 +600,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
600
600
  return assertCallable(opts[key], E, code, label, shape);
601
601
  }
602
602
 
603
+ /** @internal How a switch is read: as the boolean it documents, with an absent value standing for
604
+ * off and every other value refused. A switch that turns a requirement on is the case that decides
605
+ * the shape. `requireRevocation: "true"` out of a config file is truthy to JavaScript and unequal
606
+ * to `true`, so a boundary testing `=== true` runs without the requirement and says nothing, while
607
+ * one testing truthiness turns the requirement on for `0` and `""` as well. The value is rendered
608
+ * from its own type rather than coerced, so a caller value with a throwing `toString` reports the
609
+ * typed refusal. A boundary that reads `null` as a value its caller supplied keeps its own check:
610
+ * this one reads it as absence. */
611
+ // @enforced-by behavioral -- reading a caller switch has no rename-proof code shape; `=== true` is
612
+ // @enforced-by behavioral -- the correct read for a switch that turns a control OFF, so a lexical
613
+ // @enforced-by behavioral -- rule would flag the safe direction. The per-verb vectors are the
614
+ // @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
615
+ // @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
616
+ function booleanOption(value, E, code, label) {
617
+ if (value === undefined || value === null) return false;
618
+ if (value === true || value === false) return value;
619
+ throw E(code, label + " must be true or false, got " + _showSwitch(value));
620
+ }
621
+
622
+ /** @internal The value's own type, for the message above. Nothing here calls into the value. */
623
+ function _showSwitch(value) {
624
+ if (typeof value === "string") return _stringify(value);
625
+ if (typeof value === "number" || typeof value === "bigint") return _String(value);
626
+ return typeof value;
627
+ }
628
+
603
629
  /** @internal Whether a caller's object says something about itself, as opposed to inheriting the
604
630
  * answer. A declaration is a promise the author of that object made, so it has to sit on the object
605
631
  * they wrote: reading the flag through the object finds a value on `Function.prototype` that every
@@ -619,6 +645,7 @@ function declaresOwn(obj, name) {
619
645
 
620
646
  module.exports = intrinsic.freeze({
621
647
  assertCallable: assertCallable,
648
+ booleanOption: booleanOption,
622
649
  declaresOwn: declaresOwn,
623
650
  assertCallableOption: assertCallableOption,
624
651
  assertCanonicalOid: assertCanonicalOid,
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,
package/lib/lint.js CHANGED
@@ -4929,12 +4929,21 @@ var ALL_CRL_RULES = RFC5280_CRL_RULES.concat(CNSA2_CRL_RULES);
4929
4929
  * for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
4930
4930
  * GeneralizedTime direction of that clause can be broken. */
4931
4931
  var OID_SIGNING_TIME = oid.byName("signingTime");
4932
+ /** @internal The two identifiers RFC 6211's attribute is assigned, both read: the body's PKCS 9 leaf
4933
+ * and the S/MIME attribute arc value the IANA registry and the update draft give it. */
4934
+ var OID_ALG_PROTECTION = oid.byName("cmsAlgorithmProtection");
4935
+ var OID_ALG_PROTECTION_AA = oid.byName("cmsAlgorithmProtect");
4932
4936
  var OID_SIGNED_DATA = oid.byName("signedData");
4933
4937
 
4934
4938
  /** @internal The content type decides which of these rows apply, and the parser sets `contentType`
4935
4939
  * as an own property on every content it builds. A content type that carries no signers has no own
4936
4940
  * `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
4937
4941
  function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
4942
+ var OID_AUTH_DATA = oid.byName("authData");
4943
+ /** @internal The other content type whose originator carries attributes, and the one whose algorithm
4944
+ * fields nothing else covers: RFC 5652 sec. 9.2 makes the MAC input the DER encoding of `authAttrs`
4945
+ * alone, so `digestAlgorithm` and `macAlgorithm` sit outside it. */
4946
+ function _isAuthData(cms) { return cms.contentType === OID_AUTH_DATA; }
4938
4947
  var OID_COUNTERSIGNATURE = oid.byName("countersignature");
4939
4948
  /** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
4940
4949
  * unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
@@ -4969,14 +4978,16 @@ function _cmsAllSignerInfos(cms) {
4969
4978
  visit(cms.signerInfos, 0, false, 0);
4970
4979
  return out;
4971
4980
  }
4972
- function _cmsSignedAttr(signerInfo, dotted) {
4973
- var attrs = signerInfo.signedAttrs;
4981
+ function _cmsAttrInList(attrs, dotted) {
4974
4982
  if (!Array.isArray(attrs)) return null;
4975
4983
  for (var i = 0; i < attrs.length; i++) {
4976
4984
  if (attrs[i].type === dotted) return attrs[i];
4977
4985
  }
4978
4986
  return null;
4979
4987
  }
4988
+ function _cmsSignedAttr(signerInfo, dotted) {
4989
+ return _cmsAttrInList(signerInfo.signedAttrs, dotted);
4990
+ }
4980
4991
  /** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
4981
4992
  * rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
4982
4993
  * that adds a countersigner's digest to it, and a set written under one rule and read under another
@@ -5078,8 +5089,50 @@ var RFC5652_CMS_RULES = [
5078
5089
  },
5079
5090
  ];
5080
5091
 
5081
- var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
5082
- var ALL_CMS_RULES = RFC5652_CMS_RULES;
5092
+ /** @internal RFC 8933 updates RFC 5652 rather than restating it, so its rows are their own profile:
5093
+ * `pki.lint.cms(der, { profile: "rfc8933" })` asks only what that document added. */
5094
+ var RFC8933_CMS_RULES = [
5095
+ {
5096
+ id: "lint/rfc8933/algorithm-protection-absent", severity: "notice", source: "rfc8933", citation: "RFC 8933 4.1",
5097
+ /** @internal "the originator of a signed-data content type that includes signed attributes
5098
+ * SHOULD include the CMSAlgorithmProtection attribute [RFC6211] as one of the signed
5099
+ * attributes. Likewise, the originator of an authenticated-data content type that includes
5100
+ * authenticated attributes SHOULD include the CMSAlgorithmProtection attribute [RFC6211] as one
5101
+ * of the authenticated attributes." RFC 9882 sec. 4 and RFC 9814 sec. 5 repeat the signed-data
5102
+ * half for ML-DSA and SLH-DSA. Both content types are read, and the authenticated-data one is
5103
+ * where the exposure is widest: sec. 9.2 makes the MAC input `authAttrs` alone, so the two outer
5104
+ * algorithm fields are covered by nothing. The SHOULD is conditioned on attributes being
5105
+ * present, so an originator that signs or authenticates the content directly has nowhere to
5106
+ * carry it and draws no finding. Both identifiers count, since the two spell one attribute. A
5107
+ * notice rather than a warning: it is a SHOULD, and most CMS in the field carries no such
5108
+ * attribute. */
5109
+ message: "an originator with signed or authenticated attributes carries no CMSAlgorithmProtection attribute, which this section says it should include",
5110
+ appliesTo: function (cms) { return _isSignedData(cms) || _isAuthData(cms); },
5111
+ check: function (cms) {
5112
+ var out = [];
5113
+ if (_isAuthData(cms)) {
5114
+ var authAttrs = cms.authAttrs;
5115
+ if (!Array.isArray(authAttrs) || !authAttrs.length) return null;
5116
+ if (_cmsAttrInList(authAttrs, OID_ALG_PROTECTION) ||
5117
+ _cmsAttrInList(authAttrs, OID_ALG_PROTECTION_AA)) return null;
5118
+ return { context: { placement: "authAttrs" } };
5119
+ }
5120
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
5121
+ if (!Array.isArray(entry.signerInfo.signedAttrs) || !entry.signerInfo.signedAttrs.length) return;
5122
+ if (_cmsSignedAttr(entry.signerInfo, OID_ALG_PROTECTION) ||
5123
+ _cmsSignedAttr(entry.signerInfo, OID_ALG_PROTECTION_AA)) return;
5124
+ out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature } });
5125
+ });
5126
+ return out.length ? out : null;
5127
+ },
5128
+ },
5129
+ ];
5130
+
5131
+ var CMS_PROFILES = Object.assign(Object.create(null), {
5132
+ "rfc5652": RFC5652_CMS_RULES,
5133
+ "rfc8933": RFC8933_CMS_RULES,
5134
+ });
5135
+ var ALL_CMS_RULES = RFC5652_CMS_RULES.concat(RFC8933_CMS_RULES);
5083
5136
 
5084
5137
  /** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
5085
5138
  * short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
@@ -6978,9 +7031,10 @@ function _ingestCrl(input) {
6978
7031
  * `lint/rfc2986/extension-request-ambiguous`; every one of them is read, so nothing a later
6979
7032
  * attribute asks for escapes the rows below.
6980
7033
  *
6981
- * @opts profile `"rfc2986"` for the structural rows alone, or `"cabf-tls"` to add the TLS rows
6982
- * (default runs the structural rows). A certificate, CRL or OCSP profile name is
6983
- * refused rather than run against a request.
7034
+ * @opts profile `"rfc2986"` for the structural rows alone, `"cabf-tls"` to add the TLS rows, or
7035
+ * `"rfc9883"` for the rows about a request carrying a possession statement
7036
+ * (default runs the structural rows and the rfc9883 rows). A certificate, CRL
7037
+ * or OCSP profile name is refused rather than run against a request.
6984
7038
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6985
7039
  * always reflect the complete, unfiltered result.
6986
7040
  * @example
@@ -7217,8 +7271,9 @@ function _ingestCms(input) {
7217
7271
  * `pki.cms.verify`'s, and the certificates a message carries are linted with
7218
7272
  * `pki.lint.certificate`.
7219
7273
  *
7220
- * @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
7221
- * profile name is refused rather than run against a message.
7274
+ * @opts profile `"rfc5652"`, or `"rfc8933"` for the rows that update it (default runs both).
7275
+ * A certificate, CRL or OCSP profile name is refused rather than run against a
7276
+ * message.
7222
7277
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
7223
7278
  * always reflect the complete, unfiltered result.
7224
7279
  * @example
@@ -7517,10 +7572,11 @@ function rules(profile, artifact) {
7517
7572
  * and `"default"`, are not among them.
7518
7573
  * @example
7519
7574
  * pki.lint.profiles();
7520
- * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
7521
- * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root",
7522
- * // "nist-pqc-transition", "cnsa-2.0", "rfc2986", "rfc5280-crl", "rfc6960", "rfc5019",
7523
- * // "rfc5652", "rfc3161", "rfc5755"]
7575
+ * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633", "x509-altsig",
7576
+ * // "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root", "chrome-root",
7577
+ * // "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0", "rfc2986",
7578
+ * // "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc8933", "rfc3161",
7579
+ * // "rfc5755"]
7524
7580
  */
7525
7581
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
7526
7582
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
package/lib/oid.js CHANGED
@@ -232,7 +232,14 @@ var FAMILIES = Object.assign(Object.create(null), {
232
232
  pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
233
233
  emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
234
234
  countersignature: 6, challengePassword: 7, extensionRequest: 14,
235
- smimeCapabilities: 15, friendlyName: 20, localKeyId: 21 } },
235
+ smimeCapabilities: 15, friendlyName: 20, localKeyId: 21,
236
+ /** @internal RFC 6211 sec. 2 and Appendix A both state the CMS algorithm protection attribute
237
+ * here, directly under PKCS 9, and that is the value OpenSSL and Bouncy Castle register. The
238
+ * same document's sec. 4 says "All identifiers are assigned out of the S/MIME OID arc", which
239
+ * the IANA registry, errata 9144 and 9145, and draft-ietf-lamps-rfc6211-update-03 read as
240
+ * `1.2.840.113549.1.9.16.2.52`, registered beside it as `cmsAlgorithmProtect`. Both are
241
+ * accepted on parse, because a mismatched identifier is equivalent to no protection. */
242
+ cmsAlgorithmProtection: 52 } },
236
243
 
237
244
  keyPackageContentTypes: { base: [2, 16, 840, 1, 101, 2, 1, 2, 78], of: { aKeyPackage: 5 } },
238
245
 
@@ -324,6 +331,9 @@ var FAMILIES = Object.assign(Object.create(null), {
324
331
  cmcUnsignedData: 34,
325
332
  binarySigningTime: 46,
326
333
  asymmDecryptKeyID: 54, relatedCertRequest: 60,
334
+ /** @internal The second value the CMS algorithm protection attribute is identified by. See
335
+ * `pkcs9.cmsAlgorithmProtection` for which document states which, and why both are read. */
336
+ cmsAlgorithmProtect: 52,
327
337
  certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
328
338
 
329
339
  ansiX962: { base: [1, 2, 840, 10045], of: {
@@ -945,16 +945,20 @@ function _flagSet(callerValue, anchorFlags, name) {
945
945
  return anchorFlags !== null && anchorFlags[name] === true;
946
946
  }
947
947
 
948
- function initialize(certs, params, seeds, anchor, ta) {
948
+ /** @internal `policySwitches` carries the three sec. 6.1.1 inputs as the door read and typed them.
949
+ * They are not read off `params` here: normalizing an anchor runs caller accessors for its name,
950
+ * key and algorithm, so an options object the caller still holds can carry a different value by
951
+ * the time this runs, and the state would be built from a value nothing typed. */
952
+ function initialize(certs, params, seeds, anchor, ta, policySwitches) {
949
953
  var n = certs.length;
950
- var state = _initialState(certs, params, seeds, anchor, n, ta);
954
+ var state = _initialState(certs, params, seeds, anchor, n, ta, policySwitches);
951
955
  /** @internal The root is the depth-zero anyPolicy node, and it is indexed like any other, so the
952
956
  * "is there a node at this depth for this policy" lookup answers for depth zero too. */
953
957
  policyLayer(state, 0)[OID.anyPolicy] = state.validPolicyGraph;
954
958
  return state;
955
959
  }
956
960
 
957
- function _initialState(certs, params, seeds, anchor, n, ta) {
961
+ function _initialState(certs, params, seeds, anchor, n, ta, policySwitches) {
958
962
  /** @internal The anchor's constraints are read HERE rather than merged into a copy of `params`,
959
963
  * because a copy answers only for the own properties the copy loop enumerated. A caller option
960
964
  * defined non-enumerably would be dropped by the copy and the anchor would then WIDEN the
@@ -969,9 +973,9 @@ function _initialState(certs, params, seeds, anchor, n, ta) {
969
973
  minRsaModulusBits: params.minRsaModulusBits !== undefined ? params.minRsaModulusBits : constants.LIMITS.RSA_MIN_MODULUS_BITS,
970
974
  permittedGenerations: intrinsic.filter(seeds.permittedGenerations, function (g) { return g.length > 0; }),
971
975
  excludedSubtrees: seeds.excluded,
972
- explicitPolicy: _flagSet(params.initialExplicitPolicy, ta.policyFlags, "requireExplicitPolicy") ? 0 : n + 1,
973
- inhibitAnyPolicy: _flagSet(params.initialAnyPolicyInhibit, ta.policyFlags, "inhibitAnyPolicy") ? 0 : n + 1,
974
- policyMapping: _flagSet(params.initialPolicyMappingInhibit, ta.policyFlags, "inhibitPolicyMapping") ? 0 : n + 1,
976
+ explicitPolicy: _flagSet(policySwitches.initialExplicitPolicy, ta.policyFlags, "requireExplicitPolicy") ? 0 : n + 1,
977
+ inhibitAnyPolicy: _flagSet(policySwitches.initialAnyPolicyInhibit, ta.policyFlags, "inhibitAnyPolicy") ? 0 : n + 1,
978
+ policyMapping: _flagSet(policySwitches.initialPolicyMappingInhibit, ta.policyFlags, "inhibitPolicyMapping") ? 0 : n + 1,
975
979
  workingPublicKeyAlgorithm: anchor.algorithm,
976
980
  workingPublicKey: anchor.publicKey,
977
981
  workingPublicKeyParameters: anchor.parameters || null,
@@ -1286,7 +1290,12 @@ function _revocationRow(row, rv) {
1286
1290
  * The value-carrying options (`time`, `maxPathCerts`,
1287
1291
  * `maxPolicyNodes`, `minRsaModulusBits`, the subtree seeds, `userInitialPolicySet`, `requiredEku`)
1288
1292
  * are validated at the entry point. A mis-shaped value throws `path/bad-input`,
1289
- * so it cannot silently go unapplied. Returns `{ valid, revocationChecked, anchorConstraints,
1293
+ * so it cannot silently go unapplied. `requireRevocation` makes the revocation
1294
+ * determination mandatory, failing the path with `path/revocation-undetermined`
1295
+ * where it could not be made. It and the three sec. 6.1.1 policy inputs
1296
+ * (`initialExplicitPolicy`, `initialAnyPolicyInhibit`,
1297
+ * `initialPolicyMappingInhibit`) each take `true`, `false`, `null` or an absent
1298
+ * option, and any other value throws `path/bad-input` for the same reason. Returns `{ valid, revocationChecked, anchorConstraints,
1290
1299
  * path, results, workingPublicKey, workingPublicKeyAlgorithm,
1291
1300
  * workingPublicKeyParameters, validPolicyGraph, userConstrainedPolicySet }` where `results[i].checks`
1292
1301
  * carries a per-check reason code (`path/*`) for every step. Pure and
@@ -1473,6 +1482,23 @@ async function _validate(path, opts, run, scope) {
1473
1482
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1474
1483
  var n = certs.length;
1475
1484
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
1485
+ /** @internal The four switches that turn a requirement on, each read as the boolean it documents
1486
+ * and read HERE, before any anchor is normalized. An anchor's own copies of the three policy
1487
+ * flags are typed where they are copied, and `_flagSet` compares the caller's value with the
1488
+ * anchor's, so typing one side and not the other would accept at the door what the comparison
1489
+ * refuses. Normalizing an anchor reads its name, key and algorithm, and those reads run caller
1490
+ * code when the anchor carries accessors: the typed values below are what every anchor attempt
1491
+ * is handed, so a requirement cannot be turned off between the attempts. */
1492
+ var policySwitches = {
1493
+ requireRevocation: guard.identifier.booleanOption(opts.requireRevocation, E,
1494
+ "path/bad-input", "validate: opts.requireRevocation"),
1495
+ initialExplicitPolicy: guard.identifier.booleanOption(opts.initialExplicitPolicy, E,
1496
+ "path/bad-input", "validate: opts.initialExplicitPolicy"),
1497
+ initialAnyPolicyInhibit: guard.identifier.booleanOption(opts.initialAnyPolicyInhibit, E,
1498
+ "path/bad-input", "validate: opts.initialAnyPolicyInhibit"),
1499
+ initialPolicyMappingInhibit: guard.identifier.booleanOption(opts.initialPolicyMappingInhibit, E,
1500
+ "path/bad-input", "validate: opts.initialPolicyMappingInhibit"),
1501
+ };
1476
1502
  /** @internal Captured before any anchor is normalized: an anchor field may be an accessor, and
1477
1503
  * that getter runs caller code which could otherwise clear these lists first. */
1478
1504
  var callerPermitted = checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees");
@@ -1506,6 +1532,14 @@ async function _validate(path, opts, run, scope) {
1506
1532
  * question the caller asked. Copying `opts.identity` across would have each attempt read
1507
1533
  * the caller's own list again, after the awaits the earlier attempts spent. */
1508
1534
  if (identityRefs !== null) o.identity = identityRefs;
1535
+ /** @internal The four switches as this call typed them, carried into every attempt rather
1536
+ * than copied from an options object the caller still holds: the tuples above were built by
1537
+ * reading each anchor, which runs the caller's accessors, so a value copied here would be
1538
+ * whatever those left behind. */
1539
+ o.requireRevocation = policySwitches.requireRevocation;
1540
+ o.initialExplicitPolicy = policySwitches.initialExplicitPolicy;
1541
+ o.initialAnyPolicyInhibit = policySwitches.initialAnyPolicyInhibit;
1542
+ o.initialPolicyMappingInhibit = policySwitches.initialPolicyMappingInhibit;
1509
1543
  return o;
1510
1544
  };
1511
1545
  var lastMatched = null, anyMatched = false;
@@ -1538,7 +1572,7 @@ async function _validate(path, opts, run, scope) {
1538
1572
  var requiredEku = purposeOpts.requiredEku;
1539
1573
  var checkPurpose = purposeOpts.checkPurpose;
1540
1574
 
1541
- var state = initialize(certs, opts, seeds, anchor, taConstraints);
1575
+ var state = initialize(certs, opts, seeds, anchor, taConstraints, policySwitches);
1542
1576
  state._n = n;
1543
1577
  state.requiredEku = requiredEku;
1544
1578
  /** @internal The verifier comes from the per-call scope the door built, never from `opts`: the option
@@ -1548,7 +1582,7 @@ async function _validate(path, opts, run, scope) {
1548
1582
  var verifierVerify = scope.verify;
1549
1583
  var revocationChecker = opts.revocationChecker || null;
1550
1584
  var softFail = opts.softFail === true;
1551
- var requireRevocation = opts.requireRevocation === true;
1585
+ var requireRevocation = policySwitches.requireRevocation;
1552
1586
  var failed = false;
1553
1587
  var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
1554
1588
  var prefixAuthentic = true;
@@ -3330,7 +3364,7 @@ async function _fetchAiaIssuers(current, aia) {
3330
3364
  * @opts direction Search direction: `"forward"` (default, from the leaf toward an anchor), `"reverse"` (from an anchor toward the leaf, RFC 4158 sec. 3.1, narrower when the anchor set is small and the leaf fans out), or `"auto"` (pick by first-hop fan-out). Reverse building is pool-only and does not combine with `fetchAia`. Every direction hands each assembled path to `validate`, so the direction changes only search order, never which paths are accepted.
3331
3365
  * @opts maxDepth Chain-length depth cap (default `C.LIMITS.PATH_BUILD_MAX_DEPTH`).
3332
3366
  * @opts maxCandidatesConsidered Total-work cap on candidate expansions (default `C.LIMITS.PATH_BUILD_MAX_CANDIDATES`).
3333
- * @opts validate `false` returns the ordered path without validating (pure-builder mode; default `true`).
3367
+ * @opts validate `false` returns the ordered path without validating (pure-builder mode; default `true`). It takes `true`, `false`, `null` or an absent option, and any other value is refused `path/bad-input` at the door rather than read as on.
3334
3368
  * @opts fetchAia `true` opts in to AIA caIssuers network fetching of a missing intermediate (default `false`, fully offline). Off unless set; when set, a fetch runs only on a pool miss for a non-anchor-adjacent cert, and (with validation on) every fetched cert still flows through `validate`.
3335
3369
  * @opts transport The injectable transport seam (`fn(request) -> Promise<{ status, headers, body }>`); tests drive the fetch offline. With none, the default `pki.transport.https` is used, which fails closed unless `opts.tls` carries trust. SSRF: for a caIssuers URL with a DNS hostname, a custom transport is used only if it declares `fn.blocksPrivateAddresses = true`, which vouches that it refuses (and pins) a resolved private / loopback / link-local / special-use address, as the default transport does. Without that marker a DNS-name AIA URL is fail-closed (skipped) and only an IP-literal URL (validated up front) is fetched; set the marker on your transport when it filters resolved addresses.
3336
3370
  * @opts tls The TLS trust for the AIA HTTPS host (`{ anchors, useSystemStore, ... }`). This is distinct from `opts.trustAnchors`, the PKI trust store the path validates against. The default transport refuses an unpinned server.
@@ -3338,7 +3372,7 @@ async function _fetchAiaIssuers(current, aia) {
3338
3372
  * @opts maxAiaPerCert Cap on caIssuers URLs tried per certificate (default `C.LIMITS.PATH_AIA_MAX_PER_CERT`).
3339
3373
  * @opts aiaTimeout Per-fetch timeout in ms, forwarded to the transport.
3340
3374
  * @opts maxResponseBytes Per-fetch response size cap, forwarded to the transport (tightenable downward only).
3341
- * @opts (validate options) Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged. When `userInitialPolicySet` is supplied, build returns the accepted path whose user-constrained policy set best satisfies it (RFC 4158 sec. 4 forward policy chaining) rather than the first accepted path; `validate` still gates every path, so this changes only which valid path is chosen. The ranking explores more of the candidate graph than a first-accept build, so it is bounded by `maxCandidatesConsidered` like the rest of the search. Reaching that cap means stop looking, not nothing found: if a valid path was already accepted, build returns the best-policy one of those; it reaches `path/build-limit` only when the cap is hit before any valid path is accepted. An `anyPolicy` entry makes the set unconstrained, so ranking is skipped and the first accepted path is returned.
3375
+ * @opts (validate options) Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged. `requireRevocation` and the three policy inputs are typed at this door as well as at `validate`'s, because a build reaches an AIA fetch before it reaches `validate` and a build that assembles no path never calls it. When `userInitialPolicySet` is supplied, build returns the accepted path whose user-constrained policy set best satisfies it (RFC 4158 sec. 4 forward policy chaining) rather than the first accepted path; `validate` still gates every path, so this changes only which valid path is chosen. The ranking explores more of the candidate graph than a first-accept build, so it is bounded by `maxCandidatesConsidered` like the rest of the search. Reaching that cap means stop looking, not nothing found: if a valid path was already accepted, build returns the best-policy one of those; it reaches `path/build-limit` only when the cap is hit before any valid path is accepted. An `anyPolicy` entry makes the set unconstrained, so ranking is skipped and the first accepted path is returned.
3342
3376
  * @example
3343
3377
  * var pair = await pki.key.generate("Ed25519");
3344
3378
  * var pemString = await pki.x509.sign({ subject: "Example Root", subjectPublicKey: await pki.key.export(pair.publicKey),
@@ -3372,6 +3406,38 @@ async function build(leaf, opts) {
3372
3406
  * second time and an accessor can answer differently on each read. */
3373
3407
  var checkedTransport = guard.identifier.assertCallableOption(opts, "transport", E, "path/bad-input",
3374
3408
  "build: opts.transport", "(request) -> Promise<{ status, headers, body }>");
3409
+ /** @internal The switches build forwards to `validate` are typed at THIS door too, because build
3410
+ * reaches the network before it reaches `validate`: with `fetchAia` on it fetches AIA issuers
3411
+ * while assembling a chain, and a build that assembles none returns `path/no-path` with no
3412
+ * `validate` call having read them. A door that only the successful route passes through is not
3413
+ * a door. */
3414
+ var FORWARDED_SWITCHES = ["requireRevocation", "initialExplicitPolicy", "initialAnyPolicyInhibit",
3415
+ "initialPolicyMappingInhibit"];
3416
+ var forwardedSwitches = intrinsic.create(null);
3417
+ intrinsic.forEach(FORWARDED_SWITCHES, function (k) {
3418
+ /** @internal Every one of the four is typed and recorded, including the ones the caller left
3419
+ * out. An absent switch recorded as `false` is the same question answered the same way, and
3420
+ * recording it is what stops a later read from finding one that was not there at this door:
3421
+ * reading each anchor runs the caller's accessors, and an accessor can add an option. */
3422
+ forwardedSwitches[k] = guard.identifier.booleanOption(opts[k], E, "path/bad-input", "build: opts." + k);
3423
+ });
3424
+ /** @internal `validate` defaults ON, so absence means on here and the off form is `false`. A
3425
+ * value outside that set is a caller asking for pure-builder mode in a spelling this verb does
3426
+ * not read, and running the validation they asked to skip is as silent as skipping it. */
3427
+ var doValidateOpt = (opts.validate === undefined || opts.validate === null) ? true
3428
+ : guard.identifier.booleanOption(opts.validate, E, "path/bad-input", "build: opts.validate");
3429
+ /** @internal Pure-builder mode runs no validation, so a requirement asked for alongside it is a
3430
+ * requirement nothing will apply. The combination is refused rather than answered with an
3431
+ * ordered path, the same way an asked-for check that its input cannot satisfy is refused. */
3432
+ if (!doValidateOpt) {
3433
+ intrinsic.forEach(FORWARDED_SWITCHES, function (k) {
3434
+ if (forwardedSwitches[k] === true) {
3435
+ throw E("path/bad-input", "build: opts." + k + " asks for a check that only validation " +
3436
+ "applies, and opts.validate is false, which returns the ordered path without validating; " +
3437
+ "drop one of the two");
3438
+ }
3439
+ });
3440
+ }
3375
3441
  var leafCert;
3376
3442
  try { leafCert = coerceCert(leaf); }
3377
3443
  catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
@@ -3409,7 +3475,7 @@ async function build(leaf, opts) {
3409
3475
  // allow:guard-shape-reinlined -- opts.maxDepth here is the path-building chain depth (capped via guard.limits.cap against the PATH_BUILD_MAX_DEPTH ceiling), a different concept from the decode-recursion depth guard.limits.depthCap enforces
3410
3476
  var maxDepth = guard.limits.cap(opts.maxDepth, "build: opts.maxDepth", Math.min(constants.LIMITS.PATH_BUILD_MAX_DEPTH, depthCeiling), { E: E, code: "path/bad-input", min: 0, max: depthCeiling });
3411
3477
  var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
3412
- var doValidate = opts.validate !== false;
3478
+ var doValidate = doValidateOpt;
3413
3479
 
3414
3480
  var direction = opts.direction !== undefined ? opts.direction : "forward";
3415
3481
  if (direction !== "forward" && direction !== "reverse" && direction !== "auto") {
@@ -3448,6 +3514,10 @@ async function build(leaf, opts) {
3448
3514
  var forwarded = Object.create(null);
3449
3515
  intrinsic.forEach(intrinsic.keys(_VALIDATE_OPTS), function (k) {
3450
3516
  if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
3517
+ /** @internal A switch this door typed is forwarded as the value it typed. Reading it off the
3518
+ * options object again would ask a second time, and normalizing an anchor between the two runs
3519
+ * caller accessors, so the value `validate` is handed need not be the one that passed here. */
3520
+ if (intrinsic.hasOwn(forwardedSwitches, k)) { forwarded[k] = forwardedSwitches[k]; return; }
3451
3521
  if (k in opts) forwarded[k] = opts[k];
3452
3522
  });
3453
3523
  if (buildPermRaw != null) forwarded.initialPermittedSubtrees = buildPermSeeds;
@@ -192,9 +192,7 @@ function _no(why) { return { url: null, why: why }; }
192
192
  * under a `=== true` test, and the requests would go out carrying no nonce with nothing said. An
193
193
  * absent value is the documented default; anything else is a configuration fault. */
194
194
  function _booleanOption(value, label) {
195
- if (value === undefined || value === null) return false;
196
- if (value === true || value === false) return value;
197
- throw E("path/bad-input", label + " must be true or false, got " + guard.text.showValue(value));
195
+ return guard.identifier.booleanOption(value, E, "path/bad-input", label);
198
196
  }
199
197
 
200
198
  /** @internal Whether this transport says it filters the address it resolved. A transport is a