@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.
@@ -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, historicalMode: 1, initialAnyPolicyInhibit: 1, initialExcludedSubtrees: 1,
1282
- initialExplicitPolicy: 1, initialPermittedSubtrees: 1, initialPolicyMappingInhibit: 1,
1283
- maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1, requiredEku: 1, revocationChecker: 1,
1284
- softFail: 1, time: 1, trustAnchor: 1, userInitialPolicySet: 1, verifier: 1
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