@blamejs/pki 0.4.13 → 0.4.15

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.
@@ -1469,6 +1469,9 @@ function userConstrainedPolicies(state, n) {
1469
1469
 
1470
1470
  var OID_IDP = oid.byName("issuingDistributionPoint");
1471
1471
  var OID_DELTA_CRL = oid.byName("deltaCRLIndicator");
1472
+ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
1473
+ var OID_CRL_NUMBER = oid.byName("cRLNumber");
1474
+ var OID_FRESHEST_CRL = oid.byName("freshestCRL");
1472
1475
 
1473
1476
  // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
1474
1477
  // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
@@ -1489,6 +1492,58 @@ var IDP_SCHEMA = schema.seq([
1489
1492
  ], { minTag: 0, maxTag: 5, unexpectedCode: "path/bad-idp", orderCode: "path/bad-idp" }),
1490
1493
  ], { assert: "sequence", code: "path/bad-idp", what: "IssuingDistributionPoint" });
1491
1494
 
1495
+ // RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
1496
+ // named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
1497
+ // has no ReasonFlags bit at all). 0x1FE is bits 1..8 set.
1498
+ //
1499
+ // NOTE the numbering divergence, which is the trap in this area: a ReasonFlags BIT
1500
+ // is not a CRLReason VALUE. Bits 1..6 coincide with codes 1..6, but bit 7 is
1501
+ // privilegeWithdrawn (code 9) and bit 8 is aACompromise (code 10). sec. 6.3.3 never
1502
+ // needs a mapping between them -- the mask comes only from IDP/DP ReasonFlags and
1503
+ // cert_status only from an entry's CRLReason -- so no bit<->code table exists here,
1504
+ // deliberately. constants.NAMES.REASON_FLAGS and NAMES.CRL_REASON stay separate.
1505
+ var ALL_REASONS = 0x1FE;
1506
+
1507
+ // A ReasonFlags BIT STRING { unusedBits, bytes } -> a bit mask, or null when the
1508
+ // encoding is not minimal DER (X.690 sec. 11.2.2 NamedBitList: trailing zero bits
1509
+ // MUST be dropped). A null tells the caller the CRL's scope is unknown, which is
1510
+ // the same fail-closed posture decodeIdp already takes for a malformed IDP.
1511
+ //
1512
+ // Bits at or above 9 are undefined by sec. 4.2.1.13. They are IGNORED rather than
1513
+ // rejected: an undefined bit can never help reach all-reasons, so ignoring it can
1514
+ // only withhold coverage, never grant it. Reading a bounded prefix also means a
1515
+ // hostile multi-kilobyte ReasonFlags costs nothing.
1516
+ function reasonMaskFromBitString(bs) {
1517
+ if (!bs || !bs.bytes) return null;
1518
+ try { schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (msg) { throw E("path/bad-idp", msg); }); }
1519
+ catch (_e) { return null; }
1520
+ var mask = 0;
1521
+ // From bit 1: sec. 6.3.2(a) defines the legal members as the eight NAMED reasons, and ReasonFlags
1522
+ // bit 0 is -- not a reason at all. Admitting it could not grant coverage (the completeness
1523
+ // test masks with ALL_REASONS) but would make a shard that asserts ONLY bit 0 look like partial
1524
+ // coverage rather than none, which is the wrong verdict to report.
1525
+ for (var bit = 1; bit <= 8; bit++) {
1526
+ var byteI = bit >> 3;
1527
+ if (byteI >= bs.bytes.length) break;
1528
+ if (bs.bytes[byteI] & (0x80 >> (bit & 7))) mask |= (1 << bit);
1529
+ }
1530
+ return mask;
1531
+ }
1532
+
1533
+ // RFC 5280 sec. 6.3.3(d)(1)-(4) -- the interim_reasons_mask, in ONE place.
1534
+ // (1) both present -> intersection
1535
+ // (2) IDP present, DP absent -> the IDP's reasons
1536
+ // (3) IDP absent, DP present -> the DP's reasons
1537
+ // (4) both absent -> all-reasons
1538
+ // A null mask means "present but unreadable"; the caller has already marked such a
1539
+ // CRL unusable, so treating it as 0 here is belt-and-braces, never a coverage grant.
1540
+ function interimReasonMask(idpMask, dpMask) {
1541
+ if (idpMask != null && dpMask != null) return idpMask & dpMask; // (d)(1)
1542
+ if (idpMask != null) return idpMask; // (d)(2)
1543
+ if (dpMask != null) return dpMask; // (d)(3)
1544
+ return ALL_REASONS; // (d)(4)
1545
+ }
1546
+
1492
1547
  function decodeIdp(ext) {
1493
1548
  // Surface the scope flags the checker gates on. ANY structural or value fault
1494
1549
  // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
@@ -1523,6 +1578,15 @@ function decodeIdp(ext) {
1523
1578
  out.onlyUser = flag(m.fields.onlyContainsUserCerts);
1524
1579
  out.onlyCa = flag(m.fields.onlyContainsCACerts);
1525
1580
  out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
1581
+ // The reason BITS, not just their presence: sec. 6.3.3(d)(1)/(d)(2) need the
1582
+ // value. A present-but-non-minimal ReasonFlags leaves the CRL's scope unknown,
1583
+ // which is the same unusable posture every other IDP fault takes -- otherwise
1584
+ // the (d)(1) intersection would be computed over an encoding DER forbids.
1585
+ out.onlySomeReasonsMask = null;
1586
+ if (m.fields.onlySomeReasons.present) {
1587
+ out.onlySomeReasonsMask = reasonMaskFromBitString(m.fields.onlySomeReasons.value);
1588
+ if (out.onlySomeReasonsMask === null) out.malformed = true;
1589
+ }
1526
1590
  out.indirect = flag(m.fields.indirectCRL);
1527
1591
  out.onlyAttr = flag(m.fields.onlyContainsAttributeCerts);
1528
1592
  return out;
@@ -1582,9 +1646,128 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1582
1646
  return false;
1583
1647
  }
1584
1648
 
1649
+ // The raw extnValue octets of a CRL extension, or null. sec. 6.3.3(c)(2)/(c)(3)
1650
+ // compare the IDP and authorityKeyIdentifier of a delta and its base for identity;
1651
+ // the comparison is over the exact encoded octets (the sec. 5.2.5 @3648
1652
+ // "identical encoding" discipline already used for distributionPoint matching),
1653
+ // never over a re-normalized decode.
1654
+ function crlExtValue(theCrl, wantOid) {
1655
+ for (var i = 0; i < theCrl.crlExtensions.length; i++) {
1656
+ if (theCrl.crlExtensions[i].oid === wantOid) return theCrl.crlExtensions[i].value;
1657
+ }
1658
+ return null;
1659
+ }
1660
+
1661
+ // One pass over the bundle: split complete CRLs from deltas and decode, per CRL,
1662
+ // the values the merge preconditions compare. A delta whose BaseCRLNumber will not
1663
+ // decode as a non-negative INTEGER is unusable AS A DELTA but stays consultable for
1664
+ // revocation -- dropping it wholesale would lose a revocation it lists (T1).
1665
+ // RFC 5280 sec. 5.2.3 @3404 bounds a CRL number at 20 octets, and pki.crl.sign enforces that on
1666
+ // emission. A number past it is non-conforming, so it does not earn the merge -- which can RELEASE
1667
+ // a certificate. The value still decodes and the CRL is still consulted for revocation; only the
1668
+ // new capability is withheld. Measured on the ENCODED length, matching the producer-side check
1669
+ // rather than a re-derived numeric bound.
1670
+ function crlNumberWithinBound(n) {
1671
+ if (typeof n !== "bigint" || n < 0n) return false;
1672
+ try { return asn1.decode(asn1.build.integer(n)).content.length <= 20; }
1673
+ catch (_e) { return false; }
1674
+ }
1675
+
1676
+ function classifyCrls(parsed) {
1677
+ var completes = [], deltas = [];
1678
+ for (var i = 0; i < parsed.length; i++) {
1679
+ var theCrl = parsed[i];
1680
+ var deltaRaw = crlExtValue(theCrl, OID_DELTA_CRL);
1681
+ var deltaCritical = false;
1682
+ for (var dz = 0; dz < theCrl.crlExtensions.length; dz++) {
1683
+ if (theCrl.crlExtensions[dz].oid === OID_DELTA_CRL) { deltaCritical = theCrl.crlExtensions[dz].critical === true; break; }
1684
+ }
1685
+ // schema-crl already decoded cRLNumber to a non-negative BigInt; the IDP,
1686
+ // authorityKeyIdentifier and deltaCRLIndicator values stay RAW octets there,
1687
+ // which is exactly what the byte-identical compares below need.
1688
+ var num = crlExtValue(theCrl, OID_CRL_NUMBER);
1689
+ var rec = {
1690
+ crl: theCrl,
1691
+ crlNumber: crlNumberWithinBound(num) ? num : null,
1692
+ idpRaw: crlExtValue(theCrl, OID_IDP),
1693
+ akiRaw: crlExtValue(theCrl, OID_AUTHORITY_KEY_ID),
1694
+ baseCrlNumber: null,
1695
+ mergeable: false,
1696
+ };
1697
+ if (deltaRaw === null) { completes.push(rec); continue; }
1698
+ try {
1699
+ var n = asn1.read.integer(asn1.decode(deltaRaw));
1700
+ // sec. 5.2.3 @3404: a CRL number may be up to 20 octets. It stays a BigInt
1701
+ // through every comparison below -- narrowing through Number would collapse
1702
+ // large values and make 5.2.4(c)/(d) compare the wrong things.
1703
+ // sec. 5.2.4 @3447 makes deltaCRLIndicator a MUST-be-critical extension. A non-critical one
1704
+ // is non-conforming, so it does not earn the NEW capability of being merged -- merging can
1705
+ // RELEASE a certificate, and that must rest on a conforming indicator. It is still classified
1706
+ // as a delta and still consulted for revocation, which is the shipped behaviour and the
1707
+ // conservative direction.
1708
+ if (crlNumberWithinBound(n) && deltaCritical) { rec.baseCrlNumber = n; rec.mergeable = true; }
1709
+ } catch (_e) {
1710
+ // Unusable AS A DELTA, but still scanned for revocation. That is the conservative direction:
1711
+ // the CRL is still the issuer's, signed and current (it must pass every gate before it is
1712
+ // consulted), so a serial it lists is a genuine revocation and dropping it wholesale could
1713
+ // lose one. Consulting it can only ever find MORE revocations, never grant coverage -- a
1714
+ // delta contributes no reason mask.
1715
+ }
1716
+ deltas.push(rec);
1717
+ }
1718
+ return { completes: completes, deltas: deltas };
1719
+ }
1720
+
1721
+ // RFC 5280 sec. 6.3.3(c)(1)-(3) + sec. 5.2.4(a)-(d), as one predicate. Every
1722
+ // comparison is BigInt or byte-exact: no narrowing, no canonicalization. An
1723
+ // extension ABSENT on both sides matches; present on one side only does not.
1724
+ function deltaMergesWith(delta, complete) {
1725
+ if (!delta.mergeable) return false;
1726
+ // Both numbers are required: 5.2.4(c)/(d) are ordering rules, and a missing
1727
+ // cRLNumber on either side leaves the ordering unknowable -- fail closed.
1728
+ if (complete.crlNumber === null || delta.crlNumber === null) return false;
1729
+ if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false; // (c)(1)
1730
+ if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false; // (c)(2) / 5.2.4(b)
1731
+ if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false; // (c)(3)
1732
+ if (!(complete.crlNumber >= delta.baseCrlNumber)) return false; // 5.2.4(c)
1733
+ if (!(complete.crlNumber < delta.crlNumber)) return false; // 5.2.4(d)
1734
+ return true;
1735
+ }
1736
+ // dnEqual THROWS on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
1737
+ // For the merge preconditions a DN that cannot be compared is simply not a match:
1738
+ // the delta and the base are not shown to share an issuer, so they are not merged.
1739
+ // The throw is contained here rather than at the call site so the predicate itself
1740
+ // stays a straight-line sequence of comparisons.
1741
+ function dnEqualUsable(a, b) {
1742
+ try { return dnEqual(a, b); }
1743
+ catch (_e) { return false; }
1744
+ }
1745
+
1746
+ function sameRawExt(a, b) {
1747
+ if (a === null && b === null) return true;
1748
+ if (a === null || b === null) return false;
1749
+ return a.equals(b);
1750
+ }
1751
+
1752
+ // sec. 5.2.4 @3580 makes MULTIPLE current deltas for one scope legal and says the
1753
+ // application SHOULD take the one with the latest thisUpdate. Selection, never
1754
+ // rejection: treating two current deltas as a fault would refuse a conforming
1755
+ // publication. Ties break on the greater cRLNumber, then first-wins.
1756
+ function selectDelta(candidates) {
1757
+ var best = null;
1758
+ for (var i = 0; i < candidates.length; i++) {
1759
+ var c = candidates[i];
1760
+ if (best === null) { best = c; continue; }
1761
+ var t = c.crl.thisUpdate.getTime(), bt = best.crl.thisUpdate.getTime();
1762
+ if (t > bt) { best = c; continue; }
1763
+ if (t === bt && c.crlNumber !== null && best.crlNumber !== null && c.crlNumber > best.crlNumber) best = c;
1764
+ }
1765
+ return best;
1766
+ }
1767
+
1585
1768
  /**
1586
1769
  * @primitive pki.path.crlChecker
1587
- * @signature pki.path.crlChecker(crls) -> RevocationChecker
1770
+ * @signature pki.path.crlChecker(crls, opts?) -> RevocationChecker
1588
1771
  * @since 0.1.16
1589
1772
  * @status experimental
1590
1773
  * @spec RFC 5280
@@ -1599,17 +1782,38 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1599
1782
  * "unknown" }`. A partitioned/sharded CRL (a critical IDP naming a
1600
1783
  * distribution point) establishes "good" when it corresponds to one of the
1601
1784
  * certificate's own cRLDistributionPoints -- at least one identically-encoded
1602
- * name in common (RFC 5280 sec. 6.3.3) -- and neither side restricts reason
1603
- * codes; a non-corresponding or reason-restricted shard is consulted for
1604
- * revocation only. An out-of-scope, stale, unauthorized, or unverifiable CRL
1605
- * yields `unknown`, which the validator fails closed unless `softFail` is set.
1785
+ * name in common (RFC 5280 sec. 6.3.3). Reason-sharded CRLs ACCUMULATE: each
1786
+ * corresponding CRL contributes its interim reason mask (sec. 6.3.3(d)) and the
1787
+ * certificate is "good" once the shards together cover all eight revocation
1788
+ * reasons, so a CA that partitions by reason code is served. A DELTA CRL is
1789
+ * merged onto a complete CRL it may be combined with (sec. 5.2.4 / 6.3.3(c)):
1790
+ * the delta is searched first, the complete CRL only if the delta left the
1791
+ * status unrevoked, and `removeFromCRL` then releases the certificate -- so a
1792
+ * base+delta pair reports a real verdict where the base alone could not. A
1793
+ * delta that merges with nothing is still consulted for revocation and still
1794
+ * blocks "good": merging may turn undetermined into good or revoked, never a
1795
+ * revoked into a good. A non-corresponding shard is consulted for revocation
1796
+ * only. An out-of-scope, stale, unauthorized, or unverifiable CRL yields
1797
+ * `unknown`, which the validator fails closed unless `softFail` is set.
1798
+ *
1799
+ * A `revoked` verdict carries `reasonCode` (the CRLReason integer, 0 for
1800
+ * `unspecified`) and a `reason` naming it.
1801
+ *
1802
+ * @opts
1803
+ * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
1804
+ * Default true. When false a delta is never merged; it is
1805
+ * still consulted for revocation.
1606
1806
  *
1607
1807
  * @example
1608
1808
  * var checker = pki.path.crlChecker([]); // no CRLs -> every cert is "unknown"
1609
1809
  * typeof checker.check; // "function"
1610
1810
  */
1611
- function crlChecker(crls) {
1811
+ function crlChecker(crls, opts) {
1612
1812
  var parsed = (crls || []).map(function (c) { return (c && c.tbsBytes) ? c : crl.parse(c); });
1813
+ // RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
1814
+ // a caller holding a delta wants it used -- and turning it off never makes a
1815
+ // verdict weaker, only less determined.
1816
+ var useDeltas = !(opts && opts.useDeltas === false);
1613
1817
  return {
1614
1818
  check: async function (cert, issuer, ctx) {
1615
1819
  var time = ctx.time;
@@ -1660,42 +1864,58 @@ function crlChecker(crls) {
1660
1864
  }
1661
1865
 
1662
1866
  // Consult EVERY CRL issued by the cert's issuer -- a clean CRL must not
1663
- // shadow a revoking one (RFC 5280 6.3.3). A serial listed in ANY
1867
+ // shadow a revoking one (RFC 5280 sec. 6.3.3). A serial listed in ANY
1664
1868
  // authoritative, in-scope, current, verified CRL is revoked; the cert is
1665
- // "good" only if at least one such CRL was consulted and none list it;
1666
- // otherwise the status is undetermined.
1667
- var sawAuthoritative = false;
1668
- var sawDelta = false;
1669
- var sawDeltaRemoval = false; // a delta released this serial from hold
1670
- var revokedResult = null; // a base/full CRL revocation, decided at the end
1671
- for (var k = 0; k < parsed.length; k++) {
1672
- var theCrl = parsed[k];
1869
+ // "good" only when the CRLs consulted together cover ALL EIGHT revocation
1870
+ // reasons (sec. 6.3.3(l) + the termination rule at @5291) and none list it.
1871
+ //
1872
+ // Note there is deliberately no early exit once the mask is complete: the
1873
+ // shipped checker scans every CRL so a clean one cannot shadow a revoking
1874
+ // one, and stopping at full coverage would reintroduce exactly that.
1875
+ var certStatus = null; // sec. 6.3.2(b) cert_status; null = UNREVOKED
1876
+ var reasonsMask = 0; // sec. 6.3.2(a) reasons_mask; the empty set
1877
+ var releasedByUnmergedDelta = false;
1878
+ // A CURRENT, authoritative delta that merged with nothing means the local
1879
+ // revocation picture is incomplete: the base it names may be newer than any
1880
+ // complete CRL held here, so a clean base proves less than it appears to.
1881
+ // The shipped checker blocks on exactly this, and the merge must only
1882
+ // ever turn undetermined INTO good -- never make an unmerged delta weaker
1883
+ // than it was before this feature existed.
1884
+ var sawUnmergedDelta = false;
1885
+ var classified = classifyCrls(parsed);
1886
+ var consumedDeltas = [];
1887
+
1888
+ // Every gate a CRL must pass before it may speak for this certificate, in
1889
+ // the shipped order. Returns null when the CRL is unusable, otherwise the
1890
+ // interim_reasons_mask it may contribute -- 0 meaning "consulted for
1891
+ // revocation only", which is how a non-corresponding shard is honestly
1892
+ // encoded (it can reveal a revocation but can never establish coverage).
1893
+ async function gateCrl(rec) {
1894
+ // Memoized per RECORD, and the records are rebuilt by classifyCrls on every check(), so the
1895
+ // cache is per-call and the checker stays re-entrant. Without it the base-by-delta pairing
1896
+ // below would repeat an asynchronous public-key signature verification for every pair --
1897
+ // O(completes x deltas) verifications where the shipped loop did O(crls).
1898
+ if (rec._gated) return rec._gate;
1899
+ rec._gate = await gateCrlUncached(rec);
1900
+ rec._gated = true;
1901
+ return rec._gate;
1902
+ }
1903
+ async function gateCrlUncached(rec) {
1904
+ var theCrl = rec.crl;
1673
1905
  // dnEqual throws on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
1674
1906
  // A single malformed CRL in the bundle must NOT abort the whole check (which
1675
1907
  // would mask a later authoritative CRL and pass under softFail) -- treat it
1676
1908
  // as unusable and skip it, consulting the remaining CRLs.
1677
1909
  var issuerMatches;
1678
1910
  try { issuerMatches = dnEqual(theCrl.issuer.rdns, cert.issuer.rdns); }
1679
- catch (_e) { continue; }
1680
- if (!issuerMatches) continue;
1681
- if (!signerAuthorized) continue;
1682
-
1683
- // A CRL carrying deltaCRLIndicator is a DELTA CRL: it lists only the
1684
- // CHANGES since a base CRL (RFC 5280 sec. 5.2.4). deltaCRLIndicator is a
1685
- // RECOGNIZED extension (so a critical one is not "unhandled"); the delta
1686
- // is acted on only AFTER it passes the currency + signature checks below,
1687
- // so a stale, malformed, or unverifiable delta cannot spuriously block a
1688
- // good result. An AUTHORITATIVE delta blocks "good" (its base is not
1689
- // merged here) and can still reveal a revocation for a serial it lists.
1690
- var isDelta = false;
1691
- for (var dz = 0; dz < theCrl.crlExtensions.length; dz++) {
1692
- if (theCrl.crlExtensions[dz].oid === OID_DELTA_CRL) { isDelta = true; break; }
1693
- }
1911
+ catch (_e) { return null; }
1912
+ if (!issuerMatches) return null;
1913
+ if (!signerAuthorized) return null;
1694
1914
 
1695
1915
  // A validly-signed CRL carrying a CRITICAL extension this checker does
1696
1916
  // not understand (anything but issuingDistributionPoint / deltaCRLIndicator)
1697
1917
  // may change the CRL's scope or meaning -- treat it as unusable (RFC 5280
1698
- // 5.2 critical-extension semantics), never authoritative.
1918
+ // sec. 5.2 critical-extension semantics), never authoritative.
1699
1919
  var unhandledCritical = false;
1700
1920
  for (var x = 0; x < theCrl.crlExtensions.length; x++) {
1701
1921
  var xe = theCrl.crlExtensions[x];
@@ -1715,28 +1935,38 @@ function crlChecker(crls) {
1715
1935
  if (ees[ex].critical && ees[ex].oid !== OID_REASON_CODE) { unhandledCritical = true; break; }
1716
1936
  }
1717
1937
  }
1718
- if (unhandledCritical) continue;
1719
-
1720
- // A partition-scoped CRL (a specific distributionPoint, or reason-sharded
1721
- // via onlySomeReasons) covers only part of the issuer's revocations, so it
1722
- // cannot by itself establish "good" (full coverage is unconfirmed). But a
1723
- // serial it LISTS is a genuine revocation of this certificate (serials are
1724
- // unique per issuer), so such a CRL must still be consulted for revocation
1725
- // -- dropping it wholesale would let a revoked cert slip under softFail.
1726
- var scopeRevocationOnly = false;
1727
- var idpExt = null;
1728
- for (var e = 0; e < theCrl.crlExtensions.length; e++) if (theCrl.crlExtensions[e].oid === OID_IDP) idpExt = theCrl.crlExtensions[e];
1729
- if (idpExt) {
1730
- var idp = decodeIdp(idpExt);
1731
- if (idp.malformed) continue; // scope unknown -> unusable
1938
+ if (unhandledCritical) return null;
1939
+
1940
+ // sec. 6.3.3(d): the interim reason mask this CRL contributes. Its two
1941
+ // inputs are the IDP's onlySomeReasons and the CORRESPONDING certificate
1942
+ // DistributionPoint's reasons -- never an entry's reasonCode (sec. 5.2.5
1943
+ // @3628 explicitly permits a shard entry to omit one).
1944
+ // Set when the shard covers no reasons for this certificate. It is recorded rather than
1945
+ // RETURNED here: the currency and signature gates below still have to run, or an expired,
1946
+ // not-yet-valid or FORGED shard could be scanned for revocations and falsely revoke.
1947
+ var noCoverage = false;
1948
+ var idpMask = null, dpMask = null, sawIdp = false;
1949
+ var idpExtension = null;
1950
+ for (var e = 0; e < theCrl.crlExtensions.length; e++) if (theCrl.crlExtensions[e].oid === OID_IDP) idpExtension = theCrl.crlExtensions[e];
1951
+ if (idpExtension) {
1952
+ sawIdp = true;
1953
+ var idp = decodeIdp(idpExtension);
1954
+ if (idp.malformed) return null; // scope unknown -> unusable
1732
1955
  // An indirect CRL carries entries for other issuers keyed by the
1733
1956
  // per-entry certificateIssuer attribute (not tracked here) -- matching
1734
1957
  // by serial alone could revoke the wrong cert or falsely cover it, so
1735
1958
  // treat an indirect CRL as unusable until certificateIssuer is honored.
1736
- if (idp.indirect) continue;
1737
- if (idp.onlyAttr) continue; // scoped to attribute certs, not this public-key cert
1738
- if (idp.onlyCa && certIsCa !== true) continue; // out of scope (or CA-ness undeterminable)
1739
- if (idp.onlyUser && certIsCa !== false) continue;
1959
+ if (idp.indirect) return null;
1960
+ if (idp.onlyAttr) return null; // scoped to attribute certs, not this public-key cert
1961
+ if (idp.onlyCa && certIsCa !== true) return null; // out of scope (or CA-ness undeterminable)
1962
+ if (idp.onlyUser && certIsCa !== false) return null;
1963
+ // The same fail-closed decision the distributionPoint correspondence rests on (sec. 5.2.5
1964
+ // @3601 lets a relying party not support the IDP at all): a scope a non-supporting
1965
+ // verifier would IGNORE is not a scope to build coverage on. So a non-critical IDP's
1966
+ // onlySomeReasons contributes nothing -- which also preserves the shipped property that
1967
+ // an onlySomeReasons shard could only ever WITHHOLD good, never establish it.
1968
+ if (idpExtension.critical === true) idpMask = idp.onlySomeReasonsMask;
1969
+ else if (idp.onlySomeReasons) noCoverage = true;
1740
1970
  if (idp.hasDistributionPoint) {
1741
1971
  // RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
1742
1972
  // certificate only when the IDP's distribution point shares at
@@ -1744,50 +1974,50 @@ function crlChecker(crls) {
1744
1974
  // own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
1745
1975
  // be used in the distributionPoint fields of the certificate and
1746
1976
  // the CRL"). The IDP must also be CRITICAL to be relied on for
1747
- // scope: sec. 5.2.5 defines the IDP as "a critical CRL extension"
1748
- // (descriptive phrasing, not an imperative MUST), and a partition
1749
- // scope a non-supporting relying party would ignore is not a scope
1750
- // to build "good" on -- a deliberate fail-closed decision. A
1751
- // non-corresponding shard cannot establish "good" but is still
1752
- // consulted for revocation below: serials are unique per issuer,
1753
- // so a listed serial is a genuine revocation (fail closed toward
1754
- // revoked).
1755
- var matchedDp = idpExt.critical === true
1977
+ // scope: sec. 5.2.5 describes the IDP as "a critical CRL extension"
1978
+ // (descriptive phrasing, not an imperative MUST -- and @3601 lets a
1979
+ // relying party not support it at all), so building coverage on a
1980
+ // scope a non-supporting verifier would ignore is a deliberate
1981
+ // fail-closed decision. A non-corresponding shard contributes NO
1982
+ // coverage but is still consulted for revocation below: serials are
1983
+ // unique per issuer, so a listed serial is a genuine revocation.
1984
+ var matchedDp = idpExtension.critical === true
1756
1985
  ? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
1757
1986
  : null;
1758
- if (!matchedDp) scopeRevocationOnly = true;
1759
- // sec. 6.3.3(d)(3): a matched DP carrying `reasons` bounds the
1760
- // interim reason mask below all-reasons -- under the coarse rule
1761
- // ("good" only at the (d)(4) all-reasons case) that shard is
1762
- // revocation-only.
1763
- else if (matchedDp.reasons) scopeRevocationOnly = true;
1987
+ if (!matchedDp) noCoverage = true;
1988
+ // sec. 6.3.3(d)(1)/(d)(3): a matched DP carrying `reasons` bounds the
1989
+ // interim mask. A present-but-unreadable value cannot bound anything
1990
+ // safely, so it contributes nothing rather than defaulting open.
1991
+ else if (matchedDp.reasons) {
1992
+ dpMask = reasonMaskFromBitString(matchedDp.reasons);
1993
+ if (dpMask === null) noCoverage = true;
1994
+ }
1764
1995
  }
1765
- // sec. 6.3.3(d)(1)/(d)(2): any onlySomeReasons restriction keeps the
1766
- // interim reason mask below all-reasons -- revocation-only (coarse).
1767
- if (idp.onlySomeReasons) scopeRevocationOnly = true;
1768
1996
  }
1769
- if (theCrl.thisUpdate > time) continue; // not yet valid
1997
+ if (theCrl.thisUpdate > time) return null; // not yet valid
1770
1998
  // A CRL with no nextUpdate has no bounded validity -- its currency
1771
1999
  // cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
1772
2000
  // replayed old CRL must not read "good". Treat it as unusable.
1773
- if (!theCrl.nextUpdate || theCrl.nextUpdate < time) continue; // stale / no bound
2001
+ if (!theCrl.nextUpdate || theCrl.nextUpdate < time) return null; // stale / no bound
1774
2002
 
1775
2003
  var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
1776
- if (!sigOk) continue; // unverifiable -> not authoritative
1777
-
1778
- // The CRL is now authoritative + current + verified. An authoritative
1779
- // delta blocks a "good" result (its base is not merged here) and can only
1780
- // reveal a revocation -- never establish "good" on its own.
1781
- if (isDelta) { sawDelta = true; scopeRevocationOnly = true; }
2004
+ if (!sigOk) return null; // unverifiable -> not authoritative
2005
+
2006
+ // sec. 6.3.3 @5295: a CRL not named by any distribution point is processed
2007
+ // as though under a DP whose `reasons` and `cRLIssuer` are absent -- i.e.
2008
+ // all-reasons. That is why a full-scope CRL still covers a certificate
2009
+ // that happens to carry a cRLDistributionPoints extension.
2010
+ void sawIdp;
2011
+ return { interim: noCoverage ? 0 : interimReasonMask(idpMask, dpMask) };
2012
+ }
1782
2013
 
2014
+ // sec. 6.3.3(i)/(j): the certificate's entry on one CRL, as a CRLReason
2015
+ // value, or null for UNREVOKED. sec. 6.3.3(i)(2): an entry with no
2016
+ // reasonCode extension is `unspecified` (0), which is a revocation.
2017
+ function scanCrl(theCrl) {
1783
2018
  for (var r = 0; r < theCrl.revokedCertificates.length; r++) {
1784
2019
  var entry = theCrl.revokedCertificates[r];
1785
2020
  if (entry.serialNumberHex !== cert.serialNumberHex) continue;
1786
- // reasonCode removeFromCRL (8) means the entry was un-revoked. In a
1787
- // DELTA this releases the serial from hold; because the base is not
1788
- // merged here, a definitive "revoked" is no longer possible for it -- a
1789
- // base CRL that still lists it must not override the delta removal.
1790
- if (crlEntryReason(entry) === 8) { if (isDelta) sawDeltaRemoval = true; continue; }
1791
2021
  // A revocation is effective as of its revocationDate (RFC 5280 sec. 5.3).
1792
2022
  // In the DEFAULT present-time validation a listed serial is revoked
1793
2023
  // regardless of that date -- a future revocationDate is post-dating or
@@ -1797,28 +2027,113 @@ function crlChecker(crls) {
1797
2027
  // validation time not yet apply.
1798
2028
  // 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.
1799
2029
  if (historical && entry.revocationDate instanceof Date && entry.revocationDate.getTime() > time.getTime()) continue;
1800
- // Record the revocation but keep scanning: a delta removeFromCRL for the
1801
- // same serial (in another CRL) overrides it (base/delta not merged).
1802
- revokedResult = { status: "revoked", reason: "serial listed in a CRL" };
1803
- break;
2030
+ var rc = crlEntryReason(entry);
2031
+ return rc === null ? 0 : rc;
2032
+ }
2033
+ return null;
2034
+ }
2035
+
2036
+ // sec. 6.3.3(a)(2): a delta is obtained only when use-deltas is set AND a
2037
+ // locator exists -- freshestCRL on the certificate or on the complete CRL.
2038
+ // sec. 5.2.6 @3719: the locator's CONTENTS are only ever used to find a
2039
+ // delta, never to validate one, so its presence is the whole gate.
2040
+ // PRESENCE only, deliberately. sec. 5.2.6 @3719 says the freshestCRL contents are used to
2041
+ // LOCATE a delta and never to validate one, so decoding them would be using a value the RFC
2042
+ // says not to use. It is also not a security control: enabling the merge grants nothing on its
2043
+ // own, because a delta still has to pass every gate -- issuer, authorization, criticality,
2044
+ // scope, currency and SIGNATURE -- plus the sec. 5.2.4 merge preconditions. An attacker who
2045
+ // could satisfy those already controls the issuing key.
2046
+ var certHasFreshest = !!findExt(cert, OID_FRESHEST_CRL);
2047
+ function deltaLocatorPresent(completeRec) {
2048
+ return certHasFreshest || crlExtValue(completeRec.crl, OID_FRESHEST_CRL) !== null;
2049
+ }
2050
+
2051
+ for (var ci = 0; ci < classified.completes.length; ci++) {
2052
+ var rec = classified.completes[ci];
2053
+ var gate = await gateCrl(rec);
2054
+ if (!gate) continue;
2055
+
2056
+ // sec. 6.3.3(c): pair this complete CRL with a delta it may be combined
2057
+ // with. The delta passes its OWN gates first -- sec. 6.3.3(h) requires
2058
+ // its signature verified and (f) its issuer authorized, exactly as for a
2059
+ // complete CRL -- so a stale, unverifiable or out-of-scope delta is never
2060
+ // merged.
2061
+ var chosenDelta = null;
2062
+ if (useDeltas && deltaLocatorPresent(rec)) {
2063
+ var candidates = [];
2064
+ for (var di = 0; di < classified.deltas.length; di++) {
2065
+ var cand = classified.deltas[di];
2066
+ if (!deltaMergesWith(cand, rec)) continue;
2067
+ if (!(await gateCrl(cand))) continue;
2068
+ // Mergeable and usable: this delta's scope IS covered by a merge, even
2069
+ // if sec. 5.2.4 @3580 selection prefers a sibling with a later
2070
+ // thisUpdate. Only a delta that pairs with NO complete CRL leaves the
2071
+ // picture incomplete -- a losing candidate must not block a good result,
2072
+ // or publishing two current deltas (which the RFC permits) would be
2073
+ // worse than publishing one.
2074
+ cand.accounted = true;
2075
+ candidates.push(cand);
2076
+ }
2077
+ chosenDelta = selectDelta(candidates);
2078
+ }
2079
+
2080
+ var status;
2081
+ if (chosenDelta) {
2082
+ consumedDeltas.push(chosenDelta);
2083
+ status = scanCrl(chosenDelta.crl); // (i) search the delta FIRST
2084
+ if (status === null) status = scanCrl(rec.crl); // (j) the complete CRL only if still UNREVOKED
2085
+ } else {
2086
+ status = scanCrl(rec.crl);
1804
2087
  }
1805
- // A partition-scoped CRL that did not list this serial does NOT prove the
1806
- // cert is unrevoked (another shard/reason may revoke it) -- only a
1807
- // full-scope CRL can establish "good".
1808
- if (!scopeRevocationOnly) sawAuthoritative = true; // covered this cert, not listed
2088
+ // (k): removeFromCRL means the certificate is no longer revoked. It is
2089
+ // normalized wherever it appears -- sec. 5.3.1's "only in delta CRLs" binds
2090
+ // the CA that emits it, not this consumer, and rejecting a complete CRL
2091
+ // that carries one would make an unusual-but-harmless CRL unusable.
2092
+ if (status === 8) status = null;
2093
+
2094
+ if (status !== null && certStatus === null) certStatus = status;
2095
+ else if (status === null) reasonsMask |= gate.interim; // (l): only a clean scope covers
1809
2096
  }
1810
- // A delta released this serial from hold: without merging its base we cannot
1811
- // return a definitive revoked (else a released cert stays rejected) -- the
2097
+
2098
+ // An unmerged delta still speaks, under the shipped fail-closed posture: a
2099
+ // serial it lists is a genuine revocation (serials are unique per issuer),
2100
+ // and a removeFromCRL it carries blocks a definitive revoked without being
2101
+ // able to establish good. This is what keeps the merge MONOTONIC -- it may
2102
+ // turn undetermined into good or revoked, but a delta that merges with
2103
+ // nothing can never erase a revocation the checker would otherwise report.
2104
+ for (var dj = 0; dj < classified.deltas.length; dj++) {
2105
+ var dRec = classified.deltas[dj];
2106
+ if (consumedDeltas.indexOf(dRec) !== -1) continue;
2107
+ // A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
2108
+ // does not speak for its scope at all -- the selected delta does, and it was evaluated
2109
+ // against the base. Letting a superseded delta contribute its revocation while ignoring its
2110
+ // release would be incoherent: an older delta revoking a certificate that the newer one
2111
+ // releases would resurrect the revocation the CA withdrew.
2112
+ if (dRec.accounted) continue;
2113
+ if (!(await gateCrl(dRec))) continue;
2114
+ sawUnmergedDelta = true;
2115
+ var dStatus = scanCrl(dRec.crl);
2116
+ if (dStatus === null) continue;
2117
+ if (dStatus === 8) { releasedByUnmergedDelta = true; continue; }
2118
+ if (certStatus === null) certStatus = dStatus;
2119
+ }
2120
+
2121
+ // A delta released this serial from hold but its base was not merged: a
2122
+ // definitive revoked would leave a released certificate rejected, so the
1812
2123
  // status is undetermined. This outranks a base CRL's revocation.
1813
- if (sawDeltaRemoval) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
1814
- if (revokedResult) return revokedResult;
1815
- // A delta CRL for this issuer was seen but cannot be merged with its base,
1816
- // so the current revocation picture is incomplete -- never report "good".
1817
- if (sawDelta) return { status: "unknown", reason: "a delta CRL cannot be evaluated without combining it with its base CRL, so the revocation status is undetermined" };
1818
- if (sawAuthoritative) return { status: "good" };
2124
+ if (releasedByUnmergedDelta) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
2125
+ if (certStatus !== null) {
2126
+ var reasonName = constants.NAMES.CRL_REASON[String(certStatus)] || "unspecified";
2127
+ return { status: "revoked", reasonCode: certStatus, reason: "serial listed in a CRL (" + reasonName + ")" };
2128
+ }
2129
+ if (sawUnmergedDelta) return { status: "unknown", reason: "a delta CRL cannot be combined with any complete CRL held here, so the revocation picture is incomplete" };
2130
+ if ((reasonsMask & ALL_REASONS) === ALL_REASONS) return { status: "good" };
1819
2131
  if (certScopeFault) {
1820
2132
  return { status: "unknown", reason: "no authoritative in-scope CRL covers this certificate; its basicConstraints extension is unreadable (" + certScopeFault + "), so scope-limited CRLs were skipped" };
1821
2133
  }
2134
+ if (reasonsMask !== 0) {
2135
+ return { status: "unknown", reason: "the CRLs available cover only some revocation reasons for this certificate; no combination covers all of them" };
2136
+ }
1822
2137
  return { status: "unknown", reason: "no authoritative in-scope CRL covers this certificate" };
1823
2138
  },
1824
2139
  };