@blamejs/pki 0.8.25 → 0.8.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/lint.js CHANGED
@@ -36,6 +36,9 @@ var x509 = require("./schema-x509");
36
36
  var crlSchema = require("./schema-crl");
37
37
  var csrSchema = require("./schema-csr");
38
38
  var ocspSchema = require("./schema-ocsp");
39
+ var cmsSchema = require("./schema-cms");
40
+ var tspSchema = require("./schema-tsp");
41
+ var acSchema = require("./schema-attrcert");
39
42
  var pkix = require("./schema-pkix");
40
43
  var pkiBuild = require("./pki-build");
41
44
  var schema = require("./schema-engine");
@@ -4615,6 +4618,421 @@ var RFC5280_CRL_RULES = [
4615
4618
  var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
4616
4619
  var ALL_CRL_RULES = RFC5280_CRL_RULES;
4617
4620
 
4621
+ /** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
4622
+ * this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
4623
+ * clauses reach an operator as `lint/unparseable` carrying the parser's own code rather than as a
4624
+ * row here. A row for one of them would be a branch no input can drive. Probed against the
4625
+ * shipped parser, the SignedData and SignerInfo versions, the presence of signedAttrs when the
4626
+ * content type is not id-data, the content-type and message-digest attributes and their binding
4627
+ * to eContentType, every attribute's cardinality and placement, a countersignature's own
4628
+ * restrictions, and a time carrying no seconds, fractional seconds or a local offset are all
4629
+ * refused before a rule sees them. What is left is below.
4630
+ * The UTCTime alternative of sec. 11.3 carries two year digits, which the clause reads as 19YY
4631
+ * for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
4632
+ * GeneralizedTime direction of that clause can be broken. */
4633
+ var OID_SIGNING_TIME = oid.byName("signingTime");
4634
+ var OID_SIGNED_DATA = oid.byName("signedData");
4635
+
4636
+ /** @internal The content type decides which of these rows apply, and the parser sets `contentType`
4637
+ * as an own property on every content it builds. A content type that carries no signers has no own
4638
+ * `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
4639
+ function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
4640
+ var OID_COUNTERSIGNATURE = oid.byName("countersignature");
4641
+ /** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
4642
+ * unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
4643
+ * signedAttrs. A rule about a signed attribute governs one wherever it appears, so a message
4644
+ * cannot put the shape a clause forbids one level down and escape it. The parser validates each
4645
+ * countersignature's shape without keeping the result, so it is decoded here, and a nested one is
4646
+ * reached by the same walk. */
4647
+ function _cmsAllSignerInfos(cms) {
4648
+ var out = [];
4649
+ /** @internal Each entry keeps the index of the message's OWN signer it hangs from, not a
4650
+ * position in this walk: a reader given a flattened index would look up a signer that is not
4651
+ * the one at fault, or one that does not exist. Whether the attribute sat on a countersignature
4652
+ * under that signer is the other half of the answer. */
4653
+ function visit(signerInfos, signerIndex, isCounter, depth) {
4654
+ if (!Array.isArray(signerInfos) || depth > C.LIMITS.CMS_MAX_COUNTERSIGNATURE_DEPTH) return;
4655
+ signerInfos.forEach(function (si, i) {
4656
+ var index = isCounter ? signerIndex : i;
4657
+ out.push({ signerInfo: si, signerIndex: index, countersignature: isCounter });
4658
+ (Array.isArray(si.unsignedAttrs) ? si.unsignedAttrs : []).forEach(function (attr) {
4659
+ if (attr.type !== OID_COUNTERSIGNATURE) return;
4660
+ if (!Array.isArray(attr.values)) return;
4661
+ attr.values.forEach(function (value) {
4662
+ var nested;
4663
+ // allow:swallow-unverified the parser already walked this value, so a fault here leaves the countersignature unread rather than reported
4664
+ try { nested = cmsSchema.walkCountersignature(asn1.decode(value), null); }
4665
+ catch (_e) { return; }
4666
+ if (nested) visit([nested], index, true, depth + 1);
4667
+ });
4668
+ });
4669
+ });
4670
+ }
4671
+ visit(cms.signerInfos, 0, false, 0);
4672
+ return out;
4673
+ }
4674
+ function _cmsSignedAttr(signerInfo, dotted) {
4675
+ var attrs = signerInfo.signedAttrs;
4676
+ if (!Array.isArray(attrs)) return null;
4677
+ for (var i = 0; i < attrs.length; i++) {
4678
+ if (attrs[i].type === dotted) return attrs[i];
4679
+ }
4680
+ return null;
4681
+ }
4682
+ /** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
4683
+ * rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
4684
+ * that adds a countersigner's digest to it, and a set written under one rule and read under another
4685
+ * would list one algorithm twice. Only digest identifiers pass through here, so no row is loosened
4686
+ * for an algorithm whose parameters carry meaning. */
4687
+ var _cmsSameAlgorithm = cmsSchema.sameDigestAlgorithm;
4688
+
4689
+ var RFC5652_CMS_RULES = [
4690
+ {
4691
+ id: "lint/rfc5652/signing-time-encoding", severity: "error", source: "rfc5652", citation: "RFC 5652 11.3",
4692
+ /** @internal "Dates between 1 January 1950 and 31 December 2049 (inclusive) MUST be encoded as
4693
+ * UTCTime. Any dates with year values before 1950 or after 2049 MUST be encoded as
4694
+ * GeneralizedTime." The value is held as its DER bytes, so the encoding is the first byte and
4695
+ * the year is what the value decodes to. */
4696
+ message: "a signing-time naming a year from 1950 to 2049 must be encoded as UTCTime, not GeneralizedTime",
4697
+ appliesTo: _isSignedData,
4698
+ check: function (cms) {
4699
+ var out = [];
4700
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4701
+ var attr = _cmsSignedAttr(entry.signerInfo, OID_SIGNING_TIME);
4702
+ if (!attr || !Array.isArray(attr.values) || !attr.values.length) return;
4703
+ var value = attr.values[0];
4704
+ if (!Buffer.isBuffer(value) || value.length < 2) return;
4705
+ if (value[0] !== asn1.TAGS.GENERALIZED_TIME) return;
4706
+ var when;
4707
+ // allow:swallow-unverified the parser already read this value as a Time, so a fault here answers for no signer
4708
+ try { when = asn1.read.time(asn1.decode(value)); } catch (_e) { return; }
4709
+ var year = when.getUTCFullYear();
4710
+ if (year >= 1950 && year <= 2049) {
4711
+ out.push({ context: { signerIndex: entry.signerIndex,
4712
+ countersignature: entry.countersignature, year: year } });
4713
+ }
4714
+ });
4715
+ return out.length ? out : null;
4716
+ },
4717
+ },
4718
+ {
4719
+ id: "lint/rfc5652/signer-digest-not-listed", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.3",
4720
+ /** @internal "The message digest algorithm SHOULD be among those listed in the digestAlgorithms
4721
+ * field of the associated SignerData. Implementations MAY fail to validate signatures that use
4722
+ * a digest algorithm that is not included in the SignedData digestAlgorithms set." A SHOULD
4723
+ * whose cost is a verifier that declines, so a warning.
4724
+ * Sec. 11.4 makes a countersignature a SignerInfo and does not except it from this clause, and
4725
+ * sec. 5.1 has the collection list the digests "employed by all of the signers", so the walk is
4726
+ * the same one the signing-time rule uses and the finding says which signer it hangs from. */
4727
+ message: "a signer's digest algorithm is not among those the SignedData digestAlgorithms set lists",
4728
+ appliesTo: _isSignedData,
4729
+ check: function (cms) {
4730
+ var listed = Array.isArray(cms.digestAlgorithms) ? cms.digestAlgorithms : [];
4731
+ var out = [];
4732
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4733
+ var mine = entry.signerInfo.digestAlgorithm;
4734
+ if (!mine) return;
4735
+ var found = listed.some(function (alg) { return _cmsSameAlgorithm(alg, mine); });
4736
+ if (!found) {
4737
+ out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature,
4738
+ digestAlgorithm: mine.name || mine.oid } });
4739
+ }
4740
+ });
4741
+ return out.length ? out : null;
4742
+ },
4743
+ },
4744
+ {
4745
+ id: "lint/rfc5652/attribute-certificate-v1", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.1",
4746
+ /** @internal "The use of version 1 attribute certificates is strongly discouraged." The
4747
+ * certificates field is a CertificateChoices SET, whose `[1]` alternative is the v1 attribute
4748
+ * certificate; the parser keeps each element's outer tag for exactly this kind of question. */
4749
+ message: "the certificates field carries a version 1 attribute certificate, whose use this section strongly discourages",
4750
+ appliesTo: _isSignedData,
4751
+ check: function (cms) {
4752
+ var certs = Array.isArray(cms.certificates) ? cms.certificates : [];
4753
+ var count = 0;
4754
+ certs.forEach(function (c) {
4755
+ if (c && c.tagClass === "context" && c.tagNumber === 1) count += 1;
4756
+ });
4757
+ return count ? { context: { count: count } } : null;
4758
+ },
4759
+ },
4760
+ {
4761
+ id: "lint/rfc5652/no-signers", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.1",
4762
+ /** @internal "There MAY be any number of elements in the collection, including zero", so a
4763
+ * SignedData with no signers is conformant. It is also a message carrying certificates and
4764
+ * nothing signed, which is worth naming to whoever expected a signature. */
4765
+ message: "the SignedData carries no signers, so it signs nothing and only conveys its certificates and revocation lists",
4766
+ appliesTo: _isSignedData,
4767
+ check: function (cms) { return cms.signerInfos.length === 0 ? true : null; },
4768
+ },
4769
+ {
4770
+ id: "lint/rfc5652/detached-content", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.2",
4771
+ /** @internal eContent is optional, and its absence means a verifier must be handed the content
4772
+ * by another route. The row reads a SignedData that HAS a signer, since the notice exists for
4773
+ * whoever must verify one: a message with no signers has nothing to supply content for. */
4774
+ message: "the SignedData is detached, so verifying it needs the content supplied separately",
4775
+ appliesTo: function (cms) { return _isSignedData(cms) && cms.signerInfos.length > 0; },
4776
+ check: function (cms) {
4777
+ var encap = cms.encapContentInfo;
4778
+ return (encap && encap.eContent) ? null : true;
4779
+ },
4780
+ },
4781
+ ];
4782
+
4783
+ var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
4784
+ var ALL_CMS_RULES = RFC5652_CMS_RULES;
4785
+
4786
+ /** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
4787
+ * short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
4788
+ * rule sees it, each with its own code. A token that is not a CMS SignedData, encapsulates
4789
+ * something other than id-ct-TSTInfo, carries detached content, carries more than the TSA's own
4790
+ * signerInfo, or omits the SigningCertificate attribute the section requires arrives as
4791
+ * `lint/unparseable`. The TSTInfo version reader admits v1 alone, and the Accuracy field bounds,
4792
+ * the DEFAULT FALSE omissions and the PKIStatus range are all enforced too. */
4793
+ var TSP_SERIAL_MAX_BITS = 160;
4794
+
4795
+ /** @internal The tsa hint as a DECODED GeneralName. The TSTInfo keeps it raw, and the shared
4796
+ * GeneralNames decoder is what the rest of this file reads a name with, so the hint is wrapped in
4797
+ * a one-element SEQUENCE and walked through it rather than compared as bytes: two encodings of one
4798
+ * distinguished name are the same name, and an error-severity row must not say otherwise. */
4799
+ function _tspDecodedHint(hint) {
4800
+ if (!hint || !Buffer.isBuffer(hint.bytes)) return null;
4801
+ var names = _generalNamesOf(asn1.build.sequence([asn1.build.raw(hint.bytes)]));
4802
+ return (names && names.length === 1) ? names[0] : null;
4803
+ }
4804
+ /** @internal Every GeneralName the certificate's subjectAltName carries, decoded, each entry
4805
+ * keeping the raw TLV beside its value so a form with no useful decoded form can still be
4806
+ * compared. */
4807
+ function _tspSanNames(parsedCert) {
4808
+ var raw = null;
4809
+ (parsedCert.extensions || []).forEach(function (e) {
4810
+ if (e.oid === oid.byName("subjectAltName")) raw = e.value;
4811
+ });
4812
+ if (!Buffer.isBuffer(raw)) return [];
4813
+ return _generalNamesOf(raw) || [];
4814
+ }
4815
+ /** @internal Each form of GeneralName carrying text is compared under the RFC 5280 sec. 7 rule for
4816
+ * that form, and each rule lives in `guard.name` so this row and the AC target comparison read one
4817
+ * definition. A dNSName is case-insensitive throughout (sec. 7.2). A mailbox is not: sec. 7.5 has
4818
+ * the local part match exactly and the host part match without regard to ASCII case. A URI is not
4819
+ * either: sec. 7.4 prepares both values, lowercasing only the scheme and host, and then calls for a
4820
+ * "case-sensitive exact match". Folding case over a whole mailbox or URI read two names differing
4821
+ * only in a local part or a path as one, so a hint naming neither went unreported.
4822
+ * A verdict of null says the rule reached none, which the caller reports as nothing rather than as a
4823
+ * mismatch it did not establish. */
4824
+ function _tspSameGeneralName(a, c) {
4825
+ if (!a || !c || a.tagNumber !== c.tagNumber) return false;
4826
+ if (typeof a.value === "string" && typeof c.value === "string") {
4827
+ if (a.tagNumber === 2) return guard.name.lowerAscii(a.value) === guard.name.lowerAscii(c.value);
4828
+ if (a.tagNumber === 1 || a.tagNumber === 6) {
4829
+ var verdict = a.tagNumber === 1 ? guard.name.emailEqual(a.value, c.value)
4830
+ : guard.name.uriEqual(a.value, c.value);
4831
+ if (verdict === "match") return true;
4832
+ if (verdict === "no-match") return false;
4833
+ /** @internal A value neither rule can read is compared as its own encoding, which answers the
4834
+ * one case that is certain: identical bytes are one name. */
4835
+ return a.value === c.value ? true : null;
4836
+ }
4837
+ }
4838
+ if (Buffer.isBuffer(a.value) && Buffer.isBuffer(c.value)) return a.value.equals(c.value);
4839
+ /** @internal Anything else, including an otherName's structure and a form this profile leaves
4840
+ * undecoded, is compared as its own encoding: two decoded structures would compare by identity
4841
+ * and never match, and two undecoded ones both read as null and would compare equal. */
4842
+ return Buffer.isBuffer(a.bytes) && Buffer.isBuffer(c.bytes) && a.bytes.equals(c.bytes);
4843
+ }
4844
+ /** @internal "one of the subject names included in the certificate", which is the subject AND
4845
+ * every subjectAltName entry: the clause says names, plural, and a hint the subject does not
4846
+ * carry but an alternative name does is one of them. A directoryName is compared under the
4847
+ * RFC 5280 sec. 7.1 rules wherever it appears, since two encodings of one distinguished name are
4848
+ * one name; any other form is compared as its exact encoding. */
4849
+ /** @internal The certificate the token names as the one "to be used to verify the token", or null
4850
+ * when the token does not carry it. Carrying ONE certificate is not that fact: a token may embed an
4851
+ * intermediate and leave the signer's certificate to be supplied out of band, and comparing a hint
4852
+ * against an intermediate reports a conforming token. Which certificate it is comes from the ESS
4853
+ * certHash the schema module resolves, because that value names exactly one certificate while the
4854
+ * SignerInfo sid names none uniquely. */
4855
+ function _tspSignerCertificate(token) {
4856
+ var der = tspSchema.signerCertificateDer(token);
4857
+ if (der === null) return null;
4858
+ // allow:swallow-unverified an element this profile cannot read as a certificate names no subject, so the row passes over the token as it does when the signer is absent
4859
+ try { return x509.parse(der); } catch (_e) { return null; }
4860
+ }
4861
+ function _tspHintMatchesCert(hint, parsedCert) {
4862
+ var decoded = _tspDecodedHint(hint);
4863
+ if (decoded === null) return false;
4864
+ if (decoded.tagNumber === 4 && _sameName(decoded.value, parsedCert.subject)) return true;
4865
+ var san = _tspSanNames(parsedCert), undecided = false;
4866
+ for (var i = 0; i < san.length; i++) {
4867
+ if (decoded.tagNumber === 4) {
4868
+ if (san[i].tagNumber === 4 && _sameName(decoded.value, san[i].value)) return true;
4869
+ continue;
4870
+ }
4871
+ var verdict = _tspSameGeneralName(decoded, san[i]);
4872
+ if (verdict === true) return true;
4873
+ if (verdict === null) undecided = true;
4874
+ }
4875
+ /** @internal A name the comparison rule could not read leaves the row with no verdict on this
4876
+ * certificate, which is reported as nothing rather than as the mismatch it did not establish. */
4877
+ return undecided ? null : false;
4878
+ }
4879
+
4880
+ var RFC3161_TSP_RULES = [
4881
+ {
4882
+ id: "lint/rfc3161/tsa-name-mismatch", severity: "error", source: "rfc3161", citation: "RFC 3161 2.4.2",
4883
+ /** @internal "The purpose of the tsa field is to give a hint in identifying the name of the
4884
+ * TSA. If present, it MUST correspond to one of the subject names included in the certificate
4885
+ * that is to be used to verify the token."
4886
+ * WHICH certificate verifies the token is what the SignerInfo's own identifier names, so the
4887
+ * row resolves it rather than assuming the token carries one certificate and that it is the
4888
+ * signer's. A token that leaves the signer's certificate to be supplied out of band is passed
4889
+ * over, which is the honest edge of reading a hint against bytes that do not hold the answer. */
4890
+ message: "the tsa hint names a subject the signer's own certificate does not carry",
4891
+ appliesTo: function (token) {
4892
+ return !!(token.tstInfo && token.tstInfo.tsa) && _tspSignerCertificate(token) !== null;
4893
+ },
4894
+ check: function (token) {
4895
+ var signerCert = _tspSignerCertificate(token);
4896
+ if (signerCert === null) return null;
4897
+ var hint = token.tstInfo.tsa;
4898
+ if (_tspHintMatchesCert(hint, signerCert) !== false) return null;
4899
+ return { context: { tagNumber: hint.tagNumber } };
4900
+ },
4901
+ },
4902
+ {
4903
+ id: "lint/rfc3161/serial-over-160-bits", severity: "warn", source: "rfc3161", citation: "RFC 3161 2.4.2",
4904
+ /** @internal "Time-Stamping users MUST be ready to accommodate integers up to 160 bits", which
4905
+ * is what a conforming consumer owes and therefore all a TSA may count on. */
4906
+ message: "the serial number is wider than the 160 bits a conforming consumer must accommodate",
4907
+ appliesTo: function (token) { return typeof token.tstInfo.serialNumber === "bigint"; },
4908
+ check: function (token) {
4909
+ var n = token.tstInfo.serialNumber;
4910
+ var magnitude = n < 0n ? -n : n;
4911
+ var bits = magnitude === 0n ? 0 : magnitude.toString(2).length;
4912
+ return bits > TSP_SERIAL_MAX_BITS ? { context: { bits: bits } } : null;
4913
+ },
4914
+ },
4915
+ {
4916
+ id: "lint/rfc3161/accuracy-absent", severity: "notice", source: "rfc3161", citation: "RFC 3161 2.4.2",
4917
+ /** @internal "When the accuracy optional field is not present, then the accuracy may be
4918
+ * available through other means, e.g., the TSAPolicyId." So its absence is conformant and
4919
+ * leaves whoever must order two tokens reading the policy instead. */
4920
+ message: "the token states no accuracy, so bounding its genTime needs the TSA policy instead",
4921
+ check: function (token) {
4922
+ return (token.tstInfo.accuracy === null || token.tstInfo.accuracy === undefined) ? true : null;
4923
+ },
4924
+ },
4925
+ ];
4926
+
4927
+ var TSP_PROFILES = Object.assign(Object.create(null), { "rfc3161": RFC3161_TSP_RULES });
4928
+ var ALL_TSP_RULES = RFC3161_TSP_RULES;
4929
+
4930
+ /** @internal Sec. 4.3's six and the ProxyInfo of sec. 7.2, read from the schema module's row set
4931
+ * rather than restated here, so the criticality this profile reports, the one the AC signer writes
4932
+ * and the deviation the extension door reports cannot disagree. The keys are also what this profile
4933
+ * means by membership, which the outside-profile rule below reads. */
4934
+ var AC_FIXED_CRITICALITY = acSchema.extensionCriticality(oid);
4935
+
4936
+ /** @internal The parser has already refused sec. 4.2's structural MUSTs by the time a rule sees an
4937
+ * attribute certificate: the version being v2, the issuer being a v2Form carrying one non-empty
4938
+ * directoryName with no baseCertificateID or objectDigestInfo beside it, a serial that is not a
4939
+ * positive integer or runs past 20 octets, a repeated attribute type, an empty attribute sequence,
4940
+ * and a GeneralizedTime carrying no seconds or a fraction. Those reach an operator as
4941
+ * `lint/unparseable` with the parser's code, or are refused by the builder first. */
4942
+ var RFC5755_AC_RULES = [
4943
+ {
4944
+ id: "lint/rfc5755/extension-criticality", severity: "error", source: "rfc5755", citation: "RFC 5755 4.3",
4945
+ message: "an extension this profile defines does not carry the criticality its clause states",
4946
+ check: function (ac) {
4947
+ var out = [];
4948
+ (ac.extensions || []).forEach(function (x) {
4949
+ var row = AC_FIXED_CRITICALITY[x.oid];
4950
+ if (!row) return;
4951
+ if (x.critical !== row.critical) {
4952
+ out.push({ context: { extension: row.name, clause: row.clause, critical: x.critical === true } });
4953
+ }
4954
+ });
4955
+ return out.length ? out : null;
4956
+ },
4957
+ },
4958
+ {
4959
+ id: "lint/rfc5755/critical-extension-outside-profile", severity: "error", source: "rfc5755", citation: "RFC 5755 4.2.9",
4960
+ /** @internal "If any other critical extension is used, the AC does not conform to this
4961
+ * profile." What counts as "other" is membership in the PROFILE, which is the table above:
4962
+ * sec. 4.3's six and the ProxyInfo of sec. 7.2. Whether this toolkit happens to decode a value
4963
+ * is a different question and answers it wrongly in both directions, since the AC decoder also
4964
+ * reads AAControls, which sec. 7.4 places in "CA and AC issuer PKCs" rather than in an
4965
+ * attribute certificate. A non-critical extension outside the profile is conformant and draws
4966
+ * nothing, which the clause says outright. */
4967
+ message: "a critical extension this profile does not define makes the attribute certificate non-conforming",
4968
+ check: function (ac) {
4969
+ var out = [];
4970
+ (ac.extensions || []).forEach(function (x) {
4971
+ if (x.critical !== true) return;
4972
+ if (AC_FIXED_CRITICALITY[x.oid]) return;
4973
+ out.push({ context: { oid: x.oid, name: x.name || null } });
4974
+ });
4975
+ return out.length ? out : null;
4976
+ },
4977
+ },
4978
+ {
4979
+ id: "lint/rfc5755/holder-multiple-options", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
4980
+ /** @internal "where more than one option is used, there is a potential for confusion as to
4981
+ * which option is 'normative', which is a 'hint' ... this specification RECOMMENDS that only
4982
+ * one of the options be used in any given AC." A RECOMMENDS, so a warning. */
4983
+ message: "the Holder names more than one option, which leaves which of them is normative unstated",
4984
+ check: function (ac) {
4985
+ var holder = ac.holder;
4986
+ if (!holder) return null;
4987
+ var named = [];
4988
+ ["baseCertificateID", "entityName", "objectDigestInfo"].forEach(function (k) {
4989
+ if (holder[k] !== null && holder[k] !== undefined) named.push(k);
4990
+ });
4991
+ return named.length > 1 ? { context: { options: named } } : null;
4992
+ },
4993
+ },
4994
+ {
4995
+ id: "lint/rfc5755/holder-entity-name-multiple", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
4996
+ /** @internal "In any other case where the Holder field uses the entityName option, only one name
4997
+ * SHOULD be present." A SHOULD, so a warning. This counts the NAMES inside the one entityName
4998
+ * option, which the sibling row above does not: that one counts how many of the three options
4999
+ * the Holder names, and an entityName carrying several GeneralNames is still one option. */
5000
+ message: "the Holder entityName carries more than one name, where this clause asks for one",
5001
+ check: function (ac) {
5002
+ var entity = ac.holder && ac.holder.entityName;
5003
+ if (!entity || !Array.isArray(entity.names) || entity.names.length <= 1) return null;
5004
+ return { context: { names: entity.names.length } };
5005
+ },
5006
+ },
5007
+ {
5008
+ id: "lint/rfc5755/holder-issuer-uid-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
5009
+ /** @internal "The AC holder.baseCertificateID.issuerUID field MUST only be used if the holder's
5010
+ * PKC contains an issuerUniqueID field." Whether the certificate the baseCertificateID names
5011
+ * carries one is a fact about another certificate, so this reports below error and names the
5012
+ * condition, the way the sec. 4.2.8 row does for the AC's own issuerUniqueID. The three sec. 4.2.2
5013
+ * requirements that ARE answerable from these bytes are refused by the parser instead. */
5014
+ message: "the Holder baseCertificateID carries an issuerUID, which this clause permits only where the certificate it names carries an issuerUniqueID",
5015
+ check: function (ac) {
5016
+ var base = ac.holder && ac.holder.baseCertificateID;
5017
+ if (!base) return null;
5018
+ return (base.issuerUID === null || base.issuerUID === undefined) ? null : true;
5019
+ },
5020
+ },
5021
+ {
5022
+ id: "lint/rfc5755/issuer-unique-id-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.8",
5023
+ /** @internal "This field MUST NOT be used unless it is also used in the AC issuer's PKC, in
5024
+ * which case it MUST be used." Whether the issuer's own certificate carries one is a fact about
5025
+ * another certificate, so the row reports below error and its message names the condition. */
5026
+ message: "an issuerUniqueID is present, which this clause permits only where the AC issuer's own certificate carries one",
5027
+ check: function (ac) {
5028
+ return (ac.issuerUniqueID === null || ac.issuerUniqueID === undefined) ? null : true;
5029
+ },
5030
+ },
5031
+ ];
5032
+
5033
+ var AC_PROFILES = Object.assign(Object.create(null), { "rfc5755": RFC5755_AC_RULES });
5034
+ var ALL_AC_RULES = RFC5755_AC_RULES;
5035
+
4618
5036
 
4619
5037
  /** @internal The extensions RFC 6960 and RFC 9654 place on a response, by the list each belongs
4620
5038
  * in. The nonce and the extended revoked definition are response extensions; the archive cutoff and
@@ -5693,6 +6111,286 @@ function ocsp(input, opts) {
5693
6111
  return report;
5694
6112
  }
5695
6113
 
6114
+ var _CMS_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6115
+ var _CMS_CLAIM = ["contentType", "signerInfos", "encapContentInfo"];
6116
+
6117
+ function _ingestCms(input) {
6118
+ var der;
6119
+ var bytes = _derBytesOf(input);
6120
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6121
+ if (bytes !== null) der = bytes.der;
6122
+ else if (typeof input === "string") {
6123
+ try { der = cmsSchema.pemDecode(input); }
6124
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER CMS ContentInfo", context: { code: e.code } } }; }
6125
+ } else if (input !== null && typeof input === "object" && !guard.bytes.isByteSource(input)) {
6126
+ return { cms: guard.parsed.fromTrustedSource(input, "cms", _CMS_CLAIM, function (src) {
6127
+ if (!Buffer.isBuffer(src) && typeof src !== "string") throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
6128
+ return cmsSchema.parse(src);
6129
+ }, _cfg, "lint/bad-input",
6130
+ "pki.lint.cms input must be its DER bytes, a PEM string, or an unmodified pki.schema.cms.parse result: the signed attribute bytes, the signature and the fields those bytes encode are separate properties of a parsed object, so a rebuilt structure could have them describe different messages") };
6131
+ } else {
6132
+ throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
6133
+ }
6134
+ try { return { cms: cmsSchema.parse(der) }; }
6135
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5652", message: "input is not a well-formed CMS ContentInfo", context: { code: e.code } } }; }
6136
+ }
6137
+
6138
+ /**
6139
+ * @primitive pki.lint.cms
6140
+ * @signature pki.lint.cms(pem | der | parsed, opts?) -> LintReport
6141
+ * @since 0.8.26
6142
+ * @status stable
6143
+ * @spec RFC 5652
6144
+ * @related pki.lint.certificate, pki.lint.tsp, pki.schema.cms.parse, pki.cms.sign, pki.cms.verify
6145
+ *
6146
+ * Lint a CMS ContentInfo against the RFC 5652 profile. `input` is a PEM string (`CMS`), a DER
6147
+ * `Buffer`, or an already-parsed `pki.schema.cms.parse` object. It returns the same `LintReport`
6148
+ * shape `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
6149
+ * well-formed ContentInfo produce a single `fatal` finding `lint/unparseable` carrying the strict
6150
+ * parser's own code. A content type other than signed-data carries no signerInfos, so every row is
6151
+ * not applicable to it.
6152
+ *
6153
+ * The profile is short, and what makes it short is that the strict parser has already refused most
6154
+ * of what sections 5 and 11 require. The SignedData and SignerInfo versions against their contents,
6155
+ * signedAttrs being present when the content type is not id-data, the content-type and
6156
+ * message-digest attributes and the binding of the first to eContentType, every attribute's
6157
+ * cardinality and placement, a countersignature's own restrictions, and a time carrying no seconds,
6158
+ * fractional seconds or a local offset all arrive as `lint/unparseable` with the parser's code.
6159
+ *
6160
+ * What the rows cover is what survives that: a signing-time naming a year from 1950 to 2049 encoded
6161
+ * as GeneralizedTime where section 11.3 requires UTCTime, a signer whose digest algorithm the
6162
+ * digestAlgorithms set does not list, a version 1 attribute certificate in the certificates field,
6163
+ * and two structural notices, a SignedData carrying no signers and a detached signature whose
6164
+ * content a verifier must be handed separately.
6165
+ *
6166
+ * Verifying a signature, resolving a signer to a certificate and checking a message digest are
6167
+ * `pki.cms.verify`'s, and the certificates a message carries are linted with
6168
+ * `pki.lint.certificate`.
6169
+ *
6170
+ * @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
6171
+ * profile name is refused rather than run against a message.
6172
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6173
+ * always reflect the complete, unfiltered result.
6174
+ * @example
6175
+ * var pair = await pki.key.generate("Ed25519");
6176
+ * var key = await pki.key.export(pair.privateKey);
6177
+ * var cert = await pki.x509.sign({ subject: "Example Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
6178
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
6179
+ * extensions: { subjectKeyIdentifier: true } }, { key: key });
6180
+ * var der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
6181
+ * pki.lint.cms(der).findings.length; // -> 0
6182
+ * pki.lint.cms(await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { detached: true })).worst;
6183
+ * // -> "notice"
6184
+ */
6185
+ function cms(input, opts) {
6186
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6187
+ guard.identifier.assertKnownKeys(opts, _CMS_OPTS, _cfg, "lint/bad-input",
6188
+ "pki.lint.cms has an unknown option: ");
6189
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6190
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6191
+ }
6192
+ var profile = opts.profile;
6193
+ var rules_;
6194
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
6195
+ else if (CMS_PROFILES[profile]) rules_ = CMS_PROFILES[profile];
6196
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6197
+ else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
6198
+ else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
6199
+ else throw _cfg("lint/unknown-profile", "unknown CMS lint profile \"" + profile + "\" (known: " + Object.keys(CMS_PROFILES).join(", ") + ")");
6200
+
6201
+ var ingested = _ingestCms(input);
6202
+ var report;
6203
+ if (ingested.fatal) {
6204
+ report = { findings: [ingested.fatal], ran: [],
6205
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6206
+ } else {
6207
+ report = _runLints(rules_, ingested.cms, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6208
+ }
6209
+ report.worst = _worst(report.findings);
6210
+ report.findings = _applyThreshold(report, opts.severity);
6211
+ return report;
6212
+ }
6213
+
6214
+ var _TSP_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6215
+
6216
+ function _ingestTsp(input) {
6217
+ var der;
6218
+ var bytes = _derBytesOf(input);
6219
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6220
+ if (bytes !== null) der = bytes.der;
6221
+ else if (typeof input === "string") {
6222
+ try { der = tspSchema.pemDecode(input); }
6223
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER timestamp token", context: { code: e.code } } }; }
6224
+ } else {
6225
+ throw _cfg("lint/bad-input", "pki.lint.tsp input must be a DER Buffer or a PEM string");
6226
+ }
6227
+ try { return { token: tspSchema.parseToken(der) }; }
6228
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 3161", message: "input is not a well-formed RFC 3161 timestamp token", context: { code: e.code } } }; }
6229
+ }
6230
+
6231
+ /**
6232
+ * @primitive pki.lint.tsp
6233
+ * @signature pki.lint.tsp(pem | der, opts?) -> LintReport
6234
+ * @since 0.8.26
6235
+ * @status stable
6236
+ * @spec RFC 3161
6237
+ * @related pki.lint.cms, pki.schema.tsp.parseToken, pki.tsp.sign, pki.tsp.verify
6238
+ *
6239
+ * Lint an RFC 3161 timestamp token against the section 2.4.2 profile. `input` is a PEM string or a
6240
+ * DER `Buffer` holding a TimeStampToken, which is a CMS SignedData encapsulating a TSTInfo. It
6241
+ * returns the same `LintReport` shape `pki.lint.certificate` returns and never throws on the data
6242
+ * path: bytes that are not a well-formed token produce a single `fatal` finding `lint/unparseable`
6243
+ * carrying the strict parser's own code.
6244
+ *
6245
+ * This profile is the shortest the toolkit ships, and the reason is that `pki.schema.tsp.parseToken`
6246
+ * already refuses what the section requires. A token that is not a CMS SignedData, encapsulates
6247
+ * something other than id-ct-TSTInfo, carries detached content, carries a signature other than the
6248
+ * TSA's own, or omits the SigningCertificate attribute the section names arrives as
6249
+ * `lint/unparseable` with the parser's code rather than as a row, and so do a TSTInfo version other
6250
+ * than v1, an Accuracy field outside its stated bounds and a DEFAULT FALSE written out.
6251
+ *
6252
+ * What the rows cover is what survives that: a tsa hint naming a subject the signer's own
6253
+ * certificate does not carry, a serial number wider than the 160 bits a conforming consumer must
6254
+ * accommodate, and a token stating no accuracy. The tsa row resolves which certificate verifies the
6255
+ * token from the SignerInfo's own identifier, and passes over a token that leaves that certificate
6256
+ * to be supplied out of band.
6257
+ *
6258
+ * Verifying the signature, resolving the TSA certificate and comparing the imprint against content
6259
+ * are `pki.tsp.verify`'s.
6260
+ *
6261
+ * @opts profile `"rfc3161"`, which is also what the default runs.
6262
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6263
+ * always reflect the complete, unfiltered result.
6264
+ * @example
6265
+ * var pair = await pki.key.generate("Ed25519");
6266
+ * var key = await pki.key.export(pair.privateKey);
6267
+ * var cert = await pki.x509.sign({ subject: "Example TSA", subjectPublicKey: await pki.key.export(pair.publicKey),
6268
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
6269
+ * extensions: { subjectKeyIdentifier: true, keyUsage: ["digitalSignature"],
6270
+ * extendedKeyUsage: ["timeStamping"], extendedKeyUsageCritical: true } }, { key: key });
6271
+ * var imprint = { hashAlgorithm: "sha256", hashedMessage: Buffer.alloc(32, 7) };
6272
+ * var token = await pki.tsp.sign(imprint, { cert: cert, key: key }, { policy: "1.2.3", serialNumber: 1 });
6273
+ * pki.lint.tsp(token).worst; // -> "notice", since the token states no accuracy
6274
+ */
6275
+ function tsp(input, opts) {
6276
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6277
+ guard.identifier.assertKnownKeys(opts, _TSP_OPTS, _cfg, "lint/bad-input",
6278
+ "pki.lint.tsp has an unknown option: ");
6279
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6280
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6281
+ }
6282
+ var profile = opts.profile;
6283
+ var rules_;
6284
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
6285
+ else if (TSP_PROFILES[profile]) rules_ = TSP_PROFILES[profile];
6286
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6287
+ else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6288
+ else throw _cfg("lint/unknown-profile", "unknown timestamp lint profile \"" + profile + "\" (known: " + Object.keys(TSP_PROFILES).join(", ") + ")");
6289
+
6290
+ var ingested = _ingestTsp(input);
6291
+ var report;
6292
+ if (ingested.fatal) {
6293
+ report = { findings: [ingested.fatal], ran: [],
6294
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6295
+ } else {
6296
+ report = _runLints(rules_, ingested.token, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6297
+ }
6298
+ report.worst = _worst(report.findings);
6299
+ report.findings = _applyThreshold(report, opts.severity);
6300
+ return report;
6301
+ }
6302
+
6303
+ var _AC_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6304
+
6305
+ function _ingestAttrCert(input) {
6306
+ var der;
6307
+ var bytes = _derBytesOf(input);
6308
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6309
+ if (bytes !== null) der = bytes.der;
6310
+ else if (typeof input === "string") {
6311
+ try { der = acSchema.pemDecode(input); }
6312
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER attribute certificate", context: { code: e.code } } }; }
6313
+ } else {
6314
+ throw _cfg("lint/bad-input", "pki.lint.attrcert input must be a DER Buffer or a PEM string");
6315
+ }
6316
+ try { return { ac: acSchema.parse(der) }; }
6317
+ catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5755", message: "input is not a well-formed X.509 attribute certificate", context: { code: e.code } } }; }
6318
+ }
6319
+
6320
+ /**
6321
+ * @primitive pki.lint.attrcert
6322
+ * @signature pki.lint.attrcert(pem | der, opts?) -> LintReport
6323
+ * @since 0.8.26
6324
+ * @status stable
6325
+ * @spec RFC 5755
6326
+ * @related pki.lint.certificate, pki.schema.attrcert.parse, pki.attrcert.sign, pki.attrcert.verify
6327
+ *
6328
+ * Lint an X.509 attribute certificate against the RFC 5755 profile. `input` is a PEM string
6329
+ * (`ATTRIBUTE CERTIFICATE`) or a DER `Buffer`. It returns the same `LintReport` shape
6330
+ * `pki.lint.certificate` returns and never throws on the data path: bytes that are not a
6331
+ * well-formed attribute certificate produce a single `fatal` finding `lint/unparseable` carrying
6332
+ * the strict parser's own code.
6333
+ *
6334
+ * Section 4.2's structural requirements are already refused at parse and so carry no rows: the
6335
+ * version being v2, the issuer being a v2Form carrying one non-empty directoryName with no
6336
+ * baseCertificateID or objectDigestInfo beside it, a serial that is not a positive integer or runs
6337
+ * past 20 octets, a repeated attribute type, an empty attribute sequence, a GeneralizedTime carrying
6338
+ * no seconds or a fractional part, and a Holder baseCertificateID whose issuer is anything other than
6339
+ * one non-empty directoryName, which section 4.2.2 requires in the same words section 4.2.3 uses for
6340
+ * the issuer.
6341
+ *
6342
+ * The rows are the clauses left, which are the ones these bytes cannot settle alone or that state a
6343
+ * recommendation rather than a requirement: the criticality section 4.3 fixes for each extension it
6344
+ * defines and the ProxyInfo of section 7.2, a critical extension the profile does not define, a
6345
+ * Holder naming more than one of its three options, a Holder entityName carrying more than one name,
6346
+ * a Holder baseCertificateID issuerUID, and an issuerUniqueID. The last two are permitted only where
6347
+ * another certificate carries the matching field, so each is a warning naming that condition.
6348
+ * Section 4.2.2's remaining requirements are about the public-key certificate the Holder names rather
6349
+ * than about this one, so no row can answer them.
6350
+ *
6351
+ * Verifying the signature and resolving the holder against a public-key certificate are
6352
+ * `pki.attrcert.verify`'s.
6353
+ *
6354
+ * @opts profile `"rfc5755"`, which is also what the default runs.
6355
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6356
+ * always reflect the complete, unfiltered result.
6357
+ * @example
6358
+ * var pair = await pki.key.generate("Ed25519");
6359
+ * var key = await pki.key.export(pair.privateKey);
6360
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
6361
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
6362
+ * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
6363
+ * { name: "CN=Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
6364
+ * pki.lint.attrcert(der).findings.length; // -> 0 for a conforming attribute certificate
6365
+ */
6366
+ function attrcert(input, opts) {
6367
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6368
+ guard.identifier.assertKnownKeys(opts, _AC_OPTS, _cfg, "lint/bad-input",
6369
+ "pki.lint.attrcert has an unknown option: ");
6370
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6371
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6372
+ }
6373
+ var profile = opts.profile;
6374
+ var rules_;
6375
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
6376
+ else if (AC_PROFILES[profile]) rules_ = AC_PROFILES[profile];
6377
+ else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6378
+ else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6379
+ else throw _cfg("lint/unknown-profile", "unknown attribute certificate lint profile \"" + profile + "\" (known: " + Object.keys(AC_PROFILES).join(", ") + ")");
6380
+
6381
+ var ingested = _ingestAttrCert(input);
6382
+ var report;
6383
+ if (ingested.fatal) {
6384
+ report = { findings: [ingested.fatal], ran: [],
6385
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6386
+ } else {
6387
+ report = _runLints(rules_, ingested.ac, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6388
+ }
6389
+ report.worst = _worst(report.findings);
6390
+ report.findings = _applyThreshold(report, opts.severity);
6391
+ return report;
6392
+ }
6393
+
5696
6394
  /**
5697
6395
  * @primitive pki.lint.rules
5698
6396
  * @signature pki.lint.rules(profile?, artifact?) -> [{id, severity, source, citation}]
@@ -5718,6 +6416,7 @@ function ocsp(input, opts) {
5718
6416
  * literal would answer for "constructor" and the rest of Object.prototype. */
5719
6417
  var BY_ARTIFACT = Object.assign(Object.create(null), {
5720
6418
  certificate: PROFILES, csr: CSR_PROFILES, crl: CRL_PROFILES, ocsp: OCSP_PROFILES,
6419
+ cms: CMS_PROFILES, tsp: TSP_PROFILES, attrcert: AC_PROFILES,
5721
6420
  });
5722
6421
 
5723
6422
  function rules(profile, artifact) {
@@ -5739,7 +6438,8 @@ function rules(profile, artifact) {
5739
6438
  BY_ARTIFACT[artifact][name].forEach(function (r) { set.push(r); });
5740
6439
  });
5741
6440
  } else if (profile == null) {
5742
- set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES);
6441
+ set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES)
6442
+ .concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
5743
6443
  } else if (artifact != null) {
5744
6444
  set = BY_ARTIFACT[artifact][profile];
5745
6445
  if (!set) {
@@ -5747,7 +6447,9 @@ function rules(profile, artifact) {
5747
6447
  " lint profile (known: " + Object.keys(BY_ARTIFACT[artifact]).join(", ") + ")");
5748
6448
  }
5749
6449
  } else {
5750
- set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] || CSR_PROFILES[profile];
6450
+ set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
6451
+ CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
6452
+ AC_PROFILES[profile];
5751
6453
  if (!set) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\"");
5752
6454
  }
5753
6455
  /** @internal One row per id: a rule reused across artifacts, like the RFC 5280 sec. 4.2
@@ -5769,16 +6471,29 @@ function rules(profile, artifact) {
5769
6471
  * @status stable
5770
6472
  * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
5771
6473
  *
5772
- * List the known lint-profile names.
6474
+ * List the known lint-profile names. The two names every verb reads as naming no profile, `"all"`
6475
+ * and `"default"`, are not among them.
5773
6476
  * @example
5774
- * pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl", "rfc6960", "rfc5019"]
6477
+ * pki.lint.profiles();
6478
+ * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
6479
+ * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root", "rfc2986",
6480
+ * // "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc3161", "rfc5755"]
5775
6481
  */
5776
6482
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
5777
6483
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
5778
6484
  * asks for, so one name means one set of requirements whichever artifact carries it. */
6485
+ /** @internal Enumerated from the SAME registry-of-registries `rules()` dispatches on, so a profile a
6486
+ * verb can run is a profile this answers with. Naming the registries here instead left the three
6487
+ * profiles added with the CMS, timestamp and attribute-certificate verbs undiscoverable while
6488
+ * `rules()` returned their rows, and a fourth verb would have repeated it. The order follows
6489
+ * BY_ARTIFACT's, and a name reachable under two artifacts is listed once. */
5779
6490
  function profiles() {
5780
- var out = Object.keys(PROFILES).concat(Object.keys(CRL_PROFILES)).concat(Object.keys(OCSP_PROFILES));
5781
- Object.keys(CSR_PROFILES).forEach(function (name) { if (out.indexOf(name) === -1) out.push(name); });
6491
+ var out = [];
6492
+ Object.keys(BY_ARTIFACT).forEach(function (artifact) {
6493
+ Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
6494
+ if (out.indexOf(name) === -1) out.push(name);
6495
+ });
6496
+ });
5782
6497
  return out;
5783
6498
  }
5784
6499
 
@@ -5787,6 +6502,9 @@ module.exports = {
5787
6502
  csr: csr,
5788
6503
  crl: crl,
5789
6504
  ocsp: ocsp,
6505
+ cms: cms,
6506
+ tsp: tsp,
6507
+ attrcert: attrcert,
5790
6508
  rules: rules,
5791
6509
  profiles: profiles,
5792
6510
  };