@blamejs/pki 0.5.9 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +80 -19
  2. package/README.md +1 -1
  3. package/lib/acme.js +5 -5
  4. package/lib/asn1-der.js +1 -1
  5. package/lib/attrcert-sign.js +1 -1
  6. package/lib/cbor-det.js +4 -4
  7. package/lib/cmc-build.js +5 -5
  8. package/lib/cmc-verify.js +14 -11
  9. package/lib/cms-decrypt.js +2 -2
  10. package/lib/cms-sign.js +3 -5
  11. package/lib/cms-verify.js +9 -6
  12. package/lib/constants.js +1 -1
  13. package/lib/crl-sign.js +1 -1
  14. package/lib/crmf-sign.js +1 -1
  15. package/lib/ct.js +8 -5
  16. package/lib/est.js +20 -14
  17. package/lib/guard-all.js +12 -12
  18. package/lib/guard-bytes.js +568 -82
  19. package/lib/guard-compress.js +2 -2
  20. package/lib/guard-der.js +1 -1
  21. package/lib/guard-identifier.js +694 -6
  22. package/lib/guard-limits.js +2 -2
  23. package/lib/guard-parsed.js +10 -6
  24. package/lib/guard-range.js +1 -1
  25. package/lib/guard-secret.js +1 -1
  26. package/lib/guard-text.js +1 -1
  27. package/lib/guard-time.js +65 -4
  28. package/lib/hpke.js +1 -1
  29. package/lib/http-digest.js +1 -1
  30. package/lib/inspect.js +8 -3
  31. package/lib/jose.js +1 -1
  32. package/lib/key.js +48 -6
  33. package/lib/lint.js +15 -8
  34. package/lib/ocsp-verify.js +10 -4
  35. package/lib/ocsp.js +35 -6
  36. package/lib/oid.js +1 -1
  37. package/lib/path-validate.js +99 -22
  38. package/lib/schema-c509.js +5 -5
  39. package/lib/schema-cmp.js +1 -1
  40. package/lib/schema-crl.js +1 -1
  41. package/lib/schema-engine.js +2 -2
  42. package/lib/schema-pkix.js +1 -1
  43. package/lib/sigstore.js +6 -2
  44. package/lib/smime.js +1 -1
  45. package/lib/trust.js +2 -2
  46. package/lib/tsp-sign.js +2 -2
  47. package/lib/validator-attcert.js +3 -3
  48. package/lib/validator-cose.js +3 -3
  49. package/lib/webauthn-mds.js +17 -17
  50. package/lib/webauthn.js +12 -12
  51. package/lib/webcrypto.js +16 -7
  52. package/lib/x509-sign.js +1 -1
  53. package/package.json +4 -2
  54. package/sbom.cdx.json +6 -6
@@ -90,7 +90,7 @@ function depthCap(value, key, dflt) {
90
90
  function counter(max, E, code, label) {
91
91
  // The ceiling is an authoring input: an undefined / NaN / fractional max
92
92
  // builds a counter whose `n > max` never fires, leaving a silently dead fanout
93
- // defence. Reject at construction (config-time TypeError).
93
+ // defense. Reject at construction (config-time TypeError).
94
94
  if (!Number.isInteger(max) || max < 0) {
95
95
  throw new TypeError("guard.limits.counter: max must be a non-negative integer");
96
96
  }
@@ -117,7 +117,7 @@ function counter(max, E, code, label) {
117
117
  // over-size RED vectors driving the composing verifiers are the guard.
118
118
  function byteCap(buf, max, E, code, label) {
119
119
  // The ceiling is an authoring input: an undefined / NaN / fractional / negative
120
- // max makes `length > max` never fire, and the size defence is then silently
120
+ // max makes `length > max` never fire, and the size defense is then silently
121
121
  // dead. Reject at config time (TypeError), regardless of the value currency E selects.
122
122
  if (!Number.isInteger(max) || max < 0) {
123
123
  throw new TypeError("guard.limits.byteCap: max must be a non-negative integer");
@@ -39,15 +39,19 @@
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
45
46
  // verbs document, and the narrowness is invisible in the good case: an ArrayBuffer would fall
46
47
  // through to the object branch and be refused as a rebuilt structure, which reads to the caller as
47
- // "your certificate is malformed" when what happened is that their container was not recognised.
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
  }
@@ -105,7 +105,7 @@ function uint64(value, E, code, label) {
105
105
  // take. Accepts a bigint, or a Number that is a safe integer (isSafeInteger, not
106
106
  // isInteger, because a Number above 2^53 has already lost precision by the time it
107
107
  // arrives), so a large identifier MUST come as a bigint and cannot be silently
108
- // rounded to a neighbour. Everything else is a config-time reject.
108
+ // rounded to a neighbor. Everything else is a config-time reject.
109
109
  //
110
110
  // Distinct from int() on purpose: int() BOUNDS a value the wire decoded and narrows
111
111
  // it to Number; this one is unbounded and returns BigInt, because the field has no
@@ -72,7 +72,7 @@ function zeroize(value, ErrorClass, code, label) {
72
72
  // @enforced-by behavioral -- this is a loop over zeroize, which carries the family's only
73
73
  // rename-proof shape (the `.fill(0)` above). It introduces no shape of its own, so a lexical
74
74
  // detector here would anchor on a renameable symbol and go silently green (drift rule sec. 3).
75
- // The behavioural guards are guard-secret.test.js (holes tolerated, every member cleared) and the
75
+ // The behavioral guards are guard-secret.test.js (holes tolerated, every member cleared) and the
76
76
  // CMS vectors that assert the shared secret and KEK are wiped on both the success and failure paths.
77
77
  function zeroizeAll(list, ErrorClass, code, label) {
78
78
  if (!list) return list;
package/lib/guard-text.js CHANGED
@@ -50,7 +50,7 @@ function decode(input, maxBytes, ErrorClass, spec) {
50
50
  if (Buffer.isBuffer(input)) {
51
51
  // Re-view through the byte guard first so a detached backing ArrayBuffer (a
52
52
  // transferred / structuredClone'd Buffer, which reads as zero-length) fails
53
- // closed here -- the same detached-buffer defence the byte boundaries get --
53
+ // closed here -- the same detached-buffer defense the byte boundaries get --
54
54
  // instead of being decoded as an empty string. Then cap, then decode.
55
55
  input = bytes.view(input, ErrorClass, spec.badInput, spec.label);
56
56
  if (input.length > maxBytes) throw new ErrorClass(spec.tooLarge, spec.label + " exceeds the size cap");
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/hpke.js CHANGED
@@ -390,7 +390,7 @@ function _recipPrivate(suite, sk) {
390
390
  // psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
391
391
  // with an out-of-registry mode byte.
392
392
  // The sender and the recipient read the same option object from OPPOSITE ends, so each direction
393
- // gets its own table. A shared union recognises every name at both ends and so accepts the one that
393
+ // gets its own table. A shared union recognizes every name at both ends and so accepts the one that
394
394
  // cannot do anything there: `senderPublicKey` handed to setupS, or
395
395
  // `senderKey` handed to setupR, is silently ignored, which is the exact silence these tables exist
396
396
  // to remove, in a wider form. An option that means nothing where it was passed is a misunderstanding
@@ -144,7 +144,7 @@ function _validateDigest(paramText, E, code) {
144
144
  ? p.domain.value.replace(/^\s+|\s+$/g, "").split(/\s+/) : null;
145
145
  // charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
146
146
  // other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
147
- // rejected (and thus skipped during multi-offer selection in favour of a conforming offer).
147
+ // rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
148
148
  if (p.charset && (p.charset.quoted || String(p.charset.value).toUpperCase() !== "UTF-8")) throw E(code, "the Digest charset must be the unquoted token UTF-8 (RFC 7616 sec. 3.3)");
149
149
  // stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
150
150
  // (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
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/jose.js CHANGED
@@ -368,7 +368,7 @@ function assertPublicJwk(jwk) {
368
368
  // `key` NAMES the key this message must be signed under and `profile` selects which header rules
369
369
  // apply -- both narrow what verifies, so a misspelling of either silently widens it back to the
370
370
  // default. That is the shape this check exists for: the caller asked for something stricter and got
371
- // the looser behaviour, with nothing said.
371
+ // the looser behavior, with nothing said.
372
372
  var _VERIFY_KEYS = { key: 1, profile: 1 };
373
373
  async function verify(jws, opts) {
374
374
  opts = opts || {};
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) {
@@ -231,7 +231,7 @@ function _subjectCNs(cert) {
231
231
  return out;
232
232
  }
233
233
 
234
- // An extension's criticality is honoured (RFC 5280 4.2: a consumer MUST reject a critical extension
234
+ // An extension's criticality is honored (RFC 5280 4.2: a consumer MUST reject a critical extension
235
235
  // it cannot process) only when this toolkit actually processes its semantics. That authority is
236
236
  // path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), never the decoder
237
237
  // table: certExtensionDecoders also decodes many extensions purely for display (qcStatements, the MS
@@ -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 {
@@ -530,7 +559,7 @@ function verify(response, opts) {
530
559
  // the closed direction for `revoked`: revocation does not expire the way
531
560
  // non-revocation does, so discarding a signed, current, authorized revoked
532
561
  // verdict because it was replayed would hand a soft-fail caller the certificate
533
- // the responder just refused -- turning the anti-replay defence into the thing
562
+ // the responder just refused -- turning the anti-replay defense into the thing
534
563
  // that accepts a revoked certificate. `nonceMatched: false` still reports that
535
564
  // this response was not bound to this request.
536
565
  if (!matched && verdict.status === "good") {
package/lib/oid.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * attribute types, `2.5.29` for the extensions, `2.16.840.1.101.3.4` for
23
23
  * the NIST algorithms), and each member names only its trailing arc. The
24
24
  * full OID is derived from base + leaf at load, so the arc hierarchy that
25
- * IS the OID namespace is modelled directly instead of re-spelled per
25
+ * IS the OID namespace is modeled directly instead of re-spelled per
26
26
  * entry. It covers the RFC 5280 attribute types and extensions, the
27
27
  * classical signature / public-key / digest algorithms, and the
28
28
  * NIST-assigned post-quantum arcs (ML-DSA, ML-KEM, SLH-DSA). Operators