@blamejs/pki 0.1.31 → 0.2.0

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.
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
37
37
  var schema = require("./schema-engine");
38
38
  var x509 = require("./schema-x509");
39
39
  var crl = require("./schema-crl");
40
+ var ocsp = require("./schema-ocsp");
40
41
  var guard = require("./guard-all");
41
42
  var constants = require("./constants");
42
43
 
@@ -69,6 +70,7 @@ var OID = {
69
70
  emailAddress: oid.byName("emailAddress"),
70
71
  extKeyUsage: oid.byName("extKeyUsage"),
71
72
  anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
73
+ cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
72
74
  };
73
75
 
74
76
  // The set of extension OIDs the validator PROCESSES -- an unrecognized critical
@@ -79,9 +81,15 @@ var OID = {
79
81
  // is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
80
82
  // processing step -- the required purpose is application context).
81
83
  var PROCESSED_EXTENSIONS = {};
84
+ // cRLDistributionPoints is processed: the CRL checker consults it for the
85
+ // sec. 6.3.3 shard correspondence, and a critical instance (sec. 4.2.1.13 is a
86
+ // SHOULD-non-critical) is structurally validated by
87
+ // validateCriticalExtensionStructure via the registered decoder. freshestCRL
88
+ // stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
89
+ // consult it (no delta merge), so a critical instance fails unrecognized.
82
90
  [OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
83
91
  OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
84
- OID.extKeyUsage].
92
+ OID.extKeyUsage, OID.cRLDistributionPoints].
85
93
  forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
86
94
 
87
95
  // ---- signature verify bridge (NEW 6) ---------------------------------------
@@ -317,7 +325,7 @@ function builtinVerify(state, cert) {
317
325
  } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
318
326
  // The signature is an octet-aligned BIT STRING (no unused bits) for every
319
327
  // supported algorithm; a non-zero unused-bit count is malformed.
320
- if (cert.signatureValue.unusedBits !== 0) return Promise.resolve({ ok: false, code: "path/bad-signature" });
328
+ if (!guard.crypto.isOctetAligned(cert.signatureValue)) return Promise.resolve({ ok: false, code: "path/bad-signature" });
321
329
  var key;
322
330
  return subtle.importKey("spki", state.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
323
331
  key = k;
@@ -336,42 +344,17 @@ function builtinVerify(state, cert) {
336
344
 
337
345
  // ---- 7.1 name comparison ---------------------------------------------------
338
346
 
339
- function normalizeAttrValue(v) {
340
- // RFC 5280 7.1 canonical DN comparison: case-fold + collapse internal
341
- // whitespace. Every standard X.520 DN attribute (CN/O/OU/L/ST/C/serialNumber/
342
- // dnQualifier) uses caseIgnoreMatch, and this canonical form matches OpenSSL's
343
- // X509_NAME_cmp -- applying it uniformly is the interoperable behavior, not a
344
- // per-attribute caseExact rule that would reject a chain OpenSSL accepts.
345
- // Reject embedded NUL / control bytes so a truncation attack (CVE-2009-2408)
346
- // cannot make two different names compare equal.
347
- if (typeof v !== "string") return v;
348
- guard.name.assertNoControlBytes(v, E, "path/name-chaining", "distinguished name");
349
- return v.trim().replace(/\s+/g, " ").toLowerCase();
347
+ // RFC 5280 sec. 7.1 canonical DN / RDN comparison, via the shared name guard: the
348
+ // canonical form (case-fold + internal-whitespace collapse) and the embedded
349
+ // control-byte reject (CVE-2009-2408 -> path/name-chaining) live once in
350
+ // guard-name, so no path-validation caller can reintroduce a raw-byte DN
351
+ // comparison that treats two RFC 5280-equal names as different.
352
+ function dnEqual(rdnsA, rdnsB) {
353
+ return guard.name.dnEqual(rdnsA, rdnsB, E, "path/name-chaining", "distinguished name");
350
354
  }
351
355
 
352
356
  function rdnEqual(a, b) {
353
- if (a.length !== b.length) return false;
354
- // An RDN is an unordered SET of type/value pairs; compare as multisets.
355
- var used = [];
356
- for (var i = 0; i < a.length; i++) {
357
- var found = false;
358
- for (var j = 0; j < b.length; j++) {
359
- if (used[j]) continue;
360
- if (a[i].type === b[j].type && normalizeAttrValue(a[i].value) === normalizeAttrValue(b[j].value)) {
361
- used[j] = true; found = true; break;
362
- }
363
- }
364
- if (!found) return false;
365
- }
366
- return true;
367
- }
368
-
369
- function dnEqual(rdnsA, rdnsB) {
370
- if (rdnsA.length !== rdnsB.length) return false;
371
- for (var i = 0; i < rdnsA.length; i++) {
372
- if (!rdnEqual(rdnsA[i], rdnsB[i])) return false;
373
- }
374
- return true;
357
+ return guard.name.rdnEqual(a, b, E, "path/name-chaining", "distinguished name");
375
358
  }
376
359
 
377
360
  // ---- extension access ------------------------------------------------------
@@ -1074,8 +1057,7 @@ async function validate(path, opts) {
1074
1057
  // Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
1075
1058
  // (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
1076
1059
  // crypto amplification from an oversized path). Entry-point tier: throw.
1077
- var maxCerts = opts.maxPathCerts !== undefined ? opts.maxPathCerts : constants.LIMITS.PATH_MAX_CERTS;
1078
- if (typeof maxCerts !== "number" || !isFinite(maxCerts) || maxCerts < 1) throw E("path/bad-input", "validate: opts.maxPathCerts must be a positive number");
1060
+ var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1079
1061
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1080
1062
  var certs = path.map(function (c) { return (c && c.tbsBytes) ? c : x509.parse(c); });
1081
1063
  var n = certs.length;
@@ -1088,9 +1070,9 @@ async function validate(path, opts) {
1088
1070
  }
1089
1071
  // Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
1090
1072
  // throws here rather than silently disabling the behavior it configures.
1091
- if (opts.maxPolicyNodes !== undefined && (typeof opts.maxPolicyNodes !== "number" || !isFinite(opts.maxPolicyNodes) || opts.maxPolicyNodes < 1)) {
1092
- throw E("path/bad-input", "validate: opts.maxPolicyNodes must be a positive number");
1093
- }
1073
+ // Validate-if-present (the default is applied where the policy state is built);
1074
+ // the shared integer cap rejects a fractional / negative / non-numeric budget.
1075
+ guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
1094
1076
  // user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
1095
1077
  // Membership is tested with indexOf, so a raw string here would be consulted
1096
1078
  // as a SUBSTRING match -- validated to an array of strings instead.
@@ -1098,6 +1080,11 @@ async function validate(path, opts) {
1098
1080
  var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1099
1081
  opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
1100
1082
  if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1083
+ // Each entry is compared (indexOf) against canonical decoder output, so a
1084
+ // non-canonical key would silently never match -- fail the typo closed here.
1085
+ opts.userInitialPolicySet.forEach(function (p) {
1086
+ guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.userInitialPolicySet entry " + JSON.stringify(p));
1087
+ });
1101
1088
  }
1102
1089
  var seeds = {
1103
1090
  permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
@@ -1113,12 +1100,33 @@ async function validate(path, opts) {
1113
1100
  }
1114
1101
  requiredEku = opts.requiredEku.map(function (p) {
1115
1102
  if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
1116
- if (/^\d+(\.\d+)+$/.test(p)) return p;
1103
+ // A dotted-form attempt (leads with a digit) must be a canonical OID -- a
1104
+ // loose regex accepted a leading-zero / out-of-bounds key that would never
1105
+ // match the canonical EKU the target advertises; anything else is a name.
1106
+ if (/^[0-9]/.test(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
1117
1107
  var dotted = oid.byName(p);
1118
1108
  if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
1119
1109
  return dotted;
1120
1110
  });
1121
1111
  }
1112
+ // opts.checkPurpose -- the single key purpose the ANCHOR's NSS trust metadata
1113
+ // (distrustAfter / purposes) is consulted for. Independent of requiredEku
1114
+ // (which gates the leaf's own EKU extension): this selects the per-purpose
1115
+ // key in the trust-anchor constraint contract. A purpose OID name (or a
1116
+ // canonical dotted OID normalized to its name); a bad value throws here.
1117
+ var checkPurpose = null;
1118
+ if (opts.checkPurpose !== undefined) {
1119
+ if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
1120
+ throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
1121
+ }
1122
+ if (/^[0-9]/.test(opts.checkPurpose)) {
1123
+ var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
1124
+ checkPurpose = oid.name(cpDotted) || cpDotted; // normalize a dotted purpose OID to its name for the anchor map
1125
+ } else {
1126
+ if (typeof oid.byName(opts.checkPurpose) !== "string") throw E("path/bad-input", "validate: opts.checkPurpose " + JSON.stringify(opts.checkPurpose) + " is not a registered OID name");
1127
+ checkPurpose = opts.checkPurpose;
1128
+ }
1129
+ }
1122
1130
 
1123
1131
  var state = initialize(certs, opts, seeds);
1124
1132
  state._n = n;
@@ -1257,6 +1265,22 @@ async function validate(path, opts) {
1257
1265
  // default). An ABSENT extension leaves the key unrestricted, so it
1258
1266
  // satisfies any required purpose.
1259
1267
  if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
1268
+ // Trust-anchor constraint contract (NSS / CCADB metadata; gated so a bare
1269
+ // anchor or an absent checkPurpose is byte-identical to today). The anchor's
1270
+ // per-purpose distrust-after date and delegator purposes apply to the
1271
+ // end-entity leaf it ultimately certifies.
1272
+ var ta = opts.trustAnchor;
1273
+ if (checkPurpose && ta.distrustAfter && ta.distrustAfter[checkPurpose] instanceof Date &&
1274
+ cert.validity.notBefore > ta.distrustAfter[checkPurpose]) {
1275
+ // STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
1276
+ // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1277
+ // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
1278
+ // convention keeps the whole boundary day trusted).
1279
+ checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1280
+ }
1281
+ if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
1282
+ checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1283
+ }
1260
1284
  updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1261
1285
  }
1262
1286
 
@@ -1354,11 +1378,24 @@ var IDP_SCHEMA = schema.seq([
1354
1378
  function decodeIdp(ext) {
1355
1379
  // Surface the scope flags the checker gates on. ANY structural or value fault
1356
1380
  // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
1357
- // an encoded-FALSE default -- leaves the CRL's scope unknown: the CRL is
1358
- // unusable, never assumed unrestricted.
1359
- var out = { hasDistributionPoint: false, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1381
+ // an encoded-FALSE default, a malformed DistributionPointName -- leaves the
1382
+ // CRL's scope unknown: the CRL is unusable, never assumed unrestricted.
1383
+ var out = { hasDistributionPoint: false, distributionPoint: null, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1360
1384
  var m;
1361
- try { m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS); }
1385
+ try {
1386
+ m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS);
1387
+ if (m.fields.distributionPoint.present) {
1388
+ // distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE
1389
+ // (a context tag on a CHOICE-typed field is always EXPLICIT). The decoded
1390
+ // name feeds the sec. 6.3.3(b)(2)(i) correspondence against the
1391
+ // certificate's own DistributionPoints in the checker below.
1392
+ var dpnWrap = m.fields.distributionPoint.node;
1393
+ if (!dpnWrap.children || dpnWrap.children.length !== 1) {
1394
+ throw E("path/bad-idp", "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
1395
+ }
1396
+ out.distributionPoint = pkix.distributionPointName(NS, dpnWrap.children[0], "path/bad-idp");
1397
+ }
1398
+ }
1362
1399
  catch (_e) { out.malformed = true; return out; }
1363
1400
  function flag(f) {
1364
1401
  if (!f.present) return false;
@@ -1377,6 +1414,58 @@ function decodeIdp(ext) {
1377
1414
  return out;
1378
1415
  }
1379
1416
 
1417
+ // RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
1418
+ // CORRESPONDS to a shard CRL's IDP distribution point name -- at least one
1419
+ // name in common between the two DistributionPointNames, compared by
1420
+ // BYTE-IDENTICAL DER encoding (Buffer.equals on the raw GeneralName / RDN
1421
+ // TLVs). sec. 5.2.5 pins the comparison key: "The identical encoding MUST be
1422
+ // used in the distributionPoint fields of the certificate and the CRL" -- so
1423
+ // a canonicalized or semantic comparison (which would equate two
1424
+ // differently-encoded names) is forbidden, and whole-set equality (which
1425
+ // would falsely reject a legitimate multi-URI DP sharing one name) is
1426
+ // over-strict. Mixed forms -- fullName on one side, nameRelativeToCRLIssuer
1427
+ // on the other -- never correspond here: resolving an RDN fragment against
1428
+ // the issuer DN is not attempted (fail closed). Per sec. 6.3.3(b)(1), a DP
1429
+ // naming a cRLIssuer participates only when that cRLIssuer is the certificate
1430
+ // issuer itself: the checker only consults CRLs issued BY the certificate
1431
+ // issuer and rejects indirect CRLs, so a DP delegated to another CRL issuer
1432
+ // is out of play. Returns the matched DP (its `reasons` feeds the coarse
1433
+ // reason-mask rule) or null.
1434
+ function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
1435
+ if (!idpDpn || !certDPs) return null;
1436
+ for (var i = 0; i < certDPs.length; i++) {
1437
+ var dp = certDPs[i];
1438
+ if (!dp.distributionPoint) continue;
1439
+ if (dp.cRLIssuer && !crlIssuerNamesIssuer(dp.cRLIssuer, issuerRdns)) continue;
1440
+ var cdpn = dp.distributionPoint;
1441
+ if (idpDpn.kind === "fullName" && cdpn.kind === "fullName") {
1442
+ for (var a = 0; a < idpDpn.names.length; a++) {
1443
+ for (var c = 0; c < cdpn.names.length; c++) {
1444
+ if (idpDpn.names[a].equals(cdpn.names[c])) return dp;
1445
+ }
1446
+ }
1447
+ } else if (idpDpn.kind === "rdn" && cdpn.kind === "rdn") {
1448
+ if (idpDpn.bytes.equals(cdpn.bytes)) return dp;
1449
+ }
1450
+ }
1451
+ return null;
1452
+ }
1453
+
1454
+ // Does a DistributionPoint's cRLIssuer name the certificate issuer? True iff
1455
+ // one of its GeneralNames is a directoryName equal to the issuer DN under the
1456
+ // RFC 5280 sec. 7.1 comparison (the shared name guard, via this file's dnEqual
1457
+ // wrapper). Any fault -- a DN the comparison rejects for an embedded control
1458
+ // byte -- resolves false: the DP stays out of the correspondence (fail closed).
1459
+ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1460
+ for (var i = 0; i < cRLIssuer.names.length; i++) {
1461
+ var n = cRLIssuer.names[i];
1462
+ if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
1463
+ try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
1464
+ catch (_e) { return false; }
1465
+ }
1466
+ return false;
1467
+ }
1468
+
1380
1469
  /**
1381
1470
  * @primitive pki.path.crlChecker
1382
1471
  * @signature pki.path.crlChecker(crls) -> RevocationChecker
@@ -1391,7 +1480,12 @@ function decodeIdp(ext) {
1391
1480
  * verifies the CRL signature over its `tbsBytes`, honors the issuing
1392
1481
  * distribution point scope and reason coverage, checks currency
1393
1482
  * (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
1394
- * "unknown" }`. An out-of-scope, stale, unauthorized, or unverifiable CRL
1483
+ * "unknown" }`. A partitioned/sharded CRL (a critical IDP naming a
1484
+ * distribution point) establishes "good" when it corresponds to one of the
1485
+ * certificate's own cRLDistributionPoints -- at least one identically-encoded
1486
+ * name in common (RFC 5280 sec. 6.3.3) -- and neither side restricts reason
1487
+ * codes; a non-corresponding or reason-restricted shard is consulted for
1488
+ * revocation only. An out-of-scope, stale, unauthorized, or unverifiable CRL
1395
1489
  * yields `unknown`, which the validator fails closed unless `softFail` is set.
1396
1490
  *
1397
1491
  * @example
@@ -1419,6 +1513,18 @@ function crlChecker(crls) {
1419
1513
  } catch (e) {
1420
1514
  certScopeFault = pathCode(e, "path/bad-extension-value");
1421
1515
  }
1516
+ // The certificate's own cRLDistributionPoints, decoded once per check:
1517
+ // the sec. 6.3.3(b)(2)(i) correspondence gate below needs the DP names.
1518
+ // A decode fault leaves certDPs null -- every DP-scoped shard then stays
1519
+ // revocation-only (no correspondence can be shown; fail closed) while
1520
+ // the full-CRL path is unaffected (mirrors the basicConstraints
1521
+ // handling above); the check never crashes on a malformed extension.
1522
+ var certDPs = null;
1523
+ var certCdpExt = findExt(cert, OID.cRLDistributionPoints);
1524
+ if (certCdpExt) {
1525
+ try { certDPs = EXT_DECODERS[OID.cRLDistributionPoints](certCdpExt.value); }
1526
+ catch (_e) { certDPs = null; }
1527
+ }
1422
1528
  // RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
1423
1529
  // certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
1424
1530
  // keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
@@ -1513,7 +1619,34 @@ function crlChecker(crls) {
1513
1619
  if (idp.onlyAttr) continue; // scoped to attribute certs, not this public-key cert
1514
1620
  if (idp.onlyCa && certIsCa !== true) continue; // out of scope (or CA-ness undeterminable)
1515
1621
  if (idp.onlyUser && certIsCa !== false) continue;
1516
- if (idp.hasDistributionPoint || idp.onlySomeReasons) scopeRevocationOnly = true;
1622
+ if (idp.hasDistributionPoint) {
1623
+ // RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
1624
+ // certificate only when the IDP's distribution point shares at
1625
+ // least one IDENTICALLY-ENCODED name with one of the certificate's
1626
+ // own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
1627
+ // be used in the distributionPoint fields of the certificate and
1628
+ // the CRL"). The IDP must also be CRITICAL to be relied on for
1629
+ // scope: sec. 5.2.5 defines the IDP as "a critical CRL extension"
1630
+ // (descriptive phrasing, not an imperative MUST), and a partition
1631
+ // scope a non-supporting relying party would ignore is not a scope
1632
+ // to build "good" on -- a deliberate fail-closed decision. A
1633
+ // non-corresponding shard cannot establish "good" but is still
1634
+ // consulted for revocation below: serials are unique per issuer,
1635
+ // so a listed serial is a genuine revocation (fail closed toward
1636
+ // revoked).
1637
+ var matchedDp = idpExt.critical === true
1638
+ ? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
1639
+ : null;
1640
+ if (!matchedDp) scopeRevocationOnly = true;
1641
+ // sec. 6.3.3(d)(3): a matched DP carrying `reasons` bounds the
1642
+ // interim reason mask below all-reasons -- under the coarse rule
1643
+ // ("good" only at the (d)(4) all-reasons case) that shard is
1644
+ // revocation-only.
1645
+ else if (matchedDp.reasons) scopeRevocationOnly = true;
1646
+ }
1647
+ // sec. 6.3.3(d)(1)/(d)(2): any onlySomeReasons restriction keeps the
1648
+ // interim reason mask below all-reasons -- revocation-only (coarse).
1649
+ if (idp.onlySomeReasons) scopeRevocationOnly = true;
1517
1650
  }
1518
1651
  if (theCrl.thisUpdate > time) continue; // not yet valid
1519
1652
  // A CRL with no nextUpdate has no bounded validity -- its currency
@@ -1583,23 +1716,295 @@ function crlEntryReason(entry) {
1583
1716
  return null;
1584
1717
  }
1585
1718
 
1586
- function verifyCrlSignature(theCrl, issuer) {
1719
+ // Verify a raw signature over tbsBytes with an SPKI public key -- the shared core
1720
+ // of every certificate / CRL / OCSP signature check. Resolve the algorithm
1721
+ // descriptor, enforce the key-OID <-> sig-OID binding (the algorithm-confusion
1722
+ // guard, RFC 9814 sec. 4), import the SPKI, bridge an ECDSA DER signature to
1723
+ // P1363, and verify. Any fault -- an unresolvable/forbidden algorithm, a
1724
+ // key/sig mismatch, an import or verify failure -- resolves false: a signature
1725
+ // check never throws out of this path, it fails closed. `rawSig` is the raw
1726
+ // signature octets (the caller has already unwrapped any BIT STRING and rejected
1727
+ // a non-octet-aligned one).
1728
+ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
1587
1729
  var d;
1588
1730
  try {
1589
- d = resolveDescriptor(theCrl.signatureAlgorithm);
1590
- assertKeyMatchesSigAlg(issuer.workingPublicKey, theCrl.signatureAlgorithm.oid, d);
1731
+ d = resolveDescriptor(sigAlg);
1732
+ assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
1591
1733
  } catch (_e) { return Promise.resolve(false); }
1592
- if (theCrl.signatureValue.unusedBits !== 0) return Promise.resolve(false); // non-octet-aligned signature
1593
- var key;
1594
- return subtle.importKey("spki", issuer.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
1595
- key = k;
1596
- var sig = theCrl.signatureValue.bytes;
1734
+ return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]).then(function (key) {
1735
+ var sig = rawSig;
1597
1736
  if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
1598
- return subtle.verify(d.verify, key, sig, theCrl.tbsBytes);
1737
+ return subtle.verify(d.verify, key, sig, tbsBytes);
1599
1738
  }).then(function (ok) { return ok === true; }, function () { return false; });
1600
1739
  }
1601
1740
 
1741
+ function verifyCrlSignature(theCrl, issuer) {
1742
+ if (!guard.crypto.isOctetAligned(theCrl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
1743
+ return _verifyWithSpki(theCrl.signatureAlgorithm, theCrl.signatureValue.bytes, issuer.workingPublicKey, theCrl.tbsBytes);
1744
+ }
1745
+
1746
+ // ---- the OCSP revocation checker (RFC 6960) -------------------------------
1747
+
1748
+ // CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). Kept SEPARATE from the
1749
+ // signature SIG_ALGS/HASH_BY_OID set (which omits SHA-1 -- a SHA-1 *signature* is
1750
+ // refused, SHAttered): a CertID hash is an identity binding of an already-known
1751
+ // issuer, not a signature, so collision resistance is irrelevant to the lookup
1752
+ // and RFC 6960's default SHA-1 CertID MUST interoperate. A hash OID outside this
1753
+ // set cannot be reproduced -> no CertID match (fail closed), never an assumption.
1754
+ var OCSP_CERTID_HASHES = {};
1755
+ OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
1756
+ OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
1757
+ OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
1758
+ OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
1759
+ var OID_OCSP_SIGNING = oid.byName("ocspSigning");
1760
+ var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
1761
+
1762
+ // The subjectPublicKey BIT STRING VALUE (excluding the unused-bits octet) of an
1763
+ // SPKI DER -- the exact bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash
1764
+ // over (RFC 6960 sec. 4.1.1). Throws on a malformed SPKI; the caller fails closed.
1765
+ function ocspKeyValue(spkiDer) {
1766
+ return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
1767
+ }
1768
+
1769
+ // The delegate responder's importable SPKI. If its key omits algorithm parameters
1770
+ // and inherits them from the issuing CA (same key algorithm -- an EC public key
1771
+ // whose SPKI omits the namedCurve, RFC 5280 sec. 4.1.2.7), splice the issuer's
1772
+ // parameters in so importKey("spki", ...) has a complete key; otherwise a valid
1773
+ // response would verify as unknown. Mirrors updateWorkingKey's inheritance.
1774
+ function ocspResponderSpki(rc, issuer) {
1775
+ var keyAlg = rc.subjectPublicKeyInfo.algorithm;
1776
+ if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
1777
+ var issuerOid, issuerParams;
1778
+ try {
1779
+ var alg = asn1.decode(issuer.workingPublicKey).children[0];
1780
+ issuerOid = asn1.read.oid(alg.children[0]);
1781
+ issuerParams = alg.children[1] ? alg.children[1].bytes : null;
1782
+ } catch (_e) { return rc.subjectPublicKeyInfo.bytes; }
1783
+ if (issuerOid === keyAlg.oid && !isNullOrAbsentParams(issuerParams)) {
1784
+ return spliceSpkiParameters(rc.subjectPublicKeyInfo, keyAlg.oid, issuerParams);
1785
+ }
1786
+ return rc.subjectPublicKeyInfo.bytes;
1787
+ }
1788
+ function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
1789
+
1790
+ // The checker implements NO OCSP response-extension semantics (the nonce is the
1791
+ // live client's, RFC 6960 sec. 4.4.1; CRL References / Archive Cutoff / Service
1792
+ // Locator are not consulted), so a CRITICAL responseExtension / singleExtension
1793
+ // may change a response's meaning in a way this code cannot honor -- treat it as
1794
+ // unusable (RFC 6960 sec. 4.4 / the RFC 5280 critical-extension contract, the OCSP
1795
+ // analogue of the crlChecker unhandled-critical skip). Returns true if `extList`
1796
+ // carries any critical extension.
1797
+ function ocspHasCriticalExtension(extList) {
1798
+ if (!extList) return false;
1799
+ for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
1800
+ return false;
1801
+ }
1802
+
1803
+ // A SingleResponse's CertID names the target cert IFF its serial equals the
1804
+ // target's AND issuerNameHash + issuerKeyHash, recomputed under the CertID's OWN
1805
+ // hashAlgorithm, match the issuer (RFC 6960 sec. 4.1.1). A serial-only match would
1806
+ // let a "good" for issuer A serial N answer a query for issuer B serial N
1807
+ // (cross-CA substitution), so BOTH issuer hashes must match under the responder's
1808
+ // chosen algorithm -- never an assumed SHA-1.
1809
+ //
1810
+ // `issuerNameCandidates` is every byte encoding of the (name-chaining-validated)
1811
+ // issuer DN to try: the checked cert's issuer field (RFC 6960 sec. 4.1.1) plus,
1812
+ // when available, the issuer certificate's own subject encoding. These are RFC
1813
+ // 5280 sec. 7.1-equal (they chained) but MAY differ in DER -- case, whitespace, a
1814
+ // PrintableString-vs-UTF8String DirectoryString choice -- and a responder MAY have
1815
+ // hashed any of them; matching the CertID against any is still the SAME validated
1816
+ // issuer, because the issuerKeyHash + serial + authorized signature carry the
1817
+ // binding. The key (raw octets, no encoding variance) is matched once.
1818
+ async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
1819
+ if (certID.serialNumberHex !== cert.serialNumberHex) return false;
1820
+ var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
1821
+ if (!hashName) return false; // an unreproducible hash -> the match cannot be confirmed
1822
+ var keyHash = await ocspDigest(hashName, issuerKeyBits);
1823
+ if (!certID.issuerKeyHash.equals(keyHash)) return false;
1824
+ for (var i = 0; i < issuerNameCandidates.length; i++) {
1825
+ if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
1826
+ }
1827
+ return false;
1828
+ }
1829
+
1830
+ // Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder's SPKI DER,
1831
+ // or null when none is authorized (RFC 6960 sec. 4.2.2.2). Two models: the issuing
1832
+ // CA signing directly (responderID identifies the issuer), or a CA-delegated
1833
+ // responder -- a certificate in `certs` issued by the SAME CA, valid at `time`,
1834
+ // bearing id-kp-OCSPSigning in its extendedKeyUsage. anyExtendedKeyUsage and an
1835
+ // ABSENT EKU do NOT authorize an OCSP delegate (the opposite of the path-validation
1836
+ // EKU default), so an ordinary leaf the CA issued cannot forge revocation status.
1837
+ // Fails closed at every branch (a malformed delegate, a control-byte DN, an
1838
+ // unreadable EKU -> skip that candidate).
1839
+ async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
1840
+ var rid = basicResponse.responderID;
1841
+ var matchesIssuer = false;
1842
+ try {
1843
+ if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
1844
+ else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
1845
+ } catch (_e) { matchesIssuer = false; }
1846
+ if (matchesIssuer) return issuer.workingPublicKey;
1847
+
1848
+ for (var i = 0; i < basicResponse.certs.length; i++) {
1849
+ var rc;
1850
+ try { rc = x509.parse(basicResponse.certs[i]); }
1851
+ catch (_e) { continue; }
1852
+ var identifies = false;
1853
+ try {
1854
+ if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
1855
+ else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
1856
+ } catch (_e) { identifies = false; }
1857
+ if (!identifies) continue;
1858
+ // The delegate MUST be issued directly by the CA that issued the target.
1859
+ var issuedByCa;
1860
+ try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
1861
+ catch (_e) { continue; }
1862
+ if (!issuedByCa) continue;
1863
+ if (!guard.crypto.isOctetAligned(rc.signatureValue)) continue;
1864
+ if (!(await _verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
1865
+ // The delegate certificate MUST itself be valid at the validation instant.
1866
+ if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
1867
+ // The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
1868
+ var eku;
1869
+ try { eku = decodeExt(rc, OID.extKeyUsage); }
1870
+ catch (_e) { continue; }
1871
+ if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
1872
+ // A delegate asserting keyUsage MUST permit digitalSignature -- signing an OCSP
1873
+ // response is a digitalSignature operation (RFC 5280 sec. 4.2.1.3); an absent
1874
+ // keyUsage is unrestricted, an unreadable one is not authoritative.
1875
+ var ku;
1876
+ try { ku = decodeExt(rc, OID.keyUsage); }
1877
+ catch (_e) { continue; }
1878
+ if (ku && ku.value.digitalSignature !== true) continue;
1879
+ // A delegate carrying a critical extension this code does not process is
1880
+ // unusable, the same fail-closed rule the path validator applies to any cert
1881
+ // (RFC 5280 sec. 6.1.4(o)) -- an unknown critical constraint must not be ignored
1882
+ // while its key is trusted to authenticate revocation status. A RECOGNIZED
1883
+ // critical extension whose value is malformed is likewise rejected via the
1884
+ // shared structure validation, exactly as the path validator rejects it.
1885
+ if (unrecognizedCriticalExtension(rc, false)) continue;
1886
+ if (validateCriticalExtensionStructure(rc)) continue;
1887
+ // The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a
1888
+ // transport-free checker cannot otherwise confirm the responder certificate has
1889
+ // not itself been revoked, and a revoked OCSP-signing certificate would keep
1890
+ // signing "good" until its notAfter. A deployment that instead supplies the
1891
+ // responder's own status opts in through a future checker; absent nocheck, fail
1892
+ // closed (unknown) rather than trust an unvalidated responder.
1893
+ if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
1894
+ return ocspResponderSpki(rc, issuer);
1895
+ }
1896
+ return null;
1897
+ }
1898
+
1899
+ /**
1900
+ * @primitive pki.path.ocspChecker
1901
+ * @signature pki.path.ocspChecker(responses) -> RevocationChecker
1902
+ * @since 0.1.32
1903
+ * @status experimental
1904
+ * @spec RFC 6960
1905
+ * @related pki.path.validate, pki.schema.ocsp.parseResponse, pki.path.crlChecker
1906
+ *
1907
+ * Build an OCSP-backed `RevocationChecker` for `pki.path.validate`'s
1908
+ * `revocationChecker` option from a set of pre-fetched OCSP responses (DER/PEM
1909
+ * or already-parsed). For each certificate it locates a SingleResponse whose
1910
+ * CertID binds this cert's serial to its issuer -- recomputing `issuerNameHash`
1911
+ * and `issuerKeyHash` under the CertID's own hashAlgorithm (SHA-1 or SHA-2), so
1912
+ * a response using either matches -- confirms the responder is authorized (the
1913
+ * issuing CA directly, or a valid CA-issued delegate bearing both id-kp-OCSPSigning
1914
+ * and id-pkix-ocsp-nocheck), verifies the response signature over
1915
+ * `tbsResponseDataBytes`, checks currency
1916
+ * (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
1917
+ * "unknown" }`. A wrong-issuer CertID, an unauthorized responder, a stale,
1918
+ * not-yet-valid, nextUpdate-less, non-successful, or unverifiable response
1919
+ * yields `unknown`, which the validator fails closed unless `softFail` is set;
1920
+ * a `revoked` status surfaces its `revocationReason`. It is transport-free: the
1921
+ * caller supplies bytes it collected (an OCSP fetch or a stapled response), so
1922
+ * nonce anti-replay is the live client's responsibility and the residual replay
1923
+ * defense is the `thisUpdate`/`nextUpdate` currency window.
1924
+ *
1925
+ * @example
1926
+ * var checker = pki.path.ocspChecker([]); // no responses -> every cert is "unknown"
1927
+ * typeof checker.check; // "function"
1928
+ */
1929
+ function ocspChecker(responses) {
1930
+ var parsed = (responses || []).map(function (r) { return (r && r.responseStatus) ? r : ocsp.parseResponse(r); });
1931
+ return {
1932
+ check: async function (cert, issuer, ctx) {
1933
+ var time = ctx.time;
1934
+ var historical = ctx.historicalMode === true;
1935
+ // Issuer DN candidates to match the CertID against (RFC 6960 sec. 4.1.1 names
1936
+ // the checked cert's issuer field; a response MAY instead carry the issuer
1937
+ // certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
1938
+ var issuerNameCandidates = [cert.issuer.bytes];
1939
+ function addNameCandidate(nm) {
1940
+ if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
1941
+ }
1942
+ if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
1943
+ addNameCandidate(issuer.workingIssuerName);
1944
+ var issuerKeyBits;
1945
+ try { issuerKeyBits = ocspKeyValue(issuer.workingPublicKey); }
1946
+ catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
1947
+
1948
+ // A serial is revoked if ANY authoritative, verified, current response says
1949
+ // so -- a clean response must never shadow a revoking one (the crlChecker
1950
+ // fail-closed law). "good" needs at least one authoritative match; every
1951
+ // other outcome is undetermined.
1952
+ var revokedResult = null;
1953
+ var sawGood = false;
1954
+ var sawUnknownStatus = false;
1955
+
1956
+ for (var k = 0; k < parsed.length; k++) {
1957
+ var resp = parsed[k];
1958
+ if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
1959
+ var br = resp.basicResponse;
1960
+ if (!br) continue;
1961
+ var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
1962
+ if (!signerSpki) continue; // unauthorized responder
1963
+ if (!(await _verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) continue;
1964
+ // A critical responseExtension changes the meaning of the WHOLE response
1965
+ // and this code processes none -> the response is unusable.
1966
+ if (ocspHasCriticalExtension(br.responseExtensions)) continue;
1967
+ for (var s = 0; s < br.responses.length; s++) {
1968
+ var sr = br.responses[s];
1969
+ if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue; // not about this cert
1970
+ if (ocspHasCriticalExtension(sr.singleExtensions)) continue; // a critical per-response extension this code cannot process -> skip
1971
+ if (sr.thisUpdate > time) continue; // not yet valid
1972
+ if (!sr.nextUpdate || sr.nextUpdate < time) continue; // no bounded validity / stale -> fail closed
1973
+ var st = sr.certStatus;
1974
+ if (st.type === "revoked") {
1975
+ // A revocation is effective as of its revocationTime. Present-time
1976
+ // validation revokes regardless (a future revocationTime is
1977
+ // post-dating/skew, never "good"); only an EXPLICIT historical
1978
+ // validation defers a strictly-future revocation.
1979
+ if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { sawGood = true; }
1980
+ else {
1981
+ revokedResult = {
1982
+ status: "revoked",
1983
+ revocationReason: st.revocationReason || null,
1984
+ reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : ""),
1985
+ };
1986
+ }
1987
+ } else if (st.type === "good") {
1988
+ sawGood = true;
1989
+ } else {
1990
+ sawUnknownStatus = true; // an explicit responder "unknown"
1991
+ }
1992
+ }
1993
+ }
1994
+ if (revokedResult) return revokedResult;
1995
+ if (sawGood) return { status: "good" };
1996
+ return {
1997
+ status: "unknown",
1998
+ reason: sawUnknownStatus
1999
+ ? "the OCSP responder reported certStatus unknown for this certificate"
2000
+ : "no authoritative, current, in-scope OCSP response covers this certificate",
2001
+ };
2002
+ },
2003
+ };
2004
+ }
2005
+
1602
2006
  module.exports = {
1603
2007
  validate: validate,
1604
2008
  crlChecker: crlChecker,
2009
+ ocspChecker: ocspChecker,
1605
2010
  };