@blamejs/pki 0.8.17 → 0.8.19

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,14 @@ 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
+ /** @internal A code signing subscriber certificate names its kind through a reserved policy
158
+ * identifier the same way, so this flag only records that the caller also named the profile. */
159
+ explicitCsProfile: profile === "cabf-cs",
160
+ isCaCert: isCa,
131
161
  isTlsServerCert: explicitTls || (_hasEku(cert, "serverAuth") && !isCa),
132
162
  decode: function (name) { return _decodeOrNull(cert, name); },
133
163
  raw: function (name) { return _findRaw(cert, name); },
@@ -257,23 +287,40 @@ function _hasControlChar(str) {
257
287
  }
258
288
  return false;
259
289
  }
260
- function _policyDisplayTexts(ctx) {
290
+ /** @internal Every policy qualifier of every PolicyInformation, handed to `fn` as its qualifier
291
+ * OID, its value node, and the PolicyInformation carrying it. The decoder surfaces the qualifiers
292
+ * as raw DER, because a qualifier may carry any type and the reader does not choose one for the
293
+ * caller, so every walk re-decodes bytes that already passed the policy-qualifier door. */
294
+ function _eachPolicyQualifier(ctx, fn) {
261
295
  var d = ctx.decode("certificatePolicies");
262
- if (!d || !Array.isArray(d.value)) return [];
263
- var out = [];
296
+ if (!d || !Array.isArray(d.value)) return;
264
297
  d.value.forEach(function (pi) {
265
298
  if (!pi.qualifiersBytes || !pi.qualifiersBytes.length) return;
266
299
  var quals;
267
300
  // allow:swallow-unverified re-decoding bytes that already decoded under assertPolicyQualifiers cannot throw
268
- try { quals = asn1.decode(pi.qualifiersBytes).children; } catch (_e) { return; }
301
+ try {
302
+ quals = asn1.decode(pi.qualifiersBytes).children;
303
+ } catch (_e) {
304
+ return;
305
+ }
269
306
  (quals || []).forEach(function (pq) {
270
307
  var qid;
271
308
  // allow:swallow-unverified assertPolicyQualifiers already read this OID, so re-reading it cannot throw
272
- try { qid = asn1.read.oid(pq.children[0]); } catch (_e2) { return; }
273
- if (qid !== OID_UNOTICE) return;
274
- out = out.concat(pkix.userNoticeTexts(pq.children[1]));
309
+ try {
310
+ qid = asn1.read.oid(pq.children[0]);
311
+ } catch (_e2) {
312
+ return;
313
+ }
314
+ fn(qid, pq.children[1], pi);
275
315
  });
276
316
  });
317
+ }
318
+ function _policyDisplayTexts(ctx) {
319
+ var out = [];
320
+ _eachPolicyQualifier(ctx, function (qid, value) {
321
+ if (qid !== OID_UNOTICE) return;
322
+ out = out.concat(pkix.userNoticeTexts(value));
323
+ });
277
324
  return out;
278
325
  }
279
326
  function _hasPolicyDisplayText(cert, ctx) { return _policyDisplayTexts(ctx).length > 0; }
@@ -600,25 +647,1235 @@ function _subjectIsEmpty(cert) {
600
647
  return !(Array.isArray(rdns) && rdns.length);
601
648
  }
602
649
 
603
- /** @internal Every policy qualifier of every PolicyInformation, as its qualifier OID. The bytes
604
- * are re-decoded the way the display-text walk above re-decodes them. */
650
+ /** @internal Every policy qualifier of every PolicyInformation, as its qualifier OID. */
605
651
  function _policyQualifierIds(ctx) {
652
+ var out = [];
653
+ _eachPolicyQualifier(ctx, function (qid) { out.push(qid); });
654
+ return out;
655
+ }
656
+
657
+ /** @internal The S/MIME BR sec. 7.1.6.1 reserved policy identifiers, keyed by OID to the
658
+ * generation that OID declares. A subscriber certificate asserts exactly one, so reading it is
659
+ * how this profile learns which generation's table to hold the certificate to, and the presence
660
+ * of one is also what says the certificate is one of these at all. */
661
+ var SMIME_GENERATION_BY_POLICY = Object.create(null);
662
+ /** @internal The same twelve identifiers keyed to the certificate TYPE they declare. The tables of
663
+ * sec. 7.1.2.3 (l) are keyed on the type where the rest of the section is keyed on the
664
+ * generation, so both directions of the matrix are read. */
665
+ var SMIME_TYPE_BY_POLICY = Object.create(null);
666
+ ["mailbox", "organization", "sponsor", "individual"].forEach(function (type) {
667
+ ["legacy", "multipurpose", "strict"].forEach(function (gen) {
668
+ var d = oid.byName("smime-" + type + "-" + gen);
669
+ if (d) { SMIME_GENERATION_BY_POLICY[d] = gen; SMIME_TYPE_BY_POLICY[d] = type; }
670
+ });
671
+ });
672
+
673
+ /** @internal The keyUsage tables of sec. 7.1.2.3 (e). Each cell states its usage MODES (signing
674
+ * only, key management only, dual use), and each mode fixes bits with SHALL and admits others
675
+ * with MAY, so a conforming certificate asserts every bit of some one mode and no bit outside
676
+ * that same mode. Flattening a cell into one permitted set loses that: on RSA, digitalSignature
677
+ * with dataEncipherment sets only bits the cell names somewhere and still matches no mode,
678
+ * because dataEncipherment belongs to the key-management modes and those require keyEncipherment.
679
+ * The RSA cell is the one that also differs by GENERATION, Multipurpose and Legacy admitting
680
+ * dataEncipherment where Strict does not. The elliptic-curve cell names encipherOnly and
681
+ * decipherOnly only in the modes that require keyAgreement, which is how it states "only if
682
+ * keyAgreement is set". A certificate declaring no generation is held to the Strict cell, the
683
+ * narrowest of the three. */
684
+ function _smimeKuModes(kind, gen) {
685
+ var relaxed = gen === "multipurpose" || gen === "legacy";
686
+ if (kind === "rsa") {
687
+ return [
688
+ { required: ["digitalSignature"], optional: ["nonRepudiation"] },
689
+ { required: ["keyEncipherment"], optional: relaxed ? ["dataEncipherment"] : [] },
690
+ { required: ["digitalSignature", "keyEncipherment"],
691
+ optional: relaxed ? ["nonRepudiation", "dataEncipherment"] : ["nonRepudiation"] },
692
+ ];
693
+ }
694
+ if (kind === "ec") {
695
+ return [
696
+ { required: ["digitalSignature"], optional: ["nonRepudiation"] },
697
+ { required: ["keyAgreement"], optional: ["encipherOnly", "decipherOnly"] },
698
+ { required: ["digitalSignature", "keyAgreement"],
699
+ optional: ["nonRepudiation", "encipherOnly", "decipherOnly"] },
700
+ ];
701
+ }
702
+ if (kind === "eddsa" || kind === "mldsa") {
703
+ return [{ required: ["digitalSignature"], optional: ["nonRepudiation"] }];
704
+ }
705
+ if (kind === "mlkem") return [{ required: ["keyEncipherment"], optional: [] }];
706
+ return null;
707
+ }
708
+ /** @internal Every bit any mode of the cell names, which is the complement of "Other bit positions
709
+ * SHALL NOT be set". */
710
+ function _smimeKuNamedAnywhere(modes) {
711
+ var out = [];
712
+ modes.forEach(function (m) {
713
+ m.required.concat(m.optional).forEach(function (bit) {
714
+ if (out.indexOf(bit) === -1) out.push(bit);
715
+ });
716
+ });
717
+ return out;
718
+ }
719
+ function _smimeKuAsserted(value) {
720
+ var out = [];
721
+ KU_BIT_NAMES.forEach(function (bit) { if (value[bit] === true) out.push(bit); });
722
+ return out;
723
+ }
724
+ /** @internal Whether the asserted bits are exactly some mode: every bit that mode requires, and no
725
+ * bit it does not name. */
726
+ function _smimeKuMatchesAMode(modes, asserted) {
727
+ for (var i = 0; i < modes.length; i++) {
728
+ var allowed = modes[i].required.concat(modes[i].optional);
729
+ var ok = true;
730
+ modes[i].required.forEach(function (bit) { if (asserted.indexOf(bit) === -1) ok = false; });
731
+ asserted.forEach(function (bit) { if (allowed.indexOf(bit) === -1) ok = false; });
732
+ if (ok) return true;
733
+ }
734
+ return false;
735
+ }
736
+
737
+ /** @internal Code Signing BR sec. 7.1.6.1 reserves three policy identifiers, keyed here to the KIND
738
+ * of certificate each declares. The EV and the non-EV code signing identifiers declare the same
739
+ * kind, because sec. 7.1.2.3 (f) asks one question of both; only the timestamping identifier takes
740
+ * the other arm. */
741
+ var CS_KIND_BY_POLICY = Object.create(null);
742
+ [["code-signing", "code-signing"], ["code-signing-ev", "code-signing"],
743
+ ["code-signing-timestamping", "timestamping"]].forEach(function (row) {
744
+ var dotted = oid.byName(row[0]);
745
+ if (dotted) CS_KIND_BY_POLICY[dotted] = row[1];
746
+ });
747
+ /** @internal "Effective September 15, 2026 a Certificate issued to a Subscriber MUST contain
748
+ * exactly one of the reserved policy OIDs specified in Section 7.1.6.1" (sec. 7.1.6.4). Dated, so
749
+ * a certificate issued before it is not held to a rule that did not yet apply. */
750
+ var CS_RESERVED_POLICY_REQUIRED = new Date("2026-09-15T00:00:00Z");
751
+ /** @internal Sec. 6.3.2 states three ceilings: 39 months for a code signing certificate issued
752
+ * before this date, 460 days for one issued on or after it, and 135 months for a timestamp
753
+ * certificate whichever side of it. */
754
+ var CS_VALIDITY_DAYS_START = new Date("2026-03-01T00:00:00Z");
755
+ /** @internal Sec. 6.1.5.2, the curves, the modulus floor and the two DSA parameter options for a
756
+ * subscriber key. The TLS profile names the same three curves from its own document; these are
757
+ * this one's. The section names RSA, ECDSA and DSA, so each family is read on its own terms and a
758
+ * family it does not name is what the row reports. */
759
+ var CS_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
760
+ var CS_RSA_MIN_BITS = 3072;
761
+ var CS_DSA_L = 2048;
762
+ var CS_DSA_N = [224, 256];
763
+ /** @internal The DSA key length L and modulus length N, read as the bit lengths of p and q in the
764
+ * Dss-Parms the AlgorithmIdentifier carries. */
765
+ function _dsaParamBits(spki) {
766
+ try {
767
+ var params = spki.algorithm && spki.algorithm.parameters;
768
+ if (!Buffer.isBuffer(params)) return null;
769
+ var seq = asn1.decode(params);
770
+ if (!seq.children || seq.children.length !== 3) return null;
771
+ var p = asn1.read.integer(seq.children[0]);
772
+ var q = asn1.read.integer(seq.children[1]);
773
+ asn1.read.integer(seq.children[2]);
774
+ if (p <= 0n || q <= 0n) return null;
775
+ return { L: p.toString(2).length, N: q.toString(2).length };
776
+ } catch (_e) { return null; }
777
+ }
778
+ /** @internal Sec. 7.1.2.3 (c) names these two accessMethods and no other. */
779
+ var CS_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
780
+ /** @internal Sec. 7.1.2.3 (f): forbidden at every kind, and the three the clause names as MAY
781
+ * beside id-kp-codeSigning, which the discouraged-purpose row passes over. */
782
+ var CS_FORBIDDEN_EKU = ["anyExtendedKeyUsage", "serverAuth"].map(function (n) { return oid.byName(n); });
783
+ var CS_PERMITTED_EXTRA_EKU = ["ms-lifetime-signing", "emailProtection", "ms-document-signing",
784
+ "ms-document-signing-as-printed"].map(function (n) { return oid.byName(n); });
785
+ /** @internal Sec. 7.1.2.3 (e) names these two as MUST NOT, and every other bit beside
786
+ * digitalSignature as SHOULD NOT, so one sentence becomes two rows at two strengths. */
787
+ var CS_FORBIDDEN_KU = ["keyCertSign", "cRLSign"];
788
+
789
+ /** @internal The kind the asserted reserved identifier declares, or null where none is asserted. */
790
+ function _csKind(ctx) {
606
791
  var d = ctx.decode("certificatePolicies");
792
+ if (!d || !Array.isArray(d.value)) return null;
793
+ var found = null;
794
+ for (var i = 0; i < d.value.length; i++) {
795
+ var k = CS_KIND_BY_POLICY[d.value[i].policyIdentifier];
796
+ if (k && found === null) found = k;
797
+ }
798
+ return found;
799
+ }
800
+ /** @internal Sec. 7.1.6.3 gives a subordinate CA these identifiers too, so detection asks for the
801
+ * identifier AND that the certificate is not a CA, as the S/MIME profile does and for the same
802
+ * reason. Naming the profile overrides, which is how a malformed subscriber is checked. */
803
+ function _isCs(cert, ctx) {
804
+ if (ctx.explicitCsProfile) return true;
805
+ return _csKind(ctx) !== null && !ctx.isCaCert;
806
+ }
807
+ /** @internal A certificate declaring no kind is read as a code signing one, which is the arm the
808
+ * section states first and the only arm a caller who named the profile without a reserved
809
+ * identifier can have meant. */
810
+ function _isCsTimestamp(cert, ctx) { return _isCs(cert, ctx) && _csKind(ctx) === "timestamping"; }
811
+ function _isCsCodeSigning(cert, ctx) { return _isCs(cert, ctx) && _csKind(ctx) !== "timestamping"; }
812
+ /** @internal The accessLocation URIs of the two accessMethods sec. 7.1.2.3 (c) names, flat: unlike
813
+ * the S/MIME clause this one states one rule for both, so they are not grouped by method. A
814
+ * location that is not a uniformResourceIdentifier is kept as null, since the method is still
815
+ * provided and still carries no HTTP URL. */
816
+ function _csAiaLocations(ctx) {
817
+ var d = ctx.decode("authorityInfoAccess");
607
818
  if (!d || !Array.isArray(d.value)) return [];
608
819
  var out = [];
820
+ for (var i = 0; i < d.value.length; i++) {
821
+ var ad = d.value[i];
822
+ if (CS_AIA_METHODS.indexOf(ad.accessMethod) === -1) continue;
823
+ var loc = ad.accessLocation;
824
+ out.push(loc && loc.tag === 6 && typeof loc.value === "string" ? loc.value : null);
825
+ }
826
+ return out;
827
+ }
828
+
829
+ /** @internal Sec. 7.1.2.3 (c) gives a URI scheme table for each of these two accessMethods and
830
+ * none for any other, so a scheme is measured on these and an unrecognized accessMethod has no
831
+ * table to be held to. */
832
+ var SMIME_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
833
+
834
+ /** @internal Sec. 7.1.2.3 (l): the Legal Entity Identifier table is keyed on the certificate type.
835
+ * The identifier is prohibited outright for two types; for Organization-validated the role
836
+ * identifier alone is prohibited, and Sponsor-validated admits both. */
837
+ var SMIME_LEI_EXTS = Object.assign(Object.create(null), { identifier: "lei-identifier", role: "lei-role" });
838
+ /** @internal Whether the cell for a certificate type ADMITS one of the two extensions. A cell that
839
+ * reads "Prohibited" carries no criticality sentence, so the criticality row is asked only where
840
+ * this is true and a prohibited extension breaks one clause rather than two. A certificate
841
+ * declaring no type names no cell and is held to neither row. */
842
+ function _smimeLeiAdmits(type, which) {
843
+ if (type === "sponsor") return true;
844
+ if (type === "organization") return which === "identifier";
845
+ return false;
846
+ }
847
+
848
+ /** @internal Sec. 7.1.2.3 (m): both Adobe extensions the table names, prohibited under Strict. */
849
+ var SMIME_ADOBE_EXTS = ["adobe-timestamp", "adobe-archive-rev-info"];
850
+
851
+ /** @internal Sec. 7.1.2.3 (f): these four SHALL NOT be present at any generation, where any other
852
+ * purpose is forbidden under Strict alone. */
853
+ var SMIME_FORBIDDEN_EKU = ["serverAuth", "codeSigning", "timeStamping", "anyExtendedKeyUsage"]
854
+ .map(function (n) { var d = oid.byName(n); return d ? { name: n, oid: d } : null; })
855
+ .filter(function (e) { return !!e; });
856
+
857
+ /** @internal "Effective July 15, 2025 S/MIME Subscriber Certificates SHALL NOT be issued using the
858
+ * Legacy Generation profiles" (sec. 7.1.6.1). Dated, so a certificate issued before it is not
859
+ * held to a rule that did not yet apply. */
860
+ var SMIME_LEGACY_RETIRED = new Date("2025-07-15T00:00:00Z");
861
+
862
+ function _smimeGeneration(ctx) {
863
+ var d = ctx.decode("certificatePolicies");
864
+ if (!d || !Array.isArray(d.value)) return null;
865
+ var found = null;
609
866
  d.value.forEach(function (pi) {
610
- if (!pi.qualifiersBytes || !pi.qualifiersBytes.length) return;
611
- var quals;
612
- // allow:swallow-unverified bytes that already decoded under the policy-qualifier door cannot throw
613
- try { quals = asn1.decode(pi.qualifiersBytes).children; } catch (_e) { return; }
614
- (quals || []).forEach(function (pq) {
615
- // allow:swallow-unverified the same door already read this OID
616
- try { out.push(asn1.read.oid(pq.children[0])); } catch (_e2) { void 0; }
867
+ var gen = SMIME_GENERATION_BY_POLICY[pi.policyIdentifier];
868
+ if (gen && found === null) found = gen;
869
+ });
870
+ return found;
871
+ }
872
+ /** @internal A reserved policy identifier alone does not make a certificate a SUBSCRIBER one: sec.
873
+ * 7.1.6.3 says a certificate issued to a Subordinate CA "SHALL include one or more explicit policy
874
+ * identifiers defined in Section 7.1.6.1", so an intermediate carries them too. Detection
875
+ * therefore asks for the identifier AND that the certificate is not a CA, or these subscriber-only
876
+ * rows would report an intermediate for asserting cA and keyCertSign and naming no mailbox.
877
+ * Naming the profile still overrides, which is how a malformed subscriber is checked. */
878
+ function _isSmime(cert, ctx) {
879
+ if (ctx.explicitSmimeProfile) return true;
880
+ return _smimeGeneration(ctx) !== null && !ctx.isCaCert;
881
+ }
882
+ function _smimeStrict(cert, ctx) { return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "strict"; }
883
+ /** @internal The cell every table in sec. 7.1.2.3 is read against. A certificate declaring no
884
+ * generation names no cell, and is held to Strict, the narrowest of the three. */
885
+ function _smimeGenerationOrStrict(ctx) {
886
+ var gen = _smimeGeneration(ctx);
887
+ return gen === null ? "strict" : gen;
888
+ }
889
+ /** @internal The certificate type the asserted reserved identifier declares, for the sec.
890
+ * 7.1.2.3 (l) tables. Null where none is asserted, which is every certificate the caller named
891
+ * the profile for rather than one that declared itself. */
892
+ function _smimeType(ctx) {
893
+ var d = ctx.decode("certificatePolicies");
894
+ if (!d || !Array.isArray(d.value)) return null;
895
+ var found = null;
896
+ for (var i = 0; i < d.value.length; i++) {
897
+ var t = SMIME_TYPE_BY_POLICY[d.value[i].policyIdentifier];
898
+ if (t && found === null) found = t;
899
+ }
900
+ return found;
901
+ }
902
+
903
+ /** @internal Which sec. 7.1.2.3 (e) table applies, from the subject key's own algorithm. A key
904
+ * outside them has no table to be held to. */
905
+ function _smimeKeyKind(cert) {
906
+ var spki = cert.subjectPublicKeyInfo;
907
+ var name = spki && spki.algorithm && spki.algorithm.name;
908
+ if (RSA_KEY_ALGS[name]) return "rsa";
909
+ if (name === "ecPublicKey") return "ec";
910
+ if (name === "Ed25519" || name === "Ed448") return "eddsa";
911
+ if (typeof name === "string" && name.indexOf("ml-dsa") !== -1) return "mldsa";
912
+ if (typeof name === "string" && name.indexOf("ml-kem") !== -1) return "mlkem";
913
+ return null;
914
+ }
915
+
916
+ /** @internal The URI of a GeneralName carried as raw DER, or null where it is not one. The
917
+ * cRLDistributionPoints decoder surfaces each fullName entry as bytes, since a distribution point
918
+ * may name any GeneralName type and the reader does not choose one for the caller. */
919
+ function _generalNameUri(der) {
920
+ var n;
921
+ // allow:swallow-unverified an entry that does not decode is answered by the extension-syntax row
922
+ try { n = asn1.decode(der); } catch (_e) { return null; }
923
+ if (!n || n.tagClass !== "context" || n.tagNumber !== 6) return null;
924
+ return n.content.toString("latin1");
925
+ }
926
+ /** @internal HTTP, not HTTPS. The S/MIME BR distinguishes the two by wording: the policy qualifier
927
+ * of sec. 7.1.2.3 (a) "SHALL be a HTTP or HTTPS URL", where the distribution point and access
928
+ * method tables say "SHALL have the URI scheme HTTP" and "Other schemes SHALL NOT be present".
929
+ * HTTPS is another scheme, and a revocation list fetched over TLS is a circular dependency.
930
+ * Having that scheme means being a URI of it, so both readings go through the one place that
931
+ * decides what an http or https URL is and differ only in which answer they accept. */
932
+ function _isHttpUri(u) {
933
+ return pkiBuild.httpUrlScheme(u) === "http";
934
+ }
935
+ /** @internal The policy-qualifier clause of sec. 7.1.2.3 (a) names both schemes, where the URI
936
+ * scheme tables name HTTP alone. It also asks for a URL rather than a scheme, so a bare
937
+ * "http://" naming no host does not satisfy it. The shape is the one a reader holds a URL it was
938
+ * given to, not the narrower one the builders hold what they emit to. */
939
+ function _isHttpOrHttpsUrl(u) {
940
+ return pkiBuild.httpUrlScheme(u) !== null;
941
+ }
942
+ /** @internal The accessLocation URIs of sec. 7.1.2.3 (c), grouped BY accessMethod. The clause
943
+ * gives id-ad-ocsp and id-ad-caIssuers a table each, where clause (b) gives the whole extension
944
+ * one, so each method's URIs answer that method's table alone and an accessMethod with no table
945
+ * is not measured. An accessLocation of another GeneralName type carries no URI to measure a
946
+ * scheme on. */
947
+ function _smimeAiaLocationsByMethod(ctx) {
948
+ var d = ctx.decode("authorityInfoAccess");
949
+ var out = Object.create(null);
950
+ if (!d || !Array.isArray(d.value)) return out;
951
+ for (var i = 0; i < d.value.length; i++) {
952
+ var ad = d.value[i];
953
+ if (SMIME_AIA_METHODS.indexOf(ad.accessMethod) === -1) continue;
954
+ if (!out[ad.accessMethod]) out[ad.accessMethod] = [];
955
+ /** @internal A location that is not a uniformResourceIdentifier carries no scheme, so it is
956
+ * kept as null rather than dropped: the method is still provided and still has no HTTP URI. */
957
+ var loc = ad.accessLocation;
958
+ out[ad.accessMethod].push(loc && loc.tag === 6 && typeof loc.value === "string" ? loc.value : null);
959
+ }
960
+ return out;
961
+ }
962
+ function _crldpUris(ctx) {
963
+ var d = ctx.decode("cRLDistributionPoints");
964
+ if (!d || !Array.isArray(d.value)) return [];
965
+ var out = [];
966
+ d.value.forEach(function (dp) {
967
+ var point = dp.distributionPoint;
968
+ if (!point || point.kind !== "fullName" || !Array.isArray(point.names)) return;
969
+ point.names.forEach(function (raw) {
970
+ var u = _generalNameUri(raw);
971
+ if (u !== null) out.push(u);
617
972
  });
618
973
  });
619
974
  return out;
620
975
  }
621
976
 
977
+ var SMIME_OID_SMTP_UTF8 = oid.byName("smtpUtf8Mailbox");
978
+
979
+ /** @internal The nine named keyUsage bits of RFC 5280 sec. 4.2.1.3, in their bit order. */
980
+ var KU_BIT_NAMES = ["digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
981
+ "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
982
+
983
+ /** @internal The type OID of an otherName GeneralName. Every otherName is a [0], so the type is
984
+ * the only thing telling one apart from another, and a decoded entry carries it on `value` where
985
+ * the reader recognized the form and as bytes where it did not. */
986
+ function _otherNameTypeOf(gn) {
987
+ if (gn && gn.value && typeof gn.value.typeId === "string") return gn.value.typeId;
988
+ if (gn && gn.value && typeof gn.value.type === "string") return gn.value.type;
989
+ var src = gn && (gn.bytes || gn.value);
990
+ if (!Buffer.isBuffer(src)) return null;
991
+ var n;
992
+ // allow:swallow-unverified an entry that does not decode is answered by the extension-syntax row
993
+ try { n = asn1.decode(src); } catch (_e) { return null; }
994
+ if (!n || !n.children || !n.children.length) return null;
995
+ // allow:swallow-unverified a first child that is not an OID is not an otherName type
996
+ try { return asn1.read.oid(n.children[0]); } catch (_e2) { return null; }
997
+ }
998
+
999
+ var CABF_SMIME_RULES = [
1000
+ {
1001
+ id: "lint/cabf-smime/policies-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1002
+ /** @internal SHOULD NOT, so a warning rather than an error: the clause states a preference and
1003
+ * the certificate is still conformant against the MUSTs around it. */
1004
+ message: "certificatePolicies should not be marked critical",
1005
+ appliesTo: _isSmime,
1006
+ check: function (cert, ctx) { var e = ctx.raw("certificatePolicies"); return (e && e.critical === true) ? true : null; },
1007
+ },
1008
+ {
1009
+ id: "lint/cabf-smime/policies-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1010
+ /** @internal Reachable only where the caller named the profile: without the extension there is
1011
+ * no reserved identifier, and the reserved identifier is what otherwise says the certificate
1012
+ * is one of these. Naming the profile is the caller supplying that statement. */
1013
+ message: "a subscriber certificate must carry a certificatePolicies extension",
1014
+ appliesTo: _isSmime,
1015
+ check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
1016
+ },
1017
+ {
1018
+ id: "lint/cabf-smime/policies-reserved-identifier-count", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1019
+ /** @internal "It SHALL include exactly one of the reserved policyIdentifiers", so two is as
1020
+ * much a fault as none and the count says which. A certificate's own generation is read from
1021
+ * that identifier, so two of them also leaves the generation ambiguous. */
1022
+ message: "certificatePolicies must include exactly one reserved policy identifier",
1023
+ appliesTo: _isSmime,
1024
+ check: function (cert, ctx) {
1025
+ /** @internal An absent extension is the presence row's answer, not this one's, so it is
1026
+ * skipped here and counted where it is present but names no reserved identifier. */
1027
+ if (!ctx.raw("certificatePolicies")) return null;
1028
+ var d = ctx.decode("certificatePolicies");
1029
+ var list = (d && Array.isArray(d.value)) ? d.value : [];
1030
+ var found = list.filter(function (pi) { return !!SMIME_GENERATION_BY_POLICY[pi.policyIdentifier]; });
1031
+ return found.length === 1 ? null : { context: { count: found.length } };
1032
+ },
1033
+ },
1034
+ {
1035
+ id: "lint/cabf-smime/legacy-generation-retired", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.6.1",
1036
+ message: "a subscriber certificate must not be issued under a Legacy generation profile",
1037
+ appliesTo: _isSmime,
1038
+ effectiveDate: SMIME_LEGACY_RETIRED,
1039
+ check: function (cert, ctx) {
1040
+ return _smimeGeneration(ctx) === "legacy" ? true : null;
1041
+ },
1042
+ },
1043
+ {
1044
+ id: "lint/cabf-smime/crldp-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
1045
+ message: "a subscriber certificate must carry a cRLDistributionPoints extension",
1046
+ appliesTo: _isSmime,
1047
+ check: function (cert, ctx) { return ctx.raw("cRLDistributionPoints") ? null : true; },
1048
+ },
1049
+ {
1050
+ id: "lint/cabf-smime/crldp-non-http-uri", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
1051
+ /** @internal The scheme rule differs by generation: Strict and Multipurpose require EVERY
1052
+ * uniformResourceIdentifier to be HTTP, where Legacy requires at least one and permits others
1053
+ * beside it. One row measures both, because the generation decides which question to ask. */
1054
+ message: "a cRLDistributionPoints URI does not meet the URI scheme rule for this generation",
1055
+ appliesTo: _isSmime,
1056
+ check: function (cert, ctx) {
1057
+ /** @internal An extension carrying no URI at all fails both readings: every generation
1058
+ * requires an HTTP one, so an empty list is a violation rather than nothing to measure. The
1059
+ * presence row answers only for the extension being absent. */
1060
+ if (!ctx.raw("cRLDistributionPoints")) return null;
1061
+ var uris = _crldpUris(ctx);
1062
+ var gen = _smimeGeneration(ctx);
1063
+ if (gen === "legacy" || !uris.length) {
1064
+ return uris.some(_isHttpUri) ? null : { context: { generation: gen || "strict", uris: uris } };
1065
+ }
1066
+ var bad = uris.filter(function (u) { return !_isHttpUri(u); });
1067
+ return bad.length ? { context: { generation: gen, uris: bad } } : null;
1068
+ },
1069
+ },
1070
+ {
1071
+ id: "lint/cabf-smime/aia-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1072
+ message: "authorityInformationAccess must not be marked critical",
1073
+ appliesTo: _isSmime,
1074
+ check: function (cert, ctx) { var e = ctx.raw("authorityInfoAccess"); return (e && e.critical === true) ? true : null; },
1075
+ },
1076
+ {
1077
+ id: "lint/cabf-smime/basic-constraints-ca-true", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (d)",
1078
+ message: "a subscriber certificate's basicConstraints cA field must not be true",
1079
+ appliesTo: _isSmime,
1080
+ check: function (cert, ctx) {
1081
+ var d = ctx.decode("basicConstraints");
1082
+ return (d && d.value && d.value.cA === true) ? true : null;
1083
+ },
1084
+ },
1085
+ {
1086
+ id: "lint/cabf-smime/basic-constraints-path-len", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (d)",
1087
+ message: "a subscriber certificate's basicConstraints must not carry a pathLenConstraint",
1088
+ appliesTo: _isSmime,
1089
+ check: function (cert, ctx) {
1090
+ var d = ctx.decode("basicConstraints");
1091
+ var pl = d && d.value ? d.value.pathLenConstraint : null;
1092
+ return pl === null || pl === undefined ? null : { context: { pathLenConstraint: pl } };
1093
+ },
1094
+ },
1095
+ {
1096
+ id: "lint/cabf-smime/key-usage-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1097
+ message: "a subscriber certificate must carry a keyUsage extension",
1098
+ appliesTo: _isSmime,
1099
+ check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
1100
+ },
1101
+ {
1102
+ id: "lint/cabf-smime/key-usage-forbidden-bit", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1103
+ /** @internal "Other bit positions SHALL NOT be set", over a table that differs by key
1104
+ * algorithm: keyEncipherment belongs to the RSA table and keyAgreement to the EC one, so the
1105
+ * same bit is conformant on one key and not on the other. A key algorithm the section gives no
1106
+ * table for is held to neither. "Other bit positions" reaches past the nine RFC 5280 names as
1107
+ * well, so a bit set beyond them is reported for every key algorithm. */
1108
+ message: "a keyUsage bit this key algorithm's table does not permit is set",
1109
+ appliesTo: _isSmime,
1110
+ check: function (cert, ctx) {
1111
+ var d = ctx.decode("keyUsage");
1112
+ if (!d || !d.value) return null;
1113
+ var kind = _smimeKeyKind(cert);
1114
+ if (kind === null) return null;
1115
+ var modes = _smimeKuModes(kind, _smimeGeneration(ctx));
1116
+ if (modes === null) return null;
1117
+ var named = _smimeKuNamedAnywhere(modes);
1118
+ var out = [];
1119
+ KU_BIT_NAMES.forEach(function (bit) {
1120
+ if (d.value[bit] === true && named.indexOf(bit) === -1) {
1121
+ out.push({ context: { keyUsage: bit, keyAlgorithm: kind } });
1122
+ }
1123
+ });
1124
+ if (d.value.reservedBitsSet === true) {
1125
+ out.push({ context: { reservedBitSet: true, keyAlgorithm: kind } });
1126
+ }
1127
+ return out.length ? out : null;
1128
+ },
1129
+ },
1130
+ {
1131
+ id: "lint/cabf-smime/key-usage-matches-no-mode", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1132
+ /** @internal A cell is a set of modes rather than a set of bits, so bits that all appear
1133
+ * somewhere in the cell can still combine into no mode: digitalSignature with dataEncipherment
1134
+ * on RSA, or encipherOnly without keyAgreement on an elliptic curve. This row runs only where
1135
+ * every asserted bit is one the cell names, since a bit outside the cell is the row above. */
1136
+ message: "the keyUsage bits set match no usage mode of this key algorithm's table",
1137
+ appliesTo: _isSmime,
1138
+ check: function (cert, ctx) {
1139
+ var d = ctx.decode("keyUsage");
1140
+ if (!d || !d.value || d.value.reservedBitsSet === true) return null;
1141
+ var kind = _smimeKeyKind(cert);
1142
+ if (kind === null) return null;
1143
+ var gen = _smimeGeneration(ctx);
1144
+ var modes = _smimeKuModes(kind, gen);
1145
+ if (modes === null) return null;
1146
+ var asserted = _smimeKuAsserted(d.value);
1147
+ var named = _smimeKuNamedAnywhere(modes);
1148
+ for (var i = 0; i < asserted.length; i++) {
1149
+ if (named.indexOf(asserted[i]) === -1) return null;
1150
+ }
1151
+ return _smimeKuMatchesAMode(modes, asserted)
1152
+ ? null
1153
+ : { context: { keyAlgorithm: kind, generation: gen || "strict", keyUsage: asserted } };
1154
+ },
1155
+ },
1156
+ {
1157
+ id: "lint/cabf-smime/eku-missing-email-protection", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1158
+ message: "extKeyUsage must include id-kp-emailProtection",
1159
+ appliesTo: _isSmime,
1160
+ check: function (cert, ctx) {
1161
+ var d = ctx.decode("extKeyUsage");
1162
+ if (!d || !Array.isArray(d.value)) return true;
1163
+ return d.value.indexOf(oid.byName("emailProtection")) === -1 ? true : null;
1164
+ },
1165
+ },
1166
+ {
1167
+ id: "lint/cabf-smime/eku-forbidden-purpose", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1168
+ message: "a key purpose the profile forbids at every generation is present in extKeyUsage",
1169
+ appliesTo: _isSmime,
1170
+ check: function (cert, ctx) {
1171
+ var d = ctx.decode("extKeyUsage");
1172
+ if (!d || !Array.isArray(d.value)) return null;
1173
+ var out = [];
1174
+ SMIME_FORBIDDEN_EKU.forEach(function (e) {
1175
+ if (d.value.indexOf(e.oid) !== -1) out.push({ context: { keyPurpose: e.name, oid: e.oid } });
1176
+ });
1177
+ return out.length ? out : null;
1178
+ },
1179
+ },
1180
+ {
1181
+ id: "lint/cabf-smime/eku-extra-purpose-strict", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (f)",
1182
+ /** @internal Strict alone says "Other values SHALL NOT be present"; Multipurpose and Legacy
1183
+ * say they MAY be. So this row is the one place the generation turns a permission into a
1184
+ * prohibition, and it runs only where the certificate declared Strict. The four values the
1185
+ * sentence after the table forbids at every generation are left to the row that owns them, so
1186
+ * one purpose draws one finding citing the narrower of the two rules it breaks. */
1187
+ message: "under the Strict generation, extKeyUsage must carry no purpose beyond id-kp-emailProtection",
1188
+ appliesTo: _smimeStrict,
1189
+ check: function (cert, ctx) {
1190
+ var d = ctx.decode("extKeyUsage");
1191
+ if (!d || !Array.isArray(d.value)) return null;
1192
+ var email = oid.byName("emailProtection");
1193
+ var extra = d.value.filter(function (o) {
1194
+ if (o === email) return false;
1195
+ for (var i = 0; i < SMIME_FORBIDDEN_EKU.length; i++) {
1196
+ if (SMIME_FORBIDDEN_EKU[i].oid === o) return false;
1197
+ }
1198
+ return true;
1199
+ });
1200
+ return extra.length ? { context: { purposes: extra } } : null;
1201
+ },
1202
+ },
1203
+ {
1204
+ id: "lint/cabf-smime/aki-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1205
+ message: "a subscriber certificate must carry an authorityKeyIdentifier extension",
1206
+ appliesTo: _isSmime,
1207
+ check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
1208
+ },
1209
+ {
1210
+ id: "lint/cabf-smime/aki-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1211
+ message: "authorityKeyIdentifier must not be marked critical",
1212
+ appliesTo: _isSmime,
1213
+ check: function (cert, ctx) { var e = ctx.raw("authorityKeyIdentifier"); return (e && e.critical === true) ? true : null; },
1214
+ },
1215
+ {
1216
+ id: "lint/cabf-smime/san-no-mailbox", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.4.2.1",
1217
+ /** @internal "SHALL contain at least one GeneralName entry of the following types: Rfc822Name
1218
+ * and/or otherName of type id-on-SmtpUTF8Mailbox". An otherName is read for its type OID,
1219
+ * since any otherName is a [0] and only that one carries a mailbox. */
1220
+ message: "subjectAltName must carry at least one rfc822Name or SmtpUTF8Mailbox otherName",
1221
+ appliesTo: _isSmime,
1222
+ check: function (cert, ctx) {
1223
+ var d = ctx.decode("subjectAltName");
1224
+ if (!d || !d.value || !Array.isArray(d.value.names)) return true;
1225
+ var found = d.value.names.some(function (gn) {
1226
+ if (gn.tagNumber === 1) return true;
1227
+ if (gn.tagNumber !== 0) return false;
1228
+ return _otherNameTypeOf(gn) === SMIME_OID_SMTP_UTF8;
1229
+ });
1230
+ return found ? null : true;
1231
+ },
1232
+ },
1233
+ {
1234
+ id: "lint/cabf-smime/ski-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (n)",
1235
+ message: "subjectKeyIdentifier must not be marked critical",
1236
+ appliesTo: _isSmime,
1237
+ check: function (cert, ctx) { var e = ctx.raw("subjectKeyIdentifier"); return (e && e.critical === true) ? true : null; },
1238
+ },
1239
+ {
1240
+ id: "lint/cabf-smime/smime-capabilities-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (i)",
1241
+ message: "smimeCapabilities must not be marked critical",
1242
+ appliesTo: _isSmime,
1243
+ check: function (cert, ctx) { var e = ctx.raw("smimeCapabilities"); return (e && e.critical === true) ? true : null; },
1244
+ },
1245
+ {
1246
+ id: "lint/cabf-smime/subject-directory-attributes-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (j)",
1247
+ /** @internal Prohibited under Strict and Multipurpose, permitted under Legacy. The generation
1248
+ * the certificate declares decides, so this runs at two of the three. */
1249
+ message: "subjectDirectoryAttributes is prohibited under the Strict and Multipurpose generations",
1250
+ appliesTo: function (cert, ctx) {
1251
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) !== "legacy";
1252
+ },
1253
+ check: function (cert, ctx) { return ctx.raw("subjectDirectoryAttributes") ? true : null; },
1254
+ },
1255
+ {
1256
+ id: "lint/cabf-smime/subject-directory-attributes-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (j)",
1257
+ /** @internal The criticality sentence sits in the Legacy row alone, which reads "MAY be present
1258
+ * and SHALL NOT be marked critical". The other row reads "Prohibited" and carries no
1259
+ * criticality sentence, so a critical extension there breaks one clause rather than two. */
1260
+ message: "subjectDirectoryAttributes must not be marked critical",
1261
+ appliesTo: function (cert, ctx) {
1262
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "legacy";
1263
+ },
1264
+ check: function (cert, ctx) {
1265
+ var e = ctx.raw("subjectDirectoryAttributes");
1266
+ return (e && e.critical === true) ? true : null;
1267
+ },
1268
+ },
1269
+ {
1270
+ id: "lint/cabf-smime/policies-cps-not-http-url", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1271
+ /** @internal "the value of the qualifier SHALL be a HTTP or HTTPS URL". This clause names both
1272
+ * schemes where the URI scheme tables of (b) and (c) name HTTP alone. */
1273
+ message: "an id-qt-cps policy qualifier must be a HTTP or HTTPS URL",
1274
+ appliesTo: _isSmime,
1275
+ check: function (cert, ctx) {
1276
+ var out = [];
1277
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1278
+ if (qid !== OID_CPS) return;
1279
+ var uri = null;
1280
+ // 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
1281
+ try {
1282
+ uri = asn1.read.string(value);
1283
+ } catch (_e) {
1284
+ void 0;
1285
+ }
1286
+ if (!_isHttpOrHttpsUrl(uri)) {
1287
+ out.push({ context: { policyIdentifier: pi.policyIdentifier, cps: uri } });
1288
+ }
1289
+ });
1290
+ return out.length ? out : null;
1291
+ },
1292
+ },
1293
+ {
1294
+ id: "lint/cabf-smime/policies-unotice-no-explicit-text", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1295
+ /** @internal "If a qualifier of type id-qt-unotice is included, then it SHALL contain
1296
+ * explicitText". Both halves of that sentence are rows, since a notice can fail either. */
1297
+ message: "an id-qt-unotice policy qualifier must contain explicitText",
1298
+ appliesTo: _isSmime,
1299
+ check: function (cert, ctx) {
1300
+ var out = [];
1301
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1302
+ if (qid !== OID_UNOTICE) return;
1303
+ var carries = pkix.userNoticeTexts(value).some(function (t) { return t.field === "explicitText"; });
1304
+ if (!carries) out.push({ context: { policyIdentifier: pi.policyIdentifier } });
1305
+ });
1306
+ return out.length ? out : null;
1307
+ },
1308
+ },
1309
+ {
1310
+ id: "lint/cabf-smime/policies-unotice-notice-ref", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (a)",
1311
+ /** @internal "...and SHALL NOT contain noticeRef." */
1312
+ message: "an id-qt-unotice policy qualifier must not contain a noticeRef",
1313
+ appliesTo: _isSmime,
1314
+ check: function (cert, ctx) {
1315
+ var out = [];
1316
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1317
+ if (qid !== OID_UNOTICE) return;
1318
+ /** @internal NoticeReference requires its organization DisplayText, which userNoticeTexts
1319
+ * surfaces under that field name, so the field's presence is the noticeRef's presence. */
1320
+ var carries = pkix.userNoticeTexts(value).some(function (t) { return t.field === "organization"; });
1321
+ if (carries) out.push({ context: { policyIdentifier: pi.policyIdentifier } });
1322
+ });
1323
+ return out.length ? out : null;
1324
+ },
1325
+ },
1326
+ {
1327
+ id: "lint/cabf-smime/crldp-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (b)",
1328
+ message: "cRLDistributionPoints should not be marked critical",
1329
+ appliesTo: _isSmime,
1330
+ check: function (cert, ctx) {
1331
+ var e = ctx.raw("cRLDistributionPoints");
1332
+ return (e && e.critical === true) ? true : null;
1333
+ },
1334
+ },
1335
+ {
1336
+ id: "lint/cabf-smime/aia-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1337
+ /** @internal "(SHOULD be present)", where the TLS profile's own heading for the same extension
1338
+ * reads SHALL, so the two profiles report it at different strengths. */
1339
+ message: "a subscriber certificate should carry an authorityInformationAccess extension",
1340
+ appliesTo: _isSmime,
1341
+ check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
1342
+ },
1343
+ {
1344
+ id: "lint/cabf-smime/aia-ca-issuers-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1345
+ /** @internal "SHOULD contain at least one accessMethod value of type id-ad-caIssuers". Read
1346
+ * only where the extension is there at all, since its absence is the row above. */
1347
+ message: "authorityInformationAccess should carry an id-ad-caIssuers accessMethod",
1348
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1349
+ check: function (cert, ctx) {
1350
+ var d = ctx.decode("authorityInfoAccess");
1351
+ if (!d || !Array.isArray(d.value)) return null;
1352
+ var wanted = oid.byName("caIssuers");
1353
+ for (var i = 0; i < d.value.length; i++) {
1354
+ if (d.value[i].accessMethod === wanted) return null;
1355
+ }
1356
+ return true;
1357
+ },
1358
+ },
1359
+ {
1360
+ id: "lint/cabf-smime/aia-non-http-uri", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (c)",
1361
+ /** @internal Strict and Multipurpose require every accessMethod URI to be HTTP, Legacy at
1362
+ * least one. Each accessMethod has its OWN table, so "at least one" is answered per method and
1363
+ * an HTTP URI on one does not satisfy the other's row. */
1364
+ message: "an authorityInformationAccess accessMethod URI does not meet the URI scheme rule for this generation",
1365
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1366
+ check: function (cert, ctx) {
1367
+ var byMethod = _smimeAiaLocationsByMethod(ctx);
1368
+ var gen = _smimeGeneration(ctx);
1369
+ var out = [];
1370
+ function report(method, offending) {
1371
+ out.push({ context: { generation: gen || "strict", accessMethod: oid.name(method) || method,
1372
+ uris: offending.filter(function (u) { return u !== null; }),
1373
+ nonUriLocations: offending.filter(function (u) { return u === null; }).length } });
1374
+ }
1375
+ SMIME_AIA_METHODS.forEach(function (method) {
1376
+ var locs = byMethod[method];
1377
+ if (!locs || !locs.length) return;
1378
+ if (gen === "legacy") {
1379
+ if (!locs.some(_isHttpUri)) report(method, locs);
1380
+ return;
1381
+ }
1382
+ var bad = locs.filter(function (u) { return !_isHttpUri(u); });
1383
+ if (bad.length) report(method, bad);
1384
+ });
1385
+ return out.length ? out : null;
1386
+ },
1387
+ },
1388
+ {
1389
+ id: "lint/cabf-smime/key-usage-not-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (e)",
1390
+ message: "keyUsage should be marked critical",
1391
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("keyUsage"); },
1392
+ check: function (cert, ctx) {
1393
+ var e = ctx.raw("keyUsage");
1394
+ return (e && e.critical === true) ? null : true;
1395
+ },
1396
+ },
1397
+ {
1398
+ id: "lint/cabf-smime/aki-key-identifier-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1399
+ /** @internal "The keyIdentifier field SHALL be present", which is a field rule rather than the
1400
+ * extension-presence one above it: an authorityKeyIdentifier naming only an issuer and serial
1401
+ * satisfies that row and not this one. */
1402
+ message: "the authorityKeyIdentifier keyIdentifier field must be present",
1403
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityKeyIdentifier"); },
1404
+ check: function (cert, ctx) {
1405
+ var d = ctx.decode("authorityKeyIdentifier");
1406
+ if (!d || !d.value) return null;
1407
+ var kid = d.value.keyIdentifier;
1408
+ return (kid === null || kid === undefined) ? true : null;
1409
+ },
1410
+ },
1411
+ {
1412
+ id: "lint/cabf-smime/aki-issuer-or-serial-present", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (g)",
1413
+ /** @internal "authorityCertIssuer and authorityCertSerialNumber fields SHALL NOT be present."
1414
+ * Either one alone is a violation, so both are reported by name. */
1415
+ message: "the authorityKeyIdentifier must not carry authorityCertIssuer or authorityCertSerialNumber",
1416
+ appliesTo: function (cert, ctx) { return _isSmime(cert, ctx) && !!ctx.raw("authorityKeyIdentifier"); },
1417
+ check: function (cert, ctx) {
1418
+ var d = ctx.decode("authorityKeyIdentifier");
1419
+ if (!d || !d.value) return null;
1420
+ var out = [];
1421
+ ["authorityCertIssuer", "authorityCertSerialNumber"].forEach(function (f) {
1422
+ var v = d.value[f];
1423
+ if (v !== null && v !== undefined) out.push({ context: { field: f } });
1424
+ });
1425
+ return out.length ? out : null;
1426
+ },
1427
+ },
1428
+ {
1429
+ id: "lint/cabf-smime/san-missing", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (h)",
1430
+ message: "a subscriber certificate must carry a subjectAltName extension",
1431
+ appliesTo: _isSmime,
1432
+ check: function (cert, ctx) { return ctx.raw("subjectAltName") ? null : true; },
1433
+ },
1434
+ {
1435
+ id: "lint/cabf-smime/san-critical", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (h)",
1436
+ /** @internal "SHOULD NOT be marked critical unless the subject field is an empty sequence",
1437
+ * which is the one condition that lifts it, so the subject is read rather than assumed. */
1438
+ message: "subjectAltName should not be marked critical where the subject is not an empty sequence",
1439
+ appliesTo: _isSmime,
1440
+ check: function (cert, ctx) {
1441
+ var e = ctx.raw("subjectAltName");
1442
+ if (!e || e.critical !== true) return null;
1443
+ var rdns = cert.subject && cert.subject.rdns;
1444
+ var empty = !Array.isArray(rdns) || rdns.length === 0;
1445
+ return empty ? null : true;
1446
+ },
1447
+ },
1448
+ {
1449
+ id: "lint/cabf-smime/qc-statements-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (k)",
1450
+ message: "qcStatements must not be marked critical",
1451
+ appliesTo: _isSmime,
1452
+ check: function (cert, ctx) {
1453
+ var e = ctx.raw("qcStatements");
1454
+ return (e && e.critical === true) ? true : null;
1455
+ },
1456
+ },
1457
+ {
1458
+ id: "lint/cabf-smime/lei-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (l)",
1459
+ /** @internal The table is keyed on the certificate TYPE: Mailbox-validated and
1460
+ * Individual-validated admit neither identifier, Organization-validated admits the LEI but not
1461
+ * the role, and Sponsor-validated admits both. A certificate declaring no type is held to
1462
+ * none, since the table has no row to select. */
1463
+ message: "a Legal Entity Identifier extension is prohibited for this certificate type",
1464
+ appliesTo: _isSmime,
1465
+ check: function (cert, ctx) {
1466
+ var type = _smimeType(ctx);
1467
+ if (type === null) return null;
1468
+ var out = [];
1469
+ ["identifier", "role"].forEach(function (which) {
1470
+ if (_smimeLeiAdmits(type, which)) return;
1471
+ if (ctx.raw(SMIME_LEI_EXTS[which])) out.push({ context: { certificateType: type, extension: which } });
1472
+ });
1473
+ return out.length ? out : null;
1474
+ },
1475
+ },
1476
+ {
1477
+ id: "lint/cabf-smime/lei-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (l)",
1478
+ /** @internal Where the table admits one it reads "MAY be present and SHALL NOT be marked
1479
+ * critical", so criticality is fixed in the cells that admit it and nowhere else. */
1480
+ message: "a Legal Entity Identifier extension must not be marked critical",
1481
+ appliesTo: _isSmime,
1482
+ check: function (cert, ctx) {
1483
+ var type = _smimeType(ctx);
1484
+ if (type === null) return null;
1485
+ var out = [];
1486
+ ["identifier", "role"].forEach(function (which) {
1487
+ if (!_smimeLeiAdmits(type, which)) return;
1488
+ var e = ctx.raw(SMIME_LEI_EXTS[which]);
1489
+ if (e && e.critical === true) out.push({ context: { certificateType: type, extension: which } });
1490
+ });
1491
+ return out.length ? out : null;
1492
+ },
1493
+ },
1494
+ {
1495
+ id: "lint/cabf-smime/adobe-extension-prohibited", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (m)",
1496
+ /** @internal Prohibited under Strict, permitted under Multipurpose and Legacy. */
1497
+ message: "an Adobe extension is prohibited under the Strict generation",
1498
+ appliesTo: function (cert, ctx) {
1499
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) === "strict";
1500
+ },
1501
+ check: function (cert, ctx) {
1502
+ var out = [];
1503
+ SMIME_ADOBE_EXTS.forEach(function (name) {
1504
+ if (ctx.raw(name)) out.push({ context: { extension: name } });
1505
+ });
1506
+ return out.length ? out : null;
1507
+ },
1508
+ },
1509
+ {
1510
+ id: "lint/cabf-smime/adobe-extension-critical", severity: "error", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (m)",
1511
+ /** @internal The criticality sentence sits in the Multipurpose and Legacy row. The Strict row
1512
+ * reads "Prohibited" and carries none, so a critical extension there breaks one clause. */
1513
+ message: "an Adobe extension must not be marked critical",
1514
+ appliesTo: function (cert, ctx) {
1515
+ return _isSmime(cert, ctx) && _smimeGenerationOrStrict(ctx) !== "strict";
1516
+ },
1517
+ check: function (cert, ctx) {
1518
+ var out = [];
1519
+ SMIME_ADOBE_EXTS.forEach(function (name) {
1520
+ var e = ctx.raw(name);
1521
+ if (e && e.critical === true) out.push({ context: { extension: name } });
1522
+ });
1523
+ return out.length ? out : null;
1524
+ },
1525
+ },
1526
+ {
1527
+ id: "lint/cabf-smime/ski-missing", severity: "warn", source: "cabf-smime", citation: "CABF S/MIME BR 7.1.2.3 (n)",
1528
+ /** @internal "(SHOULD be present)". The same clause adds that the value SHOULD be derived from
1529
+ * the public key, which names no derivation, so a certificate using one this cannot recompute
1530
+ * is conformant and no row reports it. */
1531
+ message: "a subscriber certificate should carry a subjectKeyIdentifier extension",
1532
+ appliesTo: _isSmime,
1533
+ check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
1534
+ },
1535
+ ];
1536
+
1537
+ /** @internal The CA/Browser Forum Code Signing BR v3.11.0 subscriber profile. Every severity is
1538
+ * graded against the clause the row cites, never against the same-named row of another profile:
1539
+ * this document says MUST where the S/MIME one says SHOULD for cRLDistributionPoints criticality,
1540
+ * authorityInformationAccess presence and keyUsage criticality. */
1541
+ var CABF_CS_RULES = [
1542
+ {
1543
+ id: "lint/cabf-cs/policies-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1544
+ message: "a subscriber certificate must carry a certificatePolicies extension",
1545
+ appliesTo: _isCs,
1546
+ check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
1547
+ },
1548
+ {
1549
+ id: "lint/cabf-cs/policies-critical", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1550
+ message: "certificatePolicies should not be marked critical",
1551
+ appliesTo: _isCs,
1552
+ check: function (cert, ctx) {
1553
+ var e = ctx.raw("certificatePolicies");
1554
+ return (e && e.critical === true) ? true : null;
1555
+ },
1556
+ },
1557
+ {
1558
+ id: "lint/cabf-cs/policies-reserved-identifier-count", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.6.4",
1559
+ /** @internal "a Certificate issued to a Subscriber MUST contain exactly one of the reserved
1560
+ * policy OIDs specified in Section 7.1.6.1", so both none and two are reported. */
1561
+ message: "a subscriber certificate must assert exactly one reserved code signing policy identifier",
1562
+ appliesTo: _isCs,
1563
+ effectiveDate: CS_RESERVED_POLICY_REQUIRED,
1564
+ check: function (cert, ctx) {
1565
+ var d = ctx.decode("certificatePolicies");
1566
+ if (!d || !Array.isArray(d.value)) return null;
1567
+ var found = d.value.filter(function (pi) { return !!CS_KIND_BY_POLICY[pi.policyIdentifier]; });
1568
+ return found.length === 1 ? null : { context: { count: found.length } };
1569
+ },
1570
+ },
1571
+ {
1572
+ id: "lint/cabf-cs/policies-cps-not-http-url", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1573
+ /** @internal "HTTP URL for the Subordinate CA's Certification Practice Statement". This clause
1574
+ * names one scheme where the S/MIME clause for the same qualifier names two, so an HTTPS
1575
+ * qualifier is conformant there and reported here. */
1576
+ message: "an id-qt-cps policy qualifier must be a HTTP URL",
1577
+ appliesTo: _isCs,
1578
+ check: function (cert, ctx) {
1579
+ var out = [];
1580
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1581
+ if (qid !== OID_CPS) return;
1582
+ var uri = null;
1583
+ // 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
1584
+ try {
1585
+ uri = asn1.read.string(value);
1586
+ } catch (_e) {
1587
+ void 0;
1588
+ }
1589
+ if (pkiBuild.httpUrlScheme(uri) !== "http") {
1590
+ out.push({ context: { policyIdentifier: pi.policyIdentifier, cps: uri } });
1591
+ }
1592
+ });
1593
+ return out.length ? out : null;
1594
+ },
1595
+ },
1596
+ {
1597
+ id: "lint/cabf-cs/crldp-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1598
+ message: "a subscriber certificate must carry a cRLDistributionPoints extension",
1599
+ appliesTo: _isCs,
1600
+ check: function (cert, ctx) { return ctx.raw("cRLDistributionPoints") ? null : true; },
1601
+ },
1602
+ {
1603
+ id: "lint/cabf-cs/crldp-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1604
+ /** @internal "It MUST NOT be marked critical", where the S/MIME clause for the same extension
1605
+ * says SHOULD NOT, so the same certificate draws an error here and a warning there. */
1606
+ message: "cRLDistributionPoints must not be marked critical",
1607
+ appliesTo: _isCs,
1608
+ check: function (cert, ctx) {
1609
+ var e = ctx.raw("cRLDistributionPoints");
1610
+ return (e && e.critical === true) ? true : null;
1611
+ },
1612
+ },
1613
+ {
1614
+ id: "lint/cabf-cs/crldp-no-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1615
+ /** @internal "it MUST contain the HTTP URL of the CA's CRL service". One HTTP URL is required
1616
+ * and the clause forbids no other scheme beside it. */
1617
+ message: "cRLDistributionPoints must contain a HTTP URL",
1618
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("cRLDistributionPoints"); },
1619
+ check: function (cert, ctx) {
1620
+ var uris = _crldpUris(ctx);
1621
+ return uris.some(_isHttpUri) ? null : { context: { uris: uris } };
1622
+ },
1623
+ },
1624
+ {
1625
+ id: "lint/cabf-cs/aia-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1626
+ /** @internal "This extension MUST be present", where the S/MIME clause asks at SHOULD. */
1627
+ message: "a subscriber certificate must carry an authorityInformationAccess extension",
1628
+ appliesTo: _isCs,
1629
+ check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
1630
+ },
1631
+ {
1632
+ id: "lint/cabf-cs/aia-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1633
+ message: "authorityInformationAccess must not be marked critical",
1634
+ appliesTo: _isCs,
1635
+ check: function (cert, ctx) {
1636
+ var e = ctx.raw("authorityInfoAccess");
1637
+ return (e && e.critical === true) ? true : null;
1638
+ },
1639
+ },
1640
+ {
1641
+ id: "lint/cabf-cs/aia-ca-issuers-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1642
+ /** @internal "It MUST contain the HTTP URL of the Issuing CA's certificate", so the method is
1643
+ * required rather than recommended, unlike the S/MIME clause's SHOULD. */
1644
+ message: "authorityInformationAccess must carry an id-ad-caIssuers accessMethod",
1645
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1646
+ check: function (cert, ctx) {
1647
+ var d = ctx.decode("authorityInfoAccess");
1648
+ if (!d || !Array.isArray(d.value)) return null;
1649
+ var wanted = oid.byName("caIssuers");
1650
+ for (var i = 0; i < d.value.length; i++) {
1651
+ if (d.value[i].accessMethod === wanted) return null;
1652
+ }
1653
+ return true;
1654
+ },
1655
+ },
1656
+ {
1657
+ id: "lint/cabf-cs/aia-non-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1658
+ /** @internal The clause states one rule for both accessMethods it names, so unlike the S/MIME
1659
+ * clause there is no table per method and every location is held to the same reading. */
1660
+ message: "an authorityInformationAccess accessLocation must be a HTTP URL",
1661
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1662
+ check: function (cert, ctx) {
1663
+ var locs = _csAiaLocations(ctx);
1664
+ var bad = locs.filter(function (u) { return !_isHttpUri(u); });
1665
+ if (!bad.length) return null;
1666
+ return { context: { uris: bad.filter(function (u) { return u !== null; }),
1667
+ nonUriLocations: bad.filter(function (u) { return u === null; }).length } };
1668
+ },
1669
+ },
1670
+ {
1671
+ id: "lint/cabf-cs/basic-constraints-ca-true", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (d)",
1672
+ message: "a subscriber certificate's basicConstraints cA field must not be true",
1673
+ appliesTo: _isCs,
1674
+ check: function (cert, ctx) {
1675
+ var d = ctx.decode("basicConstraints");
1676
+ return (d && d.value && d.value.cA === true) ? true : null;
1677
+ },
1678
+ },
1679
+ {
1680
+ id: "lint/cabf-cs/key-usage-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1681
+ message: "a subscriber certificate must carry a keyUsage extension",
1682
+ appliesTo: _isCs,
1683
+ check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
1684
+ },
1685
+ {
1686
+ id: "lint/cabf-cs/key-usage-not-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1687
+ /** @internal "MUST be marked critical", where the S/MIME clause says SHOULD. */
1688
+ message: "keyUsage must be marked critical",
1689
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
1690
+ check: function (cert, ctx) {
1691
+ var e = ctx.raw("keyUsage");
1692
+ return (e && e.critical === true) ? null : true;
1693
+ },
1694
+ },
1695
+ {
1696
+ id: "lint/cabf-cs/key-usage-missing-digital-signature", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1697
+ message: "keyUsage must assert digitalSignature",
1698
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
1699
+ check: function (cert, ctx) {
1700
+ var d = ctx.decode("keyUsage");
1701
+ if (!d || !d.value) return null;
1702
+ return d.value.digitalSignature === true ? null : true;
1703
+ },
1704
+ },
1705
+ {
1706
+ id: "lint/cabf-cs/key-usage-forbidden-bit", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1707
+ /** @internal "Bit positions for keyCertSign and cRLSign MUST NOT be set." The next sentence
1708
+ * covers every other bit at SHOULD NOT, so the two lists are two rows at two strengths. */
1709
+ message: "keyUsage must not assert keyCertSign or cRLSign",
1710
+ appliesTo: _isCs,
1711
+ check: function (cert, ctx) {
1712
+ var d = ctx.decode("keyUsage");
1713
+ if (!d || !d.value) return null;
1714
+ var out = [];
1715
+ CS_FORBIDDEN_KU.forEach(function (bit) {
1716
+ if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
1717
+ });
1718
+ return out.length ? out : null;
1719
+ },
1720
+ },
1721
+ {
1722
+ id: "lint/cabf-cs/key-usage-discouraged-bit", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1723
+ /** @internal "All other bit positions SHOULD NOT be set", which reaches every bit other than
1724
+ * the required digitalSignature and the two the row above forbids, including one set past the
1725
+ * nine RFC 5280 names. */
1726
+ message: "keyUsage should not assert a bit beyond digitalSignature",
1727
+ appliesTo: _isCs,
1728
+ check: function (cert, ctx) {
1729
+ var d = ctx.decode("keyUsage");
1730
+ if (!d || !d.value) return null;
1731
+ var out = [];
1732
+ KU_BIT_NAMES.forEach(function (bit) {
1733
+ if (bit === "digitalSignature" || CS_FORBIDDEN_KU.indexOf(bit) !== -1) return;
1734
+ if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
1735
+ });
1736
+ if (d.value.reservedBitsSet === true) out.push({ context: { reservedBitSet: true } });
1737
+ return out.length ? out : null;
1738
+ },
1739
+ },
1740
+ {
1741
+ id: "lint/cabf-cs/eku-missing-code-signing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1742
+ message: "a code signing certificate must carry id-kp-codeSigning",
1743
+ appliesTo: _isCsCodeSigning,
1744
+ check: function (cert, ctx) {
1745
+ var d = ctx.decode("extKeyUsage");
1746
+ if (!d || !Array.isArray(d.value)) return true;
1747
+ return d.value.indexOf(oid.byName("codeSigning")) === -1 ? true : null;
1748
+ },
1749
+ },
1750
+ {
1751
+ id: "lint/cabf-cs/eku-missing-time-stamping", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1752
+ message: "a timestamp certificate must carry id-kp-timeStamping",
1753
+ appliesTo: _isCsTimestamp,
1754
+ check: function (cert, ctx) {
1755
+ var d = ctx.decode("extKeyUsage");
1756
+ if (!d || !Array.isArray(d.value)) return true;
1757
+ return d.value.indexOf(oid.byName("timeStamping")) === -1 ? true : null;
1758
+ },
1759
+ },
1760
+ {
1761
+ id: "lint/cabf-cs/eku-not-critical-timestamp", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1762
+ /** @internal "id-kp-timeStamping MUST be present and MUST be marked critical", the one place
1763
+ * this section requires a critical extKeyUsage. */
1764
+ message: "a timestamp certificate's extKeyUsage must be marked critical",
1765
+ appliesTo: function (cert, ctx) { return _isCsTimestamp(cert, ctx) && !!ctx.raw("extKeyUsage"); },
1766
+ check: function (cert, ctx) {
1767
+ var e = ctx.raw("extKeyUsage");
1768
+ return (e && e.critical === true) ? null : true;
1769
+ },
1770
+ },
1771
+ {
1772
+ id: "lint/cabf-cs/eku-forbidden-purpose", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1773
+ message: "extKeyUsage must not carry anyExtendedKeyUsage or id-kp-serverAuth",
1774
+ appliesTo: _isCs,
1775
+ check: function (cert, ctx) {
1776
+ var d = ctx.decode("extKeyUsage");
1777
+ if (!d || !Array.isArray(d.value)) return null;
1778
+ var out = [];
1779
+ CS_FORBIDDEN_EKU.forEach(function (dotted) {
1780
+ if (d.value.indexOf(dotted) !== -1) {
1781
+ out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
1782
+ }
1783
+ });
1784
+ return out.length ? out : null;
1785
+ },
1786
+ },
1787
+ {
1788
+ id: "lint/cabf-cs/eku-discouraged-purpose", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1789
+ /** @internal "Other values SHOULD NOT be present." The clause continues that a CA presenting
1790
+ * one must hold a business agreement with a platform vendor, which is a fact about the CA
1791
+ * rather than about the certificate, so this row reports the value and says nothing about the
1792
+ * agreement. The purpose each kind requires, the three the clause names as MAY, and the two
1793
+ * the row above forbids are all passed over here. */
1794
+ message: "extKeyUsage should not carry a purpose beyond the ones this profile names",
1795
+ appliesTo: _isCs,
1796
+ check: function (cert, ctx) {
1797
+ var d = ctx.decode("extKeyUsage");
1798
+ if (!d || !Array.isArray(d.value)) return null;
1799
+ var required = _isCsTimestamp(cert, ctx) ? oid.byName("timeStamping") : oid.byName("codeSigning");
1800
+ var out = [];
1801
+ d.value.forEach(function (dotted) {
1802
+ if (dotted === required) return;
1803
+ if (CS_PERMITTED_EXTRA_EKU.indexOf(dotted) !== -1) return;
1804
+ if (CS_FORBIDDEN_EKU.indexOf(dotted) !== -1) return;
1805
+ out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
1806
+ });
1807
+ return out.length ? out : null;
1808
+ },
1809
+ },
1810
+ {
1811
+ id: "lint/cabf-cs/aki-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
1812
+ message: "a subscriber certificate must carry an authorityKeyIdentifier extension",
1813
+ appliesTo: _isCs,
1814
+ check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
1815
+ },
1816
+ {
1817
+ id: "lint/cabf-cs/aki-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
1818
+ message: "authorityKeyIdentifier must not be marked critical",
1819
+ appliesTo: _isCs,
1820
+ check: function (cert, ctx) {
1821
+ var e = ctx.raw("authorityKeyIdentifier");
1822
+ return (e && e.critical === true) ? true : null;
1823
+ },
1824
+ },
1825
+ {
1826
+ id: "lint/cabf-cs/validity-too-long", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.3.2",
1827
+ /** @internal Three ceilings, keyed on the kind and on the issuance date: 39 months for a code
1828
+ * signing certificate issued before 1 March 2026, 460 days for one issued on or after it, and
1829
+ * 135 months for a timestamp certificate either side of that date. A ceiling stated in months
1830
+ * is measured in months, through guard.time.addMonths, since how many days 39 months hold
1831
+ * depends on which 39. */
1832
+ message: "the validity period exceeds the maximum this profile gives for the certificate's kind and issuance date",
1833
+ appliesTo: _isCs,
1834
+ check: function (cert, ctx) {
1835
+ var v = cert.validity;
1836
+ if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
1837
+ var notAfter = guard.time.instantOf(v.notAfter);
1838
+ if (_isCsTimestamp(cert, ctx)) {
1839
+ var tsCeiling = guard.time.addMonths(v.notBefore, 135, _cfg, "lint/bad-input", "notBefore");
1840
+ return notAfter > guard.time.instantOf(tsCeiling)
1841
+ ? { context: { maxMonths: 135 } } : null;
1842
+ }
1843
+ if (guard.time.instantOf(v.notBefore) < guard.time.instantOf(CS_VALIDITY_DAYS_START)) {
1844
+ var monthCeiling = guard.time.addMonths(v.notBefore, 39, _cfg, "lint/bad-input", "notBefore");
1845
+ return notAfter > guard.time.instantOf(monthCeiling)
1846
+ ? { context: { maxMonths: 39 } } : null;
1847
+ }
1848
+ var days = (notAfter - guard.time.instantOf(v.notBefore)) / MS_PER_DAY;
1849
+ return days > 460 ? { context: { days: Math.round(days), maxDays: 460 } } : null;
1850
+ },
1851
+ },
1852
+ {
1853
+ id: "lint/cabf-cs/weak-key", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.1.5.2",
1854
+ /** @internal The section names RSA, ECDSA and DSA and no other family, so a key outside the
1855
+ * three is reported: a certificate the profile covers must carry one it names. */
1856
+ message: "the subject public key is below the Code Signing BR minimum (RSA < 3072 bits, or a curve or family the section does not name)",
1857
+ appliesTo: _isCs,
1858
+ check: function (cert) {
1859
+ var spki = cert.subjectPublicKeyInfo, name = spki && spki.algorithm && spki.algorithm.name;
1860
+ if (RSA_KEY_ALGS[name]) {
1861
+ var bits = _rsaModulusBits(spki);
1862
+ return (bits === null || bits < CS_RSA_MIN_BITS) ? { context: { rsaBits: bits } } : null;
1863
+ }
1864
+ if (name === "ecPublicKey") {
1865
+ var curve = _ecCurveName(spki);
1866
+ return CS_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
1867
+ }
1868
+ if (name === "dsa") {
1869
+ var dsa = _dsaParamBits(spki);
1870
+ if (dsa === null) return { context: { keyAlgorithm: "dsa", dsaL: null, dsaN: null } };
1871
+ return (dsa.L === CS_DSA_L && CS_DSA_N.indexOf(dsa.N) !== -1)
1872
+ ? null : { context: { keyAlgorithm: "dsa", dsaL: dsa.L, dsaN: dsa.N } };
1873
+ }
1874
+ return { context: { keyAlgorithm: name || null } };
1875
+ },
1876
+ },
1877
+ ];
1878
+
622
1879
  var CABF_TLS_RULES = [
623
1880
  {
624
1881
  id: "lint/cabf-tls/aia-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
@@ -2253,28 +3510,33 @@ var CABF_TLS_CSR_RULE_IDS = [
2253
3510
  "lint/cabf-tls/weak-key",
2254
3511
  ];
2255
3512
 
3513
+ /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried. Every row in
3514
+ * every profile below asks for an extension BY NAME, and the shared decoder returns null both for
3515
+ * one that is absent and for one whose value does not decode, so without this row a malformed
3516
+ * value reads as absence and passes every row that asks for it. The certificate's own row is
3517
+ * reused rather than restated, and it is folded into each named profile rather than into the
3518
+ * *_RULES arrays, which `ALL_RULES` concatenates and would otherwise report it twice. */
3519
+ var RFC5280_EXTENSION_SYNTAX_RULES = RFC5280_RULES.filter(function (r) {
3520
+ return r.id === "lint/rfc5280/extension-undecodable";
3521
+ });
2256
3522
  var PROFILES = Object.assign(Object.create(null), {
2257
3523
  "rfc5280": RFC5280_RULES,
2258
- "rfc9881": RFC9881_RULES,
2259
- "rfc9909": RFC9909_RULES,
2260
- "rfc9935": RFC9935_RULES,
2261
- "cabf-tls": CABF_TLS_RULES,
3524
+ "rfc9881": RFC9881_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3525
+ "rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3526
+ "rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3527
+ "cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3528
+ "cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3529
+ "cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
2262
3530
  });
2263
- var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES).concat(CABF_TLS_RULES);
3531
+ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
3532
+ .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES);
2264
3533
 
2265
3534
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
2266
3535
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
2267
3536
  });
2268
- /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried, and a
2269
- * requested extension whose value does not decode is the same fault in a request as in a
2270
- * certificate. The certificate's own row reads `extensions` and the request view supplies it, so
2271
- * the row is reused rather than restated. Without it a malformed requested value counted as a
2272
- * passing check, because the shared decoder returns null and every row that asks by name reads
2273
- * that null as absence. */
2274
- var RFC5280_CSR_RULES = RFC5280_RULES.filter(function (r) {
2275
- return r.id === "lint/rfc5280/extension-undecodable";
2276
- });
2277
- var CSR_STRUCTURAL_RULES = RFC2986_CSR_RULES.concat(RFC5280_CSR_RULES);
3537
+ /** @internal A requested extension whose value does not decode is the same fault in a request as
3538
+ * in a certificate, so the request profiles take the same row the certificate profiles do. */
3539
+ var CSR_STRUCTURAL_RULES = RFC2986_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES);
2278
3540
  var CSR_PROFILES = Object.assign(Object.create(null), {
2279
3541
  "rfc2986": CSR_STRUCTURAL_RULES,
2280
3542
  "cabf-tls": CSR_STRUCTURAL_RULES.concat(CABF_TLS_CSR_RULES),
@@ -2378,7 +3640,9 @@ function _ingestCsr(input) {
2378
3640
  var derived = _derivedCsr(input);
2379
3641
  if (derived) return { csr: derived };
2380
3642
  var der;
2381
- if (Buffer.isBuffer(input)) der = input;
3643
+ var bytes = _derBytesOf(input);
3644
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3645
+ if (bytes !== null) der = bytes.der;
2382
3646
  else if (typeof input === "string") {
2383
3647
  try { der = csrSchema.pemDecode(input); }
2384
3648
  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 } } }; }
@@ -2487,7 +3751,9 @@ function _ingestCrl(input) {
2487
3751
  var derived = _derivedCrl(input);
2488
3752
  if (derived) return { crl: derived };
2489
3753
  var der;
2490
- if (Buffer.isBuffer(input)) der = input;
3754
+ var bytes = _derBytesOf(input);
3755
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3756
+ if (bytes !== null) der = bytes.der;
2491
3757
  else if (typeof input === "string") {
2492
3758
  try { der = crlSchema.pemDecode(input); }
2493
3759
  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 } } }; }
@@ -2660,7 +3926,9 @@ var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
2660
3926
  * responses. Bytes and PEM take the fatal path below, never a throw. */
2661
3927
  function _ingestOcsp(input) {
2662
3928
  var der;
2663
- if (Buffer.isBuffer(input)) der = input;
3929
+ var bytes = _derBytesOf(input);
3930
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
3931
+ if (bytes !== null) der = bytes.der;
2664
3932
  else if (typeof input === "string") {
2665
3933
  try { der = ocspSchema.pemDecode(input); }
2666
3934
  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 } } }; }
@@ -2793,6 +4061,10 @@ var BY_ARTIFACT = Object.assign(Object.create(null), {
2793
4061
  });
2794
4062
 
2795
4063
  function rules(profile, artifact) {
4064
+ /** @internal Every lint verb reads "all" and "default" as naming no profile, so the enumerator
4065
+ * reads them the same way: a caller who linted under one of those names and then asked which
4066
+ * rows ran would otherwise be refused the answer by the same registry that ran them. */
4067
+ if (profile === "all" || profile === "default") profile = null;
2796
4068
  if (artifact != null && !Object.prototype.hasOwnProperty.call(BY_ARTIFACT, artifact)) {
2797
4069
  throw _cfg("lint/bad-input", "unknown lint artifact \"" + artifact +
2798
4070
  "\" (known: " + Object.keys(BY_ARTIFACT).join(", ") + ")");