@blamejs/pki 0.6.0 → 0.6.2
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 +20 -0
- package/README.md +5 -3
- package/index.js +2 -0
- package/lib/cms-verify.js +17 -0
- package/lib/constants.js +5 -0
- package/lib/ct.js +270 -13
- package/lib/framework-error.js +7 -0
- package/lib/oid.js +10 -0
- package/lib/path-validate.js +70 -4
- package/lib/scep.js +562 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
var webcrypto = require("./webcrypto");
|
|
34
34
|
var pkix = require("./schema-pkix");
|
|
35
35
|
var oid = require("./oid");
|
|
36
|
+
var ct = require("./ct");
|
|
36
37
|
var errors = require("./framework-error");
|
|
37
38
|
var asn1 = require("./asn1-der");
|
|
38
39
|
var schema = require("./schema-engine");
|
|
@@ -1278,17 +1279,73 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1278
1279
|
// inputs appear together. A grep of one function would have shipped a list missing four real
|
|
1279
1280
|
// options and rejected callers that work today.
|
|
1280
1281
|
var _VALIDATE_OPTS = {
|
|
1281
|
-
checkPurpose: 1,
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1282
|
+
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
|
|
1283
|
+
initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
|
|
1284
|
+
initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1,
|
|
1285
|
+
requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchor: 1,
|
|
1286
|
+
userInitialPolicySet: 1, verifier: 1
|
|
1285
1287
|
};
|
|
1286
1288
|
|
|
1289
|
+
// The opt-in Certificate Transparency gate (RFC 6962). When opts.ctLogList is supplied, the target's
|
|
1290
|
+
// embedded SCTs are verified against the trusted log-list and the caller's CT policy, and the verdict is
|
|
1291
|
+
// recorded on the target's checks. Fail-closed: a target that declares a CT policy but carries no
|
|
1292
|
+
// SCT-list extension is refused. Reached only through opts.ctLogList, so every existing caller is
|
|
1293
|
+
// unaffected. The SCT-list extension OID is looked up through the shared registry, never a literal.
|
|
1294
|
+
var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
|
|
1295
|
+
async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
|
|
1296
|
+
var exts = leaf.extensions || [], sctExt = null;
|
|
1297
|
+
for (var i = 0; i < exts.length; i++) {
|
|
1298
|
+
if (exts[i] && (exts[i].oid === OID_SCT_LIST || exts[i].name === "signedCertificateTimestampList")) { sctExt = exts[i]; break; }
|
|
1299
|
+
}
|
|
1300
|
+
if (sctExt == null) return { name: "ct", ok: false, code: "path/ct-required" };
|
|
1301
|
+
// The leaf's issuer contributes issuerKeyHash: the certificate that signed it (certs[n-2] in the
|
|
1302
|
+
// anchor->target order), or the anchor itself when the target is validated directly beneath it.
|
|
1303
|
+
var issuer = n >= 2 ? certs[n - 2] : { subjectPublicKeyInfo: { bytes: anchor.publicKey } };
|
|
1304
|
+
var entry, list;
|
|
1305
|
+
try {
|
|
1306
|
+
entry = ct.x509CertEntry(leaf, issuer);
|
|
1307
|
+
list = ct.parseSctList(sctExt.value);
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
return { name: "ct", ok: false, code: pathCode(e, "path/ct-required") };
|
|
1310
|
+
}
|
|
1311
|
+
// verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
|
|
1312
|
+
// ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
|
|
1313
|
+
// boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
|
|
1314
|
+
// verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
|
|
1315
|
+
// ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
|
|
1316
|
+
// boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
|
|
1317
|
+
var verdict;
|
|
1318
|
+
try {
|
|
1319
|
+
verdict = await ct.verifySctList(entry, list, ctLogList,
|
|
1320
|
+
{ minScts: ctPolicy.minScts, minOperators: ctPolicy.minOperators, certNotAfter: leaf.validity.notAfter, at: at });
|
|
1321
|
+
} catch (e) {
|
|
1322
|
+
if (e && e.isPkiError) throw E("path/bad-input", "validate: the CT policy or log-list is invalid (" + (e.code || "ct/bad-input") + "): " + e.message);
|
|
1323
|
+
throw e;
|
|
1324
|
+
}
|
|
1325
|
+
return { name: "ct", ok: verdict.policyOk === true, code: verdict.policyOk ? undefined : "path/ct-policy-not-met" };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1287
1328
|
async function validate(path, opts) {
|
|
1288
1329
|
opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
|
|
1289
1330
|
guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
|
|
1290
1331
|
"pki.path.validate has an unknown option. The anchor here is `trustAnchor`, singular; " +
|
|
1291
1332
|
"pki.path.build takes `trustAnchors`. The unknown option was: ");
|
|
1333
|
+
// A CT policy without a log-list cannot be enforced -- refuse it at entry rather than silently
|
|
1334
|
+
// skipping the CT gate and returning a valid path the caller believes was CT-checked (fail-closed).
|
|
1335
|
+
if (opts.ctPolicy != null && opts.ctLogList == null) throw E("path/bad-input", "validate: opts.ctPolicy requires opts.ctLogList (the trusted CT log-list to verify the certificate's SCTs against)");
|
|
1336
|
+
// Validate the CT policy's KEYS at entry: a misspelled key (e.g. `minSCTs`) would otherwise read as
|
|
1337
|
+
// undefined and silently default to the RFC floor of 1, applying a weaker policy than the caller asked
|
|
1338
|
+
// for. The values are validated by verifySctList when the gate runs.
|
|
1339
|
+
// Validate the whole CT configuration at entry, independent of whether the target carries an SCT
|
|
1340
|
+
// extension, so a malformed policy or log-list is always path/bad-input -- never masked as a
|
|
1341
|
+
// per-certificate path/ct-required just because the target happens to lack SCTs.
|
|
1342
|
+
if (opts.ctLogList != null) {
|
|
1343
|
+
if (typeof opts.ctLogList !== "object" || opts.ctLogList.byLogId == null) throw E("path/bad-input", "validate: opts.ctLogList must be a pki.ct.parseLogList result");
|
|
1344
|
+
var ctp = guard.identifier.optionsObject(opts.ctPolicy, E, "path/bad-input", "validate: opts.ctPolicy");
|
|
1345
|
+
guard.identifier.assertKnownKeys(ctp, { minScts: 1, minOperators: 1 }, E, "path/bad-input", "validate: opts.ctPolicy has an unknown key (the CT policy keys are minScts / minOperators): ");
|
|
1346
|
+
if (ctp.minScts != null && (typeof ctp.minScts !== "number" || !intrinsic.isInteger(ctp.minScts) || ctp.minScts < 1)) throw E("path/bad-input", "validate: opts.ctPolicy.minScts must be a positive integer");
|
|
1347
|
+
if (ctp.minOperators != null && (typeof ctp.minOperators !== "number" || !intrinsic.isInteger(ctp.minOperators) || ctp.minOperators < 1)) throw E("path/bad-input", "validate: opts.ctPolicy.minOperators must be a positive integer");
|
|
1348
|
+
}
|
|
1292
1349
|
if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
|
|
1293
1350
|
// Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
|
|
1294
1351
|
// (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
|
|
@@ -1607,6 +1664,15 @@ async function validate(path, opts) {
|
|
|
1607
1664
|
failed = true;
|
|
1608
1665
|
}
|
|
1609
1666
|
|
|
1667
|
+
// Opt-in CT gate: verify the target's embedded SCTs and record the verdict on its checks (RFC 6962).
|
|
1668
|
+
if (opts.ctLogList != null) {
|
|
1669
|
+
var ctCheck = await ctGateCheck(certs[n - 1], certs, anchor, opts.ctLogList, opts.ctPolicy || {}, n, opts.time);
|
|
1670
|
+
var target = null;
|
|
1671
|
+
for (var tr = 0; tr < state.results.length; tr++) { if (state.results[tr].index === n - 1) { target = state.results[tr]; break; } }
|
|
1672
|
+
if (target) intrinsic.push(target.checks, ctCheck);
|
|
1673
|
+
if (!ctCheck.ok) failed = true;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1610
1676
|
return {
|
|
1611
1677
|
valid: !failed,
|
|
1612
1678
|
// What the revocation rule and the anchor's own trust metadata actually decided. `valid` alone
|