@blamejs/pki 0.8.24 → 0.8.26

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/lib/lint.js CHANGED
@@ -36,6 +36,9 @@ var x509 = require("./schema-x509");
36
36
  var crlSchema = require("./schema-crl");
37
37
  var csrSchema = require("./schema-csr");
38
38
  var ocspSchema = require("./schema-ocsp");
39
+ var cmsSchema = require("./schema-cms");
40
+ var tspSchema = require("./schema-tsp");
41
+ var acSchema = require("./schema-attrcert");
39
42
  var pkix = require("./schema-pkix");
40
43
  var pkiBuild = require("./pki-build");
41
44
  var schema = require("./schema-engine");
@@ -2527,6 +2530,219 @@ var MICROSOFT_RULES = [
2527
2530
  },
2528
2531
  ];
2529
2532
 
2533
+ /** @internal The seven Trust Purposes appendix A defines, each as the extended key usage set it
2534
+ * requires. Dedication is a SUBSET test rather than equality, which the appendix states twice: a
2535
+ * subordinate CA "MUST contain only the EKU(s) specified for that Trust Purpose", and sec. 2.1.3
2536
+ * repeats it. So {id-kp-serverAuth} is dedicated under Server Authentication and under Legacy TLS
2537
+ * alike, and a set spanning two purposes is under neither. */
2538
+ var APPLE_TRUST_PURPOSES = [
2539
+ ["serverAuth"],
2540
+ ["serverAuth", "clientAuth"],
2541
+ ["emailProtection"],
2542
+ ["emailProtection", "clientAuth"],
2543
+ ["clientAuth"],
2544
+ ["timeStamping"],
2545
+ ["brandIndicatorforMessageIdentification"],
2546
+ ].map(function (ekus) {
2547
+ return { ekus: ekus.map(function (n) { return oid.byName(n); }) };
2548
+ });
2549
+ /** @internal Every identifier the table names, across all seven purposes. */
2550
+ var APPLE_PURPOSE_EKUS = [];
2551
+ APPLE_TRUST_PURPOSES.forEach(function (p) {
2552
+ p.ekus.forEach(function (d) {
2553
+ if (APPLE_PURPOSE_EKUS.indexOf(d) === -1) APPLE_PURPOSE_EKUS.push(d);
2554
+ });
2555
+ });
2556
+ /** @internal Sec. 1.7 and the change log: an extended key usage extension and the prohibition on
2557
+ * anyExtendedKeyUsage from this date, dedication to a single Trust Purpose from the later one.
2558
+ * Sec. 2's note fixes the comparison at "00:00:00 UTC" and sec. 1.7 defines the signing date as
2559
+ * the date of issuance, including for a renewal, a re-key or a cross-sign. */
2560
+ var APPLE_EKU_FROM = new Date("2026-08-01T00:00:00Z");
2561
+ var APPLE_SINGLE_PURPOSE_FROM = new Date("2027-07-01T00:00:00Z");
2562
+ var APPLE_SMIME_SAN_FROM = new Date("2027-02-01T00:00:00Z");
2563
+ var APPLE_ROOT_RSA_MIN_BITS = 4096;
2564
+ /** @internal "a minimum key size of ... ECDSA 384-bit", so P-384 and above. */
2565
+ var APPLE_ROOT_CURVES = ["secp384r1", "secp521r1"];
2566
+
2567
+ function _isApple(cert, ctx) { return ctx.profile === "apple-root"; }
2568
+ function _isAppleRoot(cert, ctx) {
2569
+ return _isApple(cert, ctx) && ctx.isCaCert && _sameName(cert.issuer, cert.subject) &&
2570
+ _selfSignedIdentifiers(cert, ctx);
2571
+ }
2572
+ function _isAppleSubordinate(cert, ctx) {
2573
+ return _isApple(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
2574
+ }
2575
+ function _isAppleSubscriber(cert, ctx) { return _isApple(cert, ctx) && !ctx.isCaCert; }
2576
+ function _applePurposes(ctx) {
2577
+ var d = ctx.decode("extKeyUsage");
2578
+ return (d && Array.isArray(d.value)) ? d.value : null;
2579
+ }
2580
+ /** @internal Whether an extended key usage set is dedicated to one Trust Purpose: non-empty, and
2581
+ * contained in at least one purpose's required set. WHICH purpose is not returned, and cannot
2582
+ * honestly be: {id-kp-clientAuth} sits inside Client Authentication, Legacy TLS and Legacy S/MIME
2583
+ * alike, which is why the appendix has to say in prose that such a certificate belongs under a
2584
+ * Client Authentication CP/CPS. No finding names a purpose, so the rows ask only the question the
2585
+ * bytes answer. The subset form is the document's, and under
2586
+ * the CURRENT table it decides nothing an equality test would not: every proper non-empty subset
2587
+ * of the two purposes with two identifiers is itself another purpose's whole set. No vector
2588
+ * separates the two readings, and that is a fact about the table rather than about the rule, so a
2589
+ * purpose added later is what the subset form is written for. */
2590
+ function _appleIsDedicated(purposes) {
2591
+ if (!purposes || !purposes.length) return false;
2592
+ for (var i = 0; i < APPLE_TRUST_PURPOSES.length; i++) {
2593
+ var p = APPLE_TRUST_PURPOSES[i];
2594
+ var within = true;
2595
+ for (var j = 0; j < purposes.length; j++) {
2596
+ if (p.ekus.indexOf(purposes[j]) === -1) { within = false; break; }
2597
+ }
2598
+ if (within) return true;
2599
+ }
2600
+ return false;
2601
+ }
2602
+ /** @internal Whether the set is under a Trust Purpose at all, which is what separates a certificate
2603
+ * the scoping reaches from one it does not. anyExtendedKeyUsage is not in the appendix's table and
2604
+ * is still in scope: RFC 5280 sec. 4.2.1.12 makes it a certificate "not restricted to any specific
2605
+ * key purpose", so it names EVERY purpose rather than none, and it conforms to no single one. That
2606
+ * is what separates it from id-kp-codeSigning, which names a use the table does not list and
2607
+ * leaves the certificate under no purpose. */
2608
+ function _appleNamesAPurpose(purposes) {
2609
+ if (!purposes) return false;
2610
+ if (purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1) return true;
2611
+ for (var i = 0; i < purposes.length; i++) {
2612
+ if (APPLE_PURPOSE_EKUS.indexOf(purposes[i]) !== -1) return true;
2613
+ }
2614
+ return false;
2615
+ }
2616
+ function _appleSignedBefore(cert, when) {
2617
+ var nb = cert.validity && cert.validity.notBefore;
2618
+ if (!guard.time.isDate(nb)) return false;
2619
+ return guard.time.instantOf(nb) < guard.time.instantOf(when);
2620
+ }
2621
+ function _appleEkuLabels(purposes) {
2622
+ return purposes.map(function (d) { return oid.name(d) || d; });
2623
+ }
2624
+
2625
+ /** @internal The Apple Root Program Policy v2.0 profile. */
2626
+ var APPLE_RULES = [
2627
+ {
2628
+ id: "lint/apple-root/root-key-too-small", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.5",
2629
+ /** @internal "Root Inclusion Requests MUST only contain Root CA Certificates that have a
2630
+ * minimum key size of RSA 4096-bit or ECDSA 384-bit." The clause names two families and no
2631
+ * others, so a key outside both is outside it. */
2632
+ message: "a root certificate needs an RSA modulus of at least 4096 bits or a curve of at least NIST P-384",
2633
+ appliesTo: _isAppleRoot,
2634
+ check: function (cert) {
2635
+ var spki = cert.subjectPublicKeyInfo;
2636
+ var name = spki && spki.algorithm && spki.algorithm.name;
2637
+ if (name && RSA_KEY_ALGS[name]) {
2638
+ var bits = _rsaModulusBits(spki);
2639
+ if (bits === null) return { context: { rsaBits: null } };
2640
+ return bits < APPLE_ROOT_RSA_MIN_BITS ? { context: { rsaBits: bits } } : null;
2641
+ }
2642
+ if (name === "ecPublicKey") {
2643
+ var curve = _ecCurveName(spki);
2644
+ return APPLE_ROOT_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
2645
+ }
2646
+ return { context: { algorithm: name || null } };
2647
+ },
2648
+ },
2649
+ {
2650
+ id: "lint/apple-root/subordinate-ca-eku-missing", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2651
+ message: "a subordinate CA certificate must carry an extKeyUsage extension",
2652
+ appliesTo: _isAppleSubordinate,
2653
+ effectiveDate: APPLE_EKU_FROM,
2654
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
2655
+ },
2656
+ {
2657
+ id: "lint/apple-root/subordinate-ca-eku-any", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2658
+ message: "a subordinate CA certificate must not assert anyExtendedKeyUsage",
2659
+ appliesTo: _isAppleSubordinate,
2660
+ effectiveDate: APPLE_EKU_FROM,
2661
+ check: function (cert, ctx) {
2662
+ var purposes = _applePurposes(ctx);
2663
+ if (purposes === null) return null;
2664
+ return purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
2665
+ },
2666
+ },
2667
+ {
2668
+ id: "lint/apple-root/subordinate-ca-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2669
+ /** @internal In this window the clause admits (a) dedication to a single Trust Purpose, or (b)
2670
+ * three conditions together, of which one is in the bytes: "it contains no Extended Key Usage
2671
+ * associated with a Trust Purpose defined in Appendix A". A certificate naming one of those
2672
+ * identifiers has failed (b) already, and if its set is not within a single purpose it has
2673
+ * failed (a) as well, so both arms fail on the bytes alone. A certificate naming none of them
2674
+ * is passed over here, because (b)'s other two conditions are the issuing root's configuration
2675
+ * and a CP/CPS, and the row below picks it up once (b) is gone. */
2676
+ message: "a subordinate CA certificate names extended key usages spanning more than one trust purpose",
2677
+ appliesTo: _isAppleSubordinate,
2678
+ effectiveDate: APPLE_EKU_FROM,
2679
+ check: function (cert, ctx) {
2680
+ if (!_appleSignedBefore(cert, APPLE_SINGLE_PURPOSE_FROM)) return null;
2681
+ var purposes = _applePurposes(ctx);
2682
+ if (!_appleNamesAPurpose(purposes)) return null;
2683
+ return _appleIsDedicated(purposes)
2684
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2685
+ },
2686
+ },
2687
+ {
2688
+ id: "lint/apple-root/subordinate-ca-not-single-purpose", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2689
+ /** @internal "A Subordinate CA Certificate signed on or after 2027-07-01 MUST be dedicated to a
2690
+ * single Trust Purpose." Arm (b) is gone, so the set must be within one purpose whatever it
2691
+ * names, and the certificate the row above passes over is reported here. Two rows rather than
2692
+ * one, because the two windows differ in what fixes them. */
2693
+ message: "a subordinate CA certificate must be dedicated to a single trust purpose",
2694
+ appliesTo: _isAppleSubordinate,
2695
+ effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
2696
+ check: function (cert, ctx) {
2697
+ var purposes = _applePurposes(ctx);
2698
+ if (purposes === null) return null;
2699
+ return _appleIsDedicated(purposes)
2700
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2701
+ },
2702
+ },
2703
+ {
2704
+ id: "lint/apple-root/smime-subscriber-without-rfc822-name", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 2.3",
2705
+ /** @internal "Effective 2027-02-01, all newly signed Subscriber certificates that contain the
2706
+ * id-kp-emailProtection EKU MUST include at least one rfc822Name in the subjectAltName
2707
+ * extension." */
2708
+ message: "a subscriber certificate carrying id-kp-emailProtection must name at least one rfc822Name in its subjectAltName",
2709
+ appliesTo: function (cert, ctx) {
2710
+ var purposes = _applePurposes(ctx);
2711
+ return _isAppleSubscriber(cert, ctx) && !!purposes &&
2712
+ purposes.indexOf(oid.byName("emailProtection")) !== -1;
2713
+ },
2714
+ effectiveDate: APPLE_SMIME_SAN_FROM,
2715
+ check: function (cert, ctx) {
2716
+ var san = ctx.decode("subjectAltName");
2717
+ var names = san && san.value && san.value.names;
2718
+ if (!Array.isArray(names)) return true;
2719
+ for (var i = 0; i < names.length; i++) {
2720
+ if (names[i] && names[i].tagNumber === 1 && typeof names[i].value === "string" &&
2721
+ names[i].value.length) { return null; }
2722
+ }
2723
+ return true;
2724
+ },
2725
+ },
2726
+ {
2727
+ id: "lint/apple-root/subscriber-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy appendix A",
2728
+ /** @internal The appendix's last note is a dated PERMISSION rather than a prohibition:
2729
+ * "Subscriber certificates issued under a Trust Purpose ... and signed before 2027-07-01 MAY
2730
+ * contain EKU combinations that do not conform to the single Trust Purpose scoping defined in
2731
+ * this Appendix". The permission is bounded by its date, so the scoping binds once it passes.
2732
+ * A subscriber naming none of the appendix's identifiers is under no Trust Purpose and is
2733
+ * passed over, which is the reading sec. 1.7 arm (b) takes of the same shape. */
2734
+ message: "a subscriber certificate names extended key usages spanning more than one trust purpose",
2735
+ appliesTo: _isAppleSubscriber,
2736
+ effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
2737
+ check: function (cert, ctx) {
2738
+ var purposes = _applePurposes(ctx);
2739
+ if (!_appleNamesAPurpose(purposes)) return null;
2740
+ return _appleIsDedicated(purposes)
2741
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2742
+ },
2743
+ },
2744
+ ];
2745
+
2530
2746
  /** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
2531
2747
  * "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
2532
2748
  * is admitted and is not one of the purposes being counted. */
@@ -4402,6 +4618,421 @@ var RFC5280_CRL_RULES = [
4402
4618
  var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
4403
4619
  var ALL_CRL_RULES = RFC5280_CRL_RULES;
4404
4620
 
4621
+ /** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
4622
+ * this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
4623
+ * clauses reach an operator as `lint/unparseable` carrying the parser's own code rather than as a
4624
+ * row here. A row for one of them would be a branch no input can drive. Probed against the
4625
+ * shipped parser, the SignedData and SignerInfo versions, the presence of signedAttrs when the
4626
+ * content type is not id-data, the content-type and message-digest attributes and their binding
4627
+ * to eContentType, every attribute's cardinality and placement, a countersignature's own
4628
+ * restrictions, and a time carrying no seconds, fractional seconds or a local offset are all
4629
+ * refused before a rule sees them. What is left is below.
4630
+ * The UTCTime alternative of sec. 11.3 carries two year digits, which the clause reads as 19YY
4631
+ * for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
4632
+ * GeneralizedTime direction of that clause can be broken. */
4633
+ var OID_SIGNING_TIME = oid.byName("signingTime");
4634
+ var OID_SIGNED_DATA = oid.byName("signedData");
4635
+
4636
+ /** @internal The content type decides which of these rows apply, and the parser sets `contentType`
4637
+ * as an own property on every content it builds. A content type that carries no signers has no own
4638
+ * `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
4639
+ function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
4640
+ var OID_COUNTERSIGNATURE = oid.byName("countersignature");
4641
+ /** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
4642
+ * unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
4643
+ * signedAttrs. A rule about a signed attribute governs one wherever it appears, so a message
4644
+ * cannot put the shape a clause forbids one level down and escape it. The parser validates each
4645
+ * countersignature's shape without keeping the result, so it is decoded here, and a nested one is
4646
+ * reached by the same walk. */
4647
+ function _cmsAllSignerInfos(cms) {
4648
+ var out = [];
4649
+ /** @internal Each entry keeps the index of the message's OWN signer it hangs from, not a
4650
+ * position in this walk: a reader given a flattened index would look up a signer that is not
4651
+ * the one at fault, or one that does not exist. Whether the attribute sat on a countersignature
4652
+ * under that signer is the other half of the answer. */
4653
+ function visit(signerInfos, signerIndex, isCounter, depth) {
4654
+ if (!Array.isArray(signerInfos) || depth > C.LIMITS.CMS_MAX_COUNTERSIGNATURE_DEPTH) return;
4655
+ signerInfos.forEach(function (si, i) {
4656
+ var index = isCounter ? signerIndex : i;
4657
+ out.push({ signerInfo: si, signerIndex: index, countersignature: isCounter });
4658
+ (Array.isArray(si.unsignedAttrs) ? si.unsignedAttrs : []).forEach(function (attr) {
4659
+ if (attr.type !== OID_COUNTERSIGNATURE) return;
4660
+ if (!Array.isArray(attr.values)) return;
4661
+ attr.values.forEach(function (value) {
4662
+ var nested;
4663
+ // allow:swallow-unverified the parser already walked this value, so a fault here leaves the countersignature unread rather than reported
4664
+ try { nested = cmsSchema.walkCountersignature(asn1.decode(value), null); }
4665
+ catch (_e) { return; }
4666
+ if (nested) visit([nested], index, true, depth + 1);
4667
+ });
4668
+ });
4669
+ });
4670
+ }
4671
+ visit(cms.signerInfos, 0, false, 0);
4672
+ return out;
4673
+ }
4674
+ function _cmsSignedAttr(signerInfo, dotted) {
4675
+ var attrs = signerInfo.signedAttrs;
4676
+ if (!Array.isArray(attrs)) return null;
4677
+ for (var i = 0; i < attrs.length; i++) {
4678
+ if (attrs[i].type === dotted) return attrs[i];
4679
+ }
4680
+ return null;
4681
+ }
4682
+ /** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
4683
+ * rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
4684
+ * that adds a countersigner's digest to it, and a set written under one rule and read under another
4685
+ * would list one algorithm twice. Only digest identifiers pass through here, so no row is loosened
4686
+ * for an algorithm whose parameters carry meaning. */
4687
+ var _cmsSameAlgorithm = cmsSchema.sameDigestAlgorithm;
4688
+
4689
+ var RFC5652_CMS_RULES = [
4690
+ {
4691
+ id: "lint/rfc5652/signing-time-encoding", severity: "error", source: "rfc5652", citation: "RFC 5652 11.3",
4692
+ /** @internal "Dates between 1 January 1950 and 31 December 2049 (inclusive) MUST be encoded as
4693
+ * UTCTime. Any dates with year values before 1950 or after 2049 MUST be encoded as
4694
+ * GeneralizedTime." The value is held as its DER bytes, so the encoding is the first byte and
4695
+ * the year is what the value decodes to. */
4696
+ message: "a signing-time naming a year from 1950 to 2049 must be encoded as UTCTime, not GeneralizedTime",
4697
+ appliesTo: _isSignedData,
4698
+ check: function (cms) {
4699
+ var out = [];
4700
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4701
+ var attr = _cmsSignedAttr(entry.signerInfo, OID_SIGNING_TIME);
4702
+ if (!attr || !Array.isArray(attr.values) || !attr.values.length) return;
4703
+ var value = attr.values[0];
4704
+ if (!Buffer.isBuffer(value) || value.length < 2) return;
4705
+ if (value[0] !== asn1.TAGS.GENERALIZED_TIME) return;
4706
+ var when;
4707
+ // allow:swallow-unverified the parser already read this value as a Time, so a fault here answers for no signer
4708
+ try { when = asn1.read.time(asn1.decode(value)); } catch (_e) { return; }
4709
+ var year = when.getUTCFullYear();
4710
+ if (year >= 1950 && year <= 2049) {
4711
+ out.push({ context: { signerIndex: entry.signerIndex,
4712
+ countersignature: entry.countersignature, year: year } });
4713
+ }
4714
+ });
4715
+ return out.length ? out : null;
4716
+ },
4717
+ },
4718
+ {
4719
+ id: "lint/rfc5652/signer-digest-not-listed", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.3",
4720
+ /** @internal "The message digest algorithm SHOULD be among those listed in the digestAlgorithms
4721
+ * field of the associated SignerData. Implementations MAY fail to validate signatures that use
4722
+ * a digest algorithm that is not included in the SignedData digestAlgorithms set." A SHOULD
4723
+ * whose cost is a verifier that declines, so a warning.
4724
+ * Sec. 11.4 makes a countersignature a SignerInfo and does not except it from this clause, and
4725
+ * sec. 5.1 has the collection list the digests "employed by all of the signers", so the walk is
4726
+ * the same one the signing-time rule uses and the finding says which signer it hangs from. */
4727
+ message: "a signer's digest algorithm is not among those the SignedData digestAlgorithms set lists",
4728
+ appliesTo: _isSignedData,
4729
+ check: function (cms) {
4730
+ var listed = Array.isArray(cms.digestAlgorithms) ? cms.digestAlgorithms : [];
4731
+ var out = [];
4732
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4733
+ var mine = entry.signerInfo.digestAlgorithm;
4734
+ if (!mine) return;
4735
+ var found = listed.some(function (alg) { return _cmsSameAlgorithm(alg, mine); });
4736
+ if (!found) {
4737
+ out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature,
4738
+ digestAlgorithm: mine.name || mine.oid } });
4739
+ }
4740
+ });
4741
+ return out.length ? out : null;
4742
+ },
4743
+ },
4744
+ {
4745
+ id: "lint/rfc5652/attribute-certificate-v1", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.1",
4746
+ /** @internal "The use of version 1 attribute certificates is strongly discouraged." The
4747
+ * certificates field is a CertificateChoices SET, whose `[1]` alternative is the v1 attribute
4748
+ * certificate; the parser keeps each element's outer tag for exactly this kind of question. */
4749
+ message: "the certificates field carries a version 1 attribute certificate, whose use this section strongly discourages",
4750
+ appliesTo: _isSignedData,
4751
+ check: function (cms) {
4752
+ var certs = Array.isArray(cms.certificates) ? cms.certificates : [];
4753
+ var count = 0;
4754
+ certs.forEach(function (c) {
4755
+ if (c && c.tagClass === "context" && c.tagNumber === 1) count += 1;
4756
+ });
4757
+ return count ? { context: { count: count } } : null;
4758
+ },
4759
+ },
4760
+ {
4761
+ id: "lint/rfc5652/no-signers", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.1",
4762
+ /** @internal "There MAY be any number of elements in the collection, including zero", so a
4763
+ * SignedData with no signers is conformant. It is also a message carrying certificates and
4764
+ * nothing signed, which is worth naming to whoever expected a signature. */
4765
+ message: "the SignedData carries no signers, so it signs nothing and only conveys its certificates and revocation lists",
4766
+ appliesTo: _isSignedData,
4767
+ check: function (cms) { return cms.signerInfos.length === 0 ? true : null; },
4768
+ },
4769
+ {
4770
+ id: "lint/rfc5652/detached-content", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.2",
4771
+ /** @internal eContent is optional, and its absence means a verifier must be handed the content
4772
+ * by another route. The row reads a SignedData that HAS a signer, since the notice exists for
4773
+ * whoever must verify one: a message with no signers has nothing to supply content for. */
4774
+ message: "the SignedData is detached, so verifying it needs the content supplied separately",
4775
+ appliesTo: function (cms) { return _isSignedData(cms) && cms.signerInfos.length > 0; },
4776
+ check: function (cms) {
4777
+ var encap = cms.encapContentInfo;
4778
+ return (encap && encap.eContent) ? null : true;
4779
+ },
4780
+ },
4781
+ ];
4782
+
4783
+ var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
4784
+ var ALL_CMS_RULES = RFC5652_CMS_RULES;
4785
+
4786
+ /** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
4787
+ * short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
4788
+ * rule sees it, each with its own code. A token that is not a CMS SignedData, encapsulates
4789
+ * something other than id-ct-TSTInfo, carries detached content, carries more than the TSA's own
4790
+ * signerInfo, or omits the SigningCertificate attribute the section requires arrives as
4791
+ * `lint/unparseable`. The TSTInfo version reader admits v1 alone, and the Accuracy field bounds,
4792
+ * the DEFAULT FALSE omissions and the PKIStatus range are all enforced too. */
4793
+ var TSP_SERIAL_MAX_BITS = 160;
4794
+
4795
+ /** @internal The tsa hint as a DECODED GeneralName. The TSTInfo keeps it raw, and the shared
4796
+ * GeneralNames decoder is what the rest of this file reads a name with, so the hint is wrapped in
4797
+ * a one-element SEQUENCE and walked through it rather than compared as bytes: two encodings of one
4798
+ * distinguished name are the same name, and an error-severity row must not say otherwise. */
4799
+ function _tspDecodedHint(hint) {
4800
+ if (!hint || !Buffer.isBuffer(hint.bytes)) return null;
4801
+ var names = _generalNamesOf(asn1.build.sequence([asn1.build.raw(hint.bytes)]));
4802
+ return (names && names.length === 1) ? names[0] : null;
4803
+ }
4804
+ /** @internal Every GeneralName the certificate's subjectAltName carries, decoded, each entry
4805
+ * keeping the raw TLV beside its value so a form with no useful decoded form can still be
4806
+ * compared. */
4807
+ function _tspSanNames(parsedCert) {
4808
+ var raw = null;
4809
+ (parsedCert.extensions || []).forEach(function (e) {
4810
+ if (e.oid === oid.byName("subjectAltName")) raw = e.value;
4811
+ });
4812
+ if (!Buffer.isBuffer(raw)) return [];
4813
+ return _generalNamesOf(raw) || [];
4814
+ }
4815
+ /** @internal Each form of GeneralName carrying text is compared under the RFC 5280 sec. 7 rule for
4816
+ * that form, and each rule lives in `guard.name` so this row and the AC target comparison read one
4817
+ * definition. A dNSName is case-insensitive throughout (sec. 7.2). A mailbox is not: sec. 7.5 has
4818
+ * the local part match exactly and the host part match without regard to ASCII case. A URI is not
4819
+ * either: sec. 7.4 prepares both values, lowercasing only the scheme and host, and then calls for a
4820
+ * "case-sensitive exact match". Folding case over a whole mailbox or URI read two names differing
4821
+ * only in a local part or a path as one, so a hint naming neither went unreported.
4822
+ * A verdict of null says the rule reached none, which the caller reports as nothing rather than as a
4823
+ * mismatch it did not establish. */
4824
+ function _tspSameGeneralName(a, c) {
4825
+ if (!a || !c || a.tagNumber !== c.tagNumber) return false;
4826
+ if (typeof a.value === "string" && typeof c.value === "string") {
4827
+ if (a.tagNumber === 2) return guard.name.lowerAscii(a.value) === guard.name.lowerAscii(c.value);
4828
+ if (a.tagNumber === 1 || a.tagNumber === 6) {
4829
+ var verdict = a.tagNumber === 1 ? guard.name.emailEqual(a.value, c.value)
4830
+ : guard.name.uriEqual(a.value, c.value);
4831
+ if (verdict === "match") return true;
4832
+ if (verdict === "no-match") return false;
4833
+ /** @internal A value neither rule can read is compared as its own encoding, which answers the
4834
+ * one case that is certain: identical bytes are one name. */
4835
+ return a.value === c.value ? true : null;
4836
+ }
4837
+ }
4838
+ if (Buffer.isBuffer(a.value) && Buffer.isBuffer(c.value)) return a.value.equals(c.value);
4839
+ /** @internal Anything else, including an otherName's structure and a form this profile leaves
4840
+ * undecoded, is compared as its own encoding: two decoded structures would compare by identity
4841
+ * and never match, and two undecoded ones both read as null and would compare equal. */
4842
+ return Buffer.isBuffer(a.bytes) && Buffer.isBuffer(c.bytes) && a.bytes.equals(c.bytes);
4843
+ }
4844
+ /** @internal "one of the subject names included in the certificate", which is the subject AND
4845
+ * every subjectAltName entry: the clause says names, plural, and a hint the subject does not
4846
+ * carry but an alternative name does is one of them. A directoryName is compared under the
4847
+ * RFC 5280 sec. 7.1 rules wherever it appears, since two encodings of one distinguished name are
4848
+ * one name; any other form is compared as its exact encoding. */
4849
+ /** @internal The certificate the token names as the one "to be used to verify the token", or null
4850
+ * when the token does not carry it. Carrying ONE certificate is not that fact: a token may embed an
4851
+ * intermediate and leave the signer's certificate to be supplied out of band, and comparing a hint
4852
+ * against an intermediate reports a conforming token. Which certificate it is comes from the ESS
4853
+ * certHash the schema module resolves, because that value names exactly one certificate while the
4854
+ * SignerInfo sid names none uniquely. */
4855
+ function _tspSignerCertificate(token) {
4856
+ var der = tspSchema.signerCertificateDer(token);
4857
+ if (der === null) return null;
4858
+ // allow:swallow-unverified an element this profile cannot read as a certificate names no subject, so the row passes over the token as it does when the signer is absent
4859
+ try { return x509.parse(der); } catch (_e) { return null; }
4860
+ }
4861
+ function _tspHintMatchesCert(hint, parsedCert) {
4862
+ var decoded = _tspDecodedHint(hint);
4863
+ if (decoded === null) return false;
4864
+ if (decoded.tagNumber === 4 && _sameName(decoded.value, parsedCert.subject)) return true;
4865
+ var san = _tspSanNames(parsedCert), undecided = false;
4866
+ for (var i = 0; i < san.length; i++) {
4867
+ if (decoded.tagNumber === 4) {
4868
+ if (san[i].tagNumber === 4 && _sameName(decoded.value, san[i].value)) return true;
4869
+ continue;
4870
+ }
4871
+ var verdict = _tspSameGeneralName(decoded, san[i]);
4872
+ if (verdict === true) return true;
4873
+ if (verdict === null) undecided = true;
4874
+ }
4875
+ /** @internal A name the comparison rule could not read leaves the row with no verdict on this
4876
+ * certificate, which is reported as nothing rather than as the mismatch it did not establish. */
4877
+ return undecided ? null : false;
4878
+ }
4879
+
4880
+ var RFC3161_TSP_RULES = [
4881
+ {
4882
+ id: "lint/rfc3161/tsa-name-mismatch", severity: "error", source: "rfc3161", citation: "RFC 3161 2.4.2",
4883
+ /** @internal "The purpose of the tsa field is to give a hint in identifying the name of the
4884
+ * TSA. If present, it MUST correspond to one of the subject names included in the certificate
4885
+ * that is to be used to verify the token."
4886
+ * WHICH certificate verifies the token is what the SignerInfo's own identifier names, so the
4887
+ * row resolves it rather than assuming the token carries one certificate and that it is the
4888
+ * signer's. A token that leaves the signer's certificate to be supplied out of band is passed
4889
+ * over, which is the honest edge of reading a hint against bytes that do not hold the answer. */
4890
+ message: "the tsa hint names a subject the signer's own certificate does not carry",
4891
+ appliesTo: function (token) {
4892
+ return !!(token.tstInfo && token.tstInfo.tsa) && _tspSignerCertificate(token) !== null;
4893
+ },
4894
+ check: function (token) {
4895
+ var signerCert = _tspSignerCertificate(token);
4896
+ if (signerCert === null) return null;
4897
+ var hint = token.tstInfo.tsa;
4898
+ if (_tspHintMatchesCert(hint, signerCert) !== false) return null;
4899
+ return { context: { tagNumber: hint.tagNumber } };
4900
+ },
4901
+ },
4902
+ {
4903
+ id: "lint/rfc3161/serial-over-160-bits", severity: "warn", source: "rfc3161", citation: "RFC 3161 2.4.2",
4904
+ /** @internal "Time-Stamping users MUST be ready to accommodate integers up to 160 bits", which
4905
+ * is what a conforming consumer owes and therefore all a TSA may count on. */
4906
+ message: "the serial number is wider than the 160 bits a conforming consumer must accommodate",
4907
+ appliesTo: function (token) { return typeof token.tstInfo.serialNumber === "bigint"; },
4908
+ check: function (token) {
4909
+ var n = token.tstInfo.serialNumber;
4910
+ var magnitude = n < 0n ? -n : n;
4911
+ var bits = magnitude === 0n ? 0 : magnitude.toString(2).length;
4912
+ return bits > TSP_SERIAL_MAX_BITS ? { context: { bits: bits } } : null;
4913
+ },
4914
+ },
4915
+ {
4916
+ id: "lint/rfc3161/accuracy-absent", severity: "notice", source: "rfc3161", citation: "RFC 3161 2.4.2",
4917
+ /** @internal "When the accuracy optional field is not present, then the accuracy may be
4918
+ * available through other means, e.g., the TSAPolicyId." So its absence is conformant and
4919
+ * leaves whoever must order two tokens reading the policy instead. */
4920
+ message: "the token states no accuracy, so bounding its genTime needs the TSA policy instead",
4921
+ check: function (token) {
4922
+ return (token.tstInfo.accuracy === null || token.tstInfo.accuracy === undefined) ? true : null;
4923
+ },
4924
+ },
4925
+ ];
4926
+
4927
+ var TSP_PROFILES = Object.assign(Object.create(null), { "rfc3161": RFC3161_TSP_RULES });
4928
+ var ALL_TSP_RULES = RFC3161_TSP_RULES;
4929
+
4930
+ /** @internal Sec. 4.3's six and the ProxyInfo of sec. 7.2, read from the schema module's row set
4931
+ * rather than restated here, so the criticality this profile reports, the one the AC signer writes
4932
+ * and the deviation the extension door reports cannot disagree. The keys are also what this profile
4933
+ * means by membership, which the outside-profile rule below reads. */
4934
+ var AC_FIXED_CRITICALITY = acSchema.extensionCriticality(oid);
4935
+
4936
+ /** @internal The parser has already refused sec. 4.2's structural MUSTs by the time a rule sees an
4937
+ * attribute certificate: the version being v2, the issuer being a v2Form carrying one non-empty
4938
+ * directoryName with no baseCertificateID or objectDigestInfo beside it, a serial that is not a
4939
+ * positive integer or runs past 20 octets, a repeated attribute type, an empty attribute sequence,
4940
+ * and a GeneralizedTime carrying no seconds or a fraction. Those reach an operator as
4941
+ * `lint/unparseable` with the parser's code, or are refused by the builder first. */
4942
+ var RFC5755_AC_RULES = [
4943
+ {
4944
+ id: "lint/rfc5755/extension-criticality", severity: "error", source: "rfc5755", citation: "RFC 5755 4.3",
4945
+ message: "an extension this profile defines does not carry the criticality its clause states",
4946
+ check: function (ac) {
4947
+ var out = [];
4948
+ (ac.extensions || []).forEach(function (x) {
4949
+ var row = AC_FIXED_CRITICALITY[x.oid];
4950
+ if (!row) return;
4951
+ if (x.critical !== row.critical) {
4952
+ out.push({ context: { extension: row.name, clause: row.clause, critical: x.critical === true } });
4953
+ }
4954
+ });
4955
+ return out.length ? out : null;
4956
+ },
4957
+ },
4958
+ {
4959
+ id: "lint/rfc5755/critical-extension-outside-profile", severity: "error", source: "rfc5755", citation: "RFC 5755 4.2.9",
4960
+ /** @internal "If any other critical extension is used, the AC does not conform to this
4961
+ * profile." What counts as "other" is membership in the PROFILE, which is the table above:
4962
+ * sec. 4.3's six and the ProxyInfo of sec. 7.2. Whether this toolkit happens to decode a value
4963
+ * is a different question and answers it wrongly in both directions, since the AC decoder also
4964
+ * reads AAControls, which sec. 7.4 places in "CA and AC issuer PKCs" rather than in an
4965
+ * attribute certificate. A non-critical extension outside the profile is conformant and draws
4966
+ * nothing, which the clause says outright. */
4967
+ message: "a critical extension this profile does not define makes the attribute certificate non-conforming",
4968
+ check: function (ac) {
4969
+ var out = [];
4970
+ (ac.extensions || []).forEach(function (x) {
4971
+ if (x.critical !== true) return;
4972
+ if (AC_FIXED_CRITICALITY[x.oid]) return;
4973
+ out.push({ context: { oid: x.oid, name: x.name || null } });
4974
+ });
4975
+ return out.length ? out : null;
4976
+ },
4977
+ },
4978
+ {
4979
+ id: "lint/rfc5755/holder-multiple-options", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
4980
+ /** @internal "where more than one option is used, there is a potential for confusion as to
4981
+ * which option is 'normative', which is a 'hint' ... this specification RECOMMENDS that only
4982
+ * one of the options be used in any given AC." A RECOMMENDS, so a warning. */
4983
+ message: "the Holder names more than one option, which leaves which of them is normative unstated",
4984
+ check: function (ac) {
4985
+ var holder = ac.holder;
4986
+ if (!holder) return null;
4987
+ var named = [];
4988
+ ["baseCertificateID", "entityName", "objectDigestInfo"].forEach(function (k) {
4989
+ if (holder[k] !== null && holder[k] !== undefined) named.push(k);
4990
+ });
4991
+ return named.length > 1 ? { context: { options: named } } : null;
4992
+ },
4993
+ },
4994
+ {
4995
+ id: "lint/rfc5755/holder-entity-name-multiple", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
4996
+ /** @internal "In any other case where the Holder field uses the entityName option, only one name
4997
+ * SHOULD be present." A SHOULD, so a warning. This counts the NAMES inside the one entityName
4998
+ * option, which the sibling row above does not: that one counts how many of the three options
4999
+ * the Holder names, and an entityName carrying several GeneralNames is still one option. */
5000
+ message: "the Holder entityName carries more than one name, where this clause asks for one",
5001
+ check: function (ac) {
5002
+ var entity = ac.holder && ac.holder.entityName;
5003
+ if (!entity || !Array.isArray(entity.names) || entity.names.length <= 1) return null;
5004
+ return { context: { names: entity.names.length } };
5005
+ },
5006
+ },
5007
+ {
5008
+ id: "lint/rfc5755/holder-issuer-uid-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
5009
+ /** @internal "The AC holder.baseCertificateID.issuerUID field MUST only be used if the holder's
5010
+ * PKC contains an issuerUniqueID field." Whether the certificate the baseCertificateID names
5011
+ * carries one is a fact about another certificate, so this reports below error and names the
5012
+ * condition, the way the sec. 4.2.8 row does for the AC's own issuerUniqueID. The three sec. 4.2.2
5013
+ * requirements that ARE answerable from these bytes are refused by the parser instead. */
5014
+ message: "the Holder baseCertificateID carries an issuerUID, which this clause permits only where the certificate it names carries an issuerUniqueID",
5015
+ check: function (ac) {
5016
+ var base = ac.holder && ac.holder.baseCertificateID;
5017
+ if (!base) return null;
5018
+ return (base.issuerUID === null || base.issuerUID === undefined) ? null : true;
5019
+ },
5020
+ },
5021
+ {
5022
+ id: "lint/rfc5755/issuer-unique-id-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.8",
5023
+ /** @internal "This field MUST NOT be used unless it is also used in the AC issuer's PKC, in
5024
+ * which case it MUST be used." Whether the issuer's own certificate carries one is a fact about
5025
+ * another certificate, so the row reports below error and its message names the condition. */
5026
+ message: "an issuerUniqueID is present, which this clause permits only where the AC issuer's own certificate carries one",
5027
+ check: function (ac) {
5028
+ return (ac.issuerUniqueID === null || ac.issuerUniqueID === undefined) ? null : true;
5029
+ },
5030
+ },
5031
+ ];
5032
+
5033
+ var AC_PROFILES = Object.assign(Object.create(null), { "rfc5755": RFC5755_AC_RULES });
5034
+ var ALL_AC_RULES = RFC5755_AC_RULES;
5035
+
4405
5036
 
4406
5037
  /** @internal The extensions RFC 6960 and RFC 9654 place on a response, by the list each belongs
4407
5038
  * in. The nonce and the extended revoked definition are response extensions; the archive cutoff and
@@ -4970,12 +5601,13 @@ var PROFILES = Object.assign(Object.create(null), {
4970
5601
  "mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4971
5602
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4972
5603
  "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5604
+ "apple-root": APPLE_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4973
5605
  });
4974
5606
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
4975
5607
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
4976
5608
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
4977
5609
  * inert under the default set because each one asks whether the caller named the profile. */
4978
- .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES);
5610
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES);
4979
5611
 
4980
5612
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
4981
5613
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
@@ -5479,6 +6111,286 @@ function ocsp(input, opts) {
5479
6111
  return report;
5480
6112
  }
5481
6113
 
6114
+ var _CMS_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6115
+ var _CMS_CLAIM = ["contentType", "signerInfos", "encapContentInfo"];
6116
+
6117
+ function _ingestCms(input) {
6118
+ var der;
6119
+ var bytes = _derBytesOf(input);
6120
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6121
+ if (bytes !== null) der = bytes.der;
6122
+ else if (typeof input === "string") {
6123
+ try { der = cmsSchema.pemDecode(input); }
6124
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER CMS ContentInfo", context: { code: e.code } } }; }
6125
+ } else if (input !== null && typeof input === "object" && !guard.bytes.isByteSource(input)) {
6126
+ return { cms: guard.parsed.fromTrustedSource(input, "cms", _CMS_CLAIM, function (src) {
6127
+ if (!Buffer.isBuffer(src) && typeof src !== "string") throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
6128
+ return cmsSchema.parse(src);
6129
+ }, _cfg, "lint/bad-input",
6130
+ "pki.lint.cms input must be its DER bytes, a PEM string, or an unmodified pki.schema.cms.parse result: the signed attribute bytes, the signature and the fields those bytes encode are separate properties of a parsed object, so a rebuilt structure could have them describe different messages") };
6131
+ } else {
6132
+ throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
6133
+ }
6134
+ try { return { cms: cmsSchema.parse(der) }; }
6135
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5652", message: "input is not a well-formed CMS ContentInfo", context: { code: e.code } } }; }
6136
+ }
6137
+
6138
+ /**
6139
+ * @primitive pki.lint.cms
6140
+ * @signature pki.lint.cms(pem | der | parsed, opts?) -> LintReport
6141
+ * @since 0.8.26
6142
+ * @status stable
6143
+ * @spec RFC 5652
6144
+ * @related pki.lint.certificate, pki.lint.tsp, pki.schema.cms.parse, pki.cms.sign, pki.cms.verify
6145
+ *
6146
+ * Lint a CMS ContentInfo against the RFC 5652 profile. `input` is a PEM string (`CMS`), a DER
6147
+ * `Buffer`, or an already-parsed `pki.schema.cms.parse` object. It returns the same `LintReport`
6148
+ * shape `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
6149
+ * well-formed ContentInfo produce a single `fatal` finding `lint/unparseable` carrying the strict
6150
+ * parser's own code. A content type other than signed-data carries no signerInfos, so every row is
6151
+ * not applicable to it.
6152
+ *
6153
+ * The profile is short, and what makes it short is that the strict parser has already refused most
6154
+ * of what sections 5 and 11 require. The SignedData and SignerInfo versions against their contents,
6155
+ * signedAttrs being present when the content type is not id-data, the content-type and
6156
+ * message-digest attributes and the binding of the first to eContentType, every attribute's
6157
+ * cardinality and placement, a countersignature's own restrictions, and a time carrying no seconds,
6158
+ * fractional seconds or a local offset all arrive as `lint/unparseable` with the parser's code.
6159
+ *
6160
+ * What the rows cover is what survives that: a signing-time naming a year from 1950 to 2049 encoded
6161
+ * as GeneralizedTime where section 11.3 requires UTCTime, a signer whose digest algorithm the
6162
+ * digestAlgorithms set does not list, a version 1 attribute certificate in the certificates field,
6163
+ * and two structural notices, a SignedData carrying no signers and a detached signature whose
6164
+ * content a verifier must be handed separately.
6165
+ *
6166
+ * Verifying a signature, resolving a signer to a certificate and checking a message digest are
6167
+ * `pki.cms.verify`'s, and the certificates a message carries are linted with
6168
+ * `pki.lint.certificate`.
6169
+ *
6170
+ * @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
6171
+ * profile name is refused rather than run against a message.
6172
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6173
+ * always reflect the complete, unfiltered result.
6174
+ * @example
6175
+ * var pair = await pki.key.generate("Ed25519");
6176
+ * var key = await pki.key.export(pair.privateKey);
6177
+ * var cert = await pki.x509.sign({ subject: "Example Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
6178
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
6179
+ * extensions: { subjectKeyIdentifier: true } }, { key: key });
6180
+ * var der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
6181
+ * pki.lint.cms(der).findings.length; // -> 0
6182
+ * pki.lint.cms(await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { detached: true })).worst;
6183
+ * // -> "notice"
6184
+ */
6185
+ function cms(input, opts) {
6186
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6187
+ guard.identifier.assertKnownKeys(opts, _CMS_OPTS, _cfg, "lint/bad-input",
6188
+ "pki.lint.cms has an unknown option: ");
6189
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6190
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6191
+ }
6192
+ var profile = opts.profile;
6193
+ var rules_;
6194
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
6195
+ else if (CMS_PROFILES[profile]) rules_ = CMS_PROFILES[profile];
6196
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6197
+ else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
6198
+ else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
6199
+ else throw _cfg("lint/unknown-profile", "unknown CMS lint profile \"" + profile + "\" (known: " + Object.keys(CMS_PROFILES).join(", ") + ")");
6200
+
6201
+ var ingested = _ingestCms(input);
6202
+ var report;
6203
+ if (ingested.fatal) {
6204
+ report = { findings: [ingested.fatal], ran: [],
6205
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6206
+ } else {
6207
+ report = _runLints(rules_, ingested.cms, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6208
+ }
6209
+ report.worst = _worst(report.findings);
6210
+ report.findings = _applyThreshold(report, opts.severity);
6211
+ return report;
6212
+ }
6213
+
6214
+ var _TSP_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6215
+
6216
+ function _ingestTsp(input) {
6217
+ var der;
6218
+ var bytes = _derBytesOf(input);
6219
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6220
+ if (bytes !== null) der = bytes.der;
6221
+ else if (typeof input === "string") {
6222
+ try { der = tspSchema.pemDecode(input); }
6223
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER timestamp token", context: { code: e.code } } }; }
6224
+ } else {
6225
+ throw _cfg("lint/bad-input", "pki.lint.tsp input must be a DER Buffer or a PEM string");
6226
+ }
6227
+ try { return { token: tspSchema.parseToken(der) }; }
6228
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 3161", message: "input is not a well-formed RFC 3161 timestamp token", context: { code: e.code } } }; }
6229
+ }
6230
+
6231
+ /**
6232
+ * @primitive pki.lint.tsp
6233
+ * @signature pki.lint.tsp(pem | der, opts?) -> LintReport
6234
+ * @since 0.8.26
6235
+ * @status stable
6236
+ * @spec RFC 3161
6237
+ * @related pki.lint.cms, pki.schema.tsp.parseToken, pki.tsp.sign, pki.tsp.verify
6238
+ *
6239
+ * Lint an RFC 3161 timestamp token against the section 2.4.2 profile. `input` is a PEM string or a
6240
+ * DER `Buffer` holding a TimeStampToken, which is a CMS SignedData encapsulating a TSTInfo. It
6241
+ * returns the same `LintReport` shape `pki.lint.certificate` returns and never throws on the data
6242
+ * path: bytes that are not a well-formed token produce a single `fatal` finding `lint/unparseable`
6243
+ * carrying the strict parser's own code.
6244
+ *
6245
+ * This profile is the shortest the toolkit ships, and the reason is that `pki.schema.tsp.parseToken`
6246
+ * already refuses what the section requires. A token that is not a CMS SignedData, encapsulates
6247
+ * something other than id-ct-TSTInfo, carries detached content, carries a signature other than the
6248
+ * TSA's own, or omits the SigningCertificate attribute the section names arrives as
6249
+ * `lint/unparseable` with the parser's code rather than as a row, and so do a TSTInfo version other
6250
+ * than v1, an Accuracy field outside its stated bounds and a DEFAULT FALSE written out.
6251
+ *
6252
+ * What the rows cover is what survives that: a tsa hint naming a subject the signer's own
6253
+ * certificate does not carry, a serial number wider than the 160 bits a conforming consumer must
6254
+ * accommodate, and a token stating no accuracy. The tsa row resolves which certificate verifies the
6255
+ * token from the SignerInfo's own identifier, and passes over a token that leaves that certificate
6256
+ * to be supplied out of band.
6257
+ *
6258
+ * Verifying the signature, resolving the TSA certificate and comparing the imprint against content
6259
+ * are `pki.tsp.verify`'s.
6260
+ *
6261
+ * @opts profile `"rfc3161"`, which is also what the default runs.
6262
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6263
+ * always reflect the complete, unfiltered result.
6264
+ * @example
6265
+ * var pair = await pki.key.generate("Ed25519");
6266
+ * var key = await pki.key.export(pair.privateKey);
6267
+ * var cert = await pki.x509.sign({ subject: "Example TSA", subjectPublicKey: await pki.key.export(pair.publicKey),
6268
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
6269
+ * extensions: { subjectKeyIdentifier: true, keyUsage: ["digitalSignature"],
6270
+ * extendedKeyUsage: ["timeStamping"], extendedKeyUsageCritical: true } }, { key: key });
6271
+ * var imprint = { hashAlgorithm: "sha256", hashedMessage: Buffer.alloc(32, 7) };
6272
+ * var token = await pki.tsp.sign(imprint, { cert: cert, key: key }, { policy: "1.2.3", serialNumber: 1 });
6273
+ * pki.lint.tsp(token).worst; // -> "notice", since the token states no accuracy
6274
+ */
6275
+ function tsp(input, opts) {
6276
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6277
+ guard.identifier.assertKnownKeys(opts, _TSP_OPTS, _cfg, "lint/bad-input",
6278
+ "pki.lint.tsp has an unknown option: ");
6279
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6280
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6281
+ }
6282
+ var profile = opts.profile;
6283
+ var rules_;
6284
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
6285
+ else if (TSP_PROFILES[profile]) rules_ = TSP_PROFILES[profile];
6286
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6287
+ else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6288
+ else throw _cfg("lint/unknown-profile", "unknown timestamp lint profile \"" + profile + "\" (known: " + Object.keys(TSP_PROFILES).join(", ") + ")");
6289
+
6290
+ var ingested = _ingestTsp(input);
6291
+ var report;
6292
+ if (ingested.fatal) {
6293
+ report = { findings: [ingested.fatal], ran: [],
6294
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6295
+ } else {
6296
+ report = _runLints(rules_, ingested.token, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6297
+ }
6298
+ report.worst = _worst(report.findings);
6299
+ report.findings = _applyThreshold(report, opts.severity);
6300
+ return report;
6301
+ }
6302
+
6303
+ var _AC_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6304
+
6305
+ function _ingestAttrCert(input) {
6306
+ var der;
6307
+ var bytes = _derBytesOf(input);
6308
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6309
+ if (bytes !== null) der = bytes.der;
6310
+ else if (typeof input === "string") {
6311
+ try { der = acSchema.pemDecode(input); }
6312
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER attribute certificate", context: { code: e.code } } }; }
6313
+ } else {
6314
+ throw _cfg("lint/bad-input", "pki.lint.attrcert input must be a DER Buffer or a PEM string");
6315
+ }
6316
+ try { return { ac: acSchema.parse(der) }; }
6317
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5755", message: "input is not a well-formed X.509 attribute certificate", context: { code: e.code } } }; }
6318
+ }
6319
+
6320
+ /**
6321
+ * @primitive pki.lint.attrcert
6322
+ * @signature pki.lint.attrcert(pem | der, opts?) -> LintReport
6323
+ * @since 0.8.26
6324
+ * @status stable
6325
+ * @spec RFC 5755
6326
+ * @related pki.lint.certificate, pki.schema.attrcert.parse, pki.attrcert.sign, pki.attrcert.verify
6327
+ *
6328
+ * Lint an X.509 attribute certificate against the RFC 5755 profile. `input` is a PEM string
6329
+ * (`ATTRIBUTE CERTIFICATE`) or a DER `Buffer`. It returns the same `LintReport` shape
6330
+ * `pki.lint.certificate` returns and never throws on the data path: bytes that are not a
6331
+ * well-formed attribute certificate produce a single `fatal` finding `lint/unparseable` carrying
6332
+ * the strict parser's own code.
6333
+ *
6334
+ * Section 4.2's structural requirements are already refused at parse and so carry no rows: the
6335
+ * version being v2, the issuer being a v2Form carrying one non-empty directoryName with no
6336
+ * baseCertificateID or objectDigestInfo beside it, a serial that is not a positive integer or runs
6337
+ * past 20 octets, a repeated attribute type, an empty attribute sequence, a GeneralizedTime carrying
6338
+ * no seconds or a fractional part, and a Holder baseCertificateID whose issuer is anything other than
6339
+ * one non-empty directoryName, which section 4.2.2 requires in the same words section 4.2.3 uses for
6340
+ * the issuer.
6341
+ *
6342
+ * The rows are the clauses left, which are the ones these bytes cannot settle alone or that state a
6343
+ * recommendation rather than a requirement: the criticality section 4.3 fixes for each extension it
6344
+ * defines and the ProxyInfo of section 7.2, a critical extension the profile does not define, a
6345
+ * Holder naming more than one of its three options, a Holder entityName carrying more than one name,
6346
+ * a Holder baseCertificateID issuerUID, and an issuerUniqueID. The last two are permitted only where
6347
+ * another certificate carries the matching field, so each is a warning naming that condition.
6348
+ * Section 4.2.2's remaining requirements are about the public-key certificate the Holder names rather
6349
+ * than about this one, so no row can answer them.
6350
+ *
6351
+ * Verifying the signature and resolving the holder against a public-key certificate are
6352
+ * `pki.attrcert.verify`'s.
6353
+ *
6354
+ * @opts profile `"rfc5755"`, which is also what the default runs.
6355
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6356
+ * always reflect the complete, unfiltered result.
6357
+ * @example
6358
+ * var pair = await pki.key.generate("Ed25519");
6359
+ * var key = await pki.key.export(pair.privateKey);
6360
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
6361
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
6362
+ * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
6363
+ * { name: "CN=Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
6364
+ * pki.lint.attrcert(der).findings.length; // -> 0 for a conforming attribute certificate
6365
+ */
6366
+ function attrcert(input, opts) {
6367
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6368
+ guard.identifier.assertKnownKeys(opts, _AC_OPTS, _cfg, "lint/bad-input",
6369
+ "pki.lint.attrcert has an unknown option: ");
6370
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6371
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6372
+ }
6373
+ var profile = opts.profile;
6374
+ var rules_;
6375
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
6376
+ else if (AC_PROFILES[profile]) rules_ = AC_PROFILES[profile];
6377
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6378
+ else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6379
+ else throw _cfg("lint/unknown-profile", "unknown attribute certificate lint profile \"" + profile + "\" (known: " + Object.keys(AC_PROFILES).join(", ") + ")");
6380
+
6381
+ var ingested = _ingestAttrCert(input);
6382
+ var report;
6383
+ if (ingested.fatal) {
6384
+ report = { findings: [ingested.fatal], ran: [],
6385
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6386
+ } else {
6387
+ report = _runLints(rules_, ingested.ac, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6388
+ }
6389
+ report.worst = _worst(report.findings);
6390
+ report.findings = _applyThreshold(report, opts.severity);
6391
+ return report;
6392
+ }
6393
+
5482
6394
  /**
5483
6395
  * @primitive pki.lint.rules
5484
6396
  * @signature pki.lint.rules(profile?, artifact?) -> [{id, severity, source, citation}]
@@ -5504,6 +6416,7 @@ function ocsp(input, opts) {
5504
6416
  * literal would answer for "constructor" and the rest of Object.prototype. */
5505
6417
  var BY_ARTIFACT = Object.assign(Object.create(null), {
5506
6418
  certificate: PROFILES, csr: CSR_PROFILES, crl: CRL_PROFILES, ocsp: OCSP_PROFILES,
6419
+ cms: CMS_PROFILES, tsp: TSP_PROFILES, attrcert: AC_PROFILES,
5507
6420
  });
5508
6421
 
5509
6422
  function rules(profile, artifact) {
@@ -5525,7 +6438,8 @@ function rules(profile, artifact) {
5525
6438
  BY_ARTIFACT[artifact][name].forEach(function (r) { set.push(r); });
5526
6439
  });
5527
6440
  } else if (profile == null) {
5528
- set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES);
6441
+ set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES)
6442
+ .concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
5529
6443
  } else if (artifact != null) {
5530
6444
  set = BY_ARTIFACT[artifact][profile];
5531
6445
  if (!set) {
@@ -5533,7 +6447,9 @@ function rules(profile, artifact) {
5533
6447
  " lint profile (known: " + Object.keys(BY_ARTIFACT[artifact]).join(", ") + ")");
5534
6448
  }
5535
6449
  } else {
5536
- set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] || CSR_PROFILES[profile];
6450
+ set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
6451
+ CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
6452
+ AC_PROFILES[profile];
5537
6453
  if (!set) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\"");
5538
6454
  }
5539
6455
  /** @internal One row per id: a rule reused across artifacts, like the RFC 5280 sec. 4.2
@@ -5555,16 +6471,29 @@ function rules(profile, artifact) {
5555
6471
  * @status stable
5556
6472
  * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
5557
6473
  *
5558
- * List the known lint-profile names.
6474
+ * List the known lint-profile names. The two names every verb reads as naming no profile, `"all"`
6475
+ * and `"default"`, are not among them.
5559
6476
  * @example
5560
- * pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl", "rfc6960", "rfc5019"]
6477
+ * pki.lint.profiles();
6478
+ * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
6479
+ * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root", "rfc2986",
6480
+ * // "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc3161", "rfc5755"]
5561
6481
  */
5562
6482
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
5563
6483
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
5564
6484
  * asks for, so one name means one set of requirements whichever artifact carries it. */
6485
+ /** @internal Enumerated from the SAME registry-of-registries `rules()` dispatches on, so a profile a
6486
+ * verb can run is a profile this answers with. Naming the registries here instead left the three
6487
+ * profiles added with the CMS, timestamp and attribute-certificate verbs undiscoverable while
6488
+ * `rules()` returned their rows, and a fourth verb would have repeated it. The order follows
6489
+ * BY_ARTIFACT's, and a name reachable under two artifacts is listed once. */
5565
6490
  function profiles() {
5566
- var out = Object.keys(PROFILES).concat(Object.keys(CRL_PROFILES)).concat(Object.keys(OCSP_PROFILES));
5567
- Object.keys(CSR_PROFILES).forEach(function (name) { if (out.indexOf(name) === -1) out.push(name); });
6491
+ var out = [];
6492
+ Object.keys(BY_ARTIFACT).forEach(function (artifact) {
6493
+ Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
6494
+ if (out.indexOf(name) === -1) out.push(name);
6495
+ });
6496
+ });
5568
6497
  return out;
5569
6498
  }
5570
6499
 
@@ -5573,6 +6502,9 @@ module.exports = {
5573
6502
  csr: csr,
5574
6503
  crl: crl,
5575
6504
  ocsp: ocsp,
6505
+ cms: cms,
6506
+ tsp: tsp,
6507
+ attrcert: attrcert,
5576
6508
  rules: rules,
5577
6509
  profiles: profiles,
5578
6510
  };