@blamejs/pki 0.4.14 → 0.5.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/MIGRATING.md +2 -2
  3. package/README.md +142 -137
  4. package/index.js +4 -0
  5. package/lib/acme.js +73 -1
  6. package/lib/asn1-der.js +2 -0
  7. package/lib/attrcert-sign.js +4 -0
  8. package/lib/cbor-det.js +32 -16
  9. package/lib/cmc-build.js +880 -0
  10. package/lib/cmc-verify.js +657 -0
  11. package/lib/cmp-build.js +8 -7
  12. package/lib/cmp-verify.js +11 -1
  13. package/lib/cms-sign.js +170 -8
  14. package/lib/cms-verify.js +80 -14
  15. package/lib/crl-sign.js +22 -0
  16. package/lib/crmf-sign.js +5 -2
  17. package/lib/csr-sign.js +3 -0
  18. package/lib/ct.js +72 -0
  19. package/lib/est.js +828 -32
  20. package/lib/framework-error.js +13 -0
  21. package/lib/guard-bytes.js +37 -1
  22. package/lib/guard-range.js +23 -1
  23. package/lib/http-transport.js +9 -3
  24. package/lib/inspect.js +28 -5
  25. package/lib/jose.js +15 -0
  26. package/lib/lint.js +4 -0
  27. package/lib/merkle.js +5 -5
  28. package/lib/ocsp.js +139 -11
  29. package/lib/oid.js +69 -1
  30. package/lib/path-validate.js +438 -100
  31. package/lib/pkcs12-build.js +12 -0
  32. package/lib/schema-all.js +19 -1
  33. package/lib/schema-attrcert.js +27 -0
  34. package/lib/schema-c509.js +6 -0
  35. package/lib/schema-cmc.js +791 -0
  36. package/lib/schema-cmp.js +25 -0
  37. package/lib/schema-cms.js +17 -1
  38. package/lib/schema-crl.js +23 -1
  39. package/lib/schema-crmf.js +13 -0
  40. package/lib/schema-csr.js +11 -0
  41. package/lib/schema-csrattrs.js +6 -0
  42. package/lib/schema-engine.js +6 -2
  43. package/lib/schema-ocsp.js +41 -0
  44. package/lib/schema-pkcs12.js +16 -0
  45. package/lib/schema-pkcs8.js +8 -0
  46. package/lib/schema-pkix.js +6 -0
  47. package/lib/schema-smime.js +4 -4
  48. package/lib/schema-tsp.js +32 -1
  49. package/lib/schema-x509.js +14 -1
  50. package/lib/shbs.js +12 -4
  51. package/lib/sigstore.js +4 -0
  52. package/lib/smime.js +28 -7
  53. package/lib/tls-cert-compress.js +15 -3
  54. package/lib/trust.js +27 -4
  55. package/lib/tsp-sign.js +41 -6
  56. package/lib/vendor/README.md +19 -19
  57. package/lib/webauthn.js +895 -26
  58. package/lib/x509-sign.js +3 -0
  59. package/package.json +1 -1
  60. package/sbom.cdx.json +6 -6
@@ -1107,7 +1107,7 @@ function validateCriticalExtensionStructure(cert) {
1107
1107
  * @primitive pki.path.validate
1108
1108
  * @signature pki.path.validate(path, opts) -> Promise<result>
1109
1109
  * @since 0.1.16
1110
- * @status experimental
1110
+ * @status stable
1111
1111
  * @spec RFC 5280
1112
1112
  * @related pki.schema.x509.parse, pki.path.crlChecker
1113
1113
  *
@@ -1134,6 +1134,10 @@ function validateCriticalExtensionStructure(cert) {
1134
1134
  * throws a typed `PathError`.
1135
1135
  *
1136
1136
  * @example
1137
+ * var pair = await pki.key.generate("Ed25519");
1138
+ * var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
1139
+ * notBefore: new Date("2019-01-01T00:00:00Z"), notAfter: new Date("2029-01-01T00:00:00Z") },
1140
+ * { key: await pki.key.export(pair.privateKey) });
1137
1141
  * var cert = pki.schema.x509.parse(der);
1138
1142
  * var res = await pki.path.validate([cert], {
1139
1143
  * time: new Date("2020-01-01T00:00:00Z"),
@@ -1469,6 +1473,9 @@ function userConstrainedPolicies(state, n) {
1469
1473
 
1470
1474
  var OID_IDP = oid.byName("issuingDistributionPoint");
1471
1475
  var OID_DELTA_CRL = oid.byName("deltaCRLIndicator");
1476
+ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
1477
+ var OID_CRL_NUMBER = oid.byName("cRLNumber");
1478
+ var OID_FRESHEST_CRL = oid.byName("freshestCRL");
1472
1479
 
1473
1480
  // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
1474
1481
  // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
@@ -1489,6 +1496,58 @@ var IDP_SCHEMA = schema.seq([
1489
1496
  ], { minTag: 0, maxTag: 5, unexpectedCode: "path/bad-idp", orderCode: "path/bad-idp" }),
1490
1497
  ], { assert: "sequence", code: "path/bad-idp", what: "IssuingDistributionPoint" });
1491
1498
 
1499
+ // RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
1500
+ // named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
1501
+ // has no ReasonFlags bit at all). 0x1FE is bits 1..8 set.
1502
+ //
1503
+ // NOTE the numbering divergence, which is the trap in this area: a ReasonFlags BIT
1504
+ // is not a CRLReason VALUE. Bits 1..6 coincide with codes 1..6, but bit 7 is
1505
+ // privilegeWithdrawn (code 9) and bit 8 is aACompromise (code 10). sec. 6.3.3 never
1506
+ // needs a mapping between them -- the mask comes only from IDP/DP ReasonFlags and
1507
+ // cert_status only from an entry's CRLReason -- so no bit<->code table exists here,
1508
+ // deliberately. constants.NAMES.REASON_FLAGS and NAMES.CRL_REASON stay separate.
1509
+ var ALL_REASONS = 0x1FE;
1510
+
1511
+ // A ReasonFlags BIT STRING { unusedBits, bytes } -> a bit mask, or null when the
1512
+ // encoding is not minimal DER (X.690 sec. 11.2.2 NamedBitList: trailing zero bits
1513
+ // MUST be dropped). A null tells the caller the CRL's scope is unknown, which is
1514
+ // the same fail-closed posture decodeIdp already takes for a malformed IDP.
1515
+ //
1516
+ // Bits at or above 9 are undefined by sec. 4.2.1.13. They are IGNORED rather than
1517
+ // rejected: an undefined bit can never help reach all-reasons, so ignoring it can
1518
+ // only withhold coverage, never grant it. Reading a bounded prefix also means a
1519
+ // hostile multi-kilobyte ReasonFlags costs nothing.
1520
+ function reasonMaskFromBitString(bs) {
1521
+ if (!bs || !bs.bytes) return null;
1522
+ try { schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (msg) { throw E("path/bad-idp", msg); }); }
1523
+ catch (_e) { return null; }
1524
+ var mask = 0;
1525
+ // From bit 1: sec. 6.3.2(a) defines the legal members as the eight NAMED reasons, and ReasonFlags
1526
+ // bit 0 is -- not a reason at all. Admitting it could not grant coverage (the completeness
1527
+ // test masks with ALL_REASONS) but would make a shard that asserts ONLY bit 0 look like partial
1528
+ // coverage rather than none, which is the wrong verdict to report.
1529
+ for (var bit = 1; bit <= 8; bit++) {
1530
+ var byteI = bit >> 3;
1531
+ if (byteI >= bs.bytes.length) break;
1532
+ if (bs.bytes[byteI] & (0x80 >> (bit & 7))) mask |= (1 << bit);
1533
+ }
1534
+ return mask;
1535
+ }
1536
+
1537
+ // RFC 5280 sec. 6.3.3(d)(1)-(4) -- the interim_reasons_mask, in ONE place.
1538
+ // (1) both present -> intersection
1539
+ // (2) IDP present, DP absent -> the IDP's reasons
1540
+ // (3) IDP absent, DP present -> the DP's reasons
1541
+ // (4) both absent -> all-reasons
1542
+ // A null mask means "present but unreadable"; the caller has already marked such a
1543
+ // CRL unusable, so treating it as 0 here is belt-and-braces, never a coverage grant.
1544
+ function interimReasonMask(idpMask, dpMask) {
1545
+ if (idpMask != null && dpMask != null) return idpMask & dpMask; // (d)(1)
1546
+ if (idpMask != null) return idpMask; // (d)(2)
1547
+ if (dpMask != null) return dpMask; // (d)(3)
1548
+ return ALL_REASONS; // (d)(4)
1549
+ }
1550
+
1492
1551
  function decodeIdp(ext) {
1493
1552
  // Surface the scope flags the checker gates on. ANY structural or value fault
1494
1553
  // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
@@ -1523,6 +1582,15 @@ function decodeIdp(ext) {
1523
1582
  out.onlyUser = flag(m.fields.onlyContainsUserCerts);
1524
1583
  out.onlyCa = flag(m.fields.onlyContainsCACerts);
1525
1584
  out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
1585
+ // The reason BITS, not just their presence: sec. 6.3.3(d)(1)/(d)(2) need the
1586
+ // value. A present-but-non-minimal ReasonFlags leaves the CRL's scope unknown,
1587
+ // which is the same unusable posture every other IDP fault takes -- otherwise
1588
+ // the (d)(1) intersection would be computed over an encoding DER forbids.
1589
+ out.onlySomeReasonsMask = null;
1590
+ if (m.fields.onlySomeReasons.present) {
1591
+ out.onlySomeReasonsMask = reasonMaskFromBitString(m.fields.onlySomeReasons.value);
1592
+ if (out.onlySomeReasonsMask === null) out.malformed = true;
1593
+ }
1526
1594
  out.indirect = flag(m.fields.indirectCRL);
1527
1595
  out.onlyAttr = flag(m.fields.onlyContainsAttributeCerts);
1528
1596
  return out;
@@ -1582,11 +1650,130 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1582
1650
  return false;
1583
1651
  }
1584
1652
 
1653
+ // The raw extnValue octets of a CRL extension, or null. sec. 6.3.3(c)(2)/(c)(3)
1654
+ // compare the IDP and authorityKeyIdentifier of a delta and its base for identity;
1655
+ // the comparison is over the exact encoded octets (the sec. 5.2.5 @3648
1656
+ // "identical encoding" discipline already used for distributionPoint matching),
1657
+ // never over a re-normalized decode.
1658
+ function crlExtValue(theCrl, wantOid) {
1659
+ for (var i = 0; i < theCrl.crlExtensions.length; i++) {
1660
+ if (theCrl.crlExtensions[i].oid === wantOid) return theCrl.crlExtensions[i].value;
1661
+ }
1662
+ return null;
1663
+ }
1664
+
1665
+ // One pass over the bundle: split complete CRLs from deltas and decode, per CRL,
1666
+ // the values the merge preconditions compare. A delta whose BaseCRLNumber will not
1667
+ // decode as a non-negative INTEGER is unusable AS A DELTA but stays consultable for
1668
+ // revocation -- dropping it wholesale would lose a revocation it lists (T1).
1669
+ // RFC 5280 sec. 5.2.3 @3404 bounds a CRL number at 20 octets, and pki.crl.sign enforces that on
1670
+ // emission. A number past it is non-conforming, so it does not earn the merge -- which can RELEASE
1671
+ // a certificate. The value still decodes and the CRL is still consulted for revocation; only the
1672
+ // new capability is withheld. Measured on the ENCODED length, matching the producer-side check
1673
+ // rather than a re-derived numeric bound.
1674
+ function crlNumberWithinBound(n) {
1675
+ if (typeof n !== "bigint" || n < 0n) return false;
1676
+ try { return asn1.decode(asn1.build.integer(n)).content.length <= 20; }
1677
+ catch (_e) { return false; }
1678
+ }
1679
+
1680
+ function classifyCrls(parsed) {
1681
+ var completes = [], deltas = [];
1682
+ for (var i = 0; i < parsed.length; i++) {
1683
+ var theCrl = parsed[i];
1684
+ var deltaRaw = crlExtValue(theCrl, OID_DELTA_CRL);
1685
+ var deltaCritical = false;
1686
+ for (var dz = 0; dz < theCrl.crlExtensions.length; dz++) {
1687
+ if (theCrl.crlExtensions[dz].oid === OID_DELTA_CRL) { deltaCritical = theCrl.crlExtensions[dz].critical === true; break; }
1688
+ }
1689
+ // schema-crl already decoded cRLNumber to a non-negative BigInt; the IDP,
1690
+ // authorityKeyIdentifier and deltaCRLIndicator values stay RAW octets there,
1691
+ // which is exactly what the byte-identical compares below need.
1692
+ var num = crlExtValue(theCrl, OID_CRL_NUMBER);
1693
+ var rec = {
1694
+ crl: theCrl,
1695
+ crlNumber: crlNumberWithinBound(num) ? num : null,
1696
+ idpRaw: crlExtValue(theCrl, OID_IDP),
1697
+ akiRaw: crlExtValue(theCrl, OID_AUTHORITY_KEY_ID),
1698
+ baseCrlNumber: null,
1699
+ mergeable: false,
1700
+ };
1701
+ if (deltaRaw === null) { completes.push(rec); continue; }
1702
+ try {
1703
+ var n = asn1.read.integer(asn1.decode(deltaRaw));
1704
+ // sec. 5.2.3 @3404: a CRL number may be up to 20 octets. It stays a BigInt
1705
+ // through every comparison below -- narrowing through Number would collapse
1706
+ // large values and make 5.2.4(c)/(d) compare the wrong things.
1707
+ // sec. 5.2.4 @3447 makes deltaCRLIndicator a MUST-be-critical extension. A non-critical one
1708
+ // is non-conforming, so it does not earn the NEW capability of being merged -- merging can
1709
+ // RELEASE a certificate, and that must rest on a conforming indicator. It is still classified
1710
+ // as a delta and still consulted for revocation, which is the shipped behaviour and the
1711
+ // conservative direction.
1712
+ if (crlNumberWithinBound(n) && deltaCritical) { rec.baseCrlNumber = n; rec.mergeable = true; }
1713
+ } catch (_e) {
1714
+ // Unusable AS A DELTA, but still scanned for revocation. That is the conservative direction:
1715
+ // the CRL is still the issuer's, signed and current (it must pass every gate before it is
1716
+ // consulted), so a serial it lists is a genuine revocation and dropping it wholesale could
1717
+ // lose one. Consulting it can only ever find MORE revocations, never grant coverage -- a
1718
+ // delta contributes no reason mask.
1719
+ }
1720
+ deltas.push(rec);
1721
+ }
1722
+ return { completes: completes, deltas: deltas };
1723
+ }
1724
+
1725
+ // RFC 5280 sec. 6.3.3(c)(1)-(3) + sec. 5.2.4(a)-(d), as one predicate. Every
1726
+ // comparison is BigInt or byte-exact: no narrowing, no canonicalization. An
1727
+ // extension ABSENT on both sides matches; present on one side only does not.
1728
+ function deltaMergesWith(delta, complete) {
1729
+ if (!delta.mergeable) return false;
1730
+ // Both numbers are required: 5.2.4(c)/(d) are ordering rules, and a missing
1731
+ // cRLNumber on either side leaves the ordering unknowable -- fail closed.
1732
+ if (complete.crlNumber === null || delta.crlNumber === null) return false;
1733
+ if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false; // (c)(1)
1734
+ if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false; // (c)(2) / 5.2.4(b)
1735
+ if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false; // (c)(3)
1736
+ if (!(complete.crlNumber >= delta.baseCrlNumber)) return false; // 5.2.4(c)
1737
+ if (!(complete.crlNumber < delta.crlNumber)) return false; // 5.2.4(d)
1738
+ return true;
1739
+ }
1740
+ // dnEqual THROWS on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
1741
+ // For the merge preconditions a DN that cannot be compared is simply not a match:
1742
+ // the delta and the base are not shown to share an issuer, so they are not merged.
1743
+ // The throw is contained here rather than at the call site so the predicate itself
1744
+ // stays a straight-line sequence of comparisons.
1745
+ function dnEqualUsable(a, b) {
1746
+ try { return dnEqual(a, b); }
1747
+ catch (_e) { return false; }
1748
+ }
1749
+
1750
+ function sameRawExt(a, b) {
1751
+ if (a === null && b === null) return true;
1752
+ if (a === null || b === null) return false;
1753
+ return a.equals(b);
1754
+ }
1755
+
1756
+ // sec. 5.2.4 @3580 makes MULTIPLE current deltas for one scope legal and says the
1757
+ // application SHOULD take the one with the latest thisUpdate. Selection, never
1758
+ // rejection: treating two current deltas as a fault would refuse a conforming
1759
+ // publication. Ties break on the greater cRLNumber, then first-wins.
1760
+ function selectDelta(candidates) {
1761
+ var best = null;
1762
+ for (var i = 0; i < candidates.length; i++) {
1763
+ var c = candidates[i];
1764
+ if (best === null) { best = c; continue; }
1765
+ var t = c.crl.thisUpdate.getTime(), bt = best.crl.thisUpdate.getTime();
1766
+ if (t > bt) { best = c; continue; }
1767
+ if (t === bt && c.crlNumber !== null && best.crlNumber !== null && c.crlNumber > best.crlNumber) best = c;
1768
+ }
1769
+ return best;
1770
+ }
1771
+
1585
1772
  /**
1586
1773
  * @primitive pki.path.crlChecker
1587
- * @signature pki.path.crlChecker(crls) -> RevocationChecker
1774
+ * @signature pki.path.crlChecker(crls, opts?) -> RevocationChecker
1588
1775
  * @since 0.1.16
1589
- * @status experimental
1776
+ * @status stable
1590
1777
  * @spec RFC 5280
1591
1778
  * @related pki.path.validate, pki.schema.crl.parse
1592
1779
  *
@@ -1599,17 +1786,38 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1599
1786
  * "unknown" }`. A partitioned/sharded CRL (a critical IDP naming a
1600
1787
  * distribution point) establishes "good" when it corresponds to one of the
1601
1788
  * 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.
1789
+ * name in common (RFC 5280 sec. 6.3.3). Reason-sharded CRLs ACCUMULATE: each
1790
+ * corresponding CRL contributes its interim reason mask (sec. 6.3.3(d)) and the
1791
+ * certificate is "good" once the shards together cover all eight revocation
1792
+ * reasons, so a CA that partitions by reason code is served. A DELTA CRL is
1793
+ * merged onto a complete CRL it may be combined with (sec. 5.2.4 / 6.3.3(c)):
1794
+ * the delta is searched first, the complete CRL only if the delta left the
1795
+ * status unrevoked, and `removeFromCRL` then releases the certificate -- so a
1796
+ * base+delta pair reports a real verdict where the base alone could not. A
1797
+ * delta that merges with nothing is still consulted for revocation and still
1798
+ * blocks "good": merging may turn undetermined into good or revoked, never a
1799
+ * revoked into a good. A non-corresponding shard is consulted for revocation
1800
+ * only. An out-of-scope, stale, unauthorized, or unverifiable CRL yields
1801
+ * `unknown`, which the validator fails closed unless `softFail` is set.
1802
+ *
1803
+ * A `revoked` verdict carries `reasonCode` (the CRLReason integer, 0 for
1804
+ * `unspecified`) and a `reason` naming it.
1805
+ *
1806
+ * @opts
1807
+ * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
1808
+ * Default true. When false a delta is never merged; it is
1809
+ * still consulted for revocation.
1606
1810
  *
1607
1811
  * @example
1608
1812
  * var checker = pki.path.crlChecker([]); // no CRLs -> every cert is "unknown"
1609
1813
  * typeof checker.check; // "function"
1610
1814
  */
1611
- function crlChecker(crls) {
1815
+ function crlChecker(crls, opts) {
1612
1816
  var parsed = (crls || []).map(function (c) { return (c && c.tbsBytes) ? c : crl.parse(c); });
1817
+ // RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
1818
+ // a caller holding a delta wants it used -- and turning it off never makes a
1819
+ // verdict weaker, only less determined.
1820
+ var useDeltas = !(opts && opts.useDeltas === false);
1613
1821
  return {
1614
1822
  check: async function (cert, issuer, ctx) {
1615
1823
  var time = ctx.time;
@@ -1660,42 +1868,58 @@ function crlChecker(crls) {
1660
1868
  }
1661
1869
 
1662
1870
  // 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
1871
+ // shadow a revoking one (RFC 5280 sec. 6.3.3). A serial listed in ANY
1664
1872
  // 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];
1873
+ // "good" only when the CRLs consulted together cover ALL EIGHT revocation
1874
+ // reasons (sec. 6.3.3(l) + the termination rule at @5291) and none list it.
1875
+ //
1876
+ // Note there is deliberately no early exit once the mask is complete: the
1877
+ // shipped checker scans every CRL so a clean one cannot shadow a revoking
1878
+ // one, and stopping at full coverage would reintroduce exactly that.
1879
+ var certStatus = null; // sec. 6.3.2(b) cert_status; null = UNREVOKED
1880
+ var reasonsMask = 0; // sec. 6.3.2(a) reasons_mask; the empty set
1881
+ var releasedByUnmergedDelta = false;
1882
+ // A CURRENT, authoritative delta that merged with nothing means the local
1883
+ // revocation picture is incomplete: the base it names may be newer than any
1884
+ // complete CRL held here, so a clean base proves less than it appears to.
1885
+ // The shipped checker blocks on exactly this, and the merge must only
1886
+ // ever turn undetermined INTO good -- never make an unmerged delta weaker
1887
+ // than it was before this feature existed.
1888
+ var sawUnmergedDelta = false;
1889
+ var classified = classifyCrls(parsed);
1890
+ var consumedDeltas = [];
1891
+
1892
+ // Every gate a CRL must pass before it may speak for this certificate, in
1893
+ // the shipped order. Returns null when the CRL is unusable, otherwise the
1894
+ // interim_reasons_mask it may contribute -- 0 meaning "consulted for
1895
+ // revocation only", which is how a non-corresponding shard is honestly
1896
+ // encoded (it can reveal a revocation but can never establish coverage).
1897
+ async function gateCrl(rec) {
1898
+ // Memoized per RECORD, and the records are rebuilt by classifyCrls on every check(), so the
1899
+ // cache is per-call and the checker stays re-entrant. Without it the base-by-delta pairing
1900
+ // below would repeat an asynchronous public-key signature verification for every pair --
1901
+ // O(completes x deltas) verifications where the shipped loop did O(crls).
1902
+ if (rec._gated) return rec._gate;
1903
+ rec._gate = await gateCrlUncached(rec);
1904
+ rec._gated = true;
1905
+ return rec._gate;
1906
+ }
1907
+ async function gateCrlUncached(rec) {
1908
+ var theCrl = rec.crl;
1673
1909
  // dnEqual throws on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
1674
1910
  // A single malformed CRL in the bundle must NOT abort the whole check (which
1675
1911
  // would mask a later authoritative CRL and pass under softFail) -- treat it
1676
1912
  // as unusable and skip it, consulting the remaining CRLs.
1677
1913
  var issuerMatches;
1678
1914
  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
- }
1915
+ catch (_e) { return null; }
1916
+ if (!issuerMatches) return null;
1917
+ if (!signerAuthorized) return null;
1694
1918
 
1695
1919
  // A validly-signed CRL carrying a CRITICAL extension this checker does
1696
1920
  // not understand (anything but issuingDistributionPoint / deltaCRLIndicator)
1697
1921
  // may change the CRL's scope or meaning -- treat it as unusable (RFC 5280
1698
- // 5.2 critical-extension semantics), never authoritative.
1922
+ // sec. 5.2 critical-extension semantics), never authoritative.
1699
1923
  var unhandledCritical = false;
1700
1924
  for (var x = 0; x < theCrl.crlExtensions.length; x++) {
1701
1925
  var xe = theCrl.crlExtensions[x];
@@ -1715,28 +1939,38 @@ function crlChecker(crls) {
1715
1939
  if (ees[ex].critical && ees[ex].oid !== OID_REASON_CODE) { unhandledCritical = true; break; }
1716
1940
  }
1717
1941
  }
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
1942
+ if (unhandledCritical) return null;
1943
+
1944
+ // sec. 6.3.3(d): the interim reason mask this CRL contributes. Its two
1945
+ // inputs are the IDP's onlySomeReasons and the CORRESPONDING certificate
1946
+ // DistributionPoint's reasons -- never an entry's reasonCode (sec. 5.2.5
1947
+ // @3628 explicitly permits a shard entry to omit one).
1948
+ // Set when the shard covers no reasons for this certificate. It is recorded rather than
1949
+ // RETURNED here: the currency and signature gates below still have to run, or an expired,
1950
+ // not-yet-valid or FORGED shard could be scanned for revocations and falsely revoke.
1951
+ var noCoverage = false;
1952
+ var idpMask = null, dpMask = null, sawIdp = false;
1953
+ var idpExtension = null;
1954
+ for (var e = 0; e < theCrl.crlExtensions.length; e++) if (theCrl.crlExtensions[e].oid === OID_IDP) idpExtension = theCrl.crlExtensions[e];
1955
+ if (idpExtension) {
1956
+ sawIdp = true;
1957
+ var idp = decodeIdp(idpExtension);
1958
+ if (idp.malformed) return null; // scope unknown -> unusable
1732
1959
  // An indirect CRL carries entries for other issuers keyed by the
1733
1960
  // per-entry certificateIssuer attribute (not tracked here) -- matching
1734
1961
  // by serial alone could revoke the wrong cert or falsely cover it, so
1735
1962
  // 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;
1963
+ if (idp.indirect) return null;
1964
+ if (idp.onlyAttr) return null; // scoped to attribute certs, not this public-key cert
1965
+ if (idp.onlyCa && certIsCa !== true) return null; // out of scope (or CA-ness undeterminable)
1966
+ if (idp.onlyUser && certIsCa !== false) return null;
1967
+ // The same fail-closed decision the distributionPoint correspondence rests on (sec. 5.2.5
1968
+ // @3601 lets a relying party not support the IDP at all): a scope a non-supporting
1969
+ // verifier would IGNORE is not a scope to build coverage on. So a non-critical IDP's
1970
+ // onlySomeReasons contributes nothing -- which also preserves the shipped property that
1971
+ // an onlySomeReasons shard could only ever WITHHOLD good, never establish it.
1972
+ if (idpExtension.critical === true) idpMask = idp.onlySomeReasonsMask;
1973
+ else if (idp.onlySomeReasons) noCoverage = true;
1740
1974
  if (idp.hasDistributionPoint) {
1741
1975
  // RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
1742
1976
  // certificate only when the IDP's distribution point shares at
@@ -1744,50 +1978,50 @@ function crlChecker(crls) {
1744
1978
  // own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
1745
1979
  // be used in the distributionPoint fields of the certificate and
1746
1980
  // 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
1981
+ // scope: sec. 5.2.5 describes the IDP as "a critical CRL extension"
1982
+ // (descriptive phrasing, not an imperative MUST -- and @3601 lets a
1983
+ // relying party not support it at all), so building coverage on a
1984
+ // scope a non-supporting verifier would ignore is a deliberate
1985
+ // fail-closed decision. A non-corresponding shard contributes NO
1986
+ // coverage but is still consulted for revocation below: serials are
1987
+ // unique per issuer, so a listed serial is a genuine revocation.
1988
+ var matchedDp = idpExtension.critical === true
1756
1989
  ? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
1757
1990
  : 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;
1991
+ if (!matchedDp) noCoverage = true;
1992
+ // sec. 6.3.3(d)(1)/(d)(3): a matched DP carrying `reasons` bounds the
1993
+ // interim mask. A present-but-unreadable value cannot bound anything
1994
+ // safely, so it contributes nothing rather than defaulting open.
1995
+ else if (matchedDp.reasons) {
1996
+ dpMask = reasonMaskFromBitString(matchedDp.reasons);
1997
+ if (dpMask === null) noCoverage = true;
1998
+ }
1764
1999
  }
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
2000
  }
1769
- if (theCrl.thisUpdate > time) continue; // not yet valid
2001
+ if (theCrl.thisUpdate > time) return null; // not yet valid
1770
2002
  // A CRL with no nextUpdate has no bounded validity -- its currency
1771
2003
  // cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
1772
2004
  // replayed old CRL must not read "good". Treat it as unusable.
1773
- if (!theCrl.nextUpdate || theCrl.nextUpdate < time) continue; // stale / no bound
2005
+ if (!theCrl.nextUpdate || theCrl.nextUpdate < time) return null; // stale / no bound
1774
2006
 
1775
2007
  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; }
2008
+ if (!sigOk) return null; // unverifiable -> not authoritative
2009
+
2010
+ // sec. 6.3.3 @5295: a CRL not named by any distribution point is processed
2011
+ // as though under a DP whose `reasons` and `cRLIssuer` are absent -- i.e.
2012
+ // all-reasons. That is why a full-scope CRL still covers a certificate
2013
+ // that happens to carry a cRLDistributionPoints extension.
2014
+ void sawIdp;
2015
+ return { interim: noCoverage ? 0 : interimReasonMask(idpMask, dpMask) };
2016
+ }
1782
2017
 
2018
+ // sec. 6.3.3(i)/(j): the certificate's entry on one CRL, as a CRLReason
2019
+ // value, or null for UNREVOKED. sec. 6.3.3(i)(2): an entry with no
2020
+ // reasonCode extension is `unspecified` (0), which is a revocation.
2021
+ function scanCrl(theCrl) {
1783
2022
  for (var r = 0; r < theCrl.revokedCertificates.length; r++) {
1784
2023
  var entry = theCrl.revokedCertificates[r];
1785
2024
  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
2025
  // A revocation is effective as of its revocationDate (RFC 5280 sec. 5.3).
1792
2026
  // In the DEFAULT present-time validation a listed serial is revoked
1793
2027
  // regardless of that date -- a future revocationDate is post-dating or
@@ -1797,28 +2031,113 @@ function crlChecker(crls) {
1797
2031
  // validation time not yet apply.
1798
2032
  // 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
2033
  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;
2034
+ var rc = crlEntryReason(entry);
2035
+ return rc === null ? 0 : rc;
2036
+ }
2037
+ return null;
2038
+ }
2039
+
2040
+ // sec. 6.3.3(a)(2): a delta is obtained only when use-deltas is set AND a
2041
+ // locator exists -- freshestCRL on the certificate or on the complete CRL.
2042
+ // sec. 5.2.6 @3719: the locator's CONTENTS are only ever used to find a
2043
+ // delta, never to validate one, so its presence is the whole gate.
2044
+ // PRESENCE only, deliberately. sec. 5.2.6 @3719 says the freshestCRL contents are used to
2045
+ // LOCATE a delta and never to validate one, so decoding them would be using a value the RFC
2046
+ // says not to use. It is also not a security control: enabling the merge grants nothing on its
2047
+ // own, because a delta still has to pass every gate -- issuer, authorization, criticality,
2048
+ // scope, currency and SIGNATURE -- plus the sec. 5.2.4 merge preconditions. An attacker who
2049
+ // could satisfy those already controls the issuing key.
2050
+ var certHasFreshest = !!findExt(cert, OID_FRESHEST_CRL);
2051
+ function deltaLocatorPresent(completeRec) {
2052
+ return certHasFreshest || crlExtValue(completeRec.crl, OID_FRESHEST_CRL) !== null;
2053
+ }
2054
+
2055
+ for (var ci = 0; ci < classified.completes.length; ci++) {
2056
+ var rec = classified.completes[ci];
2057
+ var gate = await gateCrl(rec);
2058
+ if (!gate) continue;
2059
+
2060
+ // sec. 6.3.3(c): pair this complete CRL with a delta it may be combined
2061
+ // with. The delta passes its OWN gates first -- sec. 6.3.3(h) requires
2062
+ // its signature verified and (f) its issuer authorized, exactly as for a
2063
+ // complete CRL -- so a stale, unverifiable or out-of-scope delta is never
2064
+ // merged.
2065
+ var chosenDelta = null;
2066
+ if (useDeltas && deltaLocatorPresent(rec)) {
2067
+ var candidates = [];
2068
+ for (var di = 0; di < classified.deltas.length; di++) {
2069
+ var cand = classified.deltas[di];
2070
+ if (!deltaMergesWith(cand, rec)) continue;
2071
+ if (!(await gateCrl(cand))) continue;
2072
+ // Mergeable and usable: this delta's scope IS covered by a merge, even
2073
+ // if sec. 5.2.4 @3580 selection prefers a sibling with a later
2074
+ // thisUpdate. Only a delta that pairs with NO complete CRL leaves the
2075
+ // picture incomplete -- a losing candidate must not block a good result,
2076
+ // or publishing two current deltas (which the RFC permits) would be
2077
+ // worse than publishing one.
2078
+ cand.accounted = true;
2079
+ candidates.push(cand);
2080
+ }
2081
+ chosenDelta = selectDelta(candidates);
1804
2082
  }
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
2083
+
2084
+ var status;
2085
+ if (chosenDelta) {
2086
+ consumedDeltas.push(chosenDelta);
2087
+ status = scanCrl(chosenDelta.crl); // (i) search the delta FIRST
2088
+ if (status === null) status = scanCrl(rec.crl); // (j) the complete CRL only if still UNREVOKED
2089
+ } else {
2090
+ status = scanCrl(rec.crl);
2091
+ }
2092
+ // (k): removeFromCRL means the certificate is no longer revoked. It is
2093
+ // normalized wherever it appears -- sec. 5.3.1's "only in delta CRLs" binds
2094
+ // the CA that emits it, not this consumer, and rejecting a complete CRL
2095
+ // that carries one would make an unusual-but-harmless CRL unusable.
2096
+ if (status === 8) status = null;
2097
+
2098
+ if (status !== null && certStatus === null) certStatus = status;
2099
+ else if (status === null) reasonsMask |= gate.interim; // (l): only a clean scope covers
2100
+ }
2101
+
2102
+ // An unmerged delta still speaks, under the shipped fail-closed posture: a
2103
+ // serial it lists is a genuine revocation (serials are unique per issuer),
2104
+ // and a removeFromCRL it carries blocks a definitive revoked without being
2105
+ // able to establish good. This is what keeps the merge MONOTONIC -- it may
2106
+ // turn undetermined into good or revoked, but a delta that merges with
2107
+ // nothing can never erase a revocation the checker would otherwise report.
2108
+ for (var dj = 0; dj < classified.deltas.length; dj++) {
2109
+ var dRec = classified.deltas[dj];
2110
+ if (consumedDeltas.indexOf(dRec) !== -1) continue;
2111
+ // A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
2112
+ // does not speak for its scope at all -- the selected delta does, and it was evaluated
2113
+ // against the base. Letting a superseded delta contribute its revocation while ignoring its
2114
+ // release would be incoherent: an older delta revoking a certificate that the newer one
2115
+ // releases would resurrect the revocation the CA withdrew.
2116
+ if (dRec.accounted) continue;
2117
+ if (!(await gateCrl(dRec))) continue;
2118
+ sawUnmergedDelta = true;
2119
+ var dStatus = scanCrl(dRec.crl);
2120
+ if (dStatus === null) continue;
2121
+ if (dStatus === 8) { releasedByUnmergedDelta = true; continue; }
2122
+ if (certStatus === null) certStatus = dStatus;
1809
2123
  }
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
2124
+
2125
+ // A delta released this serial from hold but its base was not merged: a
2126
+ // definitive revoked would leave a released certificate rejected, so the
1812
2127
  // 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" };
2128
+ if (releasedByUnmergedDelta) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
2129
+ if (certStatus !== null) {
2130
+ var reasonName = constants.NAMES.CRL_REASON[String(certStatus)] || "unspecified";
2131
+ return { status: "revoked", reasonCode: certStatus, reason: "serial listed in a CRL (" + reasonName + ")" };
2132
+ }
2133
+ if (sawUnmergedDelta) return { status: "unknown", reason: "a delta CRL cannot be combined with any complete CRL held here, so the revocation picture is incomplete" };
2134
+ if ((reasonsMask & ALL_REASONS) === ALL_REASONS) return { status: "good" };
1819
2135
  if (certScopeFault) {
1820
2136
  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
2137
  }
2138
+ if (reasonsMask !== 0) {
2139
+ return { status: "unknown", reason: "the CRLs available cover only some revocation reasons for this certificate; no combination covers all of them" };
2140
+ }
1822
2141
  return { status: "unknown", reason: "no authoritative in-scope CRL covers this certificate" };
1823
2142
  },
1824
2143
  };
@@ -1897,7 +2216,7 @@ cmpSession.setEngine({ build: build, validate: validate, toAnchor: toAnchor, coe
1897
2216
  * @primitive pki.path.ocspChecker
1898
2217
  * @signature pki.path.ocspChecker(responses) -> RevocationChecker
1899
2218
  * @since 0.1.32
1900
- * @status experimental
2219
+ * @status stable
1901
2220
  * @spec RFC 6960
1902
2221
  * @related pki.path.validate, pki.schema.ocsp.parseResponse, pki.path.crlChecker
1903
2222
  *
@@ -1976,7 +2295,7 @@ function ocspChecker(responses) {
1976
2295
  * @primitive pki.path.verifyOcspResponse
1977
2296
  * @signature pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
1978
2297
  * @since 0.2.22
1979
- * @status experimental
2298
+ * @status stable
1980
2299
  * @spec RFC 6960
1981
2300
  * @related pki.ocsp.verify, pki.path.ocspChecker
1982
2301
  *
@@ -2003,6 +2322,20 @@ function ocspChecker(responses) {
2003
2322
  * fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
2004
2323
  *
2005
2324
  * @example
2325
+ * var ca = await pki.key.generate("Ed25519");
2326
+ * var caKey = await pki.key.export(ca.privateKey);
2327
+ * var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
2328
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
2329
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } },
2330
+ * { key: caKey });
2331
+ * var leaf = await pki.key.generate("Ed25519");
2332
+ * var leafDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
2333
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
2334
+ * { cert: caDer, key: caKey });
2335
+ * var der = await pki.ocsp.sign(
2336
+ * { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
2337
+ * { cert: caDer, key: caKey });
2338
+ * var cert = pki.schema.x509.parse(leafDer), issuerCert = pki.schema.x509.parse(caDer);
2006
2339
  * var resp = pki.schema.ocsp.parseResponse(der);
2007
2340
  * var v = await pki.path.verifyOcspResponse(resp, cert, issuerCert, new Date());
2008
2341
  * v.status; // "good" | "revoked" | "unknown"
@@ -2359,6 +2692,11 @@ async function _fetchAiaIssuers(current, aia) {
2359
2692
  * @opts maxResponseBytes Per-fetch response size cap, forwarded to the transport (tightenable downward only).
2360
2693
  * @opts (validate options) Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged.
2361
2694
  * @example
2695
+ * var pair = await pki.key.generate("Ed25519");
2696
+ * var pemString = await pki.x509.sign({ subject: "Example Root", subjectPublicKey: await pki.key.export(pair.publicKey),
2697
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
2698
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } },
2699
+ * { key: await pki.key.export(pair.privateKey) }, { pem: true });
2362
2700
  * var result = await pki.path.build(pemString, {
2363
2701
  * candidates: [], // untrusted intermediates (the openssl -untrusted set)
2364
2702
  * trustAnchors: [pemString], // a self-signed root, or a { name, publicKey, algorithm } tuple