@blamejs/pki 0.5.10 → 0.5.11

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.
@@ -39,6 +39,7 @@
39
39
  // changes what is checked, never what the operator reads.
40
40
 
41
41
  var bytes = require("./guard-bytes");
42
+ var time = require("./guard-time");
42
43
 
43
44
  // Every form the toolkit calls "bytes": a Buffer, any typed-array view, a DataView, or a bare
44
45
  // ArrayBuffer. Testing `Buffer.isBuffer(x) || x instanceof Uint8Array` is narrower than what the
@@ -46,8 +47,11 @@ var bytes = require("./guard-bytes");
46
47
  // through to the object branch and be refused as a rebuilt structure, which reads to the caller as
47
48
  // "your certificate is malformed" when what happened is that their container was not recognized.
48
49
  // guard.bytes is what finally reads them, and it accepts all four, so the door has to as well.
50
+ // Asked of guard.bytes rather than restated here, because a restatement is what drifts. An
51
+ // `x instanceof ArrayBuffer` here turns away a real buffer built in another realm that guard.bytes
52
+ // goes on to accept, so one container gets opposite answers from the two doors it passes through.
49
53
  function _isBytes(x) {
50
- return Buffer.isBuffer(x) || ArrayBuffer.isView(x) || x instanceof ArrayBuffer;
54
+ return bytes.isByteSource(x);
51
55
  }
52
56
 
53
57
  // The shapes below assert the parser's complete output: every field
@@ -150,7 +154,7 @@ function isCert(o) {
150
154
  _isAlgorithmIdentifier(o.signatureAlgorithm) &&
151
155
  _isAlgorithmIdentifier(o.tbsSignatureAlgorithm) &&
152
156
  _isBitString(o.signatureValue) &&
153
- !!o.validity && o.validity.notBefore instanceof Date && o.validity.notAfter instanceof Date &&
157
+ !!o.validity && time.isDate(o.validity.notBefore) && time.isDate(o.validity.notAfter) &&
154
158
  _isName(o.issuer) &&
155
159
  _isName(o.subject) &&
156
160
  !!o.subjectPublicKeyInfo && Buffer.isBuffer(o.subjectPublicKeyInfo.bytes) &&
@@ -174,13 +178,13 @@ function isCrl(o) {
174
178
  _isAlgorithmIdentifier(o.signatureAlgorithm) &&
175
179
  _isBitString(o.signatureValue) &&
176
180
  _isName(o.issuer) &&
177
- o.thisUpdate instanceof Date &&
178
- (o.nextUpdate === null || o.nextUpdate instanceof Date) &&
181
+ time.isDate(o.thisUpdate) &&
182
+ (o.nextUpdate === null || time.isDate(o.nextUpdate)) &&
179
183
  Array.isArray(o.crlExtensions) && o.crlExtensions.every(_isCrlExtensionEntry) &&
180
184
  Array.isArray(o.revokedCertificates) && o.revokedCertificates.every(function (e) {
181
185
  return !!e && typeof e.serialNumber === "bigint" &&
182
186
  typeof e.serialNumberHex === "string" &&
183
- e.revocationDate instanceof Date &&
187
+ time.isDate(e.revocationDate) &&
184
188
  Array.isArray(e.crlEntryExtensions) && e.crlEntryExtensions.every(_isCrlExtensionEntry);
185
189
  });
186
190
  }
package/lib/guard-time.js CHANGED
@@ -27,6 +27,8 @@
27
27
  // OCSP / CRL currency callers that treat out-of-window as a `continue` skip keep
28
28
  // their control flow while a NaN operand can never slip through as a false.
29
29
 
30
+ var util = require("util");
31
+
30
32
  // assertValid(value, E, code, label) -> the same Date, once proven valid.
31
33
  // value : a Date (or an alleged one) from a caller boundary.
32
34
  // E : the (code, message[, cause]) typed-error FACTORY in scope at the call
@@ -34,9 +36,44 @@
34
36
  // would crash `class cannot be invoked without new` on the error path.
35
37
  // code : the frozen domain/reason code this boundary rejects malformed time under.
36
38
  // label : field phrase for the message.
39
+ // The instant a Date holds, asked of the language rather than of the Date.
40
+ //
41
+ // `getTime` is an ordinary method on `Date.prototype`, so `class T extends Date { getTime() {} }`
42
+ // and an own `getTime` on any instance both answer it. Reading it off the value let a caller throw
43
+ // a raw exception out of a guard whose contract is a typed error, and let one return a fabricated
44
+ // instant that passed the check while the real one did not. It also made the read repeatable: a
45
+ // method asked twice may answer twice, so a window check could validate one instant and compare
46
+ // another.
47
+ //
48
+ // The intrinsic is called against the value, which reaches the internal slot whatever the value's
49
+ // own class says, and refuses a receiver with no slot behind it.
50
+ //
51
+ // The capture is of whatever `Date.prototype.getTime` holds when this module first loads, so it
52
+ // is the language's own function only if nothing replaced it earlier in the process. Code that
53
+ // runs before this module is inside the trust boundary already and can reach further than this
54
+ // one method, so there is no defense here to add; what the capture buys is immunity to every
55
+ // replacement that happens afterwards, which is the case a caller's argument can reach.
56
+ var _dateGetTime = Date.prototype.getTime;
57
+
58
+ // instantOf(value) -> the epoch milliseconds a Date holds, read through the intrinsic.
59
+ //
60
+ // The shape is the method call itself, wherever it appears. There is no boundary at which reading
61
+ // `getTime` off a value is the safe spelling: a caller's Date reaches the read through an override,
62
+ // and this toolkit's own Date reaches it through whatever `Date.prototype.getTime` has been made
63
+ // to be, which a dependency loaded before this module gets to decide. So the rule is that lib/
64
+ // calls no `getTime` at all, and the one capture above is where it lives.
65
+ //
66
+ // @enforced-by guard-shape-reinlined
67
+ // @guard-shape \.getTime\s*\(
68
+ function instantOf(value) { return _dateGetTime.call(value); }
69
+
37
70
  // @enforced-by nan-date-comparison-unguarded
38
71
  function assertValid(value, E, code, label) {
39
- if (!(value instanceof Date) || isNaN(value.getTime())) {
72
+ // The slot, never the prototype. `instanceof Date` asks what a value inherits from, and
73
+ // `Object.create(Date.prototype)` inherits everything while holding no instant, so it passes
74
+ // that test and `getTime()` then throws a raw TypeError out of this guard. `util.types.isDate`
75
+ // reads the internal slot, which a caller cannot fake and which answers the same across realms.
76
+ if (!util.types.isDate(value) || isNaN(instantOf(value))) {
40
77
  throw E(code, (label || "value") + " must be a valid Date");
41
78
  }
42
79
  return value;
@@ -56,13 +93,37 @@ function within(instant, lower, upper, E, code, label, opts) {
56
93
  assertValid(instant, E, code, label);
57
94
  assertValid(lower, E, code, (label || "window") + " lower bound");
58
95
  assertValid(upper, E, code, (label || "window") + " upper bound");
59
- var t = instant.getTime();
60
- var lo = lower.getTime();
61
- var hi = upper.getTime();
96
+ // Read through the intrinsic, so the instants compared here are the ones assertValid just
97
+ // proved. A caller's `getTime` override answers each call afresh, which made the value checked
98
+ // and the value compared two different reads of the same argument.
99
+ var t = instantOf(instant);
100
+ var lo = instantOf(lower);
101
+ var hi = instantOf(upper);
62
102
  return t >= lo && (opts && opts.upperInclusive ? t <= hi : t < hi);
63
103
  }
64
104
 
105
+ // isDate(value) -> boolean: does this value actually hold an instant?
106
+ //
107
+ // The predicate half of assertValid, for the boundaries that choose a default rather than refuse
108
+ // (`opts.genTime instanceof Date ? opts.genTime : new Date()`). `instanceof` asks what a value
109
+ // inherits from, so `Object.create(Date.prototype)` passes it while holding no instant, and the
110
+ // `.getTime()` that follows throws a raw TypeError out of a verb whose contract is a typed error.
111
+ // A caller who supplied one and got the default instead would be worse served still: the value
112
+ // they passed would have been silently replaced. This reads the slot, which cannot be faked and
113
+ // answers the same for a Date built in another realm.
114
+ //
115
+ // An Invalid Date has the slot and a NaN instant, so this says yes to one. A boundary that
116
+ // compares instants wants assertValid, which rejects both shapes.
117
+ // @enforced-by behavioral -- a prototype test in place of a slot test has no rename-proof code
118
+ // shape; the RED vectors (a slot-less Date lookalike at cms.verify / cms.sign / tsp.sign) are
119
+ // the guard.
120
+ function isDate(value) {
121
+ return util.types.isDate(value);
122
+ }
123
+
65
124
  module.exports = {
66
125
  assertValid: assertValid,
126
+ instantOf: instantOf,
127
+ isDate: isDate,
67
128
  within: within,
68
129
  };
package/lib/inspect.js CHANGED
@@ -91,8 +91,13 @@ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oc
91
91
  function _two(n) { return (n < 10 ? "0" : "") + n; }
92
92
  // OpenSSL date: "Jul 4 07:00:27 2026 GMT" (month, space-padded day, time, year, GMT).
93
93
  function _date(iso) {
94
- var d = (iso instanceof Date) ? iso : new Date(iso);
95
- if (isNaN(d.getTime())) return String(iso);
94
+ // Rebuilt from the instant the value holds, so every field below is read off a Date of this
95
+ // realm. The calendar methods are ordinary ones a subclass answers, and a parsed structure can
96
+ // carry a Date a caller supplied, so reading them off that value let it decide the date printed
97
+ // beside a certificate rather than the one the certificate carries.
98
+ var held = guard.time.isDate(iso) ? guard.time.instantOf(iso) : Date.parse(String(iso));
99
+ if (isNaN(held)) return String(iso);
100
+ var d = new Date(held);
96
101
  var day = d.getUTCDate(), dd = (day < 10 ? " " : "") + day;
97
102
  return MONTHS[d.getUTCMonth()] + " " + dd + " " +
98
103
  _two(d.getUTCHours()) + ":" + _two(d.getUTCMinutes()) + ":" + _two(d.getUTCSeconds()) +
@@ -745,7 +750,7 @@ function _crlExtension(ext, pad) {
745
750
  var inner = pad + " ";
746
751
  if (ext.oid === OID_CRL_NUMBER && typeof ext.value === "bigint") return header + "\n" + inner + String(ext.value);
747
752
  if (ext.oid === OID_REASON_CODE && typeof ext.value === "number") return header + "\n" + inner + (NAMES.CRL_REASON[ext.value] || String(ext.value));
748
- if (ext.oid === OID_INVALIDITY_DATE && ext.value instanceof Date) return header + "\n" + inner + _date(ext.value);
753
+ if (ext.oid === OID_INVALIDITY_DATE && guard.time.isDate(ext.value)) return header + "\n" + inner + _date(ext.value);
749
754
  if (ext.oid === OID_DELTA_CRL_INDICATOR && Buffer.isBuffer(ext.value)) {
750
755
  try { return header + "\n" + inner + "BaseCRLNumber: " + String(asn1.read.integer(asn1.decode(ext.value))); }
751
756
  catch (_e) { /* malformed BaseCRLNumber -> fall through to the shared _extension (hex) */ }
package/lib/key.js CHANGED
@@ -106,8 +106,16 @@ async function _toPrivateKeyDer(input) {
106
106
  * var enc = await pki.key.encrypt(der, "s3cr3t", { pem: true });
107
107
  * var back = await pki.key.decrypt(enc, "s3cr3t");
108
108
  */
109
+ // Every option pki.key.encrypt reads. `iterations` here answers a different question from
110
+ // `maxIterations` on decrypt. This side chooses a PBKDF2 count, and that side caps the one it
111
+ // will honor. Passing either name to the other verb is silently nothing, so both are refused.
112
+ var _ENCRYPT_OPTS = { cipher: 1, iterations: 1, pem: 1, prf: 1, salt: 1 };
113
+
109
114
  async function encrypt(privateKey, password, opts) {
110
- opts = opts || {};
115
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.encrypt options");
116
+ guard.identifier.assertKnownKeys(opts, _ENCRYPT_OPTS, _err, "key/bad-input",
117
+ "pki.key.encrypt has an unknown option (the PBKDF2 count here is `iterations`; " +
118
+ "`maxIterations` is the decrypt-side cap): ");
111
119
  var der = await _toPrivateKeyDer(privateKey);
112
120
  // RFC 5958 sec. 3: the plaintext is the DER PrivateKeyInfo -- validate it IS one (the strict parser
113
121
  // rejects BER / non-canonical / trailing bytes) before encrypting, so we never wrap opaque input.
@@ -166,8 +174,15 @@ async function encrypt(privateKey, password, opts) {
166
174
  * var enc = await pki.key.encrypt(await pki.key.export(pair.privateKey), "s3cr3t", { pem: true });
167
175
  * var der = await pki.key.decrypt(enc, "s3cr3t");
168
176
  */
177
+ // Every option pki.key.decrypt reads. `maxIterations` is the cap this side honors, not the
178
+ // `iterations` the encrypt side chooses; see the note above _ENCRYPT_OPTS.
179
+ var _DECRYPT_OPTS = { maxIterations: 1, pem: 1 };
180
+
169
181
  async function decrypt(encrypted, password, opts) {
170
- opts = opts || {};
182
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.decrypt options");
183
+ guard.identifier.assertKnownKeys(opts, _DECRYPT_OPTS, _err, "key/bad-input",
184
+ "pki.key.decrypt has an unknown option (the PBKDF2 cap here is `maxIterations`; " +
185
+ "`iterations` is the encrypt-side count): ");
171
186
  // opts.maxIterations is a CALLER authoring bound (a downward-only cap): validate it config-time so an
172
187
  // invalid value is a key/bad-input, never a NaN that silently disables the DoS cap and never mis-coded as
173
188
  // a fault in the attacker-controlled input parameters.
@@ -226,8 +241,19 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
226
241
  * var pair = await pki.key.generate("Ed25519");
227
242
  * var spkiPem = await pki.key.export(pair.publicKey, { format: "pem" });
228
243
  */
244
+ // Every option pki.key.export reads. Adding one here is the only way to make it accepted.
245
+ var _EXPORT_OPTS = { format: 1, label: 1 };
246
+
229
247
  async function export_(key, opts) {
230
- opts = opts || {};
248
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.export options");
249
+ // This verb does NOT encrypt, and `password` is the option a caller reaches for when they
250
+ // think it does. Ignoring it writes the private key in the clear while the call site reads
251
+ // as though it were protected. Every other unknown option here costs a setting that does
252
+ // nothing; this one costs a plaintext key on disk. `pki.key.encrypt(key, password, opts)`
253
+ // is the verb that encrypts; export serializes whatever it is handed.
254
+ guard.identifier.assertKnownKeys(opts, _EXPORT_OPTS, _err, "key/bad-input",
255
+ "pki.key.export has an unknown option. It serializes only. To protect a private key, call " +
256
+ "pki.key.encrypt(key, password) first and export its result. The unknown option was: ");
231
257
  if (!_isCryptoKey(key)) throw _err("key/bad-input", "export expects a WebCrypto CryptoKey");
232
258
  var defaultLabel;
233
259
  if (key.type === "private") defaultLabel = "PRIVATE KEY";
@@ -264,8 +290,13 @@ async function export_(key, opts) {
264
290
  * var spki = await pki.key.export(pair.publicKey);
265
291
  * var pub = await pki.key.import(spki); // Ed25519 -- algorithm inferred
266
292
  */
293
+ // Every option pki.key.import reads.
294
+ var _IMPORT_OPTS = { algorithm: 1, extractable: 1, password: 1, usages: 1 };
295
+
267
296
  async function import_(input, opts) {
268
- opts = opts || {};
297
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.import options");
298
+ guard.identifier.assertKnownKeys(opts, _IMPORT_OPTS, _err, "key/bad-input",
299
+ "pki.key.import has an unknown option: ");
269
300
  var detected = _detectKeyInput(input);
270
301
  if (detected.format === "encrypted") {
271
302
  if (opts.password == null) throw _err("key/bad-input", "an ENCRYPTED PRIVATE KEY requires opts.password to import");
@@ -303,8 +334,14 @@ async function import_(input, opts) {
303
334
  * var pair = await pki.key.generate("Ed25519");
304
335
  * var pkcs8 = await pki.key.export(pair.privateKey);
305
336
  */
337
+ // Every option pki.key.generate reads. `extractable` follows the WebCrypto spelling; a
338
+ // misspelling would leave a key extractable by default, which is the permissive direction.
339
+ var _GENERATE_OPTS = { extractable: 1, usages: 1 };
340
+
306
341
  async function generate(algorithm, opts) {
307
- opts = opts || {};
342
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.generate options");
343
+ guard.identifier.assertKnownKeys(opts, _GENERATE_OPTS, _err, "key/bad-input",
344
+ "pki.key.generate has an unknown option (the WebCrypto spelling is `extractable`): ");
308
345
  var extractable = opts.extractable != null ? opts.extractable : true;
309
346
  var usages = opts.usages || _generateUsages(_algName(algorithm));
310
347
  var pair;
@@ -333,8 +370,13 @@ async function generate(algorithm, opts) {
333
370
  * var pair = await pki.key.generate("Ed25519");
334
371
  * var spki = await pki.key.publicFromPrivate(await pki.key.export(pair.privateKey));
335
372
  */
373
+ // Every option pki.key.publicFromPrivate reads.
374
+ var _PUBLIC_FROM_PRIVATE_OPTS = { pem: 1 };
375
+
336
376
  async function publicFromPrivate(privateKey, opts) {
337
- opts = opts || {};
377
+ opts = guard.identifier.optionsObject(opts, _err, "key/bad-input", "pki.key.publicFromPrivate options");
378
+ guard.identifier.assertKnownKeys(opts, _PUBLIC_FROM_PRIVATE_OPTS, _err, "key/bad-input",
379
+ "pki.key.publicFromPrivate has an unknown option: ");
338
380
  var der = await _toPrivateKeyDer(privateKey);
339
381
  var spki;
340
382
  try {
package/lib/lint.js CHANGED
@@ -174,7 +174,7 @@ function _effective(rule, cert) {
174
174
  if (!rule.effectiveDate) return true;
175
175
  var nb = cert.validity && cert.validity.notBefore;
176
176
  // allow:nan-date-comparison-unguarded -- nb is a codec-parsed cert notBefore (asn1 readTime rejects a NaN instant); effectiveDate is a Date literal.
177
- return (nb instanceof Date) && nb.getTime() >= rule.effectiveDate.getTime();
177
+ return guard.time.isDate(nb) && guard.time.instantOf(nb) >= guard.time.instantOf(rule.effectiveDate);
178
178
  }
179
179
 
180
180
  function _runLints(rules, cert, ctx) {
@@ -374,7 +374,8 @@ var RFC5280_RULES = [
374
374
  check: function (cert) {
375
375
  var v = cert.validity;
376
376
  // allow:nan-date-comparison-unguarded -- notBefore/notAfter are codec-parsed cert dates (asn1 readTime rejects a NaN instant).
377
- return (v.notBefore instanceof Date && v.notAfter instanceof Date && v.notBefore.getTime() > v.notAfter.getTime()) ? true : null;
377
+ return (guard.time.isDate(v.notBefore) && guard.time.isDate(v.notAfter) &&
378
+ guard.time.instantOf(v.notBefore) > guard.time.instantOf(v.notAfter)) ? true : null;
378
379
  },
379
380
  },
380
381
  {
@@ -558,7 +559,7 @@ var VALIDITY_SCHEDULE_START = VALIDITY_SCHEDULE[VALIDITY_SCHEDULE.length - 1].fr
558
559
  function _validityCeilingDays(notBefore) {
559
560
  for (var i = 0; i < VALIDITY_SCHEDULE.length; i++) {
560
561
  // allow:nan-date-comparison-unguarded -- notBefore is a codec-parsed cert date (NaN-rejected); the schedule bounds are Date literals.
561
- if (notBefore.getTime() >= VALIDITY_SCHEDULE[i].from.getTime()) return VALIDITY_SCHEDULE[i].maxDays;
562
+ if (guard.time.instantOf(notBefore) >= guard.time.instantOf(VALIDITY_SCHEDULE[i].from)) return VALIDITY_SCHEDULE[i].maxDays;
562
563
  }
563
564
  // Unreachable: the rule's effectiveDate (VALIDITY_SCHEDULE_START) reports NE for any cert
564
565
  // whose notBefore predates the schedule, so the loop always matches the last entry.
@@ -634,9 +635,9 @@ var CABF_TLS_RULES = [
634
635
  effectiveDate: VALIDITY_SCHEDULE_START,
635
636
  check: function (cert) {
636
637
  var v = cert.validity;
637
- if (!(v.notBefore instanceof Date) || !(v.notAfter instanceof Date)) return null;
638
+ if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
638
639
  var maxDays = _validityCeilingDays(v.notBefore);
639
- var days = (v.notAfter.getTime() - v.notBefore.getTime()) / MS_PER_DAY;
640
+ var days = (guard.time.instantOf(v.notAfter) - guard.time.instantOf(v.notBefore)) / MS_PER_DAY;
640
641
  return days > maxDays ? { context: { days: Math.round(days), maxDays: maxDays } } : null;
641
642
  },
642
643
  },
@@ -739,9 +740,15 @@ function _applyThreshold(report, severity) {
739
740
  * report.worst; // "notice" | "error" | ...
740
741
  * report.findings.map(function (f) { return f.id; });
741
742
  */
743
+ // Every option pki.lint.certificate reads. The values of both are already validated below;
744
+ // this refuses a key that is not one of them, so a misspelled `profile` cannot silently lint
745
+ // against the default profile while the call site reads as though it named one.
746
+ var _CERTIFICATE_OPTS = { profile: 1, severity: 1 };
747
+
742
748
  function certificate(input, opts) {
743
- opts = opts || {};
744
- if (typeof opts !== "object") throw _cfg("lint/bad-input", "pki.lint options must be an object");
749
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
750
+ guard.identifier.assertKnownKeys(opts, _CERTIFICATE_OPTS, _cfg, "lint/bad-input",
751
+ "pki.lint.certificate has an unknown option: ");
745
752
  // Validate BOTH config options up front so a config error fails fast, before any work.
746
753
  if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
747
754
  throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
@@ -28,6 +28,7 @@
28
28
  // unusable per the lightweight profile).
29
29
 
30
30
  var asn1 = require("./asn1-der");
31
+ var guard = require("./guard-all");
31
32
  var oid = require("./oid");
32
33
  var x509 = require("./schema-x509");
33
34
  var compositeSig = require("./composite-sig");
@@ -144,7 +145,11 @@ function makeOcspVerify(deps) {
144
145
  if (!isOctetAligned(rc.signatureValue)) continue;
145
146
  if (!(await verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
146
147
  // The delegate cert MUST itself be valid at the validation instant.
147
- if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
148
+ // Instants, never the Date objects: comparing two Dates coerces each through `valueOf`,
149
+ // which a caller's subclass answers, so the delegate would be checked at whatever moment
150
+ // the caller's `time` chose to report rather than the one it holds.
151
+ if (guard.time.instantOf(time) < guard.time.instantOf(rc.validity.notBefore) ||
152
+ guard.time.instantOf(time) > guard.time.instantOf(rc.validity.notAfter)) continue;
148
153
  // The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
149
154
  var eku;
150
155
  try { eku = decodeExt(rc, OID_EKU); }
@@ -199,15 +204,16 @@ function makeOcspVerify(deps) {
199
204
  var sr = br.responses[s];
200
205
  if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue;
201
206
  if (ocspHasCriticalExtension(sr.singleExtensions)) continue;
202
- if (sr.thisUpdate > time) continue;
203
- if (!sr.nextUpdate || sr.nextUpdate < time) continue;
207
+ if (guard.time.instantOf(sr.thisUpdate) > guard.time.instantOf(time)) continue;
208
+ if (!sr.nextUpdate || guard.time.instantOf(sr.nextUpdate) < guard.time.instantOf(time)) continue;
204
209
  out.matched = true; out.thisUpdate = sr.thisUpdate; out.nextUpdate = sr.nextUpdate;
205
210
  var st = sr.certStatus;
206
211
  if (st.type === "revoked") {
207
212
  // Present-time validation revokes regardless of a future revocationTime (skew/post-dating);
208
213
  // only explicit historical validation defers a strictly-future revocation (reports good).
209
214
  // allow:nan-date-comparison-unguarded -- revocationTime is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (sawGood stays unset -> not treated good), and `time` is validated at the pki.ocsp.verify / path.verifyOcspResponse entry points.
210
- if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { out.sawGood = true; }
215
+ if (historical && guard.time.isDate(st.revocationTime) &&
216
+ guard.time.instantOf(st.revocationTime) > guard.time.instantOf(time)) { out.sawGood = true; }
211
217
  else if (!out.revoked) { out.revoked = { revocationReason: st.revocationReason || null, reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : "") }; }
212
218
  } else if (st.type === "good") { out.sawGood = true; }
213
219
  else { out.sawUnknownStatus = true; }
package/lib/ocsp.js CHANGED
@@ -159,8 +159,19 @@ function buildRequest(query, opts) {
159
159
  ], _buildRequest);
160
160
  }
161
161
 
162
+ // Every option pki.ocsp.buildRequest reads. `nonce` earns the named refusal: it is the
163
+ // anti-replay binding, and a misspelling ships a request with no nonce at all, which the
164
+ // responder answers happily and the caller cannot later bind to.
165
+ // Taken from this verb's own @opts block, which is the published contract; a grep of the
166
+ // function body misses `pem`, read by the shared emitter rather than here.
167
+ var _BUILD_REQUEST_OPTS = {
168
+ hashAlgorithm: 1, nonce: 1, pem: 1, profile: 1, requestorName: 1, signer: 1
169
+ };
170
+
162
171
  function _buildRequest(query, opts) {
163
- opts = opts || {};
172
+ opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.buildRequest options");
173
+ guard.identifier.assertKnownKeys(opts, _BUILD_REQUEST_OPTS, _err, "ocsp/bad-input",
174
+ "pki.ocsp.buildRequest has an unknown option (the anti-replay nonce is `nonce`): ");
164
175
  var lightweight = opts.profile === "lightweight";
165
176
  var hashName = opts.hashAlgorithm || "sha1";
166
177
  if (lightweight && hashName !== "sha1") throw _err("ocsp/bad-input", "the lightweight profile requires a SHA-1 CertID (RFC 5019 sec. 2.1.1)");
@@ -273,8 +284,15 @@ function sign(responseData, responder, opts) {
273
284
  ], _sign);
274
285
  }
275
286
 
287
+ // Every option pki.ocsp.sign reads. `nonce` here is the request nonce this response echoes back.
288
+ // A misspelling emits a response that answers nobody's request, and the client that sent a nonce
289
+ // then reads it as unbound.
290
+ var _SIGN_OPTS = { embedCert: 1, extendedRevoke: 1, nonce: 1, pem: 1 };
291
+
276
292
  function _sign(responseData, responder, opts) {
277
- opts = opts || {};
293
+ opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.sign options");
294
+ guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "ocsp/bad-input",
295
+ "pki.ocsp.sign has an unknown option (the nonce to echo back is `nonce`): ");
278
296
  responseData = responseData || {};
279
297
  if (!responder || responder.cert == null || responder.key == null) throw _err("ocsp/bad-input", "a responder must be { cert, key }");
280
298
  var respCertDer = _normCertDer(responder.cert, "the responder certificate");
@@ -343,8 +361,8 @@ function _signResponse(responseData, responder, respCert, respCertDer, responder
343
361
  // GeneralizedTime in a SIGNED response, and compare false against every currency bound on verify.
344
362
  function _asDate(d) {
345
363
  if (d == null) return null;
346
- var dt = d instanceof Date ? d : new Date(d);
347
- if (isNaN(dt.getTime())) throw _err("ocsp/bad-input", "an invalid date value " + JSON.stringify(d));
364
+ var dt = guard.time.isDate(d) ? d : new Date(d);
365
+ if (isNaN(guard.time.instantOf(dt))) throw _err("ocsp/bad-input", "an invalid date value " + JSON.stringify(d));
348
366
  return dt;
349
367
  }
350
368
  // A private copy of whatever signer-key material the caller owns, taken at the entry
@@ -485,8 +503,19 @@ function buildErrorResponse(status) {
485
503
  * var res = await pki.ocsp.verify(responseDer, { cert: leafDer, issuer: caDer });
486
504
  * res.status; // "good" | "revoked" | "unknown"
487
505
  */
506
+ // Every option pki.ocsp.verify reads. `requestNonce` is the one worth refusing by name: it is
507
+ // what binds the response to this request. A misspelling leaves nonceMatched null on a call
508
+ // that did ask, and null is the value meaning "the client never asked".
509
+ var _VERIFY_OPTS = { cert: 1, historicalMode: 1, issuer: 1, requestNonce: 1, time: 1 };
510
+
488
511
  function verify(response, opts) {
489
- opts = opts || {};
512
+ // This verb reports faults by rejecting, not throwing, so both entry guards run inside the
513
+ // same try and surface as a rejection like every other fault on this path.
514
+ try {
515
+ opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.verify options");
516
+ guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "ocsp/bad-input",
517
+ "pki.ocsp.verify has an unknown option (the request nonce to bind against is `requestNonce`): ");
518
+ } catch (e) { return Promise.reject(e); }
490
519
  if (opts.cert == null || opts.issuer == null) return Promise.reject(_err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
491
520
  var parsed, cert, issuerCert, time;
492
521
  try {
@@ -1170,8 +1170,28 @@ function validateCriticalExtensionStructure(cert) {
1170
1170
  * });
1171
1171
  * res.valid; // boolean; res.results[0].checks carries the per-check codes
1172
1172
  */
1173
+ // Every option pki.path.validate reads. The anchor option here is the singular `trustAnchor`,
1174
+ // while pki.path.build in this same module spells it `trustAnchors` and takes a list. A caller
1175
+ // moving between the two verbs carries the wrong spelling without leaving the namespace, and
1176
+ // before this it bought them no anchoring and no error, just "a trustAnchor is required" about
1177
+ // an option they believed they had passed.
1178
+ // Derived from three sources, because no one of them is complete: the `opts.*` reads in this
1179
+ // function, the `params.*` reads in the RFC 5280 6.1.1 state it is passed down to, and the
1180
+ // documented prose above, which is the only place `requiredEku` and the three initial-policy
1181
+ // inputs appear together. A grep of one function would have shipped a list missing four real
1182
+ // options and rejected callers that work today.
1183
+ var _VALIDATE_OPTS = {
1184
+ checkPurpose: 1, historicalMode: 1, initialAnyPolicyInhibit: 1, initialExcludedSubtrees: 1,
1185
+ initialExplicitPolicy: 1, initialPermittedSubtrees: 1, initialPolicyMappingInhibit: 1,
1186
+ maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1, requiredEku: 1, revocationChecker: 1,
1187
+ softFail: 1, time: 1, trustAnchor: 1, userInitialPolicySet: 1, verifier: 1
1188
+ };
1189
+
1173
1190
  async function validate(path, opts) {
1174
- opts = opts || {};
1191
+ opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
1192
+ guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
1193
+ "pki.path.validate has an unknown option. The anchor here is `trustAnchor`, singular; " +
1194
+ "pki.path.build takes `trustAnchors`. The unknown option was: ");
1175
1195
  if (!Array.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
1176
1196
  // Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
1177
1197
  // (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
@@ -1288,10 +1308,15 @@ async function validate(path, opts) {
1288
1308
  }
1289
1309
 
1290
1310
  // 6.1.3(a)(2) validity window.
1291
- var t = opts.time;
1311
+ //
1312
+ // Instants, never the Date objects. Comparing two Dates coerces each one, and coercion runs
1313
+ // `Symbol.toPrimitive` or `valueOf`, which a caller's subclass answers. opts.time was checked
1314
+ // for a valid held instant at entry, and comparing the object here asked it again through a
1315
+ // door the check never used -- so the value validated and the value compared were two reads.
1316
+ var t = guard.time.instantOf(opts.time);
1292
1317
  var vOk = true, vCode;
1293
- if (t < cert.validity.notBefore) { vOk = false; vCode = "path/not-yet-valid"; }
1294
- else if (t > cert.validity.notAfter) { vOk = false; vCode = "path/expired"; }
1318
+ if (t < guard.time.instantOf(cert.validity.notBefore)) { vOk = false; vCode = "path/not-yet-valid"; }
1319
+ else if (t > guard.time.instantOf(cert.validity.notAfter)) { vOk = false; vCode = "path/expired"; }
1295
1320
  checks.push({ name: "validity", ok: vOk, code: vCode });
1296
1321
  if (!vOk) failed = true;
1297
1322
 
@@ -1435,7 +1460,7 @@ async function validate(path, opts) {
1435
1460
  // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1436
1461
  // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
1437
1462
  // convention keeps the whole boundary day trusted).
1438
- if (cert.validity.notBefore > distrustDate) {
1463
+ if (guard.time.instantOf(cert.validity.notBefore) > guard.time.instantOf(distrustDate)) {
1439
1464
  checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1440
1465
  }
1441
1466
  }
@@ -1808,7 +1833,7 @@ function selectDelta(candidates) {
1808
1833
  for (var i = 0; i < candidates.length; i++) {
1809
1834
  var c = candidates[i];
1810
1835
  if (best === null) { best = c; continue; }
1811
- var t = c.crl.thisUpdate.getTime(), bt = best.crl.thisUpdate.getTime();
1836
+ var t = guard.time.instantOf(c.crl.thisUpdate), bt = guard.time.instantOf(best.crl.thisUpdate);
1812
1837
  if (t > bt) { best = c; continue; }
1813
1838
  if (t === bt && c.crlNumber !== null && best.crlNumber !== null && c.crlNumber > best.crlNumber) best = c;
1814
1839
  }
@@ -2053,11 +2078,12 @@ function crlChecker(crls, opts) {
2053
2078
  }
2054
2079
  }
2055
2080
  }
2056
- if (theCrl.thisUpdate > time) return null; // not yet valid
2081
+ if (guard.time.instantOf(theCrl.thisUpdate) > guard.time.instantOf(time)) return null; // not yet valid
2057
2082
  // A CRL with no nextUpdate has no bounded validity -- its currency
2058
2083
  // cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
2059
2084
  // replayed old CRL must not read "good". Treat it as unusable.
2060
- if (!theCrl.nextUpdate || theCrl.nextUpdate < time) return null; // stale / no bound
2085
+ if (!theCrl.nextUpdate ||
2086
+ guard.time.instantOf(theCrl.nextUpdate) < guard.time.instantOf(time)) return null; // stale / no bound
2061
2087
 
2062
2088
  var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
2063
2089
  if (!sigOk) return null; // unverifiable -> not authoritative
@@ -2085,7 +2111,8 @@ function crlChecker(crls, opts) {
2085
2111
  // e.g. a timestamped signature -- does an entry dated AFTER the
2086
2112
  // validation time not yet apply.
2087
2113
  // allow:nan-date-comparison-unguarded -- revocationDate is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (the skip is not taken -> the entry is treated as revoked), and `time` is validated at the path.validate / crlChecker entry points.
2088
- if (historical && entry.revocationDate instanceof Date && entry.revocationDate.getTime() > time.getTime()) continue;
2114
+ if (historical && guard.time.isDate(entry.revocationDate) &&
2115
+ guard.time.instantOf(entry.revocationDate) > guard.time.instantOf(time)) continue;
2089
2116
  var rc = crlEntryReason(entry);
2090
2117
  return rc === null ? 0 : rc;
2091
2118
  }
@@ -2460,7 +2487,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2460
2487
  opts = opts || {};
2461
2488
  // The currency + responder-cert validity windows compare against `time`; a missing or invalid
2462
2489
  // check date must fail closed (a NaN compares false against every bound), never silently pass.
2463
- if (!(time instanceof Date) || isNaN(time.getTime())) {
2490
+ if (!guard.time.isDate(time) || isNaN(guard.time.instantOf(time))) {
2464
2491
  return Promise.reject(E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
2465
2492
  }
2466
2493
  // Bind the supplied issuer certificate to the target: the target's issuer DN must equal the
@@ -2666,8 +2693,9 @@ function scoreCandidate(cand, childAki, anchors, time) {
2666
2693
  if (ku && ku.value && ku.value.keyCertSign === true) score += 10; // sec. 3.5.3 keyUsage keyCertSign
2667
2694
  var v = cand.validity;
2668
2695
  // allow:nan-date-comparison-unguarded -- cand.validity dates are codec-parsed (asn1 readTime rejects a NaN instant) and time is guard.time.assertValid'd at entry; the validity term is a fail-safe sort hint regardless.
2669
- if (v && v.notBefore instanceof Date && v.notAfter instanceof Date &&
2670
- v.notBefore.getTime() <= time.getTime() && time.getTime() <= v.notAfter.getTime()) score += 5; // sec. 3.5.4 validity
2696
+ if (v && guard.time.isDate(v.notBefore) && guard.time.isDate(v.notAfter) &&
2697
+ guard.time.instantOf(v.notBefore) <= guard.time.instantOf(time) &&
2698
+ guard.time.instantOf(time) <= guard.time.instantOf(v.notAfter)) score += 5; // sec. 3.5.4 validity
2671
2699
  return score;
2672
2700
  }
2673
2701
 
@@ -2870,9 +2898,30 @@ async function _fetchAiaIssuers(current, aia) {
2870
2898
  * });
2871
2899
  * result.valid; // true when a path to a trust anchor was found and validated
2872
2900
  */
2901
+ // pki.path.build takes its own options AND forwards every pki.path.validate option to each
2902
+ // internal validate() call. The documented contract above says so, and validateOpts() copies
2903
+ // them wholesale. So the accepted set is the union, derived from _VALIDATE_OPTS. Retyping it
2904
+ // would let the two lists drift the first time validate gains an option.
2905
+ //
2906
+ // `trustAnchor` is removed from the union on purpose: build assigns it per candidate anchor,
2907
+ // so a caller passing the singular would have it overwritten and silently ignored. Refusing it
2908
+ // is what makes the singular/plural message below actionable rather than trivia.
2909
+ var _BUILD_OPTS = (function () {
2910
+ var o = {
2911
+ aiaTimeout: 1, candidates: 1, fetchAia: 1, intermediates: 1, maxAiaFetches: 1,
2912
+ maxAiaPerCert: 1, maxCandidatesConsidered: 1, maxDepth: 1, maxPathCerts: 1,
2913
+ maxResponseBytes: 1, time: 1, tls: 1, transport: 1, trustAnchors: 1, validate: 1
2914
+ };
2915
+ Object.keys(_VALIDATE_OPTS).forEach(function (k) { o[k] = 1; });
2916
+ delete o.trustAnchor;
2917
+ return o;
2918
+ })();
2919
+
2873
2920
  async function build(leaf, opts) {
2874
- opts = opts || {};
2875
- if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("path/bad-input", "build: opts must be an object");
2921
+ opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "build: opts");
2922
+ guard.identifier.assertKnownKeys(opts, _BUILD_OPTS, E, "path/bad-input",
2923
+ "pki.path.build has an unknown option. The anchors here are `trustAnchors`, plural; " +
2924
+ "pki.path.validate takes `trustAnchor`. The unknown option was: ");
2876
2925
  var leafCert;
2877
2926
  try { leafCert = coerceCert(leaf); }
2878
2927
  catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
@@ -2965,10 +3014,38 @@ async function build(leaf, opts) {
2965
3014
  // enumerates only own enumerable properties, so no prototype-pollution belt.
2966
3015
  var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1,
2967
3016
  fetchAia: 1, transport: 1, tls: 1, maxAiaFetches: 1, maxAiaPerCert: 1, aiaTimeout: 1, maxResponseBytes: 1 };
2968
- var forwarded = {};
2969
- Object.keys(opts).forEach(function (k) { if (!BUILD_ONLY_OPT[k]) forwarded[k] = opts[k]; });
3017
+ // No prototype, so the set forwarded to each internal validate holds what was put in it and
3018
+ // nothing a runtime carries. A plain `{}` here re-acquired a polluted name after the caller's
3019
+ // own value had been dropped, which is how an explicit `softFail: false` became a waiver.
3020
+ var forwarded = Object.create(null);
3021
+ // Driven by the contract. Walk the validate options this verb forwards and take the ones the
3022
+ // bag answers for.
3023
+ //
3024
+ // Enumerating the bag reads whatever a property walk chooses to report, and that walk skips a
3025
+ // method so a caller's class keeps its own. A known option whose value were a bare function
3026
+ // would be indistinguishable from one, and a defaults-style bag like
3027
+ // `Object.create({ someOption: fn })` would have it dropped here, leaving each internal
3028
+ // validate running without something the caller supplied.
3029
+ //
3030
+ // No option takes a bare function today. `revocationChecker` is `{ check }`, `verifier` is
3031
+ // `{ verify }`, and `transport` is build-only and never forwarded. So this is a hardening
3032
+ // rather than a live defect, and there is no vector that fails without it. It is here because
3033
+ // asking the contract cannot drop a known option whatever its value, and the alternative makes
3034
+ // the forwarded set depend on a walk whose rules exist for an unrelated question.
3035
+ //
3036
+ // `in` rather than an own check, because an option carried on a prototype is one the caller
3037
+ // supplied and `opts.softFail` answers for it. Own membership decides only whether a name is
3038
+ // build-only, which is a question about this module's own table and never about the caller.
3039
+ Object.keys(_VALIDATE_OPTS).forEach(function (k) {
3040
+ if (Object.prototype.hasOwnProperty.call(BUILD_ONLY_OPT, k)) return;
3041
+ if (k in opts) forwarded[k] = opts[k];
3042
+ });
2970
3043
  function validateOpts(anchor) {
2971
- var vo = {};
3044
+ var vo = Object.create(null); // same reason as `forwarded` above
3045
+ // Every name in the forwarded set, which was built from the contract above and has no
3046
+ // prototype, so its own keys are exactly what was put in it. A property walk here would
3047
+ // apply the method rule a second time and drop a function-valued option all over again;
3048
+ // forwarding less than was validated is worse than refusing the bag.
2972
3049
  Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
2973
3050
  vo.trustAnchor = anchor;
2974
3051
  return vo;