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