@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/CHANGELOG.md +51 -0
- package/README.md +1 -1
- package/lib/guard-time.js +50 -0
- package/lib/lint.js +1307 -35
- package/lib/oid.js +29 -1
- package/lib/pki-build.js +76 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
273
|
-
|
|
274
|
-
|
|
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.
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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)
|
|
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
|
|
2269
|
-
*
|
|
2270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(", ") + ")");
|