@blamejs/pki 0.5.10 → 0.5.12
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/CHANGELOG.md +62 -0
- package/README.md +1 -1
- package/lib/acme.js +4 -4
- package/lib/asn1-der.js +1 -1
- package/lib/attrcert-sign.js +1 -1
- package/lib/cbor-det.js +4 -4
- package/lib/cmc-verify.js +7 -4
- package/lib/cms-sign.js +3 -5
- package/lib/cms-verify.js +7 -4
- package/lib/crl-sign.js +77 -6
- package/lib/crmf-sign.js +1 -1
- package/lib/ct.js +8 -5
- package/lib/est.js +17 -11
- package/lib/guard-bytes.js +564 -78
- package/lib/guard-identifier.js +691 -3
- package/lib/guard-parsed.js +9 -5
- package/lib/guard-time.js +65 -4
- package/lib/inspect.js +8 -3
- package/lib/key.js +48 -6
- package/lib/lint.js +14 -7
- package/lib/ocsp-verify.js +10 -4
- package/lib/ocsp.js +34 -5
- package/lib/path-validate.js +94 -17
- package/lib/schema-c509.js +3 -3
- package/lib/sigstore.js +6 -2
- package/lib/trust.js +2 -2
- package/lib/tsp-sign.js +2 -2
- package/lib/validator-attcert.js +3 -3
- package/lib/webauthn-mds.js +5 -5
- package/lib/webauthn.js +1 -1
- package/lib/webcrypto.js +15 -6
- package/lib/x509-sign.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/guard-parsed.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
178
|
-
(o.nextUpdate === null || o.nextUpdate
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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(", ") + ")");
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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
|
|
347
|
-
if (isNaN(
|
|
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
|
-
|
|
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 {
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
|
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 ||
|
|
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 &&
|
|
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
|
|
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
|
|
2670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2969
|
-
|
|
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;
|