@blamejs/pki 0.8.25 → 0.8.27

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,10 +36,14 @@ 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");
42
45
  var path = require("./path-validate");
46
+ var compositeSig = require("./composite-sig");
43
47
  var C = require("./constants");
44
48
  var ipUtils = require("./ip-utils");
45
49
 
@@ -3910,8 +3914,59 @@ function _pqcSigKeyUsageRule(id, source, citation, family, appliesTo) {
3910
3914
  check: _pqcSigKeyUsage,
3911
3915
  };
3912
3916
  }
3917
+ /** @internal The FIPS 204 public-key size of each ML-DSA parameter set, keyed by the identifier a
3918
+ * certificate names it with, read from the composite module's own table so one figure serves the
3919
+ * arms that sign with it and the row that reads it. */
3920
+ var ML_DSA_PK_LEN = Object.create(null);
3921
+ var ML_DSA_SIG_LEN = Object.create(null);
3922
+ Object.keys(compositeSig.MLDSA_COMPONENT).forEach(function (name) {
3923
+ var row = compositeSig.MLDSA_COMPONENT[name];
3924
+ ML_DSA_PK_LEN[oid.byName(row.oid)] = row.pk;
3925
+ ML_DSA_SIG_LEN[oid.byName(row.oid)] = row.sig;
3926
+ });
3927
+
3913
3928
  var RFC9881_RULES = [
3914
3929
  _pqcSigKeyUsageRule("lint/rfc9881/mldsa-key-usage", "rfc9881", "RFC 9881 5", "ML-DSA", _isMlDsa),
3930
+ {
3931
+ id: "lint/rfc9881/mldsa-key-length", severity: "error", source: "rfc9881",
3932
+ citation: "RFC 9881 4 / FIPS 204",
3933
+ /** @internal Sec. 4: the subjectPublicKey is "the raw octet string" of the ML-DSA public key, whose
3934
+ * length FIPS 204 fixes per parameter set. An identifier names one set, so bytes of any other
3935
+ * length are not the key the identifier claims, and the OID alone would accept them. The ML-KEM
3936
+ * row beside this one reads its own sizes the same way. */
3937
+ message: "an ML-DSA subjectPublicKey must be the raw public key at its exact FIPS 204 size for the OID, in a byte-aligned BIT STRING",
3938
+ appliesTo: function (cert) {
3939
+ var spki = cert.subjectPublicKeyInfo;
3940
+ return !!(spki && spki.algorithm && ML_DSA_PK_LEN[spki.algorithm.oid] !== undefined);
3941
+ },
3942
+ check: function (cert) {
3943
+ var spki = cert.subjectPublicKeyInfo;
3944
+ var want = ML_DSA_PK_LEN[spki.algorithm.oid];
3945
+ var pub = spki.publicKey;
3946
+ return (guard.crypto.isOctetAligned(pub) && pub.bytes.length === want) ? null
3947
+ : { context: { expected: want, got: pub.bytes.length, unusedBits: pub.unusedBits } };
3948
+ },
3949
+ },
3950
+ {
3951
+ id: "lint/rfc9881/mldsa-signature-length", severity: "error", source: "rfc9881",
3952
+ citation: "RFC 9881 4 / FIPS 204",
3953
+ /** @internal The signature is the FIPS 204 value carried directly in the BIT STRING, and FIPS 204
3954
+ * fixes its length per parameter set as it fixes the public key's. So a signatureValue of any other
3955
+ * length is not a signature of the algorithm the identifier names, whatever a verifier would later
3956
+ * make of it, and reading the identifier alone accepted it. This reads the SIGNATURE algorithm
3957
+ * rather than the subject key, since the two name different things about one certificate. */
3958
+ message: "an ML-DSA signatureValue must be the signature at its exact FIPS 204 size for the OID, in a byte-aligned BIT STRING",
3959
+ appliesTo: function (cert) {
3960
+ var sig = cert.signatureAlgorithm;
3961
+ return !!(sig && ML_DSA_SIG_LEN[sig.oid] !== undefined);
3962
+ },
3963
+ check: function (cert) {
3964
+ var want = ML_DSA_SIG_LEN[cert.signatureAlgorithm.oid];
3965
+ var value = cert.signatureValue;
3966
+ return (guard.crypto.isOctetAligned(value) && value.bytes.length === want) ? null
3967
+ : { context: { expected: want, got: value.bytes.length, unusedBits: value.unusedBits } };
3968
+ },
3969
+ },
3915
3970
  ];
3916
3971
  var RFC9909_RULES = [
3917
3972
  _pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
@@ -4612,8 +4667,487 @@ var RFC5280_CRL_RULES = [
4612
4667
  },
4613
4668
  ];
4614
4669
 
4615
- var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
4616
- var ALL_CRL_RULES = RFC5280_CRL_RULES;
4670
+ /** @internal Sec. 8: "The signatures on CRLs in this profile MUST follow the same rules from this
4671
+ * profile that apply to signatures in the certificates", and sec. 9 asks the same of an OCSP
4672
+ * response. It is ONE rule, so the row is written once and each artifact's registry takes it with the
4673
+ * reader its own parse result needs. The rule's citation names sec. 4 and 6.1, where the rule is,
4674
+ * beside the section that carries it here.
4675
+ * ONE IDENTIFIER ANSWERS FOR BOTH. Sec. 6.1 names the inner `signature` field of a TBSCertificate or
4676
+ * TBSCertList as well as the outer `signatureAlgorithm`, and both parsers refuse an artifact whose two
4677
+ * fields disagree, each as `<domain>/bad-signature-algorithm`. So by the time a row reads one, the other cannot differ, and a vector asserts that
4678
+ * verdict rather than a second row reading the same OID twice.
4679
+ * Sec. 6.2 sends the reader to RFC 9881 for the signature value, whose length FIPS 204 fixes per
4680
+ * parameter set, and that document's own row reports one of another length wherever it runs.
4681
+ * Sec. 9's second requirement, RFC 6960 sec. 4.2.2.2, is responder authorization, which is a trust
4682
+ * decision over a chain rather than a property of these bytes, and `pki.ocsp.verify` owns it. */
4683
+ function _cnsaSignatureRow(id, citation, reader) {
4684
+ return {
4685
+ id: id, severity: "error", source: "cnsa-2.0", citation: citation,
4686
+ message: "the signature algorithm is not id-ml-dsa-87",
4687
+ appliesTo: _isCnsa,
4688
+ check: function (artifact) {
4689
+ var sig = reader(artifact);
4690
+ var o = sig && sig.oid;
4691
+ return o === CNSA_ML_DSA_87 ? null : { context: { signatureAlgorithm: (sig && sig.name) || o } };
4692
+ },
4693
+ };
4694
+ }
4695
+
4696
+ /** @internal The other half of the same sentence, for the two artifacts whose own registries carry no
4697
+ * RFC 9881 rows: FIPS 204 fixes the signature's length per parameter set, so a value of another length
4698
+ * is not a signature of the algorithm the identifier names. A certificate is told this by the RFC 9881
4699
+ * row its profile already runs, and a CRL and an OCSP response are told it here, because sec. 8 and
4700
+ * sec. 9 give them a certificate's signature rules and one of the two halves reached them. The value
4701
+ * arrives as a decoded BIT STRING from one parser and as its bytes from the other, so the reader each
4702
+ * registry passes returns the length and whether it is octet-aligned. */
4703
+ function _cnsaSignatureLengthRow(id, citation, algReader, lengthReader) {
4704
+ return {
4705
+ id: id, severity: "error", source: "cnsa-2.0", citation: citation,
4706
+ message: "the signature is not the length FIPS 204 fixes for its algorithm",
4707
+ appliesTo: function (artifact, ctx) {
4708
+ var sig = algReader(artifact);
4709
+ return _isCnsa(artifact, ctx) && !!(sig && ML_DSA_SIG_LEN[sig.oid] !== undefined);
4710
+ },
4711
+ check: function (artifact) {
4712
+ var want = ML_DSA_SIG_LEN[algReader(artifact).oid];
4713
+ var got = lengthReader(artifact);
4714
+ return (got.aligned && got.length === want) ? null
4715
+ : { context: { expected: want, got: got.length, unusedBits: got.unusedBits } };
4716
+ },
4717
+ };
4718
+ }
4719
+ function _crlSignatureAlgorithm(crl) { return crl.signatureAlgorithm; }
4720
+ var CNSA2_CRL_RULES = [
4721
+ _cnsaSignatureRow("lint/cnsa-2.0/crl-signature-not-ml-dsa-87",
4722
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 8 (sec. 4 / 6.1)", _crlSignatureAlgorithm),
4723
+ _cnsaSignatureLengthRow("lint/cnsa-2.0/crl-signature-length",
4724
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 8 (RFC 9881 4 / FIPS 204)", _crlSignatureAlgorithm,
4725
+ function (crl) {
4726
+ var v = crl.signatureValue;
4727
+ return { length: v.bytes.length, unusedBits: v.unusedBits, aligned: guard.crypto.isOctetAligned(v) };
4728
+ }),
4729
+ ];
4730
+
4731
+ var CRL_PROFILES = Object.assign(Object.create(null), {
4732
+ "rfc5280-crl": RFC5280_CRL_RULES,
4733
+ "cnsa-2.0": CNSA2_CRL_RULES,
4734
+ });
4735
+ var ALL_CRL_RULES = RFC5280_CRL_RULES.concat(CNSA2_CRL_RULES);
4736
+
4737
+ /** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
4738
+ * this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
4739
+ * clauses reach an operator as `lint/unparseable` carrying the parser's own code rather than as a
4740
+ * row here. A row for one of them would be a branch no input can drive. Probed against the
4741
+ * shipped parser, the SignedData and SignerInfo versions, the presence of signedAttrs when the
4742
+ * content type is not id-data, the content-type and message-digest attributes and their binding
4743
+ * to eContentType, every attribute's cardinality and placement, a countersignature's own
4744
+ * restrictions, and a time carrying no seconds, fractional seconds or a local offset are all
4745
+ * refused before a rule sees them. What is left is below.
4746
+ * The UTCTime alternative of sec. 11.3 carries two year digits, which the clause reads as 19YY
4747
+ * for YY >= 50 and 20YY otherwise, so every UTCTime names a year inside 1950..2049 and only the
4748
+ * GeneralizedTime direction of that clause can be broken. */
4749
+ var OID_SIGNING_TIME = oid.byName("signingTime");
4750
+ var OID_SIGNED_DATA = oid.byName("signedData");
4751
+
4752
+ /** @internal The content type decides which of these rows apply, and the parser sets `contentType`
4753
+ * as an own property on every content it builds. A content type that carries no signers has no own
4754
+ * `signerInfos`, so testing for that field answers from whatever the prototype chain holds. */
4755
+ function _isSignedData(cms) { return cms.contentType === OID_SIGNED_DATA; }
4756
+ var OID_COUNTERSIGNATURE = oid.byName("countersignature");
4757
+ /** @internal Every SignerInfo a message carries: its own signers, and the countersignatures their
4758
+ * unsigned attributes hold, which sec. 11.4 defines as SignerInfo values and which carry their own
4759
+ * signedAttrs. A rule about a signed attribute governs one wherever it appears, so a message
4760
+ * cannot put the shape a clause forbids one level down and escape it. The parser validates each
4761
+ * countersignature's shape without keeping the result, so it is decoded here, and a nested one is
4762
+ * reached by the same walk. */
4763
+ function _cmsAllSignerInfos(cms) {
4764
+ var out = [];
4765
+ /** @internal Each entry keeps the index of the message's OWN signer it hangs from, not a
4766
+ * position in this walk: a reader given a flattened index would look up a signer that is not
4767
+ * the one at fault, or one that does not exist. Whether the attribute sat on a countersignature
4768
+ * under that signer is the other half of the answer. */
4769
+ function visit(signerInfos, signerIndex, isCounter, depth) {
4770
+ if (!Array.isArray(signerInfos) || depth > C.LIMITS.CMS_MAX_COUNTERSIGNATURE_DEPTH) return;
4771
+ signerInfos.forEach(function (si, i) {
4772
+ var index = isCounter ? signerIndex : i;
4773
+ out.push({ signerInfo: si, signerIndex: index, countersignature: isCounter });
4774
+ (Array.isArray(si.unsignedAttrs) ? si.unsignedAttrs : []).forEach(function (attr) {
4775
+ if (attr.type !== OID_COUNTERSIGNATURE) return;
4776
+ if (!Array.isArray(attr.values)) return;
4777
+ attr.values.forEach(function (value) {
4778
+ var nested;
4779
+ // allow:swallow-unverified the parser already walked this value, so a fault here leaves the countersignature unread rather than reported
4780
+ try { nested = cmsSchema.walkCountersignature(asn1.decode(value), null); }
4781
+ catch (_e) { return; }
4782
+ if (nested) visit([nested], index, true, depth + 1);
4783
+ });
4784
+ });
4785
+ });
4786
+ }
4787
+ visit(cms.signerInfos, 0, false, 0);
4788
+ return out;
4789
+ }
4790
+ function _cmsSignedAttr(signerInfo, dotted) {
4791
+ var attrs = signerInfo.signedAttrs;
4792
+ if (!Array.isArray(attrs)) return null;
4793
+ for (var i = 0; i < attrs.length; i++) {
4794
+ if (attrs[i].type === dotted) return attrs[i];
4795
+ }
4796
+ return null;
4797
+ }
4798
+ /** @internal Whether two DIGEST AlgorithmIdentifiers name one algorithm, under RFC 5754 sec. 2. The
4799
+ * rule lives in the schema module because the set this row reads is also WRITTEN there, by the verb
4800
+ * that adds a countersigner's digest to it, and a set written under one rule and read under another
4801
+ * would list one algorithm twice. Only digest identifiers pass through here, so no row is loosened
4802
+ * for an algorithm whose parameters carry meaning. */
4803
+ var _cmsSameAlgorithm = cmsSchema.sameDigestAlgorithm;
4804
+
4805
+ var RFC5652_CMS_RULES = [
4806
+ {
4807
+ id: "lint/rfc5652/signing-time-encoding", severity: "error", source: "rfc5652", citation: "RFC 5652 11.3",
4808
+ /** @internal "Dates between 1 January 1950 and 31 December 2049 (inclusive) MUST be encoded as
4809
+ * UTCTime. Any dates with year values before 1950 or after 2049 MUST be encoded as
4810
+ * GeneralizedTime." The value is held as its DER bytes, so the encoding is the first byte and
4811
+ * the year is what the value decodes to. */
4812
+ message: "a signing-time naming a year from 1950 to 2049 must be encoded as UTCTime, not GeneralizedTime",
4813
+ appliesTo: _isSignedData,
4814
+ check: function (cms) {
4815
+ var out = [];
4816
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4817
+ var attr = _cmsSignedAttr(entry.signerInfo, OID_SIGNING_TIME);
4818
+ if (!attr || !Array.isArray(attr.values) || !attr.values.length) return;
4819
+ var value = attr.values[0];
4820
+ if (!Buffer.isBuffer(value) || value.length < 2) return;
4821
+ if (value[0] !== asn1.TAGS.GENERALIZED_TIME) return;
4822
+ var when;
4823
+ // allow:swallow-unverified the parser already read this value as a Time, so a fault here answers for no signer
4824
+ try { when = asn1.read.time(asn1.decode(value)); } catch (_e) { return; }
4825
+ var year = when.getUTCFullYear();
4826
+ if (year >= 1950 && year <= 2049) {
4827
+ out.push({ context: { signerIndex: entry.signerIndex,
4828
+ countersignature: entry.countersignature, year: year } });
4829
+ }
4830
+ });
4831
+ return out.length ? out : null;
4832
+ },
4833
+ },
4834
+ {
4835
+ id: "lint/rfc5652/signer-digest-not-listed", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.3",
4836
+ /** @internal "The message digest algorithm SHOULD be among those listed in the digestAlgorithms
4837
+ * field of the associated SignerData. Implementations MAY fail to validate signatures that use
4838
+ * a digest algorithm that is not included in the SignedData digestAlgorithms set." A SHOULD
4839
+ * whose cost is a verifier that declines, so a warning.
4840
+ * Sec. 11.4 makes a countersignature a SignerInfo and does not except it from this clause, and
4841
+ * sec. 5.1 has the collection list the digests "employed by all of the signers", so the walk is
4842
+ * the same one the signing-time rule uses and the finding says which signer it hangs from. */
4843
+ message: "a signer's digest algorithm is not among those the SignedData digestAlgorithms set lists",
4844
+ appliesTo: _isSignedData,
4845
+ check: function (cms) {
4846
+ var listed = Array.isArray(cms.digestAlgorithms) ? cms.digestAlgorithms : [];
4847
+ var out = [];
4848
+ _cmsAllSignerInfos(cms).forEach(function (entry) {
4849
+ var mine = entry.signerInfo.digestAlgorithm;
4850
+ if (!mine) return;
4851
+ var found = listed.some(function (alg) { return _cmsSameAlgorithm(alg, mine); });
4852
+ if (!found) {
4853
+ out.push({ context: { signerIndex: entry.signerIndex, countersignature: entry.countersignature,
4854
+ digestAlgorithm: mine.name || mine.oid } });
4855
+ }
4856
+ });
4857
+ return out.length ? out : null;
4858
+ },
4859
+ },
4860
+ {
4861
+ id: "lint/rfc5652/attribute-certificate-v1", severity: "warn", source: "rfc5652", citation: "RFC 5652 5.1",
4862
+ /** @internal "The use of version 1 attribute certificates is strongly discouraged." The
4863
+ * certificates field is a CertificateChoices SET, whose `[1]` alternative is the v1 attribute
4864
+ * certificate; the parser keeps each element's outer tag for exactly this kind of question. */
4865
+ message: "the certificates field carries a version 1 attribute certificate, whose use this section strongly discourages",
4866
+ appliesTo: _isSignedData,
4867
+ check: function (cms) {
4868
+ var certs = Array.isArray(cms.certificates) ? cms.certificates : [];
4869
+ var count = 0;
4870
+ certs.forEach(function (c) {
4871
+ if (c && c.tagClass === "context" && c.tagNumber === 1) count += 1;
4872
+ });
4873
+ return count ? { context: { count: count } } : null;
4874
+ },
4875
+ },
4876
+ {
4877
+ id: "lint/rfc5652/no-signers", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.1",
4878
+ /** @internal "There MAY be any number of elements in the collection, including zero", so a
4879
+ * SignedData with no signers is conformant. It is also a message carrying certificates and
4880
+ * nothing signed, which is worth naming to whoever expected a signature. */
4881
+ message: "the SignedData carries no signers, so it signs nothing and only conveys its certificates and revocation lists",
4882
+ appliesTo: _isSignedData,
4883
+ check: function (cms) { return cms.signerInfos.length === 0 ? true : null; },
4884
+ },
4885
+ {
4886
+ id: "lint/rfc5652/detached-content", severity: "notice", source: "rfc5652", citation: "RFC 5652 5.2",
4887
+ /** @internal eContent is optional, and its absence means a verifier must be handed the content
4888
+ * by another route. The row reads a SignedData that HAS a signer, since the notice exists for
4889
+ * whoever must verify one: a message with no signers has nothing to supply content for. */
4890
+ message: "the SignedData is detached, so verifying it needs the content supplied separately",
4891
+ appliesTo: function (cms) { return _isSignedData(cms) && cms.signerInfos.length > 0; },
4892
+ check: function (cms) {
4893
+ var encap = cms.encapContentInfo;
4894
+ return (encap && encap.eContent) ? null : true;
4895
+ },
4896
+ },
4897
+ ];
4898
+
4899
+ var CMS_PROFILES = Object.assign(Object.create(null), { "rfc5652": RFC5652_CMS_RULES });
4900
+ var ALL_CMS_RULES = RFC5652_CMS_RULES;
4901
+
4902
+ /** @internal The RFC 3161 profile is the shortest of the three, for the reason the CMS one is
4903
+ * short: `schema.tsp.parseToken` refuses nearly everything sec. 2.4.2 requires of a token before a
4904
+ * rule sees it, each with its own code. A token that is not a CMS SignedData, encapsulates
4905
+ * something other than id-ct-TSTInfo, carries detached content, carries more than the TSA's own
4906
+ * signerInfo, or omits the SigningCertificate attribute the section requires arrives as
4907
+ * `lint/unparseable`. The TSTInfo version reader admits v1 alone, and the Accuracy field bounds,
4908
+ * the DEFAULT FALSE omissions and the PKIStatus range are all enforced too. */
4909
+ var TSP_SERIAL_MAX_BITS = 160;
4910
+
4911
+ /** @internal The tsa hint as a DECODED GeneralName. The TSTInfo keeps it raw, and the shared
4912
+ * GeneralNames decoder is what the rest of this file reads a name with, so the hint is wrapped in
4913
+ * a one-element SEQUENCE and walked through it rather than compared as bytes: two encodings of one
4914
+ * distinguished name are the same name, and an error-severity row must not say otherwise. */
4915
+ function _tspDecodedHint(hint) {
4916
+ if (!hint || !Buffer.isBuffer(hint.bytes)) return null;
4917
+ var names = _generalNamesOf(asn1.build.sequence([asn1.build.raw(hint.bytes)]));
4918
+ return (names && names.length === 1) ? names[0] : null;
4919
+ }
4920
+ /** @internal Every GeneralName the certificate's subjectAltName carries, decoded, each entry
4921
+ * keeping the raw TLV beside its value so a form with no useful decoded form can still be
4922
+ * compared. */
4923
+ function _tspSanNames(parsedCert) {
4924
+ var raw = null;
4925
+ (parsedCert.extensions || []).forEach(function (e) {
4926
+ if (e.oid === oid.byName("subjectAltName")) raw = e.value;
4927
+ });
4928
+ if (!Buffer.isBuffer(raw)) return [];
4929
+ return _generalNamesOf(raw) || [];
4930
+ }
4931
+ /** @internal Each form of GeneralName carrying text is compared under the RFC 5280 sec. 7 rule for
4932
+ * that form, and each rule lives in `guard.name` so this row and the AC target comparison read one
4933
+ * definition. A dNSName is case-insensitive throughout (sec. 7.2). A mailbox is not: sec. 7.5 has
4934
+ * the local part match exactly and the host part match without regard to ASCII case. A URI is not
4935
+ * either: sec. 7.4 prepares both values, lowercasing only the scheme and host, and then calls for a
4936
+ * "case-sensitive exact match". Folding case over a whole mailbox or URI read two names differing
4937
+ * only in a local part or a path as one, so a hint naming neither went unreported.
4938
+ * A verdict of null says the rule reached none, which the caller reports as nothing rather than as a
4939
+ * mismatch it did not establish. */
4940
+ function _tspSameGeneralName(a, c) {
4941
+ if (!a || !c || a.tagNumber !== c.tagNumber) return false;
4942
+ if (typeof a.value === "string" && typeof c.value === "string") {
4943
+ if (a.tagNumber === 2) return guard.name.lowerAscii(a.value) === guard.name.lowerAscii(c.value);
4944
+ if (a.tagNumber === 1 || a.tagNumber === 6) {
4945
+ var verdict = a.tagNumber === 1 ? guard.name.emailEqual(a.value, c.value)
4946
+ : guard.name.uriEqual(a.value, c.value);
4947
+ if (verdict === "match") return true;
4948
+ if (verdict === "no-match") return false;
4949
+ /** @internal A value neither rule can read is compared as its own encoding, which answers the
4950
+ * one case that is certain: identical bytes are one name. */
4951
+ return a.value === c.value ? true : null;
4952
+ }
4953
+ }
4954
+ if (Buffer.isBuffer(a.value) && Buffer.isBuffer(c.value)) return a.value.equals(c.value);
4955
+ /** @internal Anything else, including an otherName's structure and a form this profile leaves
4956
+ * undecoded, is compared as its own encoding: two decoded structures would compare by identity
4957
+ * and never match, and two undecoded ones both read as null and would compare equal. */
4958
+ return Buffer.isBuffer(a.bytes) && Buffer.isBuffer(c.bytes) && a.bytes.equals(c.bytes);
4959
+ }
4960
+ /** @internal "one of the subject names included in the certificate", which is the subject AND
4961
+ * every subjectAltName entry: the clause says names, plural, and a hint the subject does not
4962
+ * carry but an alternative name does is one of them. A directoryName is compared under the
4963
+ * RFC 5280 sec. 7.1 rules wherever it appears, since two encodings of one distinguished name are
4964
+ * one name; any other form is compared as its exact encoding. */
4965
+ /** @internal The certificate the token names as the one "to be used to verify the token", or null
4966
+ * when the token does not carry it. Carrying ONE certificate is not that fact: a token may embed an
4967
+ * intermediate and leave the signer's certificate to be supplied out of band, and comparing a hint
4968
+ * against an intermediate reports a conforming token. Which certificate it is comes from the ESS
4969
+ * certHash the schema module resolves, because that value names exactly one certificate while the
4970
+ * SignerInfo sid names none uniquely. */
4971
+ function _tspSignerCertificate(token) {
4972
+ var der = tspSchema.signerCertificateDer(token);
4973
+ if (der === null) return null;
4974
+ // 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
4975
+ try { return x509.parse(der); } catch (_e) { return null; }
4976
+ }
4977
+ function _tspHintMatchesCert(hint, parsedCert) {
4978
+ var decoded = _tspDecodedHint(hint);
4979
+ if (decoded === null) return false;
4980
+ if (decoded.tagNumber === 4 && _sameName(decoded.value, parsedCert.subject)) return true;
4981
+ var san = _tspSanNames(parsedCert), undecided = false;
4982
+ for (var i = 0; i < san.length; i++) {
4983
+ if (decoded.tagNumber === 4) {
4984
+ if (san[i].tagNumber === 4 && _sameName(decoded.value, san[i].value)) return true;
4985
+ continue;
4986
+ }
4987
+ var verdict = _tspSameGeneralName(decoded, san[i]);
4988
+ if (verdict === true) return true;
4989
+ if (verdict === null) undecided = true;
4990
+ }
4991
+ /** @internal A name the comparison rule could not read leaves the row with no verdict on this
4992
+ * certificate, which is reported as nothing rather than as the mismatch it did not establish. */
4993
+ return undecided ? null : false;
4994
+ }
4995
+
4996
+ var RFC3161_TSP_RULES = [
4997
+ {
4998
+ id: "lint/rfc3161/tsa-name-mismatch", severity: "error", source: "rfc3161", citation: "RFC 3161 2.4.2",
4999
+ /** @internal "The purpose of the tsa field is to give a hint in identifying the name of the
5000
+ * TSA. If present, it MUST correspond to one of the subject names included in the certificate
5001
+ * that is to be used to verify the token."
5002
+ * WHICH certificate verifies the token is what the SignerInfo's own identifier names, so the
5003
+ * row resolves it rather than assuming the token carries one certificate and that it is the
5004
+ * signer's. A token that leaves the signer's certificate to be supplied out of band is passed
5005
+ * over, which is the honest edge of reading a hint against bytes that do not hold the answer. */
5006
+ message: "the tsa hint names a subject the signer's own certificate does not carry",
5007
+ appliesTo: function (token) {
5008
+ return !!(token.tstInfo && token.tstInfo.tsa) && _tspSignerCertificate(token) !== null;
5009
+ },
5010
+ check: function (token) {
5011
+ var signerCert = _tspSignerCertificate(token);
5012
+ if (signerCert === null) return null;
5013
+ var hint = token.tstInfo.tsa;
5014
+ if (_tspHintMatchesCert(hint, signerCert) !== false) return null;
5015
+ return { context: { tagNumber: hint.tagNumber } };
5016
+ },
5017
+ },
5018
+ {
5019
+ id: "lint/rfc3161/serial-over-160-bits", severity: "warn", source: "rfc3161", citation: "RFC 3161 2.4.2",
5020
+ /** @internal "Time-Stamping users MUST be ready to accommodate integers up to 160 bits", which
5021
+ * is what a conforming consumer owes and therefore all a TSA may count on. */
5022
+ message: "the serial number is wider than the 160 bits a conforming consumer must accommodate",
5023
+ appliesTo: function (token) { return typeof token.tstInfo.serialNumber === "bigint"; },
5024
+ check: function (token) {
5025
+ var n = token.tstInfo.serialNumber;
5026
+ var magnitude = n < 0n ? -n : n;
5027
+ var bits = magnitude === 0n ? 0 : magnitude.toString(2).length;
5028
+ return bits > TSP_SERIAL_MAX_BITS ? { context: { bits: bits } } : null;
5029
+ },
5030
+ },
5031
+ {
5032
+ id: "lint/rfc3161/accuracy-absent", severity: "notice", source: "rfc3161", citation: "RFC 3161 2.4.2",
5033
+ /** @internal "When the accuracy optional field is not present, then the accuracy may be
5034
+ * available through other means, e.g., the TSAPolicyId." So its absence is conformant and
5035
+ * leaves whoever must order two tokens reading the policy instead. */
5036
+ message: "the token states no accuracy, so bounding its genTime needs the TSA policy instead",
5037
+ check: function (token) {
5038
+ return (token.tstInfo.accuracy === null || token.tstInfo.accuracy === undefined) ? true : null;
5039
+ },
5040
+ },
5041
+ ];
5042
+
5043
+ var TSP_PROFILES = Object.assign(Object.create(null), { "rfc3161": RFC3161_TSP_RULES });
5044
+ var ALL_TSP_RULES = RFC3161_TSP_RULES;
5045
+
5046
+ /** @internal Sec. 4.3's six and the ProxyInfo of sec. 7.2, read from the schema module's row set
5047
+ * rather than restated here, so the criticality this profile reports, the one the AC signer writes
5048
+ * and the deviation the extension door reports cannot disagree. The keys are also what this profile
5049
+ * means by membership, which the outside-profile rule below reads. */
5050
+ var AC_FIXED_CRITICALITY = acSchema.extensionCriticality(oid);
5051
+
5052
+ /** @internal The parser has already refused sec. 4.2's structural MUSTs by the time a rule sees an
5053
+ * attribute certificate: the version being v2, the issuer being a v2Form carrying one non-empty
5054
+ * directoryName with no baseCertificateID or objectDigestInfo beside it, a serial that is not a
5055
+ * positive integer or runs past 20 octets, a repeated attribute type, an empty attribute sequence,
5056
+ * and a GeneralizedTime carrying no seconds or a fraction. Those reach an operator as
5057
+ * `lint/unparseable` with the parser's code, or are refused by the builder first. */
5058
+ var RFC5755_AC_RULES = [
5059
+ {
5060
+ id: "lint/rfc5755/extension-criticality", severity: "error", source: "rfc5755", citation: "RFC 5755 4.3",
5061
+ message: "an extension this profile defines does not carry the criticality its clause states",
5062
+ check: function (ac) {
5063
+ var out = [];
5064
+ (ac.extensions || []).forEach(function (x) {
5065
+ var row = AC_FIXED_CRITICALITY[x.oid];
5066
+ if (!row) return;
5067
+ if (x.critical !== row.critical) {
5068
+ out.push({ context: { extension: row.name, clause: row.clause, critical: x.critical === true } });
5069
+ }
5070
+ });
5071
+ return out.length ? out : null;
5072
+ },
5073
+ },
5074
+ {
5075
+ id: "lint/rfc5755/critical-extension-outside-profile", severity: "error", source: "rfc5755", citation: "RFC 5755 4.2.9",
5076
+ /** @internal "If any other critical extension is used, the AC does not conform to this
5077
+ * profile." What counts as "other" is membership in the PROFILE, which is the table above:
5078
+ * sec. 4.3's six and the ProxyInfo of sec. 7.2. Whether this toolkit happens to decode a value
5079
+ * is a different question and answers it wrongly in both directions, since the AC decoder also
5080
+ * reads AAControls, which sec. 7.4 places in "CA and AC issuer PKCs" rather than in an
5081
+ * attribute certificate. A non-critical extension outside the profile is conformant and draws
5082
+ * nothing, which the clause says outright. */
5083
+ message: "a critical extension this profile does not define makes the attribute certificate non-conforming",
5084
+ check: function (ac) {
5085
+ var out = [];
5086
+ (ac.extensions || []).forEach(function (x) {
5087
+ if (x.critical !== true) return;
5088
+ if (AC_FIXED_CRITICALITY[x.oid]) return;
5089
+ out.push({ context: { oid: x.oid, name: x.name || null } });
5090
+ });
5091
+ return out.length ? out : null;
5092
+ },
5093
+ },
5094
+ {
5095
+ id: "lint/rfc5755/holder-multiple-options", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
5096
+ /** @internal "where more than one option is used, there is a potential for confusion as to
5097
+ * which option is 'normative', which is a 'hint' ... this specification RECOMMENDS that only
5098
+ * one of the options be used in any given AC." A RECOMMENDS, so a warning. */
5099
+ message: "the Holder names more than one option, which leaves which of them is normative unstated",
5100
+ check: function (ac) {
5101
+ var holder = ac.holder;
5102
+ if (!holder) return null;
5103
+ var named = [];
5104
+ ["baseCertificateID", "entityName", "objectDigestInfo"].forEach(function (k) {
5105
+ if (holder[k] !== null && holder[k] !== undefined) named.push(k);
5106
+ });
5107
+ return named.length > 1 ? { context: { options: named } } : null;
5108
+ },
5109
+ },
5110
+ {
5111
+ id: "lint/rfc5755/holder-entity-name-multiple", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
5112
+ /** @internal "In any other case where the Holder field uses the entityName option, only one name
5113
+ * SHOULD be present." A SHOULD, so a warning. This counts the NAMES inside the one entityName
5114
+ * option, which the sibling row above does not: that one counts how many of the three options
5115
+ * the Holder names, and an entityName carrying several GeneralNames is still one option. */
5116
+ message: "the Holder entityName carries more than one name, where this clause asks for one",
5117
+ check: function (ac) {
5118
+ var entity = ac.holder && ac.holder.entityName;
5119
+ if (!entity || !Array.isArray(entity.names) || entity.names.length <= 1) return null;
5120
+ return { context: { names: entity.names.length } };
5121
+ },
5122
+ },
5123
+ {
5124
+ id: "lint/rfc5755/holder-issuer-uid-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.2",
5125
+ /** @internal "The AC holder.baseCertificateID.issuerUID field MUST only be used if the holder's
5126
+ * PKC contains an issuerUniqueID field." Whether the certificate the baseCertificateID names
5127
+ * carries one is a fact about another certificate, so this reports below error and names the
5128
+ * condition, the way the sec. 4.2.8 row does for the AC's own issuerUniqueID. The three sec. 4.2.2
5129
+ * requirements that ARE answerable from these bytes are refused by the parser instead. */
5130
+ message: "the Holder baseCertificateID carries an issuerUID, which this clause permits only where the certificate it names carries an issuerUniqueID",
5131
+ check: function (ac) {
5132
+ var base = ac.holder && ac.holder.baseCertificateID;
5133
+ if (!base) return null;
5134
+ return (base.issuerUID === null || base.issuerUID === undefined) ? null : true;
5135
+ },
5136
+ },
5137
+ {
5138
+ id: "lint/rfc5755/issuer-unique-id-present", severity: "warn", source: "rfc5755", citation: "RFC 5755 4.2.8",
5139
+ /** @internal "This field MUST NOT be used unless it is also used in the AC issuer's PKC, in
5140
+ * which case it MUST be used." Whether the issuer's own certificate carries one is a fact about
5141
+ * another certificate, so the row reports below error and its message names the condition. */
5142
+ message: "an issuerUniqueID is present, which this clause permits only where the AC issuer's own certificate carries one",
5143
+ check: function (ac) {
5144
+ return (ac.issuerUniqueID === null || ac.issuerUniqueID === undefined) ? null : true;
5145
+ },
5146
+ },
5147
+ ];
5148
+
5149
+ var AC_PROFILES = Object.assign(Object.create(null), { "rfc5755": RFC5755_AC_RULES });
5150
+ var ALL_AC_RULES = RFC5755_AC_RULES;
4617
5151
 
4618
5152
 
4619
5153
  /** @internal The extensions RFC 6960 and RFC 9654 place on a response, by the list each belongs
@@ -5002,8 +5536,28 @@ var RFC5019_OCSP_RULES = [
5002
5536
  },
5003
5537
  ];
5004
5538
 
5005
- var OCSP_PROFILES = Object.assign(Object.create(null), { "rfc6960": RFC6960_OCSP_RULES, "rfc5019": RFC5019_OCSP_RULES });
5006
- var ALL_OCSP_RULES = RFC6960_OCSP_RULES.concat(RFC5019_OCSP_RULES);
5539
+ /** @internal Sec. 9: "an OCSP response MUST be signed conformant with Section 4", which is the rule
5540
+ * the certificate and CRL rows read, over the signature the basic response carries. */
5541
+ function _ocspSignatureAlgorithm(resp) { return _ocspBasic(resp).signatureAlgorithm; }
5542
+ var CNSA2_OCSP_RULES = [
5543
+ _cnsaSignatureRow("lint/cnsa-2.0/response-signature-not-ml-dsa-87",
5544
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 9 (sec. 4 / 6.1)", _ocspSignatureAlgorithm),
5545
+ /** @internal The response parser surfaces the signature as its bytes, having read the BIT STRING, so
5546
+ * there are no unused bits left to ask about and the length is the whole answer. */
5547
+ _cnsaSignatureLengthRow("lint/cnsa-2.0/response-signature-length",
5548
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 9 (RFC 9881 4 / FIPS 204)", _ocspSignatureAlgorithm,
5549
+ function (resp) {
5550
+ var sig = _ocspBasic(resp).signature;
5551
+ return { length: sig.length, unusedBits: 0, aligned: true };
5552
+ }),
5553
+ ];
5554
+
5555
+ var OCSP_PROFILES = Object.assign(Object.create(null), {
5556
+ "rfc6960": RFC6960_OCSP_RULES,
5557
+ "rfc5019": RFC5019_OCSP_RULES,
5558
+ "cnsa-2.0": CNSA2_OCSP_RULES,
5559
+ });
5560
+ var ALL_OCSP_RULES = RFC6960_OCSP_RULES.concat(RFC5019_OCSP_RULES).concat(CNSA2_OCSP_RULES);
5007
5561
 
5008
5562
  /** @internal The extensions an issuer determines, which a request may carry and a CA does not take
5009
5563
  * from one. subjectKeyIdentifier and authorityKeyIdentifier are computed from the keys, and the
@@ -5162,6 +5716,602 @@ var CABF_TLS_CSR_RULE_IDS = [
5162
5716
  "lint/cabf-tls/weak-key",
5163
5717
  ];
5164
5718
 
5719
+ /** @internal The dates NIST IR 8547 sets, read as whole years: "deprecated after 2030" and
5720
+ * "disallowed after 2035" name the years themselves, so each transition begins on 1 January of the
5721
+ * year that follows. The engine compares the certificate's own notBefore, which is the issuance date
5722
+ * the transition governs. */
5723
+ var NIST_DEPRECATED_FROM = new Date("2031-01-01T00:00:00Z");
5724
+ var NIST_DISALLOWED_FROM = new Date("2036-01-01T00:00:00Z");
5725
+
5726
+ function _isNistTransition(cert, ctx) { return ctx.profile === "nist-pqc-transition"; }
5727
+
5728
+ /** @internal An identifier the OID registry names that is NOT an algorithm either of a certificate's
5729
+ * two algorithm fields can carry, so no family is read onto it: a digest, an attribute carrying a key
5730
+ * length, a CMS content-encryption algorithm, a message authentication code, and the two
5731
+ * proof-of-possession algorithms. Each carries a family's word in its name and none is a signature or
5732
+ * a subject key. */
5733
+ var NIST_NOT_AN_ALGORITHM = Object.assign(Object.create(null), {
5734
+ sha1: 1, rsaKeyLen: 1, rsaKeyTransport: 1, dhBasedMac: 1,
5735
+ "id-alg-dhPOP": 1, "id-dhPop-static-HMAC-SHA1": 1, "id-alg-noSignature": 1,
5736
+ });
5737
+
5738
+ /** @internal Which family of IR 8547's tables an algorithm belongs to. Table 2 lists RSA, ECDSA and
5739
+ * EdDSA; Table 4 lists finite-field DH and MQV, elliptic-curve DH and MQV, and RSA key transport.
5740
+ * `pqc` and `hybrid` mark what the transition moves TO, which no row reports, and an identifier the
5741
+ * registry does not name is undefined, which both rows also pass over.
5742
+ * THE ORDER IS THE MECHANISM. Each test is anchored at the start of the name or is an equality, and
5743
+ * the families whose names contain another family's word are claimed first, so by the time the RSA
5744
+ * test runs there is no `ecdsaWith` or `dsaWith` name left for it to take. A single unordered
5745
+ * substring test reads `ecdsaWithSHA256` as a DSA algorithm; a table of exact names owes an entry for
5746
+ * every spelling the registry knows, which is how `sha1WithRSAEncryption` came to be read as no family
5747
+ * at all; and the algorithm a VERIFIER resolves is a third question, since that table holds what this
5748
+ * toolkit will check a signature with and excludes SHA-1 by policy, so reading the family off it let
5749
+ * every SHA-1 signature past the disallowance date.
5750
+ * `lint-nist-transition.test.js` drives every identifier the registry names through the shipped verb,
5751
+ * so a family this fails to claim is reported by a test rather than by silence. */
5752
+ function _nistKindOf(alg) {
5753
+ var name = alg && typeof alg.name === "string" ? alg.name : "";
5754
+ if (name === "" || NIST_NOT_AN_ALGORITHM[name]) return undefined;
5755
+ /** @internal A composite arm names a post-quantum algorithm AND a traditional one, so it is neither
5756
+ * of the other answers. It is given its own, rather than sharing the undefined an unnamed identifier
5757
+ * returns, so a reader can tell a deliberate exclusion from an omission. Both rows pass over it: the
5758
+ * tables list pure families, and the same guidance endorses this construction for the transition
5759
+ * these dates mark. */
5760
+ if (name.indexOf("id-MLDSA") === 0 || name.indexOf("id-MLKEM") === 0) return "hybrid";
5761
+ if (name.indexOf("ML-DSA") === 0 || name.indexOf("ML-KEM") === 0 || name.indexOf("SLH-DSA") === 0 ||
5762
+ name.indexOf("id-ml-dsa-") === 0 || name.indexOf("id-ml-kem-") === 0 ||
5763
+ name.indexOf("id-slh-dsa-") === 0 || name.indexOf("id-hash-") === 0) return "pqc";
5764
+ /** @internal A key-agreement scheme names a key-derivation function for CMS rather than an algorithm
5765
+ * a certificate declares, and its name opens with the family word. */
5766
+ if (name.indexOf("dhSinglePass-") === 0) return undefined;
5767
+ if (name === "ecPublicKey" || name.indexOf("ecdsaWith") === 0 ||
5768
+ name === "X25519" || name === "X448") return "ecc";
5769
+ if (name === "Ed25519" || name === "Ed448") return "eddsa";
5770
+ if (name === "dsa" || name.indexOf("dsaWith") === 0 ||
5771
+ name === "dhpublicnumber" || name === "dhKeyAgreement") return "ffc";
5772
+ return _nistNameCarriesRsa(name) ? "ifc" : undefined;
5773
+ }
5774
+
5775
+ /** @internal Whether a name carries the RSA family's word, asked only after every other family has
5776
+ * claimed its own names, so the one reading that made this ambiguous cannot arise. */
5777
+ function _nistNameCarriesRsa(name) {
5778
+ var lower = guard.name.lowerAscii(name);
5779
+ return lower.indexOf("rsa") !== -1;
5780
+ }
5781
+ /** @internal Whether a classification names one of the families the tables date. `pqc` and `hybrid` are
5782
+ * what the transition moves TO, and an unclassified name is one the registry does not know, so none of
5783
+ * the three is a family these rows report. */
5784
+ function _nistIsClassical(kind) {
5785
+ return kind === "ifc" || kind === "ecc" || kind === "eddsa" || kind === "ffc";
5786
+ }
5787
+
5788
+ /** @internal Every classical algorithm a certificate declares, as `{ field, kind, name }` per field.
5789
+ * Both fields are read because both outlive the issuance: a post-quantum subject key signed with a
5790
+ * classical algorithm still leaves a classical signature for a relying party to verify.
5791
+ * A COMPOSITE is passed over, in both fields. `path.signatureAlgorithmName` answers null for a
5792
+ * composite identifier because it names two algorithms, and a composite subject key carries the arm's
5793
+ * own name, which neither table lists. That is the reading the IR asks for rather than a gap: its
5794
+ * tables list pure families, and sec. 3 of the same document endorses hybrid and dual-algorithm
5795
+ * operation through the transition, so reporting the traditional half would report the construction
5796
+ * these dates exist to carry. */
5797
+ function _nistClassicalDeclarations(cert) {
5798
+ var out = [];
5799
+ var keyAlg = (cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm) || null;
5800
+ var keyKind = _nistKindOf(keyAlg);
5801
+ if (_nistIsClassical(keyKind)) {
5802
+ out.push({ field: "subjectPublicKey", kind: keyKind, name: _spkiAlgName(cert) || (keyAlg && keyAlg.oid) });
5803
+ }
5804
+ var sigAlg = cert.signatureAlgorithm || null;
5805
+ var sigKind = _nistKindOf(sigAlg);
5806
+ if (_nistIsClassical(sigKind)) {
5807
+ out.push({ field: "signature", kind: sigKind, name: (sigAlg && (sigAlg.name || sigAlg.oid)) });
5808
+ }
5809
+ return out;
5810
+ }
5811
+
5812
+ /** @internal Whether a declared subject key sits at the 112-bit security strength, which is the level
5813
+ * the 2030 deprecation applies to. IR 8547 gives no mapping from a strength to a key size; SP 800-57
5814
+ * Part 1 Rev 5 Table 2 does: 112 bits is an RSA modulus of 2048, a finite-field group with L = 2048,
5815
+ * and a curve whose field is 224 to 255 bits. A key WEAKER than that level is weaker than the level
5816
+ * the clause deprecates, so it is reported by the same row rather than passed over. All three
5817
+ * families the tables list are read: an RSA modulus, an elliptic curve's field, and a finite-field
5818
+ * group's prime. Reading two of them left every DSA and Diffie-Hellman key silently outside the
5819
+ * deprecation the row promises.
5820
+ * EdDSA IS DELIBERATELY ABSENT. Ed25519's field is 255 bits, which SP 800-57's ECC column puts in the
5821
+ * 112-bit row, so reading the two documents together would deprecate it after 2030. IR 8547's own
5822
+ * EdDSA row carries a 128-bit entry alone, with no 112-bit entry and no 2030 date, and this profile
5823
+ * cites IR 8547. A reader who reaches for SP 800-57 will find the other answer. */
5824
+ var NIST_112_ECC_FIELD_MAX = 255;
5825
+ function _nistAt112OrBelow(cert, declaration) {
5826
+ var spki = cert.subjectPublicKeyInfo;
5827
+ if (declaration.kind === "ifc") {
5828
+ var bits = _rsaModulusBits(spki);
5829
+ return bits !== null && bits <= 2048 ? { rsaBits: bits } : null;
5830
+ }
5831
+ if (declaration.kind === "ecc") {
5832
+ /** @internal X25519 and X448 are read the way their signing siblings are. RFC 8410 encodes them as
5833
+ * a raw coordinate with no format byte, so there is no SEC1 point to measure, and the field size
5834
+ * SP 800-57's ECC column would place at the 112-bit level, 255 bits for X25519, is the same figure
5835
+ * IR 8547's own EdDSA row answers at 128 bits. The profile follows IR 8547 for that family, so it
5836
+ * follows it here, and both are reported by the 2036 row rather than this one. */
5837
+ if (declaration.name === "X25519" || declaration.name === "X448") return null;
5838
+ var field = _eccFieldBits(spki);
5839
+ return (field !== null && field <= NIST_112_ECC_FIELD_MAX)
5840
+ ? { curve: _ecCurveName(spki), fieldBits: field } : null;
5841
+ }
5842
+ if (declaration.kind === "ffc") {
5843
+ var ffc = _ffcParameterBits(cert, spki);
5844
+ if (ffc === null) return null;
5845
+ /** @internal SP 800-57 Table 2 states the finite-field level as a PAIR, L = 2048 with N = 224 at
5846
+ * 112 bits and L = 3072 with N = 256 at 128, so either parameter caps the strength: a group with
5847
+ * a 3072-bit prime and a 224-bit subgroup is a 112-bit key. Reading the prime alone left that key
5848
+ * outside the deprecation. A shape carrying no subgroup order settles the question on the prime,
5849
+ * which is all it states. */
5850
+ var atLevel = ffc.primeBits <= 2048 ||
5851
+ (ffc.subgroupBits !== null && ffc.subgroupBits <= 224);
5852
+ return atLevel ? { ffcPrimeBits: ffc.primeBits, ffcSubgroupBits: ffc.subgroupBits } : null;
5853
+ }
5854
+ return null;
5855
+ }
5856
+
5857
+ /** @internal Where each finite-field parameter shape carries its subgroup order, as the index of the
5858
+ * INTEGER that holds it, or null for a shape that carries none. The prime is first in all three, and
5859
+ * the subgroup order is NOT: RFC 3279's `Dss-Parms ::= SEQUENCE { p, q, g }` puts it second, its
5860
+ * `DomainParameters ::= SEQUENCE { p, g, q OPTIONAL, j OPTIONAL, validationParms OPTIONAL }` third and
5861
+ * optional, and PKCS#3's `DHParameter ::= SEQUENCE { prime, base, privateValueLength OPTIONAL }`
5862
+ * states no subgroup at all. So the position is read per algorithm rather than by counting fields,
5863
+ * which would read a generator as a subgroup order. */
5864
+ var FFC_SUBGROUP_INDEX = Object.assign(Object.create(null), {
5865
+ dsa: 1, dhpublicnumber: 2, dhKeyAgreement: null,
5866
+ });
5867
+
5868
+ /** @internal A finite-field key's L and N, the pair SP 800-57 Table 2's FFC column is stated in, as
5869
+ * `{ primeBits, subgroupBits }` with `subgroupBits` null where the shape carries no subgroup order.
5870
+ * A parameters field this cannot read answers null, which leaves the row silent rather than reporting
5871
+ * a strength it did not establish. */
5872
+ function _ffcParameterBits(cert, spki) {
5873
+ // allow:swallow-unverified the ingest door has already produced a parsed certificate, so this reads bytes the parser accepted and a shape it cannot read answers null
5874
+ try {
5875
+ var alg = spki.algorithm;
5876
+ var params = alg && alg.parameters;
5877
+ if (!Buffer.isBuffer(params)) return null;
5878
+ var seq = asn1.decode(params);
5879
+ if (!seq.children || !seq.children.length) return null;
5880
+ var p = asn1.read.integer(seq.children[0]);
5881
+ if (p <= 0n) return null;
5882
+ var at = FFC_SUBGROUP_INDEX[alg.name];
5883
+ var q = null;
5884
+ var node = typeof at === "number" ? seq.children[at] : null;
5885
+ if (node && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER) {
5886
+ var read = asn1.read.integer(node);
5887
+ if (read > 0n) q = read.toString(2).length;
5888
+ }
5889
+ return { primeBits: p.toString(2).length, subgroupBits: q };
5890
+ } catch (_e) { return null; }
5891
+ }
5892
+
5893
+ /** @internal The field size of a SEC1 elliptic-curve subject key, which is what SP 800-57 Table 2's ECC
5894
+ * column is stated in, read from THE KEY rather than from a table of curves. RFC 5480 sec. 2.2 encodes
5895
+ * the point as `0x04 || X || Y` with each coordinate padded to the field's whole octets, so its length
5896
+ * gives the field size for any curve, including the ones a table would have to list and the ones a
5897
+ * registry gains later. A table of curve names left every certificate on a curve nobody had added,
5898
+ * `sect233r1` and `brainpoolP224r1` among them, outside the deprecation.
5899
+ * ONLY AN `ecPublicKey` REACHES THIS. RFC 8410 encodes X25519 and X448 as a raw coordinate carrying no
5900
+ * format byte, so reading one here would read key material as a format and answer differently for two
5901
+ * keys of one algorithm; the caller answers for those two before calling.
5902
+ * A CERTIFICATE WHOSE NAMED CURVE AND POINT WIDTH DISAGREE is answered on its point, which is the
5903
+ * key's own size, and the disagreement itself is not this row's to report: the two fields state
5904
+ * different things and nothing here can say which the issuer meant. Such a key is not usable in any
5905
+ * case, since `pki.path.validate` refuses it as `path/bad-input` when it imports the key.
5906
+ * The answer is the field rounded up to whole octets, which is what the encoding states: a 521-bit
5907
+ * field is carried in 66 octets and reads as 528. That is exact for every curve whose field is a whole
5908
+ * number of octets and rounds away from the threshold for the rest, so the only field sizes it could
5909
+ * misread are 249 through 255, which no standard curve uses. */
5910
+ function _eccFieldBits(spki) {
5911
+ var pk = spki && spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
5912
+ if (!Buffer.isBuffer(pk) || pk.length < 2) return null;
5913
+ var form = pk[0];
5914
+ if (form === 0x04) {
5915
+ return (pk.length - 1) % 2 === 0 ? ((pk.length - 1) / 2) * 8 : null;
5916
+ }
5917
+ /** @internal RFC 5480 sec. 2.2 permits the compressed form, which carries one coordinate. */
5918
+ if (form === 0x02 || form === 0x03) return (pk.length - 1) * 8;
5919
+ return null;
5920
+ }
5921
+
5922
+ var NIST_TRANSITION_RULES = [
5923
+ {
5924
+ id: "lint/nist-pqc-transition/classical-disallowed-after-2035", severity: "error",
5925
+ source: "nist-pqc-transition", citation: "NIST IR 8547 ipd Table 2 / Table 4",
5926
+ /** @internal Every classical family the two tables list is "Disallowed after 2035" at the
5927
+ * parameter sets they list, so this row needs no strength mapping: whether the algorithm is one of
5928
+ * those families is the whole question, and the registry answers it. */
5929
+ message: "a classical algorithm is disallowed for a certificate issued after 2035",
5930
+ appliesTo: _isNistTransition,
5931
+ effectiveDate: NIST_DISALLOWED_FROM,
5932
+ check: function (cert) {
5933
+ var found = _nistClassicalDeclarations(cert);
5934
+ return found.length ? found.map(function (d) {
5935
+ return { context: { field: d.field, family: d.kind, algorithm: d.name } };
5936
+ }) : null;
5937
+ },
5938
+ },
5939
+ {
5940
+ id: "lint/nist-pqc-transition/classical-deprecated-after-2030", severity: "warn",
5941
+ source: "nist-pqc-transition", citation: "NIST IR 8547 ipd Table 2 / Table 4",
5942
+ /** @internal The 112-bit rows are "Deprecated after 2030", which SP 800-131A and sec. 4.1 of the
5943
+ * IR define as still usable with the risk accepted, so the severity is a warning where the 2035
5944
+ * row is an error. Only the subject key's own parameters are readable here: a signature
5945
+ * identifier names a hash and a family, not the issuer's key size, so the strength of the
5946
+ * signing key is in the ISSUER's certificate rather than this one. */
5947
+ message: "a classical algorithm at or below the 112-bit security strength is deprecated for a certificate issued after 2030",
5948
+ appliesTo: _isNistTransition,
5949
+ effectiveDate: NIST_DEPRECATED_FROM,
5950
+ check: function (cert) {
5951
+ var out = [];
5952
+ _nistClassicalDeclarations(cert).forEach(function (d) {
5953
+ if (d.field !== "subjectPublicKey") return;
5954
+ var at = _nistAt112OrBelow(cert, d);
5955
+ if (at) out.push({ context: Object.assign({ family: d.kind, algorithm: d.name }, at) });
5956
+ });
5957
+ return out.length ? out : null;
5958
+ },
5959
+ },
5960
+ ];
5961
+
5962
+ /** @internal The CNSA 2.0 suite, from `draft-jenkins-cnsa2-pkix-profile-05` (M. Jenkins, NSA-CCSS,
5963
+ * 5 July 2026, Informational, obsoletes RFC 8603 if approved). The NSA notice lists algorithms; this
5964
+ * document states what a CERTIFICATE must carry, which is what a profile can read, so it is the
5965
+ * citation and it is cited as a draft. Sec. 3 fixes the suite: ML-DSA-87 for signing and ML-KEM-1024
5966
+ * for key establishment.
5967
+ * A certificate declares WHICH kind it is through its own subject public key, which is what makes the
5968
+ * end-entity key-usage rows readable.
5969
+ * THE PROFILE IS A PROFILE OF RFC 5280 (sec. 6), so a clause it prefixes "in adherence with
5970
+ * [RFC5280]" is RFC 5280's requirement as well. Where the document restates one as its own MUST, it
5971
+ * gets a row here, so an operator who names this profile alone still sees it; the default rule set
5972
+ * reports the RFC 5280 rows themselves. */
5973
+ var CNSA_ML_DSA_87 = oid.byName("id-ml-dsa-87");
5974
+ var CNSA_ML_KEM_1024 = oid.byName("id-ml-kem-1024");
5975
+
5976
+ function _isCnsa(cert, ctx) { return ctx.profile === "cnsa-2.0"; }
5977
+ /** @internal Sec. 7.1 governs a SELF-SIGNED CA certificate, and sec. 7.2 every other CA. Matching
5978
+ * issuer and subject names make a certificate self-ISSUED, which is not the same thing: a CA reissued
5979
+ * in its own name under a new key carries both names and was signed by its predecessor, so sec. 7.2
5980
+ * governs it, and sec. 7.2 permits the pathLenConstraint sec. 7.1 forbids. The key identifiers
5981
+ * separate the two without a signature check, which this synchronous surface cannot make, and
5982
+ * `_selfSignedIdentifiers` is where that reading already lives. Reading the names alone reported a
5983
+ * rollover CA for a field its own section allows. */
5984
+ /** @internal Whether this certificate is one of the CA kinds sec. 7.1 and 7.2 govern. A basicConstraints
5985
+ * with cA set says so, and so does a keyUsage asserting keyCertSign: RFC 5280 sec. 4.2.1.3 requires the
5986
+ * cA boolean of any certificate whose keyUsage asserts that bit, so a certificate asserting it is
5987
+ * claiming to be a CA whether or not it carries the extension.
5988
+ * READING ONLY THE EXTENSION MADE ONE CLAUSE UNREPORTABLE. Both CA sections require basicConstraints by
5989
+ * name, and a certificate that omits it is exactly the one that breaks them; deciding the scope on that
5990
+ * same extension graded it as an end entity instead, so the operator saw extended-key-usage findings
5991
+ * written for a different kind of certificate and not the requirement it actually broke. An end entity
5992
+ * does not assert keyCertSign, so nothing that belongs to sec. 7.3 is drawn in by this. */
5993
+ function _cnsaIsCa(cert, ctx) {
5994
+ if (ctx.isCaCert) return true;
5995
+ var ku = ctx.decode("keyUsage");
5996
+ return !!(ku && ku.value && ku.value.keyCertSign === true);
5997
+ }
5998
+ /** @internal Whether a self-ISSUED CA is one sec. 7.1 governs. A rollover CA reissued in its own name
5999
+ * under a new key names its predecessor's key in its authorityKeyIdentifier, and its own key in its
6000
+ * subjectKeyIdentifier, so the two identifiers DISAGREEING is what says sec. 7.2 governs it.
6001
+ * ANYTHING ELSE READS AS SELF-SIGNED, including a certificate that carries no subjectKeyIdentifier for
6002
+ * the comparison to read. That extension is one sec. 7.1 requires by name, so a certificate omitting it
6003
+ * is one this section is about, and letting its absence move the certificate to sec. 7.2 left it with no
6004
+ * finding at all. `_selfSignedIdentifiers` takes the opposite default, because the row that reads it
6005
+ * rests on having settled the question; here the stricter scope is the one that reports the omission.
6006
+ * THE RESIDUAL IS A ROLLOVER CA CARRYING NO authorityKeyIdentifier AT ALL, which reads as a root here
6007
+ * and so is not asked for the extension sec. 7.2 requires. The bytes do not settle it: a root omits
6008
+ * that extension legitimately, and RFC 5280 sec. 4.2.1.1 permits its absence only in a self-signed
6009
+ * certificate, which is the rule the rfc5280 rows report on their own. Inventing a rule that reported
6010
+ * every root for omitting an extension its own section does not ask for would be worse than the gap. */
6011
+ function _cnsaSelfIssuedIsRoot(cert, ctx) {
6012
+ var aki = ctx.decode("authorityKeyIdentifier");
6013
+ var kid = aki && aki.value ? aki.value.keyIdentifier : null;
6014
+ if (!Buffer.isBuffer(kid)) return true;
6015
+ var ski = ctx.decode("subjectKeyIdentifier");
6016
+ if (!Buffer.isBuffer(ski && ski.value)) return true;
6017
+ return kid.equals(ski.value);
6018
+ }
6019
+ function _cnsaSelfSignedCa(cert, ctx) {
6020
+ return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx) && _sameName(cert.issuer, cert.subject) &&
6021
+ _cnsaSelfIssuedIsRoot(cert, ctx);
6022
+ }
6023
+ function _cnsaOtherCa(cert, ctx) {
6024
+ return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx) &&
6025
+ !(_sameName(cert.issuer, cert.subject) && _cnsaSelfIssuedIsRoot(cert, ctx));
6026
+ }
6027
+ function _cnsaAnyCa(cert, ctx) { return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx); }
6028
+ function _cnsaEndEntity(cert, ctx) { return _isCnsa(cert, ctx) && !_cnsaIsCa(cert, ctx); }
6029
+
6030
+ /** @internal Which kind of CNSA certificate this is, from the subject public key sec. 4 fixes: an
6031
+ * ML-DSA-87 key makes it a signature certificate and an ML-KEM-1024 key a key-establishment one.
6032
+ * A key outside the suite has no kind, and the row that reports the key answers for it rather than
6033
+ * the rows that read a kind. */
6034
+ function _cnsaKind(cert) {
6035
+ var spki = cert.subjectPublicKeyInfo;
6036
+ var o = spki && spki.algorithm && spki.algorithm.oid;
6037
+ if (o === CNSA_ML_DSA_87) return "signature";
6038
+ if (o === CNSA_ML_KEM_1024) return "key-establishment";
6039
+ return null;
6040
+ }
6041
+
6042
+ /** @internal The nine bits RFC 5280 sec. 4.2.1.3 defines, so a closed set stated as "all other bits
6043
+ * MUST NOT be set" is read as the bits outside the named ones rather than as a boolean. */
6044
+ var CNSA_KEY_USAGE_BITS = ["digitalSignature", "nonRepudiation", "keyEncipherment",
6045
+ "dataEncipherment", "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
6046
+ /** @internal Sec. 7.1 and 7.2 for a CA, sec. 7.3 for each kind of end entity. `required` must be set,
6047
+ * `optional` may be, and every other bit must not. */
6048
+ var CNSA_KEY_USAGE_SETS = Object.assign(Object.create(null), {
6049
+ ca: { required: ["keyCertSign", "cRLSign"], optional: ["digitalSignature", "nonRepudiation"] },
6050
+ signature: { required: ["digitalSignature"], optional: ["nonRepudiation"] },
6051
+ "key-establishment": { required: ["keyEncipherment"], optional: [] },
6052
+ });
6053
+ /** @internal Every way one keyUsage departs from one of those sets, as a finding each: a required bit
6054
+ * that is not set and a bit outside the set that is. One finding per bit, because a certificate that
6055
+ * sets two forbidden bits has two faults and an operator fixing one would otherwise not see the
6056
+ * other. */
6057
+ function _cnsaKeyUsageFindings(ctx, setName) {
6058
+ var set = CNSA_KEY_USAGE_SETS[setName];
6059
+ var ku = ctx.decode("keyUsage");
6060
+ if (!set || !ku || !ku.value) return null;
6061
+ var v = ku.value, out = [];
6062
+ set.required.forEach(function (bit) {
6063
+ if (v[bit] !== true) out.push({ context: { bit: bit, fault: "required bit not set", set: setName } });
6064
+ });
6065
+ CNSA_KEY_USAGE_BITS.forEach(function (bit) {
6066
+ if (v[bit] !== true) return;
6067
+ if (set.required.indexOf(bit) !== -1 || set.optional.indexOf(bit) !== -1) return;
6068
+ out.push({ context: { bit: bit, fault: "bit outside the set this profile fixes", set: setName } });
6069
+ });
6070
+ /** @internal "All other bits MUST NOT be set" reaches past the nine RFC 5280 sec. 4.2.1.3 names: a
6071
+ * bit beyond them is one of the others. The decoder reports those as one flag rather than by
6072
+ * position, so they are one finding. Reading only the nine left a certificate asserting them
6073
+ * conforming under a clause that closes the set. */
6074
+ if (v.reservedBitsSet === true) {
6075
+ out.push({ context: { bit: "beyond the nine named positions", fault: "bit outside the set this profile fixes", set: setName } });
6076
+ }
6077
+ return out.length ? out : null;
6078
+ }
6079
+
6080
+ /** @internal An extension this profile requires by name, reported one finding per absent one. The
6081
+ * shared syntax row answers for an extension whose value does not decode, so absence here is
6082
+ * absence. */
6083
+ function _cnsaMissingExtensions(ctx, names) {
6084
+ var out = [];
6085
+ names.forEach(function (name) {
6086
+ if (!ctx.raw(name)) out.push({ context: { extension: name } });
6087
+ });
6088
+ return out.length ? out : null;
6089
+ }
6090
+
6091
+ var CNSA2_RULES = [
6092
+ {
6093
+ id: "lint/cnsa-2.0/spki-not-suite", severity: "error", source: "cnsa-2.0",
6094
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 4 / 6.4",
6095
+ /** @internal "Every CNSA Suite certificate MUST use the X.509 v3 format and contain one of the
6096
+ * following as its subject public key: A ML-DSA-87 signature verification key. A ML-KEM-1024
6097
+ * public encapsulation key." Sec. 6.4's "the contents of the parameters component ... MUST be
6098
+ * absent" has no row: the shared AlgorithmIdentifier decoder refuses a present field on both
6099
+ * OIDs, so an operator sees `x509/bad-algorithm-parameters` instead. */
6100
+ message: "the subject public key is not one of the two the suite fixes (ML-DSA-87 or ML-KEM-1024)",
6101
+ appliesTo: _isCnsa,
6102
+ check: function (cert) {
6103
+ return _cnsaKind(cert) === null
6104
+ ? { context: { algorithm: _spkiAlgName(cert) || (cert.subjectPublicKeyInfo &&
6105
+ cert.subjectPublicKeyInfo.algorithm && cert.subjectPublicKeyInfo.algorithm.oid) } }
6106
+ : null;
6107
+ },
6108
+ },
6109
+ {
6110
+ id: "lint/cnsa-2.0/version-not-3", severity: "error", source: "cnsa-2.0",
6111
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 6.3",
6112
+ /** @internal "For this profile, the version field MUST be set to INTEGER value 0x02, indicating
6113
+ * that the certificate conforms to X.509 version 3", which sec. 4 repeats as "Every CNSA Suite
6114
+ * certificate MUST use the X.509 v3 format". A version 1 or 2 certificate carrying no extensions
6115
+ * is well-formed and parses, since RFC 5280 sec. 4.1.2.1 puts the field at DEFAULT v1 and the
6116
+ * parser refuses only an extensions field beneath those versions, so this clause needs a row. */
6117
+ message: "a CNSA Suite certificate must be X.509 version 3",
6118
+ appliesTo: _isCnsa,
6119
+ check: function (cert) {
6120
+ return cert.version === 3 ? null : { context: { version: cert.version } };
6121
+ },
6122
+ },
6123
+ {
6124
+ id: "lint/cnsa-2.0/signature-not-ml-dsa-87", severity: "error", source: "cnsa-2.0",
6125
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 4 / 6.1",
6126
+ /** @internal "The signature applied to all CNSA Suite certificates and CRLs MUST be made with a
6127
+ * ML-DSA-87 signing key", and sec. 6.1 names id-ml-dsa-87 as the identifier for it. The same
6128
+ * sentence that fixes the algorithm also says "HashML-DSA is not permitted", which this row
6129
+ * reports without a second one: a HashML-DSA identifier is not id-ml-dsa-87. */
6130
+ message: "the certificate signature algorithm is not id-ml-dsa-87",
6131
+ appliesTo: _isCnsa,
6132
+ check: function (cert) {
6133
+ var sig = cert.signatureAlgorithm;
6134
+ var o = sig && sig.oid;
6135
+ return o === CNSA_ML_DSA_87 ? null : { context: { signatureAlgorithm: (sig && sig.name) || o } };
6136
+ },
6137
+ },
6138
+ {
6139
+ id: "lint/cnsa-2.0/key-usage-not-critical", severity: "error", source: "cnsa-2.0",
6140
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2 / 7.3",
6141
+ /** @internal "The keyUsage extension MUST be marked as critical", said in each of the three
6142
+ * sections, so one row serves all three scopes rather than three saying the same thing. */
6143
+ message: "the keyUsage extension must be marked critical",
6144
+ appliesTo: _isCnsa,
6145
+ check: function (cert, ctx) {
6146
+ var ku = ctx.decode("keyUsage");
6147
+ if (!ku) return null;
6148
+ return ku.critical === true ? null : true;
6149
+ },
6150
+ },
6151
+ {
6152
+ id: "lint/cnsa-2.0/self-signed-ca-missing-extension", severity: "error", source: "cnsa-2.0",
6153
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1",
6154
+ /** @internal "self-signed CA certificates in this profile MUST contain the subjectKeyIdentifier,
6155
+ * keyUsage, and basicConstraints extensions." All three are reported, including the third: the
6156
+ * scope reads a keyUsage asserting keyCertSign as a CA as well, so a certificate that omits
6157
+ * basicConstraints is still read here and told which extension it owes. */
6158
+ message: "a self-signed CA certificate must carry this extension",
6159
+ appliesTo: _cnsaSelfSignedCa,
6160
+ check: function (cert, ctx) {
6161
+ return _cnsaMissingExtensions(ctx, ["subjectKeyIdentifier", "keyUsage", "basicConstraints"]);
6162
+ },
6163
+ },
6164
+ {
6165
+ id: "lint/cnsa-2.0/ca-missing-extension", severity: "error", source: "cnsa-2.0",
6166
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2",
6167
+ /** @internal "Non-self-signed CA Certificates in this profile MUST contain the
6168
+ * authorityKeyIdentifier, keyUsage, and basicConstraints extensions." A self-signed one is
6169
+ * governed by sec. 7.1 instead, which asks for a subjectKeyIdentifier rather than an authority
6170
+ * one, so the two scopes carry their own rows and a certificate draws one set. */
6171
+ message: "a non-self-signed CA certificate must carry this extension",
6172
+ appliesTo: _cnsaOtherCa,
6173
+ check: function (cert, ctx) {
6174
+ return _cnsaMissingExtensions(ctx, ["authorityKeyIdentifier", "keyUsage", "basicConstraints"]);
6175
+ },
6176
+ },
6177
+ {
6178
+ id: "lint/cnsa-2.0/ca-key-usage-bits", severity: "error", source: "cnsa-2.0",
6179
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6180
+ /** @internal "The keyCertSign and cRLSign bits MUST be set. The digitalSignature and
6181
+ * nonRepudiation bits MAY be set. All other bits MUST NOT be set." Both CA sections say it. */
6182
+ message: "a CA certificate's keyUsage departs from the bits this profile fixes",
6183
+ appliesTo: _cnsaAnyCa,
6184
+ check: function (cert, ctx) { return _cnsaKeyUsageFindings(ctx, "ca"); },
6185
+ },
6186
+ {
6187
+ id: "lint/cnsa-2.0/basic-constraints-not-critical", severity: "error", source: "cnsa-2.0",
6188
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6189
+ /** @internal "the basicConstraints extension MUST be marked as critical", in both CA sections. */
6190
+ message: "a CA certificate's basicConstraints extension must be marked critical",
6191
+ appliesTo: _cnsaAnyCa,
6192
+ check: function (cert, ctx) {
6193
+ var bc = ctx.decode("basicConstraints");
6194
+ if (!bc) return null;
6195
+ return bc.critical === true ? null : true;
6196
+ },
6197
+ },
6198
+ {
6199
+ id: "lint/cnsa-2.0/ca-boolean-not-set", severity: "error", source: "cnsa-2.0",
6200
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6201
+ /** @internal "The cA boolean MUST be set to indicate that the subject is a CA", in both CA
6202
+ * sections. A certificate reaches this scope by carrying that boolean or by asserting keyCertSign,
6203
+ * so the boolean CAN be read as unset here: a certificate whose keyUsage claims the CA role while
6204
+ * its basicConstraints denies it says both things and breaks this clause. The absent extension is
6205
+ * the missing-extension row's answer, so this one reads only an extension that is present. */
6206
+ message: "a CA certificate's basicConstraints must set the cA boolean",
6207
+ appliesTo: _cnsaAnyCa,
6208
+ check: function (cert, ctx) {
6209
+ var bc = ctx.decode("basicConstraints");
6210
+ if (!bc || !bc.value) return null;
6211
+ return bc.value.cA === true ? null : { context: { cA: bc.value.cA === true } };
6212
+ },
6213
+ },
6214
+ {
6215
+ id: "lint/cnsa-2.0/self-signed-ca-path-len-present", severity: "error", source: "cnsa-2.0",
6216
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1",
6217
+ /** @internal "the pathLenConstraint MUST NOT be present", for a self-signed CA alone: sec. 7.2
6218
+ * calls the same field OPTIONAL for a non-self-signed one. */
6219
+ message: "a self-signed CA certificate must not carry a pathLenConstraint",
6220
+ appliesTo: _cnsaSelfSignedCa,
6221
+ check: function (cert, ctx) {
6222
+ var bc = ctx.decode("basicConstraints");
6223
+ if (!bc || !bc.value) return null;
6224
+ var n = bc.value.pathLenConstraint;
6225
+ return (n === null || n === undefined) ? null : { context: { pathLenConstraint: n } };
6226
+ },
6227
+ },
6228
+ {
6229
+ id: "lint/cnsa-2.0/end-entity-missing-extension", severity: "error", source: "cnsa-2.0",
6230
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6231
+ /** @internal "end-entity certificates in this profile MUST contain the authorityKeyIdentifier
6232
+ * and keyUsage extensions." */
6233
+ message: "an end entity certificate must carry this extension",
6234
+ appliesTo: _cnsaEndEntity,
6235
+ check: function (cert, ctx) {
6236
+ return _cnsaMissingExtensions(ctx, ["authorityKeyIdentifier", "keyUsage"]);
6237
+ },
6238
+ },
6239
+ {
6240
+ id: "lint/cnsa-2.0/end-entity-no-subject-key-identifier", severity: "warn", source: "cnsa-2.0",
6241
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6242
+ /** @internal "End-entity certificates SHOULD contain the subjectKeyIdentifier extension", which
6243
+ * is a recommendation rather than a requirement, so the severity follows the clause. */
6244
+ message: "an end entity certificate should carry a subjectKeyIdentifier extension",
6245
+ appliesTo: _cnsaEndEntity,
6246
+ check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
6247
+ },
6248
+ {
6249
+ id: "lint/cnsa-2.0/end-entity-key-usage-bits", severity: "error", source: "cnsa-2.0",
6250
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6251
+ /** @internal Which bits are fixed follows the KIND the subject key declares: "For end-entity
6252
+ * digital signature certificates, the keyUsage extension MUST be set for digitalSignature. The
6253
+ * nonRepudiation bit MAY be set. All other bits ... MUST NOT be set. For end-entity key
6254
+ * establishment certificates, the keyUsage extension MUST be set for keyEncipherment. All other
6255
+ * bits ... MUST NOT be set." A key outside the suite has no kind and is reported by the subject
6256
+ * key row instead, rather than being held to one of the two tables. */
6257
+ message: "an end entity certificate's keyUsage departs from the bits this profile fixes for its key",
6258
+ appliesTo: function (cert, ctx) { return _cnsaEndEntity(cert, ctx) && _cnsaKind(cert) !== null; },
6259
+ check: function (cert, ctx) { return _cnsaKeyUsageFindings(ctx, _cnsaKind(cert)); },
6260
+ },
6261
+ {
6262
+ id: "lint/cnsa-2.0/end-entity-eku-missing", severity: "error", source: "cnsa-2.0",
6263
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6264
+ /** @internal "For all end-entity certificates, the extended key usage extension MUST be present
6265
+ * to indicate the intended use of the certificate." The sentence that follows, "The intended use
6266
+ * MUST be consistent with the keyUsage extension", has no row: which key purposes are consistent
6267
+ * with which bits is not enumerated anywhere in the document, so a table here would be invented
6268
+ * rather than cited. */
6269
+ message: "an end entity certificate must carry an extKeyUsage extension",
6270
+ appliesTo: _cnsaEndEntity,
6271
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
6272
+ },
6273
+ {
6274
+ id: "lint/cnsa-2.0/end-entity-eku-any", severity: "error", source: "cnsa-2.0",
6275
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6276
+ /** @internal "The anyExtendedKeyUsage MUST NOT be asserted." */
6277
+ message: "an end entity certificate's extKeyUsage must not name anyExtendedKeyUsage",
6278
+ appliesTo: _cnsaEndEntity,
6279
+ check: function (cert, ctx) {
6280
+ var d = ctx.decode("extKeyUsage");
6281
+ if (!d || !Array.isArray(d.value)) return null;
6282
+ return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
6283
+ },
6284
+ },
6285
+ {
6286
+ id: "lint/cnsa-2.0/certificate-policies-critical", severity: "error", source: "cnsa-2.0",
6287
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2 / 7.3",
6288
+ /** @internal "If a policy is asserted, the certificatePolicies extension MUST be marked as
6289
+ * non-critical", said in both sections. The same sentence's "MUST contain the OIDs for the
6290
+ * applicable certificate policies" is what RFC 5280 already requires of the syntax, and "If a
6291
+ * policy is not asserted, the certificatePolicies extension MUST be omitted" cannot be read from
6292
+ * the bytes: an extension that is present asserts a policy. Sec. 7.1 says neither of a
6293
+ * self-signed CA, so this row leaves one alone. */
6294
+ message: "a certificatePolicies extension must be marked non-critical",
6295
+ appliesTo: function (cert, ctx) { return _cnsaOtherCa(cert, ctx) || _cnsaEndEntity(cert, ctx); },
6296
+ check: function (cert, ctx) {
6297
+ var cp = ctx.decode("certificatePolicies");
6298
+ if (!cp) return null;
6299
+ return cp.critical === true ? true : null;
6300
+ },
6301
+ },
6302
+ {
6303
+ id: "lint/cnsa-2.0/certificate-policies-qualifiers", severity: "warn", source: "cnsa-2.0",
6304
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2 / 7.3",
6305
+ /** @internal "SHOULD NOT use the policyQualifiers option", a recommendation, so a warning. */
6306
+ message: "a certificatePolicies extension should not carry policy qualifiers",
6307
+ appliesTo: function (cert, ctx) { return _cnsaOtherCa(cert, ctx) || _cnsaEndEntity(cert, ctx); },
6308
+ check: function (cert, ctx) {
6309
+ var ids = _policyQualifierIds(ctx);
6310
+ return ids.length ? { context: { qualifiers: ids } } : null;
6311
+ },
6312
+ },
6313
+ ];
6314
+
5165
6315
  /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried. Every row in
5166
6316
  * every profile below asks for an extension BY NAME, and the shared decoder returns null both for
5167
6317
  * one that is absent and for one whose value does not decode, so without this row a malformed
@@ -5184,12 +6334,25 @@ var PROFILES = Object.assign(Object.create(null), {
5184
6334
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5185
6335
  "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5186
6336
  "apple-root": APPLE_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6337
+ "nist-pqc-transition": NIST_TRANSITION_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6338
+ /** @internal Sec. 4 of the draft names RFC 9881 and RFC 9935 for "the algorithm identifier, and the
6339
+ * syntax and semantics for the Subject Public Key Information field" of the two keys the suite
6340
+ * fixes, so those documents' subject-key rows ARE this profile's rules for them. Without them the
6341
+ * profile read the algorithm OID alone: bytes claiming to be an ML-KEM-1024 key while being the
6342
+ * wrong size for one are not the encapsulation key sec. 4 requires, and were reported as
6343
+ * conforming. The rows are reused rather than restated, so one clause has one row and one citation
6344
+ * wherever it is read. Their key-usage rows govern the same bits this profile's own rows do, at the
6345
+ * same strength, so a certificate breaking both is told twice from two documents rather than once
6346
+ * from a rule that quietly replaced the other. */
6347
+ "cnsa-2.0": CNSA2_RULES.concat(RFC9881_RULES).concat(RFC9935_RULES)
6348
+ .concat(RFC5280_EXTENSION_SYNTAX_RULES),
5187
6349
  });
5188
6350
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
5189
6351
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
5190
6352
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
5191
6353
  * inert under the default set because each one asks whether the caller named the profile. */
5192
- .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES);
6354
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES)
6355
+ .concat(NIST_TRANSITION_RULES).concat(CNSA2_RULES);
5193
6356
 
5194
6357
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
5195
6358
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
@@ -5313,13 +6476,50 @@ function _ingestCsr(input) {
5313
6476
  catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 2986", message: "input is not a well-formed PKCS#10 certification request", context: { code: e.code } } }; }
5314
6477
  }
5315
6478
 
6479
+ /** @internal The noun each verb's refusal calls its own artifact. The keys are `BY_ARTIFACT`'s, and
6480
+ * each one is also its verb's name under `pki.lint`, so a message can name the verb from the key. */
6481
+ var ARTIFACT_NOUN = Object.assign(Object.create(null), {
6482
+ certificate: "certificate", csr: "certification-request", crl: "CRL", ocsp: "OCSP",
6483
+ cms: "CMS", tsp: "timestamp", attrcert: "attribute-certificate",
6484
+ });
6485
+
6486
+ /** @internal The refusal a verb gives for a profile name it does not run, read from the SAME
6487
+ * registry-of-registries `rules` dispatches on. So a name carried by more than one artifact names
6488
+ * every verb that runs it, and every verb answers the same way about it. Each verb used to carry its
6489
+ * own chain of `else if`, naming one artifact per branch in a different order, which made the answer
6490
+ * for a name in two registries depend on which verb was asked, and each chain omitted the registries
6491
+ * nobody had added a branch for: a CMS profile name reached `pki.lint.crl` as simply unknown. */
6492
+ /** @internal "a" or "an" for a noun this file names, decided by the sound its first letter carries:
6493
+ * each of these nouns reads as a vowel exactly when it begins with one, since `CRL` and `CMS` are
6494
+ * read letter by letter and begin with a consonant sound. */
6495
+ function _article(noun) {
6496
+ var c = noun.charAt(0);
6497
+ return (c === "a" || c === "e" || c === "i" || c === "o" || c === "u" ||
6498
+ c === "A" || c === "E" || c === "I" || c === "O" || c === "U") ? "an" : "a";
6499
+ }
6500
+ function _joinAnd(list) {
6501
+ if (list.length <= 1) return list.join("");
6502
+ return list.slice(0, -1).join(", ") + " and " + list[list.length - 1];
6503
+ }
6504
+ function _profileRefusal(profile, mine) {
6505
+ var homes = Object.keys(BY_ARTIFACT).filter(function (a) {
6506
+ return a !== mine && Object.prototype.hasOwnProperty.call(BY_ARTIFACT[a], profile);
6507
+ });
6508
+ var label = _article(ARTIFACT_NOUN[mine]) + " " + ARTIFACT_NOUN[mine] + " lint profile";
6509
+ if (!homes.length) {
6510
+ return _cfg("lint/unknown-profile", "unknown " + ARTIFACT_NOUN[mine] + " lint profile \"" +
6511
+ profile + "\" (known: " + Object.keys(BY_ARTIFACT[mine]).join(", ") + ")");
6512
+ }
6513
+ var nouns = homes.map(function (a) { return ARTIFACT_NOUN[a]; });
6514
+ var verbs = homes.map(function (a) { return "pki.lint." + a; });
6515
+ return _cfg("lint/unknown-profile", "\"" + profile + "\" is not " + label + "; it is " +
6516
+ _article(nouns[0]) + " " + _joinAnd(nouns) + " profile, which " + _joinAnd(verbs) +
6517
+ (verbs.length === 1 ? " runs" : " run"));
6518
+ }
6519
+
5316
6520
  function _selectRules(profile) {
5317
6521
  if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
5318
- if (!PROFILES[profile]) {
5319
- if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
5320
- if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5321
- throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
5322
- }
6522
+ if (!PROFILES[profile]) throw _profileRefusal(profile, "certificate");
5323
6523
  return PROFILES[profile];
5324
6524
  }
5325
6525
 
@@ -5355,7 +6555,9 @@ function _applyThreshold(report, severity) {
5355
6555
  * a CA certificate reports that CA against subscriber requirements. The CA
5356
6556
  * profiles of section 7.1.2.10 are a separate set this does not yet carry. Left
5357
6557
  * unnamed, a certificate is held to those rows only where it asserts
5358
- * id-kp-serverAuth and is not a CA.
6558
+ * id-kp-serverAuth and is not a CA. `"nist-pqc-transition"` and `"cnsa-2.0"` are
6559
+ * named the same way: which algorithm policy a certificate is held to is the
6560
+ * caller's assertion, so neither is detected and both are inert until named.
5359
6561
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
5360
6562
  * `worst` always reflect the complete, unfiltered result.
5361
6563
  * @example
@@ -5454,8 +6656,10 @@ function _ingestCrl(input) {
5454
6656
  * extension repeated on the CRL or on an entry (section 4.2) each arrive as `lint/unparseable`
5455
6657
  * carrying the parser's own code.
5456
6658
  *
5457
- * @opts profile `"rfc5280-crl"`, the only CRL profile (default runs every CRL rule). A
5458
- * certificate profile name is refused rather than run against a CRL.
6659
+ * @opts profile `"rfc5280-crl"`, or `"cnsa-2.0"` for the one rule that profile states about a
6660
+ * CRL's signature (default runs every CRL rule, and the `cnsa-2.0` rule is inert
6661
+ * unless named). A profile name this verb does not run is refused, and the refusal
6662
+ * names the verb that does.
5459
6663
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
5460
6664
  * always reflect the complete, unfiltered result.
5461
6665
  * @example
@@ -5528,10 +6732,7 @@ function csr(input, opts) {
5528
6732
  var rules_;
5529
6733
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CSR_RULES;
5530
6734
  else if (CSR_PROFILES[profile]) rules_ = CSR_PROFILES[profile];
5531
- else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
5532
- else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5533
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
5534
- else throw _cfg("lint/unknown-profile", "unknown certification-request lint profile \"" + profile + "\" (known: " + Object.keys(CSR_PROFILES).join(", ") + ")");
6735
+ else throw _profileRefusal(profile, "csr");
5535
6736
 
5536
6737
  var ingested = _ingestCsr(input);
5537
6738
  var report;
@@ -5559,9 +6760,7 @@ function crl(input, opts) {
5559
6760
  var rules_;
5560
6761
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CRL_RULES;
5561
6762
  else if (CRL_PROFILES[profile]) rules_ = CRL_PROFILES[profile];
5562
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
5563
- else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5564
- else throw _cfg("lint/unknown-profile", "unknown CRL lint profile \"" + profile + "\" (known: " + Object.keys(CRL_PROFILES).join(", ") + ")");
6763
+ else throw _profileRefusal(profile, "crl");
5565
6764
 
5566
6765
  var ingested = _ingestCrl(input);
5567
6766
  var report;
@@ -5640,8 +6839,9 @@ function _ingestOcsp(input) {
5640
6839
  * is `pki.ocsp.verify`'s, and the certificates in `certs` are linted with `pki.lint.certificate`.
5641
6840
  *
5642
6841
  * @opts profile `"rfc6960"` or `"rfc5019"` (default runs the rfc6960 rows and marks the rfc5019
5643
- * rows not applicable). A certificate or CRL profile name is refused rather than
5644
- * run against a response.
6842
+ * rows not applicable), or `"cnsa-2.0"` for the one rule that profile states about
6843
+ * a response's signature. A profile name this verb does not run is refused, and the
6844
+ * refusal names the verb that does.
5645
6845
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
5646
6846
  * always reflect the complete, unfiltered result.
5647
6847
  * @example
@@ -5671,9 +6871,7 @@ function ocsp(input, opts) {
5671
6871
  var rules_;
5672
6872
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_OCSP_RULES;
5673
6873
  else if (OCSP_PROFILES[profile]) rules_ = OCSP_PROFILES[profile];
5674
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
5675
- else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
5676
- else throw _cfg("lint/unknown-profile", "unknown OCSP lint profile \"" + profile + "\" (known: " + Object.keys(OCSP_PROFILES).join(", ") + ")");
6874
+ else throw _profileRefusal(profile, "ocsp");
5677
6875
 
5678
6876
  var ingested = _ingestOcsp(input);
5679
6877
  var report;
@@ -5693,6 +6891,279 @@ function ocsp(input, opts) {
5693
6891
  return report;
5694
6892
  }
5695
6893
 
6894
+ var _CMS_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6895
+ var _CMS_CLAIM = ["contentType", "signerInfos", "encapContentInfo"];
6896
+
6897
+ function _ingestCms(input) {
6898
+ var der;
6899
+ var bytes = _derBytesOf(input);
6900
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6901
+ if (bytes !== null) der = bytes.der;
6902
+ else if (typeof input === "string") {
6903
+ try { der = cmsSchema.pemDecode(input); }
6904
+ 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 } } }; }
6905
+ } else if (input !== null && typeof input === "object" && !guard.bytes.isByteSource(input)) {
6906
+ return { cms: guard.parsed.fromTrustedSource(input, "cms", _CMS_CLAIM, function (src) {
6907
+ 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");
6908
+ return cmsSchema.parse(src);
6909
+ }, _cfg, "lint/bad-input",
6910
+ "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") };
6911
+ } else {
6912
+ throw _cfg("lint/bad-input", "pki.lint.cms input must be a parsed CMS ContentInfo, a DER Buffer, or a PEM string");
6913
+ }
6914
+ try { return { cms: cmsSchema.parse(der) }; }
6915
+ 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 } } }; }
6916
+ }
6917
+
6918
+ /**
6919
+ * @primitive pki.lint.cms
6920
+ * @signature pki.lint.cms(pem | der | parsed, opts?) -> LintReport
6921
+ * @since 0.8.26
6922
+ * @status stable
6923
+ * @spec RFC 5652
6924
+ * @related pki.lint.certificate, pki.lint.tsp, pki.schema.cms.parse, pki.cms.sign, pki.cms.verify
6925
+ *
6926
+ * Lint a CMS ContentInfo against the RFC 5652 profile. `input` is a PEM string (`CMS`), a DER
6927
+ * `Buffer`, or an already-parsed `pki.schema.cms.parse` object. It returns the same `LintReport`
6928
+ * shape `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
6929
+ * well-formed ContentInfo produce a single `fatal` finding `lint/unparseable` carrying the strict
6930
+ * parser's own code. A content type other than signed-data carries no signerInfos, so every row is
6931
+ * not applicable to it.
6932
+ *
6933
+ * The profile is short, and what makes it short is that the strict parser has already refused most
6934
+ * of what sections 5 and 11 require. The SignedData and SignerInfo versions against their contents,
6935
+ * signedAttrs being present when the content type is not id-data, the content-type and
6936
+ * message-digest attributes and the binding of the first to eContentType, every attribute's
6937
+ * cardinality and placement, a countersignature's own restrictions, and a time carrying no seconds,
6938
+ * fractional seconds or a local offset all arrive as `lint/unparseable` with the parser's code.
6939
+ *
6940
+ * What the rows cover is what survives that: a signing-time naming a year from 1950 to 2049 encoded
6941
+ * as GeneralizedTime where section 11.3 requires UTCTime, a signer whose digest algorithm the
6942
+ * digestAlgorithms set does not list, a version 1 attribute certificate in the certificates field,
6943
+ * and two structural notices, a SignedData carrying no signers and a detached signature whose
6944
+ * content a verifier must be handed separately.
6945
+ *
6946
+ * Verifying a signature, resolving a signer to a certificate and checking a message digest are
6947
+ * `pki.cms.verify`'s, and the certificates a message carries are linted with
6948
+ * `pki.lint.certificate`.
6949
+ *
6950
+ * @opts profile `"rfc5652"`, which is also what the default runs. A certificate, CRL or OCSP
6951
+ * profile name is refused rather than run against a message.
6952
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6953
+ * always reflect the complete, unfiltered result.
6954
+ * @example
6955
+ * var pair = await pki.key.generate("Ed25519");
6956
+ * var key = await pki.key.export(pair.privateKey);
6957
+ * var cert = await pki.x509.sign({ subject: "Example Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
6958
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
6959
+ * extensions: { subjectKeyIdentifier: true } }, { key: key });
6960
+ * var der = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
6961
+ * pki.lint.cms(der).findings.length; // -> 0
6962
+ * pki.lint.cms(await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key }, { detached: true })).worst;
6963
+ * // -> "notice"
6964
+ */
6965
+ function cms(input, opts) {
6966
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
6967
+ guard.identifier.assertKnownKeys(opts, _CMS_OPTS, _cfg, "lint/bad-input",
6968
+ "pki.lint.cms has an unknown option: ");
6969
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
6970
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
6971
+ }
6972
+ var profile = opts.profile;
6973
+ var rules_;
6974
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
6975
+ else if (CMS_PROFILES[profile]) rules_ = CMS_PROFILES[profile];
6976
+ else throw _profileRefusal(profile, "cms");
6977
+
6978
+ var ingested = _ingestCms(input);
6979
+ var report;
6980
+ if (ingested.fatal) {
6981
+ report = { findings: [ingested.fatal], ran: [],
6982
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
6983
+ } else {
6984
+ report = _runLints(rules_, ingested.cms, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
6985
+ }
6986
+ report.worst = _worst(report.findings);
6987
+ report.findings = _applyThreshold(report, opts.severity);
6988
+ return report;
6989
+ }
6990
+
6991
+ var _TSP_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
6992
+
6993
+ function _ingestTsp(input) {
6994
+ var der;
6995
+ var bytes = _derBytesOf(input);
6996
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
6997
+ if (bytes !== null) der = bytes.der;
6998
+ else if (typeof input === "string") {
6999
+ try { der = tspSchema.pemDecode(input); }
7000
+ 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 } } }; }
7001
+ } else {
7002
+ throw _cfg("lint/bad-input", "pki.lint.tsp input must be a DER Buffer or a PEM string");
7003
+ }
7004
+ try { return { token: tspSchema.parseToken(der) }; }
7005
+ 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 } } }; }
7006
+ }
7007
+
7008
+ /**
7009
+ * @primitive pki.lint.tsp
7010
+ * @signature pki.lint.tsp(pem | der, opts?) -> LintReport
7011
+ * @since 0.8.26
7012
+ * @status stable
7013
+ * @spec RFC 3161
7014
+ * @related pki.lint.cms, pki.schema.tsp.parseToken, pki.tsp.sign, pki.tsp.verify
7015
+ *
7016
+ * Lint an RFC 3161 timestamp token against the section 2.4.2 profile. `input` is a PEM string or a
7017
+ * DER `Buffer` holding a TimeStampToken, which is a CMS SignedData encapsulating a TSTInfo. It
7018
+ * returns the same `LintReport` shape `pki.lint.certificate` returns and never throws on the data
7019
+ * path: bytes that are not a well-formed token produce a single `fatal` finding `lint/unparseable`
7020
+ * carrying the strict parser's own code.
7021
+ *
7022
+ * This profile is the shortest the toolkit ships, and the reason is that `pki.schema.tsp.parseToken`
7023
+ * already refuses what the section requires. A token that is not a CMS SignedData, encapsulates
7024
+ * something other than id-ct-TSTInfo, carries detached content, carries a signature other than the
7025
+ * TSA's own, or omits the SigningCertificate attribute the section names arrives as
7026
+ * `lint/unparseable` with the parser's code rather than as a row, and so do a TSTInfo version other
7027
+ * than v1, an Accuracy field outside its stated bounds and a DEFAULT FALSE written out.
7028
+ *
7029
+ * What the rows cover is what survives that: a tsa hint naming a subject the signer's own
7030
+ * certificate does not carry, a serial number wider than the 160 bits a conforming consumer must
7031
+ * accommodate, and a token stating no accuracy. The tsa row resolves which certificate verifies the
7032
+ * token from the SignerInfo's own identifier, and passes over a token that leaves that certificate
7033
+ * to be supplied out of band.
7034
+ *
7035
+ * Verifying the signature, resolving the TSA certificate and comparing the imprint against content
7036
+ * are `pki.tsp.verify`'s.
7037
+ *
7038
+ * @opts profile `"rfc3161"`, which is also what the default runs.
7039
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
7040
+ * always reflect the complete, unfiltered result.
7041
+ * @example
7042
+ * var pair = await pki.key.generate("Ed25519");
7043
+ * var key = await pki.key.export(pair.privateKey);
7044
+ * var cert = await pki.x509.sign({ subject: "Example TSA", subjectPublicKey: await pki.key.export(pair.publicKey),
7045
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
7046
+ * extensions: { subjectKeyIdentifier: true, keyUsage: ["digitalSignature"],
7047
+ * extendedKeyUsage: ["timeStamping"], extendedKeyUsageCritical: true } }, { key: key });
7048
+ * var imprint = { hashAlgorithm: "sha256", hashedMessage: Buffer.alloc(32, 7) };
7049
+ * var token = await pki.tsp.sign(imprint, { cert: cert, key: key }, { policy: "1.2.3", serialNumber: 1 });
7050
+ * pki.lint.tsp(token).worst; // -> "notice", since the token states no accuracy
7051
+ */
7052
+ function tsp(input, opts) {
7053
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
7054
+ guard.identifier.assertKnownKeys(opts, _TSP_OPTS, _cfg, "lint/bad-input",
7055
+ "pki.lint.tsp has an unknown option: ");
7056
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
7057
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
7058
+ }
7059
+ var profile = opts.profile;
7060
+ var rules_;
7061
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
7062
+ else if (TSP_PROFILES[profile]) rules_ = TSP_PROFILES[profile];
7063
+ else throw _profileRefusal(profile, "tsp");
7064
+
7065
+ var ingested = _ingestTsp(input);
7066
+ var report;
7067
+ if (ingested.fatal) {
7068
+ report = { findings: [ingested.fatal], ran: [],
7069
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
7070
+ } else {
7071
+ report = _runLints(rules_, ingested.token, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
7072
+ }
7073
+ report.worst = _worst(report.findings);
7074
+ report.findings = _applyThreshold(report, opts.severity);
7075
+ return report;
7076
+ }
7077
+
7078
+ var _AC_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
7079
+
7080
+ function _ingestAttrCert(input) {
7081
+ var der;
7082
+ var bytes = _derBytesOf(input);
7083
+ if (bytes !== null && bytes.fatal) return { fatal: bytes.fatal };
7084
+ if (bytes !== null) der = bytes.der;
7085
+ else if (typeof input === "string") {
7086
+ try { der = acSchema.pemDecode(input); }
7087
+ 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 } } }; }
7088
+ } else {
7089
+ throw _cfg("lint/bad-input", "pki.lint.attrcert input must be a DER Buffer or a PEM string");
7090
+ }
7091
+ try { return { ac: acSchema.parse(der) }; }
7092
+ 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 } } }; }
7093
+ }
7094
+
7095
+ /**
7096
+ * @primitive pki.lint.attrcert
7097
+ * @signature pki.lint.attrcert(pem | der, opts?) -> LintReport
7098
+ * @since 0.8.26
7099
+ * @status stable
7100
+ * @spec RFC 5755
7101
+ * @related pki.lint.certificate, pki.schema.attrcert.parse, pki.attrcert.sign, pki.attrcert.verify
7102
+ *
7103
+ * Lint an X.509 attribute certificate against the RFC 5755 profile. `input` is a PEM string
7104
+ * (`ATTRIBUTE CERTIFICATE`) or a DER `Buffer`. It returns the same `LintReport` shape
7105
+ * `pki.lint.certificate` returns and never throws on the data path: bytes that are not a
7106
+ * well-formed attribute certificate produce a single `fatal` finding `lint/unparseable` carrying
7107
+ * the strict parser's own code.
7108
+ *
7109
+ * Section 4.2's structural requirements are already refused at parse and so carry no rows: the
7110
+ * version being v2, the issuer being a v2Form carrying one non-empty directoryName with no
7111
+ * baseCertificateID or objectDigestInfo beside it, a serial that is not a positive integer or runs
7112
+ * past 20 octets, a repeated attribute type, an empty attribute sequence, a GeneralizedTime carrying
7113
+ * no seconds or a fractional part, and a Holder baseCertificateID whose issuer is anything other than
7114
+ * one non-empty directoryName, which section 4.2.2 requires in the same words section 4.2.3 uses for
7115
+ * the issuer.
7116
+ *
7117
+ * The rows are the clauses left, which are the ones these bytes cannot settle alone or that state a
7118
+ * recommendation rather than a requirement: the criticality section 4.3 fixes for each extension it
7119
+ * defines and the ProxyInfo of section 7.2, a critical extension the profile does not define, a
7120
+ * Holder naming more than one of its three options, a Holder entityName carrying more than one name,
7121
+ * a Holder baseCertificateID issuerUID, and an issuerUniqueID. The last two are permitted only where
7122
+ * another certificate carries the matching field, so each is a warning naming that condition.
7123
+ * Section 4.2.2's remaining requirements are about the public-key certificate the Holder names rather
7124
+ * than about this one, so no row can answer them.
7125
+ *
7126
+ * Verifying the signature and resolving the holder against a public-key certificate are
7127
+ * `pki.attrcert.verify`'s.
7128
+ *
7129
+ * @opts profile `"rfc5755"`, which is also what the default runs.
7130
+ * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
7131
+ * always reflect the complete, unfiltered result.
7132
+ * @example
7133
+ * var pair = await pki.key.generate("Ed25519");
7134
+ * var key = await pki.key.export(pair.privateKey);
7135
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
7136
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
7137
+ * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
7138
+ * { name: "CN=Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
7139
+ * pki.lint.attrcert(der).findings.length; // -> 0 for a conforming attribute certificate
7140
+ */
7141
+ function attrcert(input, opts) {
7142
+ opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
7143
+ guard.identifier.assertKnownKeys(opts, _AC_OPTS, _cfg, "lint/bad-input",
7144
+ "pki.lint.attrcert has an unknown option: ");
7145
+ if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
7146
+ throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
7147
+ }
7148
+ var profile = opts.profile;
7149
+ var rules_;
7150
+ if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
7151
+ else if (AC_PROFILES[profile]) rules_ = AC_PROFILES[profile];
7152
+ else throw _profileRefusal(profile, "attrcert");
7153
+
7154
+ var ingested = _ingestAttrCert(input);
7155
+ var report;
7156
+ if (ingested.fatal) {
7157
+ report = { findings: [ingested.fatal], ran: [],
7158
+ counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
7159
+ } else {
7160
+ report = _runLints(rules_, ingested.ac, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
7161
+ }
7162
+ report.worst = _worst(report.findings);
7163
+ report.findings = _applyThreshold(report, opts.severity);
7164
+ return report;
7165
+ }
7166
+
5696
7167
  /**
5697
7168
  * @primitive pki.lint.rules
5698
7169
  * @signature pki.lint.rules(profile?, artifact?) -> [{id, severity, source, citation}]
@@ -5718,6 +7189,7 @@ function ocsp(input, opts) {
5718
7189
  * literal would answer for "constructor" and the rest of Object.prototype. */
5719
7190
  var BY_ARTIFACT = Object.assign(Object.create(null), {
5720
7191
  certificate: PROFILES, csr: CSR_PROFILES, crl: CRL_PROFILES, ocsp: OCSP_PROFILES,
7192
+ cms: CMS_PROFILES, tsp: TSP_PROFILES, attrcert: AC_PROFILES,
5721
7193
  });
5722
7194
 
5723
7195
  function rules(profile, artifact) {
@@ -5739,15 +7211,17 @@ function rules(profile, artifact) {
5739
7211
  BY_ARTIFACT[artifact][name].forEach(function (r) { set.push(r); });
5740
7212
  });
5741
7213
  } else if (profile == null) {
5742
- set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES);
7214
+ set = ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES).concat(ALL_CSR_RULES)
7215
+ .concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
5743
7216
  } else if (artifact != null) {
5744
7217
  set = BY_ARTIFACT[artifact][profile];
5745
- if (!set) {
5746
- throw _cfg("lint/unknown-profile", "\"" + profile + "\" is not a " + artifact +
5747
- " lint profile (known: " + Object.keys(BY_ARTIFACT[artifact]).join(", ") + ")");
5748
- }
7218
+ /** @internal The same refusal the VERB for this artifact gives, so asking the enumerator where a
7219
+ * profile lives and asking the verb to run it are answered by one sentence rather than two. */
7220
+ if (!set) throw _profileRefusal(profile, artifact);
5749
7221
  } else {
5750
- set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] || CSR_PROFILES[profile];
7222
+ set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
7223
+ CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
7224
+ AC_PROFILES[profile];
5751
7225
  if (!set) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\"");
5752
7226
  }
5753
7227
  /** @internal One row per id: a rule reused across artifacts, like the RFC 5280 sec. 4.2
@@ -5769,16 +7243,30 @@ function rules(profile, artifact) {
5769
7243
  * @status stable
5770
7244
  * @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS BR
5771
7245
  *
5772
- * List the known lint-profile names.
7246
+ * List the known lint-profile names. The two names every verb reads as naming no profile, `"all"`
7247
+ * and `"default"`, are not among them.
5773
7248
  * @example
5774
- * pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl", "rfc6960", "rfc5019"]
7249
+ * pki.lint.profiles();
7250
+ * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
7251
+ * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root",
7252
+ * // "nist-pqc-transition", "cnsa-2.0", "rfc2986", "rfc5280-crl", "rfc6960", "rfc5019",
7253
+ * // "rfc5652", "rfc3161", "rfc5755"]
5775
7254
  */
5776
7255
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
5777
7256
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
5778
7257
  * asks for, so one name means one set of requirements whichever artifact carries it. */
7258
+ /** @internal Enumerated from the SAME registry-of-registries `rules()` dispatches on, so a profile a
7259
+ * verb can run is a profile this answers with. Naming the registries here instead left the three
7260
+ * profiles added with the CMS, timestamp and attribute-certificate verbs undiscoverable while
7261
+ * `rules()` returned their rows, and a fourth verb would have repeated it. The order follows
7262
+ * BY_ARTIFACT's, and a name reachable under two artifacts is listed once. */
5779
7263
  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); });
7264
+ var out = [];
7265
+ Object.keys(BY_ARTIFACT).forEach(function (artifact) {
7266
+ Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
7267
+ if (out.indexOf(name) === -1) out.push(name);
7268
+ });
7269
+ });
5782
7270
  return out;
5783
7271
  }
5784
7272
 
@@ -5787,6 +7275,9 @@ module.exports = {
5787
7275
  csr: csr,
5788
7276
  crl: crl,
5789
7277
  ocsp: ocsp,
7278
+ cms: cms,
7279
+ tsp: tsp,
7280
+ attrcert: attrcert,
5790
7281
  rules: rules,
5791
7282
  profiles: profiles,
5792
7283
  };