@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.
- package/CHANGELOG.md +57 -1
- package/README.md +2 -2
- package/lib/cms-decrypt.js +123 -60
- package/lib/cms-encrypt.js +198 -139
- package/lib/hpke.js +94 -21
- package/lib/key.js +24 -6
- package/lib/path-validate.js +411 -96
- package/lib/pbes2.js +36 -7
- package/lib/pkcs12-build.js +69 -16
- package/lib/schema-pkix.js +6 -0
- package/lib/webcrypto.js +154 -62
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -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)
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
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
|
|
1666
|
-
//
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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) {
|
|
1680
|
-
if (!issuerMatches)
|
|
1681
|
-
if (!signerAuthorized)
|
|
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)
|
|
1719
|
-
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
|
|
1727
|
-
var
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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)
|
|
1737
|
-
if (idp.onlyAttr)
|
|
1738
|
-
if (idp.onlyCa && certIsCa !== true)
|
|
1739
|
-
if (idp.onlyUser && certIsCa !== false)
|
|
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
|
|
1748
|
-
// (descriptive phrasing, not an imperative MUST
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
// non-corresponding shard
|
|
1752
|
-
// consulted for revocation below: serials are
|
|
1753
|
-
// so a listed serial is a genuine revocation
|
|
1754
|
-
|
|
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)
|
|
1759
|
-
// sec. 6.3.3(d)(3): a matched DP carrying `reasons` bounds the
|
|
1760
|
-
// interim
|
|
1761
|
-
//
|
|
1762
|
-
|
|
1763
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
1777
|
-
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1780
|
-
//
|
|
1781
|
-
|
|
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
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
-
//
|
|
1806
|
-
//
|
|
1807
|
-
//
|
|
1808
|
-
|
|
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
|
-
|
|
1811
|
-
//
|
|
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 (
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
if (
|
|
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
|
};
|