@blamejs/pki 0.8.16 → 0.8.18

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
@@ -37,6 +37,7 @@ var crlSchema = require("./schema-crl");
37
37
  var csrSchema = require("./schema-csr");
38
38
  var ocspSchema = require("./schema-ocsp");
39
39
  var pkix = require("./schema-pkix");
40
+ var pkiBuild = require("./pki-build");
40
41
  var schema = require("./schema-engine");
41
42
  var path = require("./path-validate");
42
43
  var C = require("./constants");
@@ -84,11 +85,32 @@ function _derivedCert(o) {
84
85
  }
85
86
  }
86
87
 
88
+ /** @internal The bytes a lint verb was handed, as a Buffer, or null where the input is not bytes
89
+ * at all and the caller's own branches answer for it. Every parser in this toolkit reads any
90
+ * BufferSource, so the one surface that promises never to throw on the data path does not turn
91
+ * away a caller holding a Uint8Array from the network or an ArrayBuffer from a reader. Taking a
92
+ * snapshot rather than the caller's view is also what closes the detached-backing-buffer hazard,
93
+ * since a caller can neuter the buffer between the check and the read. */
94
+ function _derBytesOf(input) {
95
+ if (!guard.bytes.isByteSource(input)) return null;
96
+ try {
97
+ return { der: guard.bytes.snapshotSource(input, LintError, "lint/bad-input", "pki.lint input") };
98
+ } catch (e) {
99
+ /** @internal A byte source whose backing buffer was detached is still a byte source, and it
100
+ * fails at the door rather than at the parse. The data path answers with a verdict either
101
+ * way, or one neutered buffer aborts the scan of a whole corpus. */
102
+ return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint",
103
+ message: "input is a byte source whose bytes could not be read", context: { code: e.code } } };
104
+ }
105
+ }
106
+
87
107
  function _ingest(input) {
88
108
  var derived = _derivedCert(input);
89
109
  if (derived) return { cert: derived };
90
110
  var der;
91
- if (Buffer.isBuffer(input)) der = input;
111
+ var bytes = _derBytesOf(input);
112
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
113
+ if (bytes !== null) der = bytes.der;
92
114
  else if (typeof input === "string") {
93
115
  try { der = x509.pemDecode(input, "CERTIFICATE"); }
94
116
  catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER certificate", context: { code: e.code } } }; }
@@ -128,6 +150,11 @@ function _makeCtx(cert, profile) {
128
150
  return {
129
151
  profile: profile,
130
152
  explicitTlsProfile: explicitTls,
153
+ /** @internal An S/MIME subscriber certificate names its exact profile through a reserved
154
+ * policy identifier, so unlike the TLS case the rows detect it from the certificate itself and
155
+ * the flag only records that the caller also named it. */
156
+ explicitSmimeProfile: profile === "cabf-smime",
157
+ isCaCert: isCa,
131
158
  isTlsServerCert: explicitTls || (_hasEku(cert, "serverAuth") && !isCa),
132
159
  decode: function (name) { return _decodeOrNull(cert, name); },
133
160
  raw: function (name) { return _findRaw(cert, name); },
@@ -242,6 +269,7 @@ function _ecCurveName(spki) {
242
269
 
243
270
  var OID_UNOTICE = oid.byName("unotice");
244
271
  var OID_ANY_POLICY = oid.byName("anyPolicy");
272
+ var OID_CPS = oid.byName("cps");
245
273
  /** @internal The extensions RFC 5280 says a conforming CA SHOULD mark non-critical, keyed by OID
246
274
  * with the clause; the MUSTs live in the shared CERT_FIXED_CRITICALITY table. The subjectAltName
247
275
  * and extKeyUsage recommendations are conditional and have their own rules. */
@@ -256,23 +284,40 @@ function _hasControlChar(str) {
256
284
  }
257
285
  return false;
258
286
  }
259
- function _policyDisplayTexts(ctx) {
287
+ /** @internal Every policy qualifier of every PolicyInformation, handed to `fn` as its qualifier
288
+ * OID, its value node, and the PolicyInformation carrying it. The decoder surfaces the qualifiers
289
+ * as raw DER, because a qualifier may carry any type and the reader does not choose one for the
290
+ * caller, so every walk re-decodes bytes that already passed the policy-qualifier door. */
291
+ function _eachPolicyQualifier(ctx, fn) {
260
292
  var d = ctx.decode("certificatePolicies");
261
- if (!d || !Array.isArray(d.value)) return [];
262
- var out = [];
293
+ if (!d || !Array.isArray(d.value)) return;
263
294
  d.value.forEach(function (pi) {
264
295
  if (!pi.qualifiersBytes || !pi.qualifiersBytes.length) return;
265
296
  var quals;
266
297
  // allow:swallow-unverified re-decoding bytes that already decoded under assertPolicyQualifiers cannot throw
267
- try { quals = asn1.decode(pi.qualifiersBytes).children; } catch (_e) { return; }
298
+ try {
299
+ quals = asn1.decode(pi.qualifiersBytes).children;
300
+ } catch (_e) {
301
+ return;
302
+ }
268
303
  (quals || []).forEach(function (pq) {
269
304
  var qid;
270
305
  // allow:swallow-unverified assertPolicyQualifiers already read this OID, so re-reading it cannot throw
271
- try { qid = asn1.read.oid(pq.children[0]); } catch (_e2) { return; }
272
- if (qid !== OID_UNOTICE) return;
273
- out = out.concat(pkix.userNoticeTexts(pq.children[1]));
306
+ try {
307
+ qid = asn1.read.oid(pq.children[0]);
308
+ } catch (_e2) {
309
+ return;
310
+ }
311
+ fn(qid, pq.children[1], pi);
274
312
  });
275
313
  });
314
+ }
315
+ function _policyDisplayTexts(ctx) {
316
+ var out = [];
317
+ _eachPolicyQualifier(ctx, function (qid, value) {
318
+ if (qid !== OID_UNOTICE) return;
319
+ out = out.concat(pkix.userNoticeTexts(value));
320
+ });
276
321
  return out;
277
322
  }
278
323
  function _hasPolicyDisplayText(cert, ctx) { return _policyDisplayTexts(ctx).length > 0; }
@@ -544,7 +589,1103 @@ function _validityCeilingDays(notBefore) {
544
589
  return VALIDITY_SCHEDULE[VALIDITY_SCHEDULE.length - 1].maxDays;
545
590
  }
546
591
 
592
+ /** @internal All FOUR reserved policy identifiers of BR sec. 7.1.6.1, one per Subscriber
593
+ * Certificate type named in sec. 7.1.2.7.1: Domain, Organization and Individual Validated under
594
+ * the baseline-requirements arc, and Extended Validation under ev-guidelines. Sec. 7.1.2.7.9:
595
+ * "the Certificate Policies extension MUST contain exactly one Reserved Certificate Policy
596
+ * Identifier", so a list short of one type reports every certificate of that type as asserting
597
+ * none. */
598
+ var CABF_RESERVED_POLICIES = ["domain-validated", "organization-validated", "individual-validated",
599
+ "ev-guidelines"]
600
+ .map(function (n) { return oid.byName(n); }).filter(function (d) { return !!d; });
601
+
602
+ /** @internal BR sec. 7.1.2.7.10. Each is MUST NOT for a Subscriber Certificate. */
603
+ var CABF_FORBIDDEN_EKU = ["codeSigning", "emailProtection", "timeStamping", "ocspSigning",
604
+ "anyExtendedKeyUsage", "precertificateSigningCert"]
605
+ .map(function (n) { var d = oid.byName(n); return d ? { name: n, oid: d } : null; })
606
+ .filter(function (e) { return !!e; });
607
+
608
+ /** @internal BR sec. 7.1.2.7.7. Only these two accessMethods are permitted; any other MUST NOT. */
609
+ var CABF_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
610
+
611
+ /** @internal BR sec. 7.1.2.7.11, the two Key Usage tables. A bit marked "Permitted: N" for the
612
+ * certificate's key type may not be set. The tables differ by key type, which is the point: the
613
+ * same bit is permitted for one and forbidden for the other. */
614
+ var CABF_KU_FORBIDDEN_RSA = ["nonRepudiation", "keyAgreement", "keyCertSign", "cRLSign",
615
+ "encipherOnly", "decipherOnly"];
616
+ var CABF_KU_FORBIDDEN_ECC = ["nonRepudiation", "keyEncipherment", "dataEncipherment",
617
+ "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
618
+ var CABF_KU_RSA_BITS = ["digitalSignature", "keyEncipherment", "dataEncipherment"];
619
+
620
+ /** @internal BR sec. 7.1.2.7.12: only dNSName and iPAddress are permitted in a Subscriber
621
+ * Certificate's subjectAltName. The GeneralName context tag numbers are RFC 5280 sec. 4.2.1.6's. */
622
+ var CABF_SAN_PERMITTED_TAGS = [2, 7];
623
+ var CABF_SAN_TAG_NAMES = Object.assign(Object.create(null), {
624
+ 0: "otherName", 1: "rfc822Name", 2: "dNSName", 3: "x400Address", 4: "directoryName",
625
+ 5: "ediPartyName", 6: "uniformResourceIdentifier", 7: "iPAddress", 8: "registeredID",
626
+ });
627
+
628
+ /** @internal Which of the two Key Usage tables sec. 7.1.2.7.11 gives applies to this certificate,
629
+ * or null where neither does. The section opens "The acceptable Key Usage values vary based on
630
+ * whether the Certificate's subjectPublicKeyInfo identifies an RSA public key or an ECC public
631
+ * key", and gives a table for each and none for anything else. A key outside the two, such as an
632
+ * ML-DSA one, therefore has no table to be held to, and treating everything that is not ECC as
633
+ * RSA would report it against requirements written for a different algorithm. */
634
+ function _cabfKeyUsageTable(cert) {
635
+ var spki = cert.subjectPublicKeyInfo;
636
+ var name = spki && spki.algorithm && spki.algorithm.name;
637
+ if (name === "ecPublicKey") return "ecc";
638
+ if (RSA_KEY_ALGS[name]) return "rsa";
639
+ return null;
640
+ }
641
+ function _isEccKey(cert) { return _cabfKeyUsageTable(cert) === "ecc"; }
642
+ function _subjectIsEmpty(cert) {
643
+ var rdns = cert.subject && cert.subject.rdns;
644
+ return !(Array.isArray(rdns) && rdns.length);
645
+ }
646
+
647
+ /** @internal Every policy qualifier of every PolicyInformation, as its qualifier OID. */
648
+ function _policyQualifierIds(ctx) {
649
+ var out = [];
650
+ _eachPolicyQualifier(ctx, function (qid) { out.push(qid); });
651
+ return out;
652
+ }
653
+
654
+ /** @internal The S/MIME BR sec. 7.1.6.1 reserved policy identifiers, keyed by OID to the
655
+ * generation that OID declares. A subscriber certificate asserts exactly one, so reading it is
656
+ * how this profile learns which generation's table to hold the certificate to, and the presence
657
+ * of one is also what says the certificate is one of these at all. */
658
+ var SMIME_GENERATION_BY_POLICY = Object.create(null);
659
+ /** @internal The same twelve identifiers keyed to the certificate TYPE they declare. The tables of
660
+ * sec. 7.1.2.3 (l) are keyed on the type where the rest of the section is keyed on the
661
+ * generation, so both directions of the matrix are read. */
662
+ var SMIME_TYPE_BY_POLICY = Object.create(null);
663
+ ["mailbox", "organization", "sponsor", "individual"].forEach(function (type) {
664
+ ["legacy", "multipurpose", "strict"].forEach(function (gen) {
665
+ var d = oid.byName("smime-" + type + "-" + gen);
666
+ if (d) { SMIME_GENERATION_BY_POLICY[d] = gen; SMIME_TYPE_BY_POLICY[d] = type; }
667
+ });
668
+ });
669
+
670
+ /** @internal The keyUsage tables of sec. 7.1.2.3 (e). Each cell states its usage MODES (signing
671
+ * only, key management only, dual use), and each mode fixes bits with SHALL and admits others
672
+ * with MAY, so a conforming certificate asserts every bit of some one mode and no bit outside
673
+ * that same mode. Flattening a cell into one permitted set loses that: on RSA, digitalSignature
674
+ * with dataEncipherment sets only bits the cell names somewhere and still matches no mode,
675
+ * because dataEncipherment belongs to the key-management modes and those require keyEncipherment.
676
+ * The RSA cell is the one that also differs by GENERATION, Multipurpose and Legacy admitting
677
+ * dataEncipherment where Strict does not. The elliptic-curve cell names encipherOnly and
678
+ * decipherOnly only in the modes that require keyAgreement, which is how it states "only if
679
+ * keyAgreement is set". A certificate declaring no generation is held to the Strict cell, the
680
+ * narrowest of the three. */
681
+ function _smimeKuModes(kind, gen) {
682
+ var relaxed = gen === "multipurpose" || gen === "legacy";
683
+ if (kind === "rsa") {
684
+ return [
685
+ { required: ["digitalSignature"], optional: ["nonRepudiation"] },
686
+ { required: ["keyEncipherment"], optional: relaxed ? ["dataEncipherment"] : [] },
687
+ { required: ["digitalSignature", "keyEncipherment"],
688
+ optional: relaxed ? ["nonRepudiation", "dataEncipherment"] : ["nonRepudiation"] },
689
+ ];
690
+ }
691
+ if (kind === "ec") {
692
+ return [
693
+ { required: ["digitalSignature"], optional: ["nonRepudiation"] },
694
+ { required: ["keyAgreement"], optional: ["encipherOnly", "decipherOnly"] },
695
+ { required: ["digitalSignature", "keyAgreement"],
696
+ optional: ["nonRepudiation", "encipherOnly", "decipherOnly"] },
697
+ ];
698
+ }
699
+ if (kind === "eddsa" || kind === "mldsa") {
700
+ return [{ required: ["digitalSignature"], optional: ["nonRepudiation"] }];
701
+ }
702
+ if (kind === "mlkem") return [{ required: ["keyEncipherment"], optional: [] }];
703
+ return null;
704
+ }
705
+ /** @internal Every bit any mode of the cell names, which is the complement of "Other bit positions
706
+ * SHALL NOT be set". */
707
+ function _smimeKuNamedAnywhere(modes) {
708
+ var out = [];
709
+ modes.forEach(function (m) {
710
+ m.required.concat(m.optional).forEach(function (bit) {
711
+ if (out.indexOf(bit) === -1) out.push(bit);
712
+ });
713
+ });
714
+ return out;
715
+ }
716
+ function _smimeKuAsserted(value) {
717
+ var out = [];
718
+ KU_BIT_NAMES.forEach(function (bit) { if (value[bit] === true) out.push(bit); });
719
+ return out;
720
+ }
721
+ /** @internal Whether the asserted bits are exactly some mode: every bit that mode requires, and no
722
+ * bit it does not name. */
723
+ function _smimeKuMatchesAMode(modes, asserted) {
724
+ for (var i = 0; i < modes.length; i++) {
725
+ var allowed = modes[i].required.concat(modes[i].optional);
726
+ var ok = true;
727
+ modes[i].required.forEach(function (bit) { if (asserted.indexOf(bit) === -1) ok = false; });
728
+ asserted.forEach(function (bit) { if (allowed.indexOf(bit) === -1) ok = false; });
729
+ if (ok) return true;
730
+ }
731
+ return false;
732
+ }
733
+
734
+ /** @internal Sec. 7.1.2.3 (c) gives a URI scheme table for each of these two accessMethods and
735
+ * none for any other, so a scheme is measured on these and an unrecognized accessMethod has no
736
+ * table to be held to. */
737
+ var SMIME_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
738
+
739
+ /** @internal Sec. 7.1.2.3 (l): the Legal Entity Identifier table is keyed on the certificate type.
740
+ * The identifier is prohibited outright for two types; for Organization-validated the role
741
+ * identifier alone is prohibited, and Sponsor-validated admits both. */
742
+ var SMIME_LEI_EXTS = Object.assign(Object.create(null), { identifier: "lei-identifier", role: "lei-role" });
743
+ /** @internal Whether the cell for a certificate type ADMITS one of the two extensions. A cell that
744
+ * reads "Prohibited" carries no criticality sentence, so the criticality row is asked only where
745
+ * this is true and a prohibited extension breaks one clause rather than two. A certificate
746
+ * declaring no type names no cell and is held to neither row. */
747
+ function _smimeLeiAdmits(type, which) {
748
+ if (type === "sponsor") return true;
749
+ if (type === "organization") return which === "identifier";
750
+ return false;
751
+ }
752
+
753
+ /** @internal Sec. 7.1.2.3 (m): both Adobe extensions the table names, prohibited under Strict. */
754
+ var SMIME_ADOBE_EXTS = ["adobe-timestamp", "adobe-archive-rev-info"];
755
+
756
+ /** @internal Sec. 7.1.2.3 (f): these four SHALL NOT be present at any generation, where any other
757
+ * purpose is forbidden under Strict alone. */
758
+ var SMIME_FORBIDDEN_EKU = ["serverAuth", "codeSigning", "timeStamping", "anyExtendedKeyUsage"]
759
+ .map(function (n) { var d = oid.byName(n); return d ? { name: n, oid: d } : null; })
760
+ .filter(function (e) { return !!e; });
761
+
762
+ /** @internal "Effective July 15, 2025 S/MIME Subscriber Certificates SHALL NOT be issued using the
763
+ * Legacy Generation profiles" (sec. 7.1.6.1). Dated, so a certificate issued before it is not
764
+ * held to a rule that did not yet apply. */
765
+ var SMIME_LEGACY_RETIRED = new Date("2025-07-15T00:00:00Z");
766
+
767
+ function _smimeGeneration(ctx) {
768
+ var d = ctx.decode("certificatePolicies");
769
+ if (!d || !Array.isArray(d.value)) return null;
770
+ var found = null;
771
+ d.value.forEach(function (pi) {
772
+ var gen = SMIME_GENERATION_BY_POLICY[pi.policyIdentifier];
773
+ if (gen && found === null) found = gen;
774
+ });
775
+ return found;
776
+ }
777
+ /** @internal A reserved policy identifier alone does not make a certificate a SUBSCRIBER one: sec.
778
+ * 7.1.6.3 says a certificate issued to a Subordinate CA "SHALL include one or more explicit policy
779
+ * identifiers defined in Section 7.1.6.1", so an intermediate carries them too. Detection
780
+ * therefore asks for the identifier AND that the certificate is not a CA, or these subscriber-only
781
+ * rows would report an intermediate for asserting cA and keyCertSign and naming no mailbox.
782
+ * Naming the profile still overrides, which is how a malformed subscriber is checked. */
783
+ function _isSmime(cert, ctx) {
784
+ if (ctx.explicitSmimeProfile) return true;
785
+ return _smimeGeneration(ctx) !== null && !ctx.isCaCert;
786
+ }
787
+ function _smimeStrict(cert, ctx) { return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "strict"; }
788
+ /** @internal The cell every table in sec. 7.1.2.3 is read against. A certificate declaring no
789
+ * generation names no cell, and is held to Strict, the narrowest of the three. */
790
+ function _smimeGenerationOrStrict(ctx) {
791
+ var gen = _smimeGeneration(ctx);
792
+ return gen === null ? "strict" : gen;
793
+ }
794
+ /** @internal The certificate type the asserted reserved identifier declares, for the sec.
795
+ * 7.1.2.3 (l) tables. Null where none is asserted, which is every certificate the caller named
796
+ * the profile for rather than one that declared itself. */
797
+ function _smimeType(ctx) {
798
+ var d = ctx.decode("certificatePolicies");
799
+ if (!d || !Array.isArray(d.value)) return null;
800
+ var found = null;
801
+ for (var i = 0; i < d.value.length; i++) {
802
+ var t = SMIME_TYPE_BY_POLICY[d.value[i].policyIdentifier];
803
+ if (t && found === null) found = t;
804
+ }
805
+ return found;
806
+ }
807
+
808
+ /** @internal Which sec. 7.1.2.3 (e) table applies, from the subject key's own algorithm. A key
809
+ * outside them has no table to be held to. */
810
+ function _smimeKeyKind(cert) {
811
+ var spki = cert.subjectPublicKeyInfo;
812
+ var name = spki && spki.algorithm && spki.algorithm.name;
813
+ if (RSA_KEY_ALGS[name]) return "rsa";
814
+ if (name === "ecPublicKey") return "ec";
815
+ if (name === "Ed25519" || name === "Ed448") return "eddsa";
816
+ if (typeof name === "string" && name.indexOf("ml-dsa") !== -1) return "mldsa";
817
+ if (typeof name === "string" && name.indexOf("ml-kem") !== -1) return "mlkem";
818
+ return null;
819
+ }
820
+
821
+ /** @internal The URI of a GeneralName carried as raw DER, or null where it is not one. The
822
+ * cRLDistributionPoints decoder surfaces each fullName entry as bytes, since a distribution point
823
+ * may name any GeneralName type and the reader does not choose one for the caller. */
824
+ function _generalNameUri(der) {
825
+ var n;
826
+ // allow:swallow-unverified an entry that does not decode is answered by the extension-syntax row
827
+ try { n = asn1.decode(der); } catch (_e) { return null; }
828
+ if (!n || n.tagClass !== "context" || n.tagNumber !== 6) return null;
829
+ return n.content.toString("latin1");
830
+ }
831
+ /** @internal HTTP, not HTTPS. The S/MIME BR distinguishes the two by wording: the policy qualifier
832
+ * of sec. 7.1.2.3 (a) "SHALL be a HTTP or HTTPS URL", where the distribution point and access
833
+ * method tables say "SHALL have the URI scheme HTTP" and "Other schemes SHALL NOT be present".
834
+ * HTTPS is another scheme, and a revocation list fetched over TLS is a circular dependency.
835
+ * Having that scheme means being a URI of it, so both readings go through the one place that
836
+ * decides what an http or https URL is and differ only in which answer they accept. */
837
+ function _isHttpUri(u) {
838
+ return pkiBuild.httpUrlScheme(u) === "http";
839
+ }
840
+ /** @internal The policy-qualifier clause of sec. 7.1.2.3 (a) names both schemes, where the URI
841
+ * scheme tables name HTTP alone. It also asks for a URL rather than a scheme, so a bare
842
+ * "http://" naming no host does not satisfy it. The shape is the one a reader holds a URL it was
843
+ * given to, not the narrower one the builders hold what they emit to. */
844
+ function _isHttpOrHttpsUrl(u) {
845
+ return pkiBuild.httpUrlScheme(u) !== null;
846
+ }
847
+ /** @internal The accessLocation URIs of sec. 7.1.2.3 (c), grouped BY accessMethod. The clause
848
+ * gives id-ad-ocsp and id-ad-caIssuers a table each, where clause (b) gives the whole extension
849
+ * one, so each method's URIs answer that method's table alone and an accessMethod with no table
850
+ * is not measured. An accessLocation of another GeneralName type carries no URI to measure a
851
+ * scheme on. */
852
+ function _smimeAiaLocationsByMethod(ctx) {
853
+ var d = ctx.decode("authorityInfoAccess");
854
+ var out = Object.create(null);
855
+ if (!d || !Array.isArray(d.value)) return out;
856
+ for (var i = 0; i < d.value.length; i++) {
857
+ var ad = d.value[i];
858
+ if (SMIME_AIA_METHODS.indexOf(ad.accessMethod) === -1) continue;
859
+ if (!out[ad.accessMethod]) out[ad.accessMethod] = [];
860
+ /** @internal A location that is not a uniformResourceIdentifier carries no scheme, so it is
861
+ * kept as null rather than dropped: the method is still provided and still has no HTTP URI. */
862
+ var loc = ad.accessLocation;
863
+ out[ad.accessMethod].push(loc && loc.tag === 6 && typeof loc.value === "string" ? loc.value : null);
864
+ }
865
+ return out;
866
+ }
867
+ function _crldpUris(ctx) {
868
+ var d = ctx.decode("cRLDistributionPoints");
869
+ if (!d || !Array.isArray(d.value)) return [];
870
+ var out = [];
871
+ d.value.forEach(function (dp) {
872
+ var point = dp.distributionPoint;
873
+ if (!point || point.kind !== "fullName" || !Array.isArray(point.names)) return;
874
+ point.names.forEach(function (raw) {
875
+ var u = _generalNameUri(raw);
876
+ if (u !== null) out.push(u);
877
+ });
878
+ });
879
+ return out;
880
+ }
881
+
882
+ var SMIME_OID_SMTP_UTF8 = oid.byName("smtpUtf8Mailbox");
883
+
884
+ /** @internal The nine named keyUsage bits of RFC 5280 sec. 4.2.1.3, in their bit order. */
885
+ var KU_BIT_NAMES = ["digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
886
+ "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
887
+
888
+ /** @internal The type OID of an otherName GeneralName. Every otherName is a [0], so the type is
889
+ * the only thing telling one apart from another, and a decoded entry carries it on `value` where
890
+ * the reader recognized the form and as bytes where it did not. */
891
+ function _otherNameTypeOf(gn) {
892
+ if (gn && gn.value && typeof gn.value.typeId === "string") return gn.value.typeId;
893
+ if (gn && gn.value && typeof gn.value.type === "string") return gn.value.type;
894
+ var src = gn && (gn.bytes || gn.value);
895
+ if (!Buffer.isBuffer(src)) return null;
896
+ var n;
897
+ // allow:swallow-unverified an entry that does not decode is answered by the extension-syntax row
898
+ try { n = asn1.decode(src); } catch (_e) { return null; }
899
+ if (!n || !n.children || !n.children.length) return null;
900
+ // allow:swallow-unverified a first child that is not an OID is not an otherName type
901
+ try { return asn1.read.oid(n.children[0]); } catch (_e2) { return null; }
902
+ }
903
+
904
+ var CABF_SMIME_RULES = [
905
+ {
906
+ id: "lint/cabf-smime/policies-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
907
+ /** @internal SHOULD NOT, so a warning rather than an error: the clause states a preference and
908
+ * the certificate is still conformant against the MUSTs around it. */
909
+ message: "certificatePolicies should not be marked critical",
910
+ appliesTo: _isSmime,
911
+ check: function (cert, ctx) { var e = ctx.raw("certificatePolicies"); return (e && e.critical === true) ? true : null; },
912
+ },
913
+ {
914
+ id: "lint/cabf-smime/policies-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
915
+ /** @internal Reachable only where the caller named the profile: without the extension there is
916
+ * no reserved identifier, and the reserved identifier is what otherwise says the certificate
917
+ * is one of these. Naming the profile is the caller supplying that statement. */
918
+ message: "a subscriber certificate must carry a certificatePolicies extension",
919
+ appliesTo: _isSmime,
920
+ check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
921
+ },
922
+ {
923
+ id: "lint/cabf-smime/policies-reserved-identifier-count", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
924
+ /** @internal "It SHALL include exactly one of the reserved policyIdentifiers", so two is as
925
+ * much a fault as none and the count says which. A certificate's own generation is read from
926
+ * that identifier, so two of them also leaves the generation ambiguous. */
927
+ message: "certificatePolicies must include exactly one reserved policy identifier",
928
+ appliesTo: _isSmime,
929
+ check: function (cert, ctx) {
930
+ /** @internal An absent extension is the presence row's answer, not this one's, so it is
931
+ * skipped here and counted where it is present but names no reserved identifier. */
932
+ if (!ctx.raw("certificatePolicies")) return null;
933
+ var d = ctx.decode("certificatePolicies");
934
+ var list = (d && Array.isArray(d.value)) ? d.value : [];
935
+ var found = list.filter(function (pi) { return !!SMIME_GENERATION_BY_POLICY[pi.policyIdentifier]; });
936
+ return found.length === 1 ? null : { context: { count: found.length } };
937
+ },
938
+ },
939
+ {
940
+ id: "lint/cabf-smime/legacy-generation-retired", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.6.1",
941
+ message: "a subscriber certificate must not be issued under a Legacy generation profile",
942
+ appliesTo: _isSmime,
943
+ effectiveDate: SMIME_LEGACY_RETIRED,
944
+ check: function (cert, ctx) {
945
+ return _smimeGeneration(ctx) === "legacy" ? true : null;
946
+ },
947
+ },
948
+ {
949
+ id: "lint/cabf-smime/crldp-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
950
+ message: "a subscriber certificate must carry a cRLDistributionPoints extension",
951
+ appliesTo: _isSmime,
952
+ check: function (cert, ctx) { return ctx.raw("cRLDistributionPoints") ? null : true; },
953
+ },
954
+ {
955
+ id: "lint/cabf-smime/crldp-non-http-uri", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
956
+ /** @internal The scheme rule differs by generation: Strict and Multipurpose require EVERY
957
+ * uniformResourceIdentifier to be HTTP, where Legacy requires at least one and permits others
958
+ * beside it. One row measures both, because the generation decides which question to ask. */
959
+ message: "a cRLDistributionPoints URI does not meet the URI scheme rule for this generation",
960
+ appliesTo: _isSmime,
961
+ check: function (cert, ctx) {
962
+ /** @internal An extension carrying no URI at all fails both readings: every generation
963
+ * requires an HTTP one, so an empty list is a violation rather than nothing to measure. The
964
+ * presence row answers only for the extension being absent. */
965
+ if (!ctx.raw("cRLDistributionPoints")) return null;
966
+ var uris = _crldpUris(ctx);
967
+ var gen = _smimeGeneration(ctx);
968
+ if (gen === "legacy" || !uris.length) {
969
+ return uris.some(_isHttpUri) ? null : { context: { generation: gen || "strict", uris: uris } };
970
+ }
971
+ var bad = uris.filter(function (u) { return !_isHttpUri(u); });
972
+ return bad.length ? { context: { generation: gen, uris: bad } } : null;
973
+ },
974
+ },
975
+ {
976
+ id: "lint/cabf-smime/aia-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
977
+ message: "authorityInformationAccess must not be marked critical",
978
+ appliesTo: _isSmime,
979
+ check: function (cert, ctx) { var e = ctx.raw("authorityInfoAccess"); return (e && e.critical === true) ? true : null; },
980
+ },
981
+ {
982
+ id: "lint/cabf-smime/basic-constraints-ca-true", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (d)",
983
+ message: "a subscriber certificate's basicConstraints cA field must not be true",
984
+ appliesTo: _isSmime,
985
+ check: function (cert, ctx) {
986
+ var d = ctx.decode("basicConstraints");
987
+ return (d && d.value && d.value.cA === true) ? true : null;
988
+ },
989
+ },
990
+ {
991
+ id: "lint/cabf-smime/basic-constraints-path-len", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (d)",
992
+ message: "a subscriber certificate's basicConstraints must not carry a pathLenConstraint",
993
+ appliesTo: _isSmime,
994
+ check: function (cert, ctx) {
995
+ var d = ctx.decode("basicConstraints");
996
+ var pl = d && d.value ? d.value.pathLenConstraint : null;
997
+ return pl === null || pl === undefined ? null : { context: { pathLenConstraint: pl } };
998
+ },
999
+ },
1000
+ {
1001
+ id: "lint/cabf-smime/key-usage-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1002
+ message: "a subscriber certificate must carry a keyUsage extension",
1003
+ appliesTo: _isSmime,
1004
+ check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
1005
+ },
1006
+ {
1007
+ id: "lint/cabf-smime/key-usage-forbidden-bit", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1008
+ /** @internal "Other bit positions SHALL NOT be set", over a table that differs by key
1009
+ * algorithm: keyEncipherment belongs to the RSA table and keyAgreement to the EC one, so the
1010
+ * same bit is conformant on one key and not on the other. A key algorithm the section gives no
1011
+ * table for is held to neither. "Other bit positions" reaches past the nine RFC 5280 names as
1012
+ * well, so a bit set beyond them is reported for every key algorithm. */
1013
+ message: "a keyUsage bit this key algorithm's table does not permit is set",
1014
+ appliesTo: _isSmime,
1015
+ check: function (cert, ctx) {
1016
+ var d = ctx.decode("keyUsage");
1017
+ if (!d || !d.value) return null;
1018
+ var kind = _smimeKeyKind(cert);
1019
+ if (kind === null) return null;
1020
+ var modes = _smimeKuModes(kind, _smimeGeneration(ctx));
1021
+ if (modes === null) return null;
1022
+ var named = _smimeKuNamedAnywhere(modes);
1023
+ var out = [];
1024
+ KU_BIT_NAMES.forEach(function (bit) {
1025
+ if (d.value[bit] === true && named.indexOf(bit) === -1) {
1026
+ out.push({ context: { keyUsage: bit, keyAlgorithm: kind } });
1027
+ }
1028
+ });
1029
+ if (d.value.reservedBitsSet === true) {
1030
+ out.push({ context: { reservedBitSet: true, keyAlgorithm: kind } });
1031
+ }
1032
+ return out.length ? out : null;
1033
+ },
1034
+ },
1035
+ {
1036
+ id: "lint/cabf-smime/key-usage-matches-no-mode", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1037
+ /** @internal A cell is a set of modes rather than a set of bits, so bits that all appear
1038
+ * somewhere in the cell can still combine into no mode: digitalSignature with dataEncipherment
1039
+ * on RSA, or encipherOnly without keyAgreement on an elliptic curve. This row runs only where
1040
+ * every asserted bit is one the cell names, since a bit outside the cell is the row above. */
1041
+ message: "the keyUsage bits set match no usage mode of this key algorithm's table",
1042
+ appliesTo: _isSmime,
1043
+ check: function (cert, ctx) {
1044
+ var d = ctx.decode("keyUsage");
1045
+ if (!d || !d.value || d.value.reservedBitsSet === true) return null;
1046
+ var kind = _smimeKeyKind(cert);
1047
+ if (kind === null) return null;
1048
+ var gen = _smimeGeneration(ctx);
1049
+ var modes = _smimeKuModes(kind, gen);
1050
+ if (modes === null) return null;
1051
+ var asserted = _smimeKuAsserted(d.value);
1052
+ var named = _smimeKuNamedAnywhere(modes);
1053
+ for (var i = 0; i < asserted.length; i++) {
1054
+ if (named.indexOf(asserted[i]) === -1) return null;
1055
+ }
1056
+ return _smimeKuMatchesAMode(modes, asserted)
1057
+ ? null
1058
+ : { context: { keyAlgorithm: kind, generation: gen || "strict", keyUsage: asserted } };
1059
+ },
1060
+ },
1061
+ {
1062
+ id: "lint/cabf-smime/eku-missing-email-protection", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1063
+ message: "extKeyUsage must include id-kp-emailProtection",
1064
+ appliesTo: _isSmime,
1065
+ check: function (cert, ctx) {
1066
+ var d = ctx.decode("extKeyUsage");
1067
+ if (!d || !Array.isArray(d.value)) return true;
1068
+ return d.value.indexOf(oid.byName("emailProtection")) === -1 ? true : null;
1069
+ },
1070
+ },
1071
+ {
1072
+ id: "lint/cabf-smime/eku-forbidden-purpose", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1073
+ message: "a key purpose the profile forbids at every generation is present in extKeyUsage",
1074
+ appliesTo: _isSmime,
1075
+ check: function (cert, ctx) {
1076
+ var d = ctx.decode("extKeyUsage");
1077
+ if (!d || !Array.isArray(d.value)) return null;
1078
+ var out = [];
1079
+ SMIME_FORBIDDEN_EKU.forEach(function (e) {
1080
+ if (d.value.indexOf(e.oid) !== -1) out.push({ context: { keyPurpose: e.name, oid: e.oid } });
1081
+ });
1082
+ return out.length ? out : null;
1083
+ },
1084
+ },
1085
+ {
1086
+ id: "lint/cabf-smime/eku-extra-purpose-strict", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1087
+ /** @internal Strict alone says "Other values SHALL NOT be present"; Multipurpose and Legacy
1088
+ * say they MAY be. So this row is the one place the generation turns a permission into a
1089
+ * prohibition, and it runs only where the certificate declared Strict. The four values the
1090
+ * sentence after the table forbids at every generation are left to the row that owns them, so
1091
+ * one purpose draws one finding citing the narrower of the two rules it breaks. */
1092
+ message: "under the Strict generation, extKeyUsage must carry no purpose beyond id-kp-emailProtection",
1093
+ appliesTo: _smimeStrict,
1094
+ check: function (cert, ctx) {
1095
+ var d = ctx.decode("extKeyUsage");
1096
+ if (!d || !Array.isArray(d.value)) return null;
1097
+ var email = oid.byName("emailProtection");
1098
+ var extra = d.value.filter(function (o) {
1099
+ if (o === email) return false;
1100
+ for (var i = 0; i < SMIME_FORBIDDEN_EKU.length; i++) {
1101
+ if (SMIME_FORBIDDEN_EKU[i].oid === o) return false;
1102
+ }
1103
+ return true;
1104
+ });
1105
+ return extra.length ? { context: { purposes: extra } } : null;
1106
+ },
1107
+ },
1108
+ {
1109
+ id: "lint/cabf-smime/aki-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1110
+ message: "a subscriber certificate must carry an authorityKeyIdentifier extension",
1111
+ appliesTo: _isSmime,
1112
+ check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
1113
+ },
1114
+ {
1115
+ id: "lint/cabf-smime/aki-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1116
+ message: "authorityKeyIdentifier must not be marked critical",
1117
+ appliesTo: _isSmime,
1118
+ check: function (cert, ctx) { var e = ctx.raw("authorityKeyIdentifier"); return (e && e.critical === true) ? true : null; },
1119
+ },
1120
+ {
1121
+ id: "lint/cabf-smime/san-no-mailbox", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.4.2.1",
1122
+ /** @internal "SHALL contain at least one GeneralName entry of the following types: Rfc822Name
1123
+ * and/or otherName of type id-on-SmtpUTF8Mailbox". An otherName is read for its type OID,
1124
+ * since any otherName is a [0] and only that one carries a mailbox. */
1125
+ message: "subjectAltName must carry at least one rfc822Name or SmtpUTF8Mailbox otherName",
1126
+ appliesTo: _isSmime,
1127
+ check: function (cert, ctx) {
1128
+ var d = ctx.decode("subjectAltName");
1129
+ if (!d || !d.value || !Array.isArray(d.value.names)) return true;
1130
+ var found = d.value.names.some(function (gn) {
1131
+ if (gn.tagNumber === 1) return true;
1132
+ if (gn.tagNumber !== 0) return false;
1133
+ return _otherNameTypeOf(gn) === SMIME_OID_SMTP_UTF8;
1134
+ });
1135
+ return found ? null : true;
1136
+ },
1137
+ },
1138
+ {
1139
+ id: "lint/cabf-smime/ski-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (n)",
1140
+ message: "subjectKeyIdentifier must not be marked critical",
1141
+ appliesTo: _isSmime,
1142
+ check: function (cert, ctx) { var e = ctx.raw("subjectKeyIdentifier"); return (e && e.critical === true) ? true : null; },
1143
+ },
1144
+ {
1145
+ id: "lint/cabf-smime/smime-capabilities-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (i)",
1146
+ message: "smimeCapabilities must not be marked critical",
1147
+ appliesTo: _isSmime,
1148
+ check: function (cert, ctx) { var e = ctx.raw("smimeCapabilities"); return (e && e.critical === true) ? true : null; },
1149
+ },
1150
+ {
1151
+ id: "lint/cabf-smime/subject-directory-attributes-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (j)",
1152
+ /** @internal Prohibited under Strict and Multipurpose, permitted under Legacy. The generation
1153
+ * the certificate declares decides, so this runs at two of the three. */
1154
+ message: "subjectDirectoryAttributes is prohibited under the Strict and Multipurpose generations",
1155
+ appliesTo: function (cert, ctx) {
1156
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) !== "legacy";
1157
+ },
1158
+ check: function (cert, ctx) { return ctx.raw("subjectDirectoryAttributes") ? true : null; },
1159
+ },
1160
+ {
1161
+ id: "lint/cabf-smime/subject-directory-attributes-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (j)",
1162
+ /** @internal The criticality sentence sits in the Legacy row alone, which reads "MAY be present
1163
+ * and SHALL NOT be marked critical". The other row reads "Prohibited" and carries no
1164
+ * criticality sentence, so a critical extension there breaks one clause rather than two. */
1165
+ message: "subjectDirectoryAttributes must not be marked critical",
1166
+ appliesTo: function (cert, ctx) {
1167
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "legacy";
1168
+ },
1169
+ check: function (cert, ctx) {
1170
+ var e = ctx.raw("subjectDirectoryAttributes");
1171
+ return (e && e.critical === true) ? true : null;
1172
+ },
1173
+ },
1174
+ {
1175
+ id: "lint/cabf-smime/policies-cps-not-http-url", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1176
+ /** @internal "the value of the qualifier SHALL be a HTTP or HTTPS URL". This clause names both
1177
+ * schemes where the URI scheme tables of (b) and (c) name HTTP alone. */
1178
+ message: "an id-qt-cps policy qualifier must be a HTTP or HTTPS URL",
1179
+ appliesTo: _isSmime,
1180
+ check: function (cert, ctx) {
1181
+ var out = [];
1182
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1183
+ if (qid !== OID_CPS) return;
1184
+ var uri = null;
1185
+ // allow:swallow-unverified a CPSuri that will not read as a string is not a URL either, so it falls to the report below rather than out of the rule
1186
+ try {
1187
+ uri = asn1.read.string(value);
1188
+ } catch (_e) {
1189
+ void 0;
1190
+ }
1191
+ if (!_isHttpOrHttpsUrl(uri)) {
1192
+ out.push({ context: { policyIdentifier: pi.policyIdentifier, cps: uri } });
1193
+ }
1194
+ });
1195
+ return out.length ? out : null;
1196
+ },
1197
+ },
1198
+ {
1199
+ id: "lint/cabf-smime/policies-unotice-no-explicit-text", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1200
+ /** @internal "If a qualifier of type id-qt-unotice is included, then it SHALL contain
1201
+ * explicitText". Both halves of that sentence are rows, since a notice can fail either. */
1202
+ message: "an id-qt-unotice policy qualifier must contain explicitText",
1203
+ appliesTo: _isSmime,
1204
+ check: function (cert, ctx) {
1205
+ var out = [];
1206
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1207
+ if (qid !== OID_UNOTICE) return;
1208
+ var carries = pkix.userNoticeTexts(value).some(function (t) { return t.field === "explicitText"; });
1209
+ if (!carries) out.push({ context: { policyIdentifier: pi.policyIdentifier } });
1210
+ });
1211
+ return out.length ? out : null;
1212
+ },
1213
+ },
1214
+ {
1215
+ id: "lint/cabf-smime/policies-unotice-notice-ref", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1216
+ /** @internal "...and SHALL NOT contain noticeRef." */
1217
+ message: "an id-qt-unotice policy qualifier must not contain a noticeRef",
1218
+ appliesTo: _isSmime,
1219
+ check: function (cert, ctx) {
1220
+ var out = [];
1221
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1222
+ if (qid !== OID_UNOTICE) return;
1223
+ /** @internal NoticeReference requires its organization DisplayText, which userNoticeTexts
1224
+ * surfaces under that field name, so the field's presence is the noticeRef's presence. */
1225
+ var carries = pkix.userNoticeTexts(value).some(function (t) { return t.field === "organization"; });
1226
+ if (carries) out.push({ context: { policyIdentifier: pi.policyIdentifier } });
1227
+ });
1228
+ return out.length ? out : null;
1229
+ },
1230
+ },
1231
+ {
1232
+ id: "lint/cabf-smime/crldp-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
1233
+ message: "cRLDistributionPoints should not be marked critical",
1234
+ appliesTo: _isSmime,
1235
+ check: function (cert, ctx) {
1236
+ var e = ctx.raw("cRLDistributionPoints");
1237
+ return (e && e.critical === true) ? true : null;
1238
+ },
1239
+ },
1240
+ {
1241
+ id: "lint/cabf-smime/aia-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1242
+ /** @internal "(SHOULD be present)", where the TLS profile's own heading for the same extension
1243
+ * reads SHALL, so the two profiles report it at different strengths. */
1244
+ message: "a subscriber certificate should carry an authorityInformationAccess extension",
1245
+ appliesTo: _isSmime,
1246
+ check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
1247
+ },
1248
+ {
1249
+ id: "lint/cabf-smime/aia-ca-issuers-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1250
+ /** @internal "SHOULD contain at least one accessMethod value of type id-ad-caIssuers". Read
1251
+ * only where the extension is there at all, since its absence is the row above. */
1252
+ message: "authorityInformationAccess should carry an id-ad-caIssuers accessMethod",
1253
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1254
+ check: function (cert, ctx) {
1255
+ var d = ctx.decode("authorityInfoAccess");
1256
+ if (!d || !Array.isArray(d.value)) return null;
1257
+ var wanted = oid.byName("caIssuers");
1258
+ for (var i = 0; i < d.value.length; i++) {
1259
+ if (d.value[i].accessMethod === wanted) return null;
1260
+ }
1261
+ return true;
1262
+ },
1263
+ },
1264
+ {
1265
+ id: "lint/cabf-smime/aia-non-http-uri", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1266
+ /** @internal Strict and Multipurpose require every accessMethod URI to be HTTP, Legacy at
1267
+ * least one. Each accessMethod has its OWN table, so "at least one" is answered per method and
1268
+ * an HTTP URI on one does not satisfy the other's row. */
1269
+ message: "an authorityInformationAccess accessMethod URI does not meet the URI scheme rule for this generation",
1270
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1271
+ check: function (cert, ctx) {
1272
+ var byMethod = _smimeAiaLocationsByMethod(ctx);
1273
+ var gen = _smimeGeneration(ctx);
1274
+ var out = [];
1275
+ function report(method, offending) {
1276
+ out.push({ context: { generation: gen || "strict", accessMethod: oid.name(method) || method,
1277
+ uris: offending.filter(function (u) { return u !== null; }),
1278
+ nonUriLocations: offending.filter(function (u) { return u === null; }).length } });
1279
+ }
1280
+ SMIME_AIA_METHODS.forEach(function (method) {
1281
+ var locs = byMethod[method];
1282
+ if (!locs || !locs.length) return;
1283
+ if (gen === "legacy") {
1284
+ if (!locs.some(_isHttpUri)) report(method, locs);
1285
+ return;
1286
+ }
1287
+ var bad = locs.filter(function (u) { return !_isHttpUri(u); });
1288
+ if (bad.length) report(method, bad);
1289
+ });
1290
+ return out.length ? out : null;
1291
+ },
1292
+ },
1293
+ {
1294
+ id: "lint/cabf-smime/key-usage-not-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1295
+ message: "keyUsage should be marked critical",
1296
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("keyUsage"); },
1297
+ check: function (cert, ctx) {
1298
+ var e = ctx.raw("keyUsage");
1299
+ return (e && e.critical === true) ? null : true;
1300
+ },
1301
+ },
1302
+ {
1303
+ id: "lint/cabf-smime/aki-key-identifier-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1304
+ /** @internal "The keyIdentifier field SHALL be present", which is a field rule rather than the
1305
+ * extension-presence one above it: an authorityKeyIdentifier naming only an issuer and serial
1306
+ * satisfies that row and not this one. */
1307
+ message: "the authorityKeyIdentifier keyIdentifier field must be present",
1308
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityKeyIdentifier"); },
1309
+ check: function (cert, ctx) {
1310
+ var d = ctx.decode("authorityKeyIdentifier");
1311
+ if (!d || !d.value) return null;
1312
+ var kid = d.value.keyIdentifier;
1313
+ return (kid === null || kid === undefined) ? true : null;
1314
+ },
1315
+ },
1316
+ {
1317
+ id: "lint/cabf-smime/aki-issuer-or-serial-present", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1318
+ /** @internal "authorityCertIssuer and authorityCertSerialNumber fields SHALL NOT be present."
1319
+ * Either one alone is a violation, so both are reported by name. */
1320
+ message: "the authorityKeyIdentifier must not carry authorityCertIssuer or authorityCertSerialNumber",
1321
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityKeyIdentifier"); },
1322
+ check: function (cert, ctx) {
1323
+ var d = ctx.decode("authorityKeyIdentifier");
1324
+ if (!d || !d.value) return null;
1325
+ var out = [];
1326
+ ["authorityCertIssuer", "authorityCertSerialNumber"].forEach(function (f) {
1327
+ var v = d.value[f];
1328
+ if (v !== null && v !== undefined) out.push({ context: { field: f } });
1329
+ });
1330
+ return out.length ? out : null;
1331
+ },
1332
+ },
1333
+ {
1334
+ id: "lint/cabf-smime/san-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (h)",
1335
+ message: "a subscriber certificate must carry a subjectAltName extension",
1336
+ appliesTo: _isSmime,
1337
+ check: function (cert, ctx) { return ctx.raw("subjectAltName") ? null : true; },
1338
+ },
1339
+ {
1340
+ id: "lint/cabf-smime/san-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (h)",
1341
+ /** @internal "SHOULD NOT be marked critical unless the subject field is an empty sequence",
1342
+ * which is the one condition that lifts it, so the subject is read rather than assumed. */
1343
+ message: "subjectAltName should not be marked critical where the subject is not an empty sequence",
1344
+ appliesTo: _isSmime,
1345
+ check: function (cert, ctx) {
1346
+ var e = ctx.raw("subjectAltName");
1347
+ if (!e || e.critical !== true) return null;
1348
+ var rdns = cert.subject && cert.subject.rdns;
1349
+ var empty = !Array.isArray(rdns) || rdns.length === 0;
1350
+ return empty ? null : true;
1351
+ },
1352
+ },
1353
+ {
1354
+ id: "lint/cabf-smime/qc-statements-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (k)",
1355
+ message: "qcStatements must not be marked critical",
1356
+ appliesTo: _isSmime,
1357
+ check: function (cert, ctx) {
1358
+ var e = ctx.raw("qcStatements");
1359
+ return (e && e.critical === true) ? true : null;
1360
+ },
1361
+ },
1362
+ {
1363
+ id: "lint/cabf-smime/lei-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (l)",
1364
+ /** @internal The table is keyed on the certificate TYPE: Mailbox-validated and
1365
+ * Individual-validated admit neither identifier, Organization-validated admits the LEI but not
1366
+ * the role, and Sponsor-validated admits both. A certificate declaring no type is held to
1367
+ * none, since the table has no row to select. */
1368
+ message: "a Legal Entity Identifier extension is prohibited for this certificate type",
1369
+ appliesTo: _isSmime,
1370
+ check: function (cert, ctx) {
1371
+ var type = _smimeType(ctx);
1372
+ if (type === null) return null;
1373
+ var out = [];
1374
+ ["identifier", "role"].forEach(function (which) {
1375
+ if (_smimeLeiAdmits(type, which)) return;
1376
+ if (ctx.raw(SMIME_LEI_EXTS[which])) out.push({ context: { certificateType: type, extension: which } });
1377
+ });
1378
+ return out.length ? out : null;
1379
+ },
1380
+ },
1381
+ {
1382
+ id: "lint/cabf-smime/lei-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (l)",
1383
+ /** @internal Where the table admits one it reads "MAY be present and SHALL NOT be marked
1384
+ * critical", so criticality is fixed in the cells that admit it and nowhere else. */
1385
+ message: "a Legal Entity Identifier extension must not be marked critical",
1386
+ appliesTo: _isSmime,
1387
+ check: function (cert, ctx) {
1388
+ var type = _smimeType(ctx);
1389
+ if (type === null) return null;
1390
+ var out = [];
1391
+ ["identifier", "role"].forEach(function (which) {
1392
+ if (!_smimeLeiAdmits(type, which)) return;
1393
+ var e = ctx.raw(SMIME_LEI_EXTS[which]);
1394
+ if (e && e.critical === true) out.push({ context: { certificateType: type, extension: which } });
1395
+ });
1396
+ return out.length ? out : null;
1397
+ },
1398
+ },
1399
+ {
1400
+ id: "lint/cabf-smime/adobe-extension-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (m)",
1401
+ /** @internal Prohibited under Strict, permitted under Multipurpose and Legacy. */
1402
+ message: "an Adobe extension is prohibited under the Strict generation",
1403
+ appliesTo: function (cert, ctx) {
1404
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "strict";
1405
+ },
1406
+ check: function (cert, ctx) {
1407
+ var out = [];
1408
+ SMIME_ADOBE_EXTS.forEach(function (name) {
1409
+ if (ctx.raw(name)) out.push({ context: { extension: name } });
1410
+ });
1411
+ return out.length ? out : null;
1412
+ },
1413
+ },
1414
+ {
1415
+ id: "lint/cabf-smime/adobe-extension-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (m)",
1416
+ /** @internal The criticality sentence sits in the Multipurpose and Legacy row. The Strict row
1417
+ * reads "Prohibited" and carries none, so a critical extension there breaks one clause. */
1418
+ message: "an Adobe extension must not be marked critical",
1419
+ appliesTo: function (cert, ctx) {
1420
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) !== "strict";
1421
+ },
1422
+ check: function (cert, ctx) {
1423
+ var out = [];
1424
+ SMIME_ADOBE_EXTS.forEach(function (name) {
1425
+ var e = ctx.raw(name);
1426
+ if (e && e.critical === true) out.push({ context: { extension: name } });
1427
+ });
1428
+ return out.length ? out : null;
1429
+ },
1430
+ },
1431
+ {
1432
+ id: "lint/cabf-smime/ski-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (n)",
1433
+ /** @internal "(SHOULD be present)". The same clause adds that the value SHOULD be derived from
1434
+ * the public key, which names no derivation, so a certificate using one this cannot recompute
1435
+ * is conformant and no row reports it. */
1436
+ message: "a subscriber certificate should carry a subjectKeyIdentifier extension",
1437
+ appliesTo: _isSmime,
1438
+ check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
1439
+ },
1440
+ ];
1441
+
547
1442
  var CABF_TLS_RULES = [
1443
+ {
1444
+ id: "lint/cabf-tls/aia-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
1445
+ message: "a subscriber certificate must carry an authorityInformationAccess extension",
1446
+ appliesTo: _isTls,
1447
+ check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
1448
+ },
1449
+ {
1450
+ id: "lint/cabf-tls/aia-critical", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
1451
+ message: "authorityInformationAccess must not be marked critical",
1452
+ appliesTo: _isTls,
1453
+ check: function (cert, ctx) {
1454
+ var e = ctx.raw("authorityInfoAccess");
1455
+ return (e && e.critical === true) ? true : null;
1456
+ },
1457
+ },
1458
+ {
1459
+ id: "lint/cabf-tls/aia-forbidden-access-method", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.7",
1460
+ /** @internal The table permits id-ad-ocsp and id-ad-caIssuers and says of any other value
1461
+ * "No other accessMethods may be used". */
1462
+ message: "an authorityInformationAccess accessMethod other than id-ad-ocsp or id-ad-caIssuers is not permitted",
1463
+ appliesTo: _isTls,
1464
+ check: function (cert, ctx) {
1465
+ var d = ctx.decode("authorityInfoAccess");
1466
+ if (!d || !Array.isArray(d.value)) return null;
1467
+ var out = [];
1468
+ d.value.forEach(function (ad) {
1469
+ if (CABF_AIA_METHODS.indexOf(ad.accessMethod) === -1) {
1470
+ out.push({ context: { accessMethod: ad.accessMethod, name: oid.name(ad.accessMethod) || null } });
1471
+ }
1472
+ });
1473
+ return out.length ? out : null;
1474
+ },
1475
+ },
1476
+ {
1477
+ id: "lint/cabf-tls/aia-location-not-uri", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.7",
1478
+ /** @internal Each permitted accessMethod states uniformResourceIdentifier as its accessLocation
1479
+ * GeneralName type, and "each accessLocation MUST be encoded as the specified GeneralName type". */
1480
+ message: "an authorityInformationAccess accessLocation must be a uniformResourceIdentifier",
1481
+ appliesTo: _isTls,
1482
+ check: function (cert, ctx) {
1483
+ var d = ctx.decode("authorityInfoAccess");
1484
+ if (!d || !Array.isArray(d.value)) return null;
1485
+ var out = [];
1486
+ d.value.forEach(function (ad) {
1487
+ if (CABF_AIA_METHODS.indexOf(ad.accessMethod) === -1) return;
1488
+ var loc = ad.accessLocation;
1489
+ if (!loc || loc.tag !== 6) {
1490
+ out.push({ context: { accessMethod: oid.name(ad.accessMethod) || ad.accessMethod,
1491
+ generalNameTag: loc ? loc.tag : null } });
1492
+ }
1493
+ });
1494
+ return out.length ? out : null;
1495
+ },
1496
+ },
1497
+ {
1498
+ id: "lint/cabf-tls/policies-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
1499
+ message: "a subscriber certificate must carry a certificatePolicies extension",
1500
+ appliesTo: _isTls,
1501
+ check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
1502
+ },
1503
+ {
1504
+ id: "lint/cabf-tls/policies-any-policy", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.9",
1505
+ /** @internal "The anyPolicy Policy Identifier MUST NOT be present." */
1506
+ message: "certificatePolicies must not assert anyPolicy",
1507
+ appliesTo: _isTls,
1508
+ check: function (cert, ctx) {
1509
+ var d = ctx.decode("certificatePolicies");
1510
+ if (!d || !Array.isArray(d.value)) return null;
1511
+ return d.value.some(function (pi) { return pi.policyIdentifier === OID_ANY_POLICY; }) ? true : null;
1512
+ },
1513
+ },
1514
+ {
1515
+ id: "lint/cabf-tls/policies-reserved-identifier-count", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.9",
1516
+ /** @internal "Regardless of the order of PolicyInformation values, the Certificate Policies
1517
+ * extension MUST contain exactly one Reserved Certificate Policy Identifier." Exactly one, so
1518
+ * naming none and naming two are the same fault and the count says which. */
1519
+ message: "certificatePolicies must assert exactly one reserved certificate policy identifier",
1520
+ appliesTo: _isTls,
1521
+ check: function (cert, ctx) {
1522
+ var d = ctx.decode("certificatePolicies");
1523
+ if (!d || !Array.isArray(d.value)) return null;
1524
+ var found = d.value.filter(function (pi) {
1525
+ return CABF_RESERVED_POLICIES.indexOf(pi.policyIdentifier) !== -1;
1526
+ });
1527
+ return found.length === 1 ? null : { context: { count: found.length,
1528
+ identifiers: found.map(function (pi) { return pi.policyIdentifier; }) } };
1529
+ },
1530
+ },
1531
+ {
1532
+ id: "lint/cabf-tls/policy-qualifier-forbidden", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.9",
1533
+ /** @internal The permitted-qualifier table admits id-qt-cps and says of any other qualifier
1534
+ * MUST NOT, which includes the userNotice RFC 5280 otherwise allows. */
1535
+ message: "a certificatePolicies qualifier other than id-qt-cps is not permitted",
1536
+ appliesTo: _isTls,
1537
+ check: function (cert, ctx) {
1538
+ var out = [];
1539
+ _policyQualifierIds(ctx).forEach(function (qid) {
1540
+ if (qid !== OID_CPS) out.push({ context: { qualifier: qid, name: oid.name(qid) || null } });
1541
+ });
1542
+ return out.length ? out : null;
1543
+ },
1544
+ },
1545
+ {
1546
+ id: "lint/cabf-tls/eku-forbidden-purpose", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.10",
1547
+ message: "a key purpose the subscriber profile forbids is present in extKeyUsage",
1548
+ appliesTo: _isTls,
1549
+ check: function (cert, ctx) {
1550
+ var d = ctx.decode("extKeyUsage");
1551
+ if (!d || !Array.isArray(d.value)) return null;
1552
+ var out = [];
1553
+ CABF_FORBIDDEN_EKU.forEach(function (e) {
1554
+ if (d.value.indexOf(e.oid) !== -1) out.push({ context: { keyPurpose: e.name, oid: e.oid } });
1555
+ });
1556
+ return out.length ? out : null;
1557
+ },
1558
+ },
1559
+ {
1560
+ id: "lint/cabf-tls/name-constraints-present", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
1561
+ message: "a subscriber certificate must not carry a nameConstraints extension",
1562
+ appliesTo: _isTls,
1563
+ check: function (cert, ctx) { return ctx.raw("nameConstraints") ? true : null; },
1564
+ },
1565
+ {
1566
+ id: "lint/cabf-tls/basic-constraints-ca-true", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.8",
1567
+ message: "a subscriber certificate's basicConstraints cA must be FALSE",
1568
+ appliesTo: _isTls,
1569
+ check: function (cert, ctx) {
1570
+ var d = ctx.decode("basicConstraints");
1571
+ return (d && d.value && d.value.cA === true) ? true : null;
1572
+ },
1573
+ },
1574
+ {
1575
+ id: "lint/cabf-tls/basic-constraints-path-len", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.8",
1576
+ message: "a subscriber certificate's basicConstraints must not carry a pathLenConstraint",
1577
+ appliesTo: _isTls,
1578
+ check: function (cert, ctx) {
1579
+ var d = ctx.decode("basicConstraints");
1580
+ var pl = d && d.value ? d.value.pathLenConstraint : null;
1581
+ return pl === null || pl === undefined ? null : { context: { pathLenConstraint: pl } };
1582
+ },
1583
+ },
1584
+ {
1585
+ id: "lint/cabf-tls/key-usage-forbidden-bit", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.11",
1586
+ /** @internal The two Key Usage tables differ by key type, which is the whole point: the same
1587
+ * bit is permitted for one and forbidden for the other, so the key decides which table asks. */
1588
+ message: "a keyUsage bit the subscriber profile does not permit for this key type is set",
1589
+ appliesTo: _isTls,
1590
+ check: function (cert, ctx) {
1591
+ var d = ctx.decode("keyUsage");
1592
+ if (!d || !d.value) return null;
1593
+ var table = _cabfKeyUsageTable(cert);
1594
+ if (table === null) return null;
1595
+ var forbidden = table === "ecc" ? CABF_KU_FORBIDDEN_ECC : CABF_KU_FORBIDDEN_RSA;
1596
+ var out = [];
1597
+ forbidden.forEach(function (bit) {
1598
+ if (d.value[bit] === true) out.push({ context: { keyUsage: bit, keyType: table } });
1599
+ });
1600
+ return out.length ? out : null;
1601
+ },
1602
+ },
1603
+ {
1604
+ id: "lint/cabf-tls/key-usage-ecc-without-digital-signature", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.11",
1605
+ /** @internal The ECC table marks digitalSignature Required: MUST, where the RSA table marks it
1606
+ * SHOULD and admits a keyEncipherment-only certificate. */
1607
+ message: "an ECC subscriber certificate's keyUsage must assert digitalSignature",
1608
+ appliesTo: function (cert, ctx) { return _isTls(cert, ctx) && _isEccKey(cert); },
1609
+ check: function (cert, ctx) {
1610
+ var d = ctx.decode("keyUsage");
1611
+ if (!d || !d.value) return null;
1612
+ return d.value.digitalSignature === true ? null : true;
1613
+ },
1614
+ },
1615
+ {
1616
+ id: "lint/cabf-tls/key-usage-rsa-no-bit-set", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.11",
1617
+ /** @internal "At least one Key Usage MUST be set for RSA Public Keys." Measured over the bits
1618
+ * that table permits, since a forbidden bit being set is the row above's answer, not this one's. */
1619
+ message: "an RSA subscriber certificate's keyUsage must set at least one permitted bit",
1620
+ appliesTo: function (cert, ctx) { return _isTls(cert, ctx) && _cabfKeyUsageTable(cert) === "rsa"; },
1621
+ check: function (cert, ctx) {
1622
+ var d = ctx.decode("keyUsage");
1623
+ if (!d || !d.value) return null;
1624
+ var any = CABF_KU_RSA_BITS.some(function (bit) { return d.value[bit] === true; });
1625
+ return any ? null : true;
1626
+ },
1627
+ },
1628
+ {
1629
+ id: "lint/cabf-tls/san-forbidden-name-type", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.12",
1630
+ /** @internal The GeneralName table permits dNSName and iPAddress and marks every other type
1631
+ * Permitted: N, including the uniformResourceIdentifier and rfc822Name RFC 5280 allows. */
1632
+ message: "a subjectAltName entry of a type the subscriber profile does not permit is present",
1633
+ appliesTo: _isTls,
1634
+ check: function (cert, ctx) {
1635
+ var d = ctx.decode("subjectAltName");
1636
+ if (!d || !d.value || !Array.isArray(d.value.names)) return null;
1637
+ var out = [];
1638
+ d.value.names.forEach(function (gn) {
1639
+ if (CABF_SAN_PERMITTED_TAGS.indexOf(gn.tagNumber) === -1) {
1640
+ out.push({ context: { generalName: CABF_SAN_TAG_NAMES[gn.tagNumber] || null, tag: gn.tagNumber } });
1641
+ }
1642
+ });
1643
+ return out.length ? out : null;
1644
+ },
1645
+ },
1646
+ {
1647
+ id: "lint/cabf-tls/san-criticality-mismatch", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.12",
1648
+ /** @internal "If the subject field of the certificate is an empty SEQUENCE, this extension MUST
1649
+ * be marked critical ... Otherwise, this extension MUST NOT be marked critical." Both
1650
+ * directions are MUST, so one row measures the pair and the context says which way it failed. */
1651
+ message: "subjectAltName must be critical where the subject is empty and non-critical otherwise",
1652
+ appliesTo: _isTls,
1653
+ check: function (cert, ctx) {
1654
+ var e = ctx.raw("subjectAltName");
1655
+ if (!e) return null;
1656
+ var empty = _subjectIsEmpty(cert);
1657
+ var critical = e.critical === true;
1658
+ if (empty === critical) return null;
1659
+ return { context: { subjectEmpty: empty, critical: critical } };
1660
+ },
1661
+ },
1662
+ {
1663
+ id: "lint/cabf-tls/dnsname-trailing-dot", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.12",
1664
+ /** @internal "The zero-length Domain Label representing the root zone ... MUST NOT be included
1665
+ * (e.g. 'example.com' MUST be encoded as 'example.com' and MUST NOT be encoded as
1666
+ * 'example.com.')." */
1667
+ message: "a subjectAltName dNSName must not carry the root zone's zero-length label",
1668
+ appliesTo: _isTls,
1669
+ check: function (cert, ctx) {
1670
+ var out = [];
1671
+ _sanDnsNames(ctx).forEach(function (name) {
1672
+ if (typeof name === "string" && name.length > 0 && name.charAt(name.length - 1) === ".") {
1673
+ out.push({ context: { dnsName: name } });
1674
+ }
1675
+ });
1676
+ return out.length ? out : null;
1677
+ },
1678
+ },
1679
+ {
1680
+ id: "lint/cabf-tls/ski-present", severity: "notice", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
1681
+ /** @internal NOT RECOMMENDED here, which is the opposite of what RFC 5280 sec. 4.2.1.2 asks of
1682
+ * an end-entity certificate. Both readings ship, each under the profile that states it, so a
1683
+ * caller running one profile is not told the other's answer. Graded notice because the clause
1684
+ * is NOT RECOMMENDED rather than MUST NOT. */
1685
+ message: "a subscriber certificate's subjectKeyIdentifier is NOT RECOMMENDED by this profile",
1686
+ appliesTo: _isTls,
1687
+ check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? true : null; },
1688
+ },
548
1689
  {
549
1690
  id: "lint/cabf-tls/san-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.4.2.1",
550
1691
  message: "a TLS server certificate must include a usable subjectAltName extension",
@@ -1932,28 +3073,32 @@ var CABF_TLS_CSR_RULE_IDS = [
1932
3073
  "lint/cabf-tls/weak-key",
1933
3074
  ];
1934
3075
 
3076
+ /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried. Every row in
3077
+ * every profile below asks for an extension BY NAME, and the shared decoder returns null both for
3078
+ * one that is absent and for one whose value does not decode, so without this row a malformed
3079
+ * value reads as absence and passes every row that asks for it. The certificate's own row is
3080
+ * reused rather than restated, and it is folded into each named profile rather than into the
3081
+ * *_RULES arrays, which `ALL_RULES` concatenates and would otherwise report it twice. */
3082
+ var RFC5280_EXTENSION_SYNTAX_RULES = RFC5280_RULES.filter(function (r) {
3083
+ return r.id === "lint/rfc5280/extension-undecodable";
3084
+ });
1935
3085
  var PROFILES = Object.assign(Object.create(null), {
1936
3086
  "rfc5280": RFC5280_RULES,
1937
- "rfc9881": RFC9881_RULES,
1938
- "rfc9909": RFC9909_RULES,
1939
- "rfc9935": RFC9935_RULES,
1940
- "cabf-tls": CABF_TLS_RULES,
3087
+ "rfc9881": RFC9881_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3088
+ "rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3089
+ "rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3090
+ "cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3091
+ "cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
1941
3092
  });
1942
- var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES).concat(CABF_TLS_RULES);
3093
+ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
3094
+ .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES);
1943
3095
 
1944
3096
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
1945
3097
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
1946
3098
  });
1947
- /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried, and a
1948
- * requested extension whose value does not decode is the same fault in a request as in a
1949
- * certificate. The certificate's own row reads `extensions` and the request view supplies it, so
1950
- * the row is reused rather than restated. Without it a malformed requested value counted as a
1951
- * passing check, because the shared decoder returns null and every row that asks by name reads
1952
- * that null as absence. */
1953
- var RFC5280_CSR_RULES = RFC5280_RULES.filter(function (r) {
1954
- return r.id === "lint/rfc5280/extension-undecodable";
1955
- });
1956
- var CSR_STRUCTURAL_RULES = RFC2986_CSR_RULES.concat(RFC5280_CSR_RULES);
3099
+ /** @internal A requested extension whose value does not decode is the same fault in a request as
3100
+ * in a certificate, so the request profiles take the same row the certificate profiles do. */
3101
+ var CSR_STRUCTURAL_RULES = RFC2986_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES);
1957
3102
  var CSR_PROFILES = Object.assign(Object.create(null), {
1958
3103
  "rfc2986": CSR_STRUCTURAL_RULES,
1959
3104
  "cabf-tls": CSR_STRUCTURAL_RULES.concat(CABF_TLS_CSR_RULES),
@@ -2057,7 +3202,9 @@ function _ingestCsr(input) {
2057
3202
  var derived = _derivedCsr(input);
2058
3203
  if (derived) return { csr: derived };
2059
3204
  var der;
2060
- if (Buffer.isBuffer(input)) der = input;
3205
+ var bytes = _derBytesOf(input);
3206
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3207
+ if (bytes !== null) der = bytes.der;
2061
3208
  else if (typeof input === "string") {
2062
3209
  try { der = csrSchema.pemDecode(input); }
2063
3210
  catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER certification request", context: { code: e.code } } }; }
@@ -2104,7 +3251,13 @@ function _applyThreshold(report, severity) {
2104
3251
  * raises a typed `LintError`.
2105
3252
  *
2106
3253
  * @opts profile One of `pki.lint.profiles()` (default runs every profile). Selecting
2107
- * `"cabf-tls"` lints the input AS a TLS server certificate.
3254
+ * `"cabf-tls"` lints the input AS a TLS SUBSCRIBER certificate, which is an
3255
+ * assertion about the input rather than a question asked of it: the section
3256
+ * 7.1.2.7 rows then apply whatever the certificate says it is, so naming it for
3257
+ * a CA certificate reports that CA against subscriber requirements. The CA
3258
+ * profiles of section 7.1.2.10 are a separate set this does not yet carry. Left
3259
+ * unnamed, a certificate is held to those rows only where it asserts
3260
+ * id-kp-serverAuth and is not a CA.
2108
3261
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
2109
3262
  * `worst` always reflect the complete, unfiltered result.
2110
3263
  * @example
@@ -2160,7 +3313,9 @@ function _ingestCrl(input) {
2160
3313
  var derived = _derivedCrl(input);
2161
3314
  if (derived) return { crl: derived };
2162
3315
  var der;
2163
- if (Buffer.isBuffer(input)) der = input;
3316
+ var bytes = _derBytesOf(input);
3317
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3318
+ if (bytes !== null) der = bytes.der;
2164
3319
  else if (typeof input === "string") {
2165
3320
  try { der = crlSchema.pemDecode(input); }
2166
3321
  catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER CRL", context: { code: e.code } } }; }
@@ -2333,7 +3488,9 @@ var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
2333
3488
  * responses. Bytes and PEM take the fatal path below, never a throw. */
2334
3489
  function _ingestOcsp(input) {
2335
3490
  var der;
2336
- if (Buffer.isBuffer(input)) der = input;
3491
+ var bytes = _derBytesOf(input);
3492
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3493
+ if (bytes !== null) der = bytes.der;
2337
3494
  else if (typeof input === "string") {
2338
3495
  try { der = ocspSchema.pemDecode(input); }
2339
3496
  catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER OCSP response", context: { code: e.code } } }; }
@@ -2466,6 +3623,10 @@ var BY_ARTIFACT = Object.assign(Object.create(null), {
2466
3623
  });
2467
3624
 
2468
3625
  function rules(profile, artifact) {
3626
+ /** @internal Every lint verb reads "all" and "default" as naming no profile, so the enumerator
3627
+ * reads them the same way: a caller who linted under one of those names and then asked which
3628
+ * rows ran would otherwise be refused the answer by the same registry that ran them. */
3629
+ if (profile === "all" || profile === "default") profile = null;
2469
3630
  if (artifact != null && !Object.prototype.hasOwnProperty.call(BY_ARTIFACT, artifact)) {
2470
3631
  throw _cfg("lint/bad-input", "unknown lint artifact \"" + artifact +
2471
3632
  "\" (known: " + Object.keys(BY_ARTIFACT).join(", ") + ")");