@blamejs/pki 0.8.26 → 0.8.28

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
@@ -43,6 +43,7 @@ var pkix = require("./schema-pkix");
43
43
  var pkiBuild = require("./pki-build");
44
44
  var schema = require("./schema-engine");
45
45
  var path = require("./path-validate");
46
+ var compositeSig = require("./composite-sig");
46
47
  var C = require("./constants");
47
48
  var ipUtils = require("./ip-utils");
48
49
 
@@ -362,6 +363,7 @@ function _criticalityRule(name, id, severity, citation, message) {
362
363
  var RFC5280_RULES = [
363
364
  {
364
365
  id: "lint/rfc5280/signature-empty", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.1.3",
366
+ readsSignatureValue: true,
365
367
  /** @internal The strict parser admits an empty BIT STRING, since one is well-formed DER; a
366
368
  * signatureValue that contains no signature is not the field section 4.1.1.3 defines. */
367
369
  message: "the signatureValue contains no signature",
@@ -3913,8 +3915,59 @@ function _pqcSigKeyUsageRule(id, source, citation, family, appliesTo) {
3913
3915
  check: _pqcSigKeyUsage,
3914
3916
  };
3915
3917
  }
3918
+ /** @internal The FIPS 204 public-key size of each ML-DSA parameter set, keyed by the identifier a
3919
+ * certificate names it with, read from the composite module's own table so one figure serves the
3920
+ * arms that sign with it and the row that reads it. */
3921
+ var ML_DSA_PK_LEN = Object.create(null);
3922
+ var ML_DSA_SIG_LEN = Object.create(null);
3923
+ Object.keys(compositeSig.MLDSA_COMPONENT).forEach(function (name) {
3924
+ var row = compositeSig.MLDSA_COMPONENT[name];
3925
+ ML_DSA_PK_LEN[oid.byName(row.oid)] = row.pk;
3926
+ ML_DSA_SIG_LEN[oid.byName(row.oid)] = row.sig;
3927
+ });
3928
+
3916
3929
  var RFC9881_RULES = [
3917
3930
  _pqcSigKeyUsageRule("lint/rfc9881/mldsa-key-usage", "rfc9881", "RFC 9881 5", "ML-DSA", _isMlDsa),
3931
+ {
3932
+ id: "lint/rfc9881/mldsa-key-length", severity: "error", source: "rfc9881",
3933
+ citation: "RFC 9881 4 / FIPS 204",
3934
+ /** @internal Sec. 4: the subjectPublicKey is "the raw octet string" of the ML-DSA public key, whose
3935
+ * length FIPS 204 fixes per parameter set. An identifier names one set, so bytes of any other
3936
+ * length are not the key the identifier claims, and the OID alone would accept them. The ML-KEM
3937
+ * row beside this one reads its own sizes the same way. */
3938
+ 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",
3939
+ appliesTo: function (cert) {
3940
+ var spki = cert.subjectPublicKeyInfo;
3941
+ return !!(spki && spki.algorithm && ML_DSA_PK_LEN[spki.algorithm.oid] !== undefined);
3942
+ },
3943
+ check: function (cert) {
3944
+ var spki = cert.subjectPublicKeyInfo;
3945
+ var want = ML_DSA_PK_LEN[spki.algorithm.oid];
3946
+ var pub = spki.publicKey;
3947
+ return (guard.crypto.isOctetAligned(pub) && pub.bytes.length === want) ? null
3948
+ : { context: { expected: want, got: pub.bytes.length, unusedBits: pub.unusedBits } };
3949
+ },
3950
+ },
3951
+ {
3952
+ id: "lint/rfc9881/mldsa-signature-length", severity: "error", source: "rfc9881",
3953
+ citation: "RFC 9881 4 / FIPS 204", readsSignatureValue: true,
3954
+ /** @internal The signature is the FIPS 204 value carried directly in the BIT STRING, and FIPS 204
3955
+ * fixes its length per parameter set as it fixes the public key's. So a signatureValue of any other
3956
+ * length is not a signature of the algorithm the identifier names, whatever a verifier would later
3957
+ * make of it, and reading the identifier alone accepted it. This reads the SIGNATURE algorithm
3958
+ * rather than the subject key, since the two name different things about one certificate. */
3959
+ message: "an ML-DSA signatureValue must be the signature at its exact FIPS 204 size for the OID, in a byte-aligned BIT STRING",
3960
+ appliesTo: function (cert) {
3961
+ var sig = cert.signatureAlgorithm;
3962
+ return !!(sig && ML_DSA_SIG_LEN[sig.oid] !== undefined);
3963
+ },
3964
+ check: function (cert) {
3965
+ var want = ML_DSA_SIG_LEN[cert.signatureAlgorithm.oid];
3966
+ var value = cert.signatureValue;
3967
+ return (guard.crypto.isOctetAligned(value) && value.bytes.length === want) ? null
3968
+ : { context: { expected: want, got: value.bytes.length, unusedBits: value.unusedBits } };
3969
+ },
3970
+ },
3918
3971
  ];
3919
3972
  var RFC9909_RULES = [
3920
3973
  _pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
@@ -4166,6 +4219,7 @@ function _criticalityRows(list, table, what) {
4166
4219
  var RFC5280_CRL_RULES = [
4167
4220
  {
4168
4221
  id: "lint/rfc5280-crl/signature-empty", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.1.3",
4222
+ readsSignatureValue: true,
4169
4223
  message: "the signatureValue contains no signature",
4170
4224
  check: function (crl) { return crl.signatureValue.bytes.length === 0 ? true : null; },
4171
4225
  },
@@ -4615,8 +4669,72 @@ var RFC5280_CRL_RULES = [
4615
4669
  },
4616
4670
  ];
4617
4671
 
4618
- var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
4619
- var ALL_CRL_RULES = RFC5280_CRL_RULES;
4672
+ /** @internal Sec. 8: "The signatures on CRLs in this profile MUST follow the same rules from this
4673
+ * profile that apply to signatures in the certificates", and sec. 9 asks the same of an OCSP
4674
+ * response. It is ONE rule, so the row is written once and each artifact's registry takes it with the
4675
+ * reader its own parse result needs. The rule's citation names sec. 4 and 6.1, where the rule is,
4676
+ * beside the section that carries it here.
4677
+ * ONE IDENTIFIER ANSWERS FOR BOTH. Sec. 6.1 names the inner `signature` field of a TBSCertificate or
4678
+ * TBSCertList as well as the outer `signatureAlgorithm`, and both parsers refuse an artifact whose two
4679
+ * 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
4680
+ * verdict rather than a second row reading the same OID twice.
4681
+ * Sec. 6.2 sends the reader to RFC 9881 for the signature value, whose length FIPS 204 fixes per
4682
+ * parameter set, and that document's own row reports one of another length wherever it runs.
4683
+ * Sec. 9's second requirement, RFC 6960 sec. 4.2.2.2, is responder authorization, which is a trust
4684
+ * decision over a chain rather than a property of these bytes, and `pki.ocsp.verify` owns it. */
4685
+ function _cnsaSignatureRow(id, citation, reader) {
4686
+ return {
4687
+ id: id, severity: "error", source: "cnsa-2.0", citation: citation,
4688
+ message: "the signature algorithm is not id-ml-dsa-87",
4689
+ appliesTo: _isCnsa,
4690
+ check: function (artifact) {
4691
+ var sig = reader(artifact);
4692
+ var o = sig && sig.oid;
4693
+ return o === CNSA_ML_DSA_87 ? null : { context: { signatureAlgorithm: (sig && sig.name) || o } };
4694
+ },
4695
+ };
4696
+ }
4697
+
4698
+ /** @internal The other half of the same sentence, for the two artifacts whose own registries carry no
4699
+ * RFC 9881 rows: FIPS 204 fixes the signature's length per parameter set, so a value of another length
4700
+ * is not a signature of the algorithm the identifier names. A certificate is told this by the RFC 9881
4701
+ * row its profile already runs, and a CRL and an OCSP response are told it here, because sec. 8 and
4702
+ * sec. 9 give them a certificate's signature rules and one of the two halves reached them. The value
4703
+ * arrives as a decoded BIT STRING from one parser and as its bytes from the other, so the reader each
4704
+ * registry passes returns the length and whether it is octet-aligned. */
4705
+ function _cnsaSignatureLengthRow(id, citation, algReader, lengthReader) {
4706
+ return {
4707
+ id: id, severity: "error", source: "cnsa-2.0", citation: citation, readsSignatureValue: true,
4708
+ message: "the signature is not the length FIPS 204 fixes for its algorithm",
4709
+ appliesTo: function (artifact, ctx) {
4710
+ var sig = algReader(artifact);
4711
+ return _isCnsa(artifact, ctx) && !!(sig && ML_DSA_SIG_LEN[sig.oid] !== undefined);
4712
+ },
4713
+ check: function (artifact) {
4714
+ var want = ML_DSA_SIG_LEN[algReader(artifact).oid];
4715
+ var got = lengthReader(artifact);
4716
+ return (got.aligned && got.length === want) ? null
4717
+ : { context: { expected: want, got: got.length, unusedBits: got.unusedBits } };
4718
+ },
4719
+ };
4720
+ }
4721
+ function _crlSignatureAlgorithm(crl) { return crl.signatureAlgorithm; }
4722
+ var CNSA2_CRL_RULES = [
4723
+ _cnsaSignatureRow("lint/cnsa-2.0/crl-signature-not-ml-dsa-87",
4724
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 8 (sec. 4 / 6.1)", _crlSignatureAlgorithm),
4725
+ _cnsaSignatureLengthRow("lint/cnsa-2.0/crl-signature-length",
4726
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 8 (RFC 9881 4 / FIPS 204)", _crlSignatureAlgorithm,
4727
+ function (crl) {
4728
+ var v = crl.signatureValue;
4729
+ return { length: v.bytes.length, unusedBits: v.unusedBits, aligned: guard.crypto.isOctetAligned(v) };
4730
+ }),
4731
+ ];
4732
+
4733
+ var CRL_PROFILES = Object.assign(Object.create(null), {
4734
+ "rfc5280-crl": RFC5280_CRL_RULES,
4735
+ "cnsa-2.0": CNSA2_CRL_RULES,
4736
+ });
4737
+ var ALL_CRL_RULES = RFC5280_CRL_RULES.concat(CNSA2_CRL_RULES);
4620
4738
 
4621
4739
  /** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
4622
4740
  * this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
@@ -5144,6 +5262,7 @@ function _placementRow(name, placed, where) {
5144
5262
  var RFC6960_OCSP_RULES = [
5145
5263
  {
5146
5264
  id: "lint/rfc6960/signature-empty", severity: "error", source: "rfc6960", citation: "RFC 6960 2.2",
5265
+ readsSignatureValue: true,
5147
5266
  message: "a definitive response shall be digitally signed; the signature BIT STRING is empty",
5148
5267
  check: function (resp) { return _ocspBasic(resp).signature.length === 0 ? true : null; },
5149
5268
  },
@@ -5420,8 +5539,28 @@ var RFC5019_OCSP_RULES = [
5420
5539
  },
5421
5540
  ];
5422
5541
 
5423
- var OCSP_PROFILES = Object.assign(Object.create(null), { "rfc6960": RFC6960_OCSP_RULES, "rfc5019": RFC5019_OCSP_RULES });
5424
- var ALL_OCSP_RULES = RFC6960_OCSP_RULES.concat(RFC5019_OCSP_RULES);
5542
+ /** @internal Sec. 9: "an OCSP response MUST be signed conformant with Section 4", which is the rule
5543
+ * the certificate and CRL rows read, over the signature the basic response carries. */
5544
+ function _ocspSignatureAlgorithm(resp) { return _ocspBasic(resp).signatureAlgorithm; }
5545
+ var CNSA2_OCSP_RULES = [
5546
+ _cnsaSignatureRow("lint/cnsa-2.0/response-signature-not-ml-dsa-87",
5547
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 9 (sec. 4 / 6.1)", _ocspSignatureAlgorithm),
5548
+ /** @internal The response parser surfaces the signature as its bytes, having read the BIT STRING, so
5549
+ * there are no unused bits left to ask about and the length is the whole answer. */
5550
+ _cnsaSignatureLengthRow("lint/cnsa-2.0/response-signature-length",
5551
+ "draft-jenkins-cnsa2-pkix-profile-05 sec. 9 (RFC 9881 4 / FIPS 204)", _ocspSignatureAlgorithm,
5552
+ function (resp) {
5553
+ var sig = _ocspBasic(resp).signature;
5554
+ return { length: sig.length, unusedBits: 0, aligned: true };
5555
+ }),
5556
+ ];
5557
+
5558
+ var OCSP_PROFILES = Object.assign(Object.create(null), {
5559
+ "rfc6960": RFC6960_OCSP_RULES,
5560
+ "rfc5019": RFC5019_OCSP_RULES,
5561
+ "cnsa-2.0": CNSA2_OCSP_RULES,
5562
+ });
5563
+ var ALL_OCSP_RULES = RFC6960_OCSP_RULES.concat(RFC5019_OCSP_RULES).concat(CNSA2_OCSP_RULES);
5425
5564
 
5426
5565
  /** @internal The extensions an issuer determines, which a request may carry and a CA does not take
5427
5566
  * from one. subjectKeyIdentifier and authorityKeyIdentifier are computed from the keys, and the
@@ -5580,6 +5719,602 @@ var CABF_TLS_CSR_RULE_IDS = [
5580
5719
  "lint/cabf-tls/weak-key",
5581
5720
  ];
5582
5721
 
5722
+ /** @internal The dates NIST IR 8547 sets, read as whole years: "deprecated after 2030" and
5723
+ * "disallowed after 2035" name the years themselves, so each transition begins on 1 January of the
5724
+ * year that follows. The engine compares the certificate's own notBefore, which is the issuance date
5725
+ * the transition governs. */
5726
+ var NIST_DEPRECATED_FROM = new Date("2031-01-01T00:00:00Z");
5727
+ var NIST_DISALLOWED_FROM = new Date("2036-01-01T00:00:00Z");
5728
+
5729
+ function _isNistTransition(cert, ctx) { return ctx.profile === "nist-pqc-transition"; }
5730
+
5731
+ /** @internal An identifier the OID registry names that is NOT an algorithm either of a certificate's
5732
+ * two algorithm fields can carry, so no family is read onto it: a digest, an attribute carrying a key
5733
+ * length, a CMS content-encryption algorithm, a message authentication code, and the two
5734
+ * proof-of-possession algorithms. Each carries a family's word in its name and none is a signature or
5735
+ * a subject key. */
5736
+ var NIST_NOT_AN_ALGORITHM = Object.assign(Object.create(null), {
5737
+ sha1: 1, rsaKeyLen: 1, rsaKeyTransport: 1, dhBasedMac: 1,
5738
+ "id-alg-dhPOP": 1, "id-dhPop-static-HMAC-SHA1": 1, "id-alg-noSignature": 1,
5739
+ });
5740
+
5741
+ /** @internal Which family of IR 8547's tables an algorithm belongs to. Table 2 lists RSA, ECDSA and
5742
+ * EdDSA; Table 4 lists finite-field DH and MQV, elliptic-curve DH and MQV, and RSA key transport.
5743
+ * `pqc` and `hybrid` mark what the transition moves TO, which no row reports, and an identifier the
5744
+ * registry does not name is undefined, which both rows also pass over.
5745
+ * THE ORDER IS THE MECHANISM. Each test is anchored at the start of the name or is an equality, and
5746
+ * the families whose names contain another family's word are claimed first, so by the time the RSA
5747
+ * test runs there is no `ecdsaWith` or `dsaWith` name left for it to take. A single unordered
5748
+ * substring test reads `ecdsaWithSHA256` as a DSA algorithm; a table of exact names owes an entry for
5749
+ * every spelling the registry knows, which is how `sha1WithRSAEncryption` came to be read as no family
5750
+ * at all; and the algorithm a VERIFIER resolves is a third question, since that table holds what this
5751
+ * toolkit will check a signature with and excludes SHA-1 by policy, so reading the family off it let
5752
+ * every SHA-1 signature past the disallowance date.
5753
+ * `lint-nist-transition.test.js` drives every identifier the registry names through the shipped verb,
5754
+ * so a family this fails to claim is reported by a test rather than by silence. */
5755
+ function _nistKindOf(alg) {
5756
+ var name = alg && typeof alg.name === "string" ? alg.name : "";
5757
+ if (name === "" || NIST_NOT_AN_ALGORITHM[name]) return undefined;
5758
+ /** @internal A composite arm names a post-quantum algorithm AND a traditional one, so it is neither
5759
+ * of the other answers. It is given its own, rather than sharing the undefined an unnamed identifier
5760
+ * returns, so a reader can tell a deliberate exclusion from an omission. Both rows pass over it: the
5761
+ * tables list pure families, and the same guidance endorses this construction for the transition
5762
+ * these dates mark. */
5763
+ if (name.indexOf("id-MLDSA") === 0 || name.indexOf("id-MLKEM") === 0) return "hybrid";
5764
+ if (name.indexOf("ML-DSA") === 0 || name.indexOf("ML-KEM") === 0 || name.indexOf("SLH-DSA") === 0 ||
5765
+ name.indexOf("id-ml-dsa-") === 0 || name.indexOf("id-ml-kem-") === 0 ||
5766
+ name.indexOf("id-slh-dsa-") === 0 || name.indexOf("id-hash-") === 0) return "pqc";
5767
+ /** @internal A key-agreement scheme names a key-derivation function for CMS rather than an algorithm
5768
+ * a certificate declares, and its name opens with the family word. */
5769
+ if (name.indexOf("dhSinglePass-") === 0) return undefined;
5770
+ if (name === "ecPublicKey" || name.indexOf("ecdsaWith") === 0 ||
5771
+ name === "X25519" || name === "X448") return "ecc";
5772
+ if (name === "Ed25519" || name === "Ed448") return "eddsa";
5773
+ if (name === "dsa" || name.indexOf("dsaWith") === 0 ||
5774
+ name === "dhpublicnumber" || name === "dhKeyAgreement") return "ffc";
5775
+ return _nistNameCarriesRsa(name) ? "ifc" : undefined;
5776
+ }
5777
+
5778
+ /** @internal Whether a name carries the RSA family's word, asked only after every other family has
5779
+ * claimed its own names, so the one reading that made this ambiguous cannot arise. */
5780
+ function _nistNameCarriesRsa(name) {
5781
+ var lower = guard.name.lowerAscii(name);
5782
+ return lower.indexOf("rsa") !== -1;
5783
+ }
5784
+ /** @internal Whether a classification names one of the families the tables date. `pqc` and `hybrid` are
5785
+ * what the transition moves TO, and an unclassified name is one the registry does not know, so none of
5786
+ * the three is a family these rows report. */
5787
+ function _nistIsClassical(kind) {
5788
+ return kind === "ifc" || kind === "ecc" || kind === "eddsa" || kind === "ffc";
5789
+ }
5790
+
5791
+ /** @internal Every classical algorithm a certificate declares, as `{ field, kind, name }` per field.
5792
+ * Both fields are read because both outlive the issuance: a post-quantum subject key signed with a
5793
+ * classical algorithm still leaves a classical signature for a relying party to verify.
5794
+ * A COMPOSITE is passed over, in both fields. `path.signatureAlgorithmName` answers null for a
5795
+ * composite identifier because it names two algorithms, and a composite subject key carries the arm's
5796
+ * own name, which neither table lists. That is the reading the IR asks for rather than a gap: its
5797
+ * tables list pure families, and sec. 3 of the same document endorses hybrid and dual-algorithm
5798
+ * operation through the transition, so reporting the traditional half would report the construction
5799
+ * these dates exist to carry. */
5800
+ function _nistClassicalDeclarations(cert) {
5801
+ var out = [];
5802
+ var keyAlg = (cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm) || null;
5803
+ var keyKind = _nistKindOf(keyAlg);
5804
+ if (_nistIsClassical(keyKind)) {
5805
+ out.push({ field: "subjectPublicKey", kind: keyKind, name: _spkiAlgName(cert) || (keyAlg && keyAlg.oid) });
5806
+ }
5807
+ var sigAlg = cert.signatureAlgorithm || null;
5808
+ var sigKind = _nistKindOf(sigAlg);
5809
+ if (_nistIsClassical(sigKind)) {
5810
+ out.push({ field: "signature", kind: sigKind, name: (sigAlg && (sigAlg.name || sigAlg.oid)) });
5811
+ }
5812
+ return out;
5813
+ }
5814
+
5815
+ /** @internal Whether a declared subject key sits at the 112-bit security strength, which is the level
5816
+ * the 2030 deprecation applies to. IR 8547 gives no mapping from a strength to a key size; SP 800-57
5817
+ * Part 1 Rev 5 Table 2 does: 112 bits is an RSA modulus of 2048, a finite-field group with L = 2048,
5818
+ * and a curve whose field is 224 to 255 bits. A key WEAKER than that level is weaker than the level
5819
+ * the clause deprecates, so it is reported by the same row rather than passed over. All three
5820
+ * families the tables list are read: an RSA modulus, an elliptic curve's field, and a finite-field
5821
+ * group's prime. Reading two of them left every DSA and Diffie-Hellman key silently outside the
5822
+ * deprecation the row promises.
5823
+ * EdDSA IS DELIBERATELY ABSENT. Ed25519's field is 255 bits, which SP 800-57's ECC column puts in the
5824
+ * 112-bit row, so reading the two documents together would deprecate it after 2030. IR 8547's own
5825
+ * EdDSA row carries a 128-bit entry alone, with no 112-bit entry and no 2030 date, and this profile
5826
+ * cites IR 8547. A reader who reaches for SP 800-57 will find the other answer. */
5827
+ var NIST_112_ECC_FIELD_MAX = 255;
5828
+ function _nistAt112OrBelow(cert, declaration) {
5829
+ var spki = cert.subjectPublicKeyInfo;
5830
+ if (declaration.kind === "ifc") {
5831
+ var bits = _rsaModulusBits(spki);
5832
+ return bits !== null && bits <= 2048 ? { rsaBits: bits } : null;
5833
+ }
5834
+ if (declaration.kind === "ecc") {
5835
+ /** @internal X25519 and X448 are read the way their signing siblings are. RFC 8410 encodes them as
5836
+ * a raw coordinate with no format byte, so there is no SEC1 point to measure, and the field size
5837
+ * SP 800-57's ECC column would place at the 112-bit level, 255 bits for X25519, is the same figure
5838
+ * IR 8547's own EdDSA row answers at 128 bits. The profile follows IR 8547 for that family, so it
5839
+ * follows it here, and both are reported by the 2036 row rather than this one. */
5840
+ if (declaration.name === "X25519" || declaration.name === "X448") return null;
5841
+ var field = _eccFieldBits(spki);
5842
+ return (field !== null && field <= NIST_112_ECC_FIELD_MAX)
5843
+ ? { curve: _ecCurveName(spki), fieldBits: field } : null;
5844
+ }
5845
+ if (declaration.kind === "ffc") {
5846
+ var ffc = _ffcParameterBits(cert, spki);
5847
+ if (ffc === null) return null;
5848
+ /** @internal SP 800-57 Table 2 states the finite-field level as a PAIR, L = 2048 with N = 224 at
5849
+ * 112 bits and L = 3072 with N = 256 at 128, so either parameter caps the strength: a group with
5850
+ * a 3072-bit prime and a 224-bit subgroup is a 112-bit key. Reading the prime alone left that key
5851
+ * outside the deprecation. A shape carrying no subgroup order settles the question on the prime,
5852
+ * which is all it states. */
5853
+ var atLevel = ffc.primeBits <= 2048 ||
5854
+ (ffc.subgroupBits !== null && ffc.subgroupBits <= 224);
5855
+ return atLevel ? { ffcPrimeBits: ffc.primeBits, ffcSubgroupBits: ffc.subgroupBits } : null;
5856
+ }
5857
+ return null;
5858
+ }
5859
+
5860
+ /** @internal Where each finite-field parameter shape carries its subgroup order, as the index of the
5861
+ * INTEGER that holds it, or null for a shape that carries none. The prime is first in all three, and
5862
+ * the subgroup order is NOT: RFC 3279's `Dss-Parms ::= SEQUENCE { p, q, g }` puts it second, its
5863
+ * `DomainParameters ::= SEQUENCE { p, g, q OPTIONAL, j OPTIONAL, validationParms OPTIONAL }` third and
5864
+ * optional, and PKCS#3's `DHParameter ::= SEQUENCE { prime, base, privateValueLength OPTIONAL }`
5865
+ * states no subgroup at all. So the position is read per algorithm rather than by counting fields,
5866
+ * which would read a generator as a subgroup order. */
5867
+ var FFC_SUBGROUP_INDEX = Object.assign(Object.create(null), {
5868
+ dsa: 1, dhpublicnumber: 2, dhKeyAgreement: null,
5869
+ });
5870
+
5871
+ /** @internal A finite-field key's L and N, the pair SP 800-57 Table 2's FFC column is stated in, as
5872
+ * `{ primeBits, subgroupBits }` with `subgroupBits` null where the shape carries no subgroup order.
5873
+ * A parameters field this cannot read answers null, which leaves the row silent rather than reporting
5874
+ * a strength it did not establish. */
5875
+ function _ffcParameterBits(cert, spki) {
5876
+ // 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
5877
+ try {
5878
+ var alg = spki.algorithm;
5879
+ var params = alg && alg.parameters;
5880
+ if (!Buffer.isBuffer(params)) return null;
5881
+ var seq = asn1.decode(params);
5882
+ if (!seq.children || !seq.children.length) return null;
5883
+ var p = asn1.read.integer(seq.children[0]);
5884
+ if (p <= 0n) return null;
5885
+ var at = FFC_SUBGROUP_INDEX[alg.name];
5886
+ var q = null;
5887
+ var node = typeof at === "number" ? seq.children[at] : null;
5888
+ if (node && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER) {
5889
+ var read = asn1.read.integer(node);
5890
+ if (read > 0n) q = read.toString(2).length;
5891
+ }
5892
+ return { primeBits: p.toString(2).length, subgroupBits: q };
5893
+ } catch (_e) { return null; }
5894
+ }
5895
+
5896
+ /** @internal The field size of a SEC1 elliptic-curve subject key, which is what SP 800-57 Table 2's ECC
5897
+ * column is stated in, read from THE KEY rather than from a table of curves. RFC 5480 sec. 2.2 encodes
5898
+ * the point as `0x04 || X || Y` with each coordinate padded to the field's whole octets, so its length
5899
+ * gives the field size for any curve, including the ones a table would have to list and the ones a
5900
+ * registry gains later. A table of curve names left every certificate on a curve nobody had added,
5901
+ * `sect233r1` and `brainpoolP224r1` among them, outside the deprecation.
5902
+ * ONLY AN `ecPublicKey` REACHES THIS. RFC 8410 encodes X25519 and X448 as a raw coordinate carrying no
5903
+ * format byte, so reading one here would read key material as a format and answer differently for two
5904
+ * keys of one algorithm; the caller answers for those two before calling.
5905
+ * A CERTIFICATE WHOSE NAMED CURVE AND POINT WIDTH DISAGREE is answered on its point, which is the
5906
+ * key's own size, and the disagreement itself is not this row's to report: the two fields state
5907
+ * different things and nothing here can say which the issuer meant. Such a key is not usable in any
5908
+ * case, since `pki.path.validate` refuses it as `path/bad-input` when it imports the key.
5909
+ * The answer is the field rounded up to whole octets, which is what the encoding states: a 521-bit
5910
+ * field is carried in 66 octets and reads as 528. That is exact for every curve whose field is a whole
5911
+ * number of octets and rounds away from the threshold for the rest, so the only field sizes it could
5912
+ * misread are 249 through 255, which no standard curve uses. */
5913
+ function _eccFieldBits(spki) {
5914
+ var pk = spki && spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
5915
+ if (!Buffer.isBuffer(pk) || pk.length < 2) return null;
5916
+ var form = pk[0];
5917
+ if (form === 0x04) {
5918
+ return (pk.length - 1) % 2 === 0 ? ((pk.length - 1) / 2) * 8 : null;
5919
+ }
5920
+ /** @internal RFC 5480 sec. 2.2 permits the compressed form, which carries one coordinate. */
5921
+ if (form === 0x02 || form === 0x03) return (pk.length - 1) * 8;
5922
+ return null;
5923
+ }
5924
+
5925
+ var NIST_TRANSITION_RULES = [
5926
+ {
5927
+ id: "lint/nist-pqc-transition/classical-disallowed-after-2035", severity: "error",
5928
+ source: "nist-pqc-transition", citation: "NIST IR 8547 ipd Table 2 / Table 4",
5929
+ /** @internal Every classical family the two tables list is "Disallowed after 2035" at the
5930
+ * parameter sets they list, so this row needs no strength mapping: whether the algorithm is one of
5931
+ * those families is the whole question, and the registry answers it. */
5932
+ message: "a classical algorithm is disallowed for a certificate issued after 2035",
5933
+ appliesTo: _isNistTransition,
5934
+ effectiveDate: NIST_DISALLOWED_FROM,
5935
+ check: function (cert) {
5936
+ var found = _nistClassicalDeclarations(cert);
5937
+ return found.length ? found.map(function (d) {
5938
+ return { context: { field: d.field, family: d.kind, algorithm: d.name } };
5939
+ }) : null;
5940
+ },
5941
+ },
5942
+ {
5943
+ id: "lint/nist-pqc-transition/classical-deprecated-after-2030", severity: "warn",
5944
+ source: "nist-pqc-transition", citation: "NIST IR 8547 ipd Table 2 / Table 4",
5945
+ /** @internal The 112-bit rows are "Deprecated after 2030", which SP 800-131A and sec. 4.1 of the
5946
+ * IR define as still usable with the risk accepted, so the severity is a warning where the 2035
5947
+ * row is an error. Only the subject key's own parameters are readable here: a signature
5948
+ * identifier names a hash and a family, not the issuer's key size, so the strength of the
5949
+ * signing key is in the ISSUER's certificate rather than this one. */
5950
+ message: "a classical algorithm at or below the 112-bit security strength is deprecated for a certificate issued after 2030",
5951
+ appliesTo: _isNistTransition,
5952
+ effectiveDate: NIST_DEPRECATED_FROM,
5953
+ check: function (cert) {
5954
+ var out = [];
5955
+ _nistClassicalDeclarations(cert).forEach(function (d) {
5956
+ if (d.field !== "subjectPublicKey") return;
5957
+ var at = _nistAt112OrBelow(cert, d);
5958
+ if (at) out.push({ context: Object.assign({ family: d.kind, algorithm: d.name }, at) });
5959
+ });
5960
+ return out.length ? out : null;
5961
+ },
5962
+ },
5963
+ ];
5964
+
5965
+ /** @internal The CNSA 2.0 suite, from `draft-jenkins-cnsa2-pkix-profile-05` (M. Jenkins, NSA-CCSS,
5966
+ * 5 July 2026, Informational, obsoletes RFC 8603 if approved). The NSA notice lists algorithms; this
5967
+ * document states what a CERTIFICATE must carry, which is what a profile can read, so it is the
5968
+ * citation and it is cited as a draft. Sec. 3 fixes the suite: ML-DSA-87 for signing and ML-KEM-1024
5969
+ * for key establishment.
5970
+ * A certificate declares WHICH kind it is through its own subject public key, which is what makes the
5971
+ * end-entity key-usage rows readable.
5972
+ * THE PROFILE IS A PROFILE OF RFC 5280 (sec. 6), so a clause it prefixes "in adherence with
5973
+ * [RFC5280]" is RFC 5280's requirement as well. Where the document restates one as its own MUST, it
5974
+ * gets a row here, so an operator who names this profile alone still sees it; the default rule set
5975
+ * reports the RFC 5280 rows themselves. */
5976
+ var CNSA_ML_DSA_87 = oid.byName("id-ml-dsa-87");
5977
+ var CNSA_ML_KEM_1024 = oid.byName("id-ml-kem-1024");
5978
+
5979
+ function _isCnsa(cert, ctx) { return ctx.profile === "cnsa-2.0"; }
5980
+ /** @internal Sec. 7.1 governs a SELF-SIGNED CA certificate, and sec. 7.2 every other CA. Matching
5981
+ * issuer and subject names make a certificate self-ISSUED, which is not the same thing: a CA reissued
5982
+ * in its own name under a new key carries both names and was signed by its predecessor, so sec. 7.2
5983
+ * governs it, and sec. 7.2 permits the pathLenConstraint sec. 7.1 forbids. The key identifiers
5984
+ * separate the two without a signature check, which this synchronous surface cannot make, and
5985
+ * `_selfSignedIdentifiers` is where that reading already lives. Reading the names alone reported a
5986
+ * rollover CA for a field its own section allows. */
5987
+ /** @internal Whether this certificate is one of the CA kinds sec. 7.1 and 7.2 govern. A basicConstraints
5988
+ * with cA set says so, and so does a keyUsage asserting keyCertSign: RFC 5280 sec. 4.2.1.3 requires the
5989
+ * cA boolean of any certificate whose keyUsage asserts that bit, so a certificate asserting it is
5990
+ * claiming to be a CA whether or not it carries the extension.
5991
+ * READING ONLY THE EXTENSION MADE ONE CLAUSE UNREPORTABLE. Both CA sections require basicConstraints by
5992
+ * name, and a certificate that omits it is exactly the one that breaks them; deciding the scope on that
5993
+ * same extension graded it as an end entity instead, so the operator saw extended-key-usage findings
5994
+ * written for a different kind of certificate and not the requirement it actually broke. An end entity
5995
+ * does not assert keyCertSign, so nothing that belongs to sec. 7.3 is drawn in by this. */
5996
+ function _cnsaIsCa(cert, ctx) {
5997
+ if (ctx.isCaCert) return true;
5998
+ var ku = ctx.decode("keyUsage");
5999
+ return !!(ku && ku.value && ku.value.keyCertSign === true);
6000
+ }
6001
+ /** @internal Whether a self-ISSUED CA is one sec. 7.1 governs. A rollover CA reissued in its own name
6002
+ * under a new key names its predecessor's key in its authorityKeyIdentifier, and its own key in its
6003
+ * subjectKeyIdentifier, so the two identifiers DISAGREEING is what says sec. 7.2 governs it.
6004
+ * ANYTHING ELSE READS AS SELF-SIGNED, including a certificate that carries no subjectKeyIdentifier for
6005
+ * the comparison to read. That extension is one sec. 7.1 requires by name, so a certificate omitting it
6006
+ * is one this section is about, and letting its absence move the certificate to sec. 7.2 left it with no
6007
+ * finding at all. `_selfSignedIdentifiers` takes the opposite default, because the row that reads it
6008
+ * rests on having settled the question; here the stricter scope is the one that reports the omission.
6009
+ * THE RESIDUAL IS A ROLLOVER CA CARRYING NO authorityKeyIdentifier AT ALL, which reads as a root here
6010
+ * and so is not asked for the extension sec. 7.2 requires. The bytes do not settle it: a root omits
6011
+ * that extension legitimately, and RFC 5280 sec. 4.2.1.1 permits its absence only in a self-signed
6012
+ * certificate, which is the rule the rfc5280 rows report on their own. Inventing a rule that reported
6013
+ * every root for omitting an extension its own section does not ask for would be worse than the gap. */
6014
+ function _cnsaSelfIssuedIsRoot(cert, ctx) {
6015
+ var aki = ctx.decode("authorityKeyIdentifier");
6016
+ var kid = aki && aki.value ? aki.value.keyIdentifier : null;
6017
+ if (!Buffer.isBuffer(kid)) return true;
6018
+ var ski = ctx.decode("subjectKeyIdentifier");
6019
+ if (!Buffer.isBuffer(ski && ski.value)) return true;
6020
+ return kid.equals(ski.value);
6021
+ }
6022
+ function _cnsaSelfSignedCa(cert, ctx) {
6023
+ return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx) && _sameName(cert.issuer, cert.subject) &&
6024
+ _cnsaSelfIssuedIsRoot(cert, ctx);
6025
+ }
6026
+ function _cnsaOtherCa(cert, ctx) {
6027
+ return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx) &&
6028
+ !(_sameName(cert.issuer, cert.subject) && _cnsaSelfIssuedIsRoot(cert, ctx));
6029
+ }
6030
+ function _cnsaAnyCa(cert, ctx) { return _isCnsa(cert, ctx) && _cnsaIsCa(cert, ctx); }
6031
+ function _cnsaEndEntity(cert, ctx) { return _isCnsa(cert, ctx) && !_cnsaIsCa(cert, ctx); }
6032
+
6033
+ /** @internal Which kind of CNSA certificate this is, from the subject public key sec. 4 fixes: an
6034
+ * ML-DSA-87 key makes it a signature certificate and an ML-KEM-1024 key a key-establishment one.
6035
+ * A key outside the suite has no kind, and the row that reports the key answers for it rather than
6036
+ * the rows that read a kind. */
6037
+ function _cnsaKind(cert) {
6038
+ var spki = cert.subjectPublicKeyInfo;
6039
+ var o = spki && spki.algorithm && spki.algorithm.oid;
6040
+ if (o === CNSA_ML_DSA_87) return "signature";
6041
+ if (o === CNSA_ML_KEM_1024) return "key-establishment";
6042
+ return null;
6043
+ }
6044
+
6045
+ /** @internal The nine bits RFC 5280 sec. 4.2.1.3 defines, so a closed set stated as "all other bits
6046
+ * MUST NOT be set" is read as the bits outside the named ones rather than as a boolean. */
6047
+ var CNSA_KEY_USAGE_BITS = ["digitalSignature", "nonRepudiation", "keyEncipherment",
6048
+ "dataEncipherment", "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
6049
+ /** @internal Sec. 7.1 and 7.2 for a CA, sec. 7.3 for each kind of end entity. `required` must be set,
6050
+ * `optional` may be, and every other bit must not. */
6051
+ var CNSA_KEY_USAGE_SETS = Object.assign(Object.create(null), {
6052
+ ca: { required: ["keyCertSign", "cRLSign"], optional: ["digitalSignature", "nonRepudiation"] },
6053
+ signature: { required: ["digitalSignature"], optional: ["nonRepudiation"] },
6054
+ "key-establishment": { required: ["keyEncipherment"], optional: [] },
6055
+ });
6056
+ /** @internal Every way one keyUsage departs from one of those sets, as a finding each: a required bit
6057
+ * that is not set and a bit outside the set that is. One finding per bit, because a certificate that
6058
+ * sets two forbidden bits has two faults and an operator fixing one would otherwise not see the
6059
+ * other. */
6060
+ function _cnsaKeyUsageFindings(ctx, setName) {
6061
+ var set = CNSA_KEY_USAGE_SETS[setName];
6062
+ var ku = ctx.decode("keyUsage");
6063
+ if (!set || !ku || !ku.value) return null;
6064
+ var v = ku.value, out = [];
6065
+ set.required.forEach(function (bit) {
6066
+ if (v[bit] !== true) out.push({ context: { bit: bit, fault: "required bit not set", set: setName } });
6067
+ });
6068
+ CNSA_KEY_USAGE_BITS.forEach(function (bit) {
6069
+ if (v[bit] !== true) return;
6070
+ if (set.required.indexOf(bit) !== -1 || set.optional.indexOf(bit) !== -1) return;
6071
+ out.push({ context: { bit: bit, fault: "bit outside the set this profile fixes", set: setName } });
6072
+ });
6073
+ /** @internal "All other bits MUST NOT be set" reaches past the nine RFC 5280 sec. 4.2.1.3 names: a
6074
+ * bit beyond them is one of the others. The decoder reports those as one flag rather than by
6075
+ * position, so they are one finding. Reading only the nine left a certificate asserting them
6076
+ * conforming under a clause that closes the set. */
6077
+ if (v.reservedBitsSet === true) {
6078
+ out.push({ context: { bit: "beyond the nine named positions", fault: "bit outside the set this profile fixes", set: setName } });
6079
+ }
6080
+ return out.length ? out : null;
6081
+ }
6082
+
6083
+ /** @internal An extension this profile requires by name, reported one finding per absent one. The
6084
+ * shared syntax row answers for an extension whose value does not decode, so absence here is
6085
+ * absence. */
6086
+ function _cnsaMissingExtensions(ctx, names) {
6087
+ var out = [];
6088
+ names.forEach(function (name) {
6089
+ if (!ctx.raw(name)) out.push({ context: { extension: name } });
6090
+ });
6091
+ return out.length ? out : null;
6092
+ }
6093
+
6094
+ var CNSA2_RULES = [
6095
+ {
6096
+ id: "lint/cnsa-2.0/spki-not-suite", severity: "error", source: "cnsa-2.0",
6097
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 4 / 6.4",
6098
+ /** @internal "Every CNSA Suite certificate MUST use the X.509 v3 format and contain one of the
6099
+ * following as its subject public key: A ML-DSA-87 signature verification key. A ML-KEM-1024
6100
+ * public encapsulation key." Sec. 6.4's "the contents of the parameters component ... MUST be
6101
+ * absent" has no row: the shared AlgorithmIdentifier decoder refuses a present field on both
6102
+ * OIDs, so an operator sees `x509/bad-algorithm-parameters` instead. */
6103
+ message: "the subject public key is not one of the two the suite fixes (ML-DSA-87 or ML-KEM-1024)",
6104
+ appliesTo: _isCnsa,
6105
+ check: function (cert) {
6106
+ return _cnsaKind(cert) === null
6107
+ ? { context: { algorithm: _spkiAlgName(cert) || (cert.subjectPublicKeyInfo &&
6108
+ cert.subjectPublicKeyInfo.algorithm && cert.subjectPublicKeyInfo.algorithm.oid) } }
6109
+ : null;
6110
+ },
6111
+ },
6112
+ {
6113
+ id: "lint/cnsa-2.0/version-not-3", severity: "error", source: "cnsa-2.0",
6114
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 6.3",
6115
+ /** @internal "For this profile, the version field MUST be set to INTEGER value 0x02, indicating
6116
+ * that the certificate conforms to X.509 version 3", which sec. 4 repeats as "Every CNSA Suite
6117
+ * certificate MUST use the X.509 v3 format". A version 1 or 2 certificate carrying no extensions
6118
+ * is well-formed and parses, since RFC 5280 sec. 4.1.2.1 puts the field at DEFAULT v1 and the
6119
+ * parser refuses only an extensions field beneath those versions, so this clause needs a row. */
6120
+ message: "a CNSA Suite certificate must be X.509 version 3",
6121
+ appliesTo: _isCnsa,
6122
+ check: function (cert) {
6123
+ return cert.version === 3 ? null : { context: { version: cert.version } };
6124
+ },
6125
+ },
6126
+ {
6127
+ id: "lint/cnsa-2.0/signature-not-ml-dsa-87", severity: "error", source: "cnsa-2.0",
6128
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 4 / 6.1",
6129
+ /** @internal "The signature applied to all CNSA Suite certificates and CRLs MUST be made with a
6130
+ * ML-DSA-87 signing key", and sec. 6.1 names id-ml-dsa-87 as the identifier for it. The same
6131
+ * sentence that fixes the algorithm also says "HashML-DSA is not permitted", which this row
6132
+ * reports without a second one: a HashML-DSA identifier is not id-ml-dsa-87. */
6133
+ message: "the certificate signature algorithm is not id-ml-dsa-87",
6134
+ appliesTo: _isCnsa,
6135
+ check: function (cert) {
6136
+ var sig = cert.signatureAlgorithm;
6137
+ var o = sig && sig.oid;
6138
+ return o === CNSA_ML_DSA_87 ? null : { context: { signatureAlgorithm: (sig && sig.name) || o } };
6139
+ },
6140
+ },
6141
+ {
6142
+ id: "lint/cnsa-2.0/key-usage-not-critical", severity: "error", source: "cnsa-2.0",
6143
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2 / 7.3",
6144
+ /** @internal "The keyUsage extension MUST be marked as critical", said in each of the three
6145
+ * sections, so one row serves all three scopes rather than three saying the same thing. */
6146
+ message: "the keyUsage extension must be marked critical",
6147
+ appliesTo: _isCnsa,
6148
+ check: function (cert, ctx) {
6149
+ var ku = ctx.decode("keyUsage");
6150
+ if (!ku) return null;
6151
+ return ku.critical === true ? null : true;
6152
+ },
6153
+ },
6154
+ {
6155
+ id: "lint/cnsa-2.0/self-signed-ca-missing-extension", severity: "error", source: "cnsa-2.0",
6156
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1",
6157
+ /** @internal "self-signed CA certificates in this profile MUST contain the subjectKeyIdentifier,
6158
+ * keyUsage, and basicConstraints extensions." All three are reported, including the third: the
6159
+ * scope reads a keyUsage asserting keyCertSign as a CA as well, so a certificate that omits
6160
+ * basicConstraints is still read here and told which extension it owes. */
6161
+ message: "a self-signed CA certificate must carry this extension",
6162
+ appliesTo: _cnsaSelfSignedCa,
6163
+ check: function (cert, ctx) {
6164
+ return _cnsaMissingExtensions(ctx, ["subjectKeyIdentifier", "keyUsage", "basicConstraints"]);
6165
+ },
6166
+ },
6167
+ {
6168
+ id: "lint/cnsa-2.0/ca-missing-extension", severity: "error", source: "cnsa-2.0",
6169
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2",
6170
+ /** @internal "Non-self-signed CA Certificates in this profile MUST contain the
6171
+ * authorityKeyIdentifier, keyUsage, and basicConstraints extensions." A self-signed one is
6172
+ * governed by sec. 7.1 instead, which asks for a subjectKeyIdentifier rather than an authority
6173
+ * one, so the two scopes carry their own rows and a certificate draws one set. */
6174
+ message: "a non-self-signed CA certificate must carry this extension",
6175
+ appliesTo: _cnsaOtherCa,
6176
+ check: function (cert, ctx) {
6177
+ return _cnsaMissingExtensions(ctx, ["authorityKeyIdentifier", "keyUsage", "basicConstraints"]);
6178
+ },
6179
+ },
6180
+ {
6181
+ id: "lint/cnsa-2.0/ca-key-usage-bits", severity: "error", source: "cnsa-2.0",
6182
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6183
+ /** @internal "The keyCertSign and cRLSign bits MUST be set. The digitalSignature and
6184
+ * nonRepudiation bits MAY be set. All other bits MUST NOT be set." Both CA sections say it. */
6185
+ message: "a CA certificate's keyUsage departs from the bits this profile fixes",
6186
+ appliesTo: _cnsaAnyCa,
6187
+ check: function (cert, ctx) { return _cnsaKeyUsageFindings(ctx, "ca"); },
6188
+ },
6189
+ {
6190
+ id: "lint/cnsa-2.0/basic-constraints-not-critical", severity: "error", source: "cnsa-2.0",
6191
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6192
+ /** @internal "the basicConstraints extension MUST be marked as critical", in both CA sections. */
6193
+ message: "a CA certificate's basicConstraints extension must be marked critical",
6194
+ appliesTo: _cnsaAnyCa,
6195
+ check: function (cert, ctx) {
6196
+ var bc = ctx.decode("basicConstraints");
6197
+ if (!bc) return null;
6198
+ return bc.critical === true ? null : true;
6199
+ },
6200
+ },
6201
+ {
6202
+ id: "lint/cnsa-2.0/ca-boolean-not-set", severity: "error", source: "cnsa-2.0",
6203
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1 / 7.2",
6204
+ /** @internal "The cA boolean MUST be set to indicate that the subject is a CA", in both CA
6205
+ * sections. A certificate reaches this scope by carrying that boolean or by asserting keyCertSign,
6206
+ * so the boolean CAN be read as unset here: a certificate whose keyUsage claims the CA role while
6207
+ * its basicConstraints denies it says both things and breaks this clause. The absent extension is
6208
+ * the missing-extension row's answer, so this one reads only an extension that is present. */
6209
+ message: "a CA certificate's basicConstraints must set the cA boolean",
6210
+ appliesTo: _cnsaAnyCa,
6211
+ check: function (cert, ctx) {
6212
+ var bc = ctx.decode("basicConstraints");
6213
+ if (!bc || !bc.value) return null;
6214
+ return bc.value.cA === true ? null : { context: { cA: bc.value.cA === true } };
6215
+ },
6216
+ },
6217
+ {
6218
+ id: "lint/cnsa-2.0/self-signed-ca-path-len-present", severity: "error", source: "cnsa-2.0",
6219
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.1",
6220
+ /** @internal "the pathLenConstraint MUST NOT be present", for a self-signed CA alone: sec. 7.2
6221
+ * calls the same field OPTIONAL for a non-self-signed one. */
6222
+ message: "a self-signed CA certificate must not carry a pathLenConstraint",
6223
+ appliesTo: _cnsaSelfSignedCa,
6224
+ check: function (cert, ctx) {
6225
+ var bc = ctx.decode("basicConstraints");
6226
+ if (!bc || !bc.value) return null;
6227
+ var n = bc.value.pathLenConstraint;
6228
+ return (n === null || n === undefined) ? null : { context: { pathLenConstraint: n } };
6229
+ },
6230
+ },
6231
+ {
6232
+ id: "lint/cnsa-2.0/end-entity-missing-extension", severity: "error", source: "cnsa-2.0",
6233
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6234
+ /** @internal "end-entity certificates in this profile MUST contain the authorityKeyIdentifier
6235
+ * and keyUsage extensions." */
6236
+ message: "an end entity certificate must carry this extension",
6237
+ appliesTo: _cnsaEndEntity,
6238
+ check: function (cert, ctx) {
6239
+ return _cnsaMissingExtensions(ctx, ["authorityKeyIdentifier", "keyUsage"]);
6240
+ },
6241
+ },
6242
+ {
6243
+ id: "lint/cnsa-2.0/end-entity-no-subject-key-identifier", severity: "warn", source: "cnsa-2.0",
6244
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6245
+ /** @internal "End-entity certificates SHOULD contain the subjectKeyIdentifier extension", which
6246
+ * is a recommendation rather than a requirement, so the severity follows the clause. */
6247
+ message: "an end entity certificate should carry a subjectKeyIdentifier extension",
6248
+ appliesTo: _cnsaEndEntity,
6249
+ check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
6250
+ },
6251
+ {
6252
+ id: "lint/cnsa-2.0/end-entity-key-usage-bits", severity: "error", source: "cnsa-2.0",
6253
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6254
+ /** @internal Which bits are fixed follows the KIND the subject key declares: "For end-entity
6255
+ * digital signature certificates, the keyUsage extension MUST be set for digitalSignature. The
6256
+ * nonRepudiation bit MAY be set. All other bits ... MUST NOT be set. For end-entity key
6257
+ * establishment certificates, the keyUsage extension MUST be set for keyEncipherment. All other
6258
+ * bits ... MUST NOT be set." A key outside the suite has no kind and is reported by the subject
6259
+ * key row instead, rather than being held to one of the two tables. */
6260
+ message: "an end entity certificate's keyUsage departs from the bits this profile fixes for its key",
6261
+ appliesTo: function (cert, ctx) { return _cnsaEndEntity(cert, ctx) && _cnsaKind(cert) !== null; },
6262
+ check: function (cert, ctx) { return _cnsaKeyUsageFindings(ctx, _cnsaKind(cert)); },
6263
+ },
6264
+ {
6265
+ id: "lint/cnsa-2.0/end-entity-eku-missing", severity: "error", source: "cnsa-2.0",
6266
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6267
+ /** @internal "For all end-entity certificates, the extended key usage extension MUST be present
6268
+ * to indicate the intended use of the certificate." The sentence that follows, "The intended use
6269
+ * MUST be consistent with the keyUsage extension", has no row: which key purposes are consistent
6270
+ * with which bits is not enumerated anywhere in the document, so a table here would be invented
6271
+ * rather than cited. */
6272
+ message: "an end entity certificate must carry an extKeyUsage extension",
6273
+ appliesTo: _cnsaEndEntity,
6274
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
6275
+ },
6276
+ {
6277
+ id: "lint/cnsa-2.0/end-entity-eku-any", severity: "error", source: "cnsa-2.0",
6278
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.3",
6279
+ /** @internal "The anyExtendedKeyUsage MUST NOT be asserted." */
6280
+ message: "an end entity certificate's extKeyUsage must not name anyExtendedKeyUsage",
6281
+ appliesTo: _cnsaEndEntity,
6282
+ check: function (cert, ctx) {
6283
+ var d = ctx.decode("extKeyUsage");
6284
+ if (!d || !Array.isArray(d.value)) return null;
6285
+ return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
6286
+ },
6287
+ },
6288
+ {
6289
+ id: "lint/cnsa-2.0/certificate-policies-critical", severity: "error", source: "cnsa-2.0",
6290
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2 / 7.3",
6291
+ /** @internal "If a policy is asserted, the certificatePolicies extension MUST be marked as
6292
+ * non-critical", said in both sections. The same sentence's "MUST contain the OIDs for the
6293
+ * applicable certificate policies" is what RFC 5280 already requires of the syntax, and "If a
6294
+ * policy is not asserted, the certificatePolicies extension MUST be omitted" cannot be read from
6295
+ * the bytes: an extension that is present asserts a policy. Sec. 7.1 says neither of a
6296
+ * self-signed CA, so this row leaves one alone. */
6297
+ message: "a certificatePolicies extension must be marked non-critical",
6298
+ appliesTo: function (cert, ctx) { return _cnsaOtherCa(cert, ctx) || _cnsaEndEntity(cert, ctx); },
6299
+ check: function (cert, ctx) {
6300
+ var cp = ctx.decode("certificatePolicies");
6301
+ if (!cp) return null;
6302
+ return cp.critical === true ? true : null;
6303
+ },
6304
+ },
6305
+ {
6306
+ id: "lint/cnsa-2.0/certificate-policies-qualifiers", severity: "warn", source: "cnsa-2.0",
6307
+ citation: "draft-jenkins-cnsa2-pkix-profile-05 sec. 7.2 / 7.3",
6308
+ /** @internal "SHOULD NOT use the policyQualifiers option", a recommendation, so a warning. */
6309
+ message: "a certificatePolicies extension should not carry policy qualifiers",
6310
+ appliesTo: function (cert, ctx) { return _cnsaOtherCa(cert, ctx) || _cnsaEndEntity(cert, ctx); },
6311
+ check: function (cert, ctx) {
6312
+ var ids = _policyQualifierIds(ctx);
6313
+ return ids.length ? { context: { qualifiers: ids } } : null;
6314
+ },
6315
+ },
6316
+ ];
6317
+
5583
6318
  /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried. Every row in
5584
6319
  * every profile below asks for an extension BY NAME, and the shared decoder returns null both for
5585
6320
  * one that is absent and for one whose value does not decode, so without this row a malformed
@@ -5602,12 +6337,25 @@ var PROFILES = Object.assign(Object.create(null), {
5602
6337
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5603
6338
  "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5604
6339
  "apple-root": APPLE_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6340
+ "nist-pqc-transition": NIST_TRANSITION_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6341
+ /** @internal Sec. 4 of the draft names RFC 9881 and RFC 9935 for "the algorithm identifier, and the
6342
+ * syntax and semantics for the Subject Public Key Information field" of the two keys the suite
6343
+ * fixes, so those documents' subject-key rows ARE this profile's rules for them. Without them the
6344
+ * profile read the algorithm OID alone: bytes claiming to be an ML-KEM-1024 key while being the
6345
+ * wrong size for one are not the encapsulation key sec. 4 requires, and were reported as
6346
+ * conforming. The rows are reused rather than restated, so one clause has one row and one citation
6347
+ * wherever it is read. Their key-usage rows govern the same bits this profile's own rows do, at the
6348
+ * same strength, so a certificate breaking both is told twice from two documents rather than once
6349
+ * from a rule that quietly replaced the other. */
6350
+ "cnsa-2.0": CNSA2_RULES.concat(RFC9881_RULES).concat(RFC9935_RULES)
6351
+ .concat(RFC5280_EXTENSION_SYNTAX_RULES),
5605
6352
  });
5606
6353
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
5607
6354
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
5608
6355
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
5609
6356
  * inert under the default set because each one asks whether the caller named the profile. */
5610
- .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES);
6357
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES)
6358
+ .concat(NIST_TRANSITION_RULES).concat(CNSA2_RULES);
5611
6359
 
5612
6360
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
5613
6361
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
@@ -5731,13 +6479,50 @@ function _ingestCsr(input) {
5731
6479
  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 } } }; }
5732
6480
  }
5733
6481
 
6482
+ /** @internal The noun each verb's refusal calls its own artifact. The keys are `BY_ARTIFACT`'s, and
6483
+ * each one is also its verb's name under `pki.lint`, so a message can name the verb from the key. */
6484
+ var ARTIFACT_NOUN = Object.assign(Object.create(null), {
6485
+ certificate: "certificate", csr: "certification-request", crl: "CRL", ocsp: "OCSP",
6486
+ cms: "CMS", tsp: "timestamp", attrcert: "attribute-certificate",
6487
+ });
6488
+
6489
+ /** @internal The refusal a verb gives for a profile name it does not run, read from the SAME
6490
+ * registry-of-registries `rules` dispatches on. So a name carried by more than one artifact names
6491
+ * every verb that runs it, and every verb answers the same way about it. Each verb used to carry its
6492
+ * own chain of `else if`, naming one artifact per branch in a different order, which made the answer
6493
+ * for a name in two registries depend on which verb was asked, and each chain omitted the registries
6494
+ * nobody had added a branch for: a CMS profile name reached `pki.lint.crl` as simply unknown. */
6495
+ /** @internal "a" or "an" for a noun this file names, decided by the sound its first letter carries:
6496
+ * each of these nouns reads as a vowel exactly when it begins with one, since `CRL` and `CMS` are
6497
+ * read letter by letter and begin with a consonant sound. */
6498
+ function _article(noun) {
6499
+ var c = noun.charAt(0);
6500
+ return (c === "a" || c === "e" || c === "i" || c === "o" || c === "u" ||
6501
+ c === "A" || c === "E" || c === "I" || c === "O" || c === "U") ? "an" : "a";
6502
+ }
6503
+ function _joinAnd(list) {
6504
+ if (list.length <= 1) return list.join("");
6505
+ return list.slice(0, -1).join(", ") + " and " + list[list.length - 1];
6506
+ }
6507
+ function _profileRefusal(profile, mine) {
6508
+ var homes = Object.keys(BY_ARTIFACT).filter(function (a) {
6509
+ return a !== mine && Object.prototype.hasOwnProperty.call(BY_ARTIFACT[a], profile);
6510
+ });
6511
+ var label = _article(ARTIFACT_NOUN[mine]) + " " + ARTIFACT_NOUN[mine] + " lint profile";
6512
+ if (!homes.length) {
6513
+ return _cfg("lint/unknown-profile", "unknown " + ARTIFACT_NOUN[mine] + " lint profile \"" +
6514
+ profile + "\" (known: " + Object.keys(BY_ARTIFACT[mine]).join(", ") + ")");
6515
+ }
6516
+ var nouns = homes.map(function (a) { return ARTIFACT_NOUN[a]; });
6517
+ var verbs = homes.map(function (a) { return "pki.lint." + a; });
6518
+ return _cfg("lint/unknown-profile", "\"" + profile + "\" is not " + label + "; it is " +
6519
+ _article(nouns[0]) + " " + _joinAnd(nouns) + " profile, which " + _joinAnd(verbs) +
6520
+ (verbs.length === 1 ? " runs" : " run"));
6521
+ }
6522
+
5734
6523
  function _selectRules(profile) {
5735
6524
  if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
5736
- if (!PROFILES[profile]) {
5737
- if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
5738
- if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5739
- throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
5740
- }
6525
+ if (!PROFILES[profile]) throw _profileRefusal(profile, "certificate");
5741
6526
  return PROFILES[profile];
5742
6527
  }
5743
6528
 
@@ -5773,7 +6558,9 @@ function _applyThreshold(report, severity) {
5773
6558
  * a CA certificate reports that CA against subscriber requirements. The CA
5774
6559
  * profiles of section 7.1.2.10 are a separate set this does not yet carry. Left
5775
6560
  * unnamed, a certificate is held to those rows only where it asserts
5776
- * id-kp-serverAuth and is not a CA.
6561
+ * id-kp-serverAuth and is not a CA. `"nist-pqc-transition"` and `"cnsa-2.0"` are
6562
+ * named the same way: which algorithm policy a certificate is held to is the
6563
+ * caller's assertion, so neither is detected and both are inert until named.
5777
6564
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
5778
6565
  * `worst` always reflect the complete, unfiltered result.
5779
6566
  * @example
@@ -5872,8 +6659,10 @@ function _ingestCrl(input) {
5872
6659
  * extension repeated on the CRL or on an entry (section 4.2) each arrive as `lint/unparseable`
5873
6660
  * carrying the parser's own code.
5874
6661
  *
5875
- * @opts profile `"rfc5280-crl"`, the only CRL profile (default runs every CRL rule). A
5876
- * certificate profile name is refused rather than run against a CRL.
6662
+ * @opts profile `"rfc5280-crl"`, or `"cnsa-2.0"` for the one rule that profile states about a
6663
+ * CRL's signature (default runs every CRL rule, and the `cnsa-2.0` rule is inert
6664
+ * unless named). A profile name this verb does not run is refused, and the refusal
6665
+ * names the verb that does.
5877
6666
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
5878
6667
  * always reflect the complete, unfiltered result.
5879
6668
  * @example
@@ -5946,10 +6735,7 @@ function csr(input, opts) {
5946
6735
  var rules_;
5947
6736
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CSR_RULES;
5948
6737
  else if (CSR_PROFILES[profile]) rules_ = CSR_PROFILES[profile];
5949
- else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
5950
- else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5951
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
5952
- else throw _cfg("lint/unknown-profile", "unknown certification-request lint profile \"" + profile + "\" (known: " + Object.keys(CSR_PROFILES).join(", ") + ")");
6738
+ else throw _profileRefusal(profile, "csr");
5953
6739
 
5954
6740
  var ingested = _ingestCsr(input);
5955
6741
  var report;
@@ -5977,9 +6763,7 @@ function crl(input, opts) {
5977
6763
  var rules_;
5978
6764
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CRL_RULES;
5979
6765
  else if (CRL_PROFILES[profile]) rules_ = CRL_PROFILES[profile];
5980
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
5981
- else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
5982
- else throw _cfg("lint/unknown-profile", "unknown CRL lint profile \"" + profile + "\" (known: " + Object.keys(CRL_PROFILES).join(", ") + ")");
6766
+ else throw _profileRefusal(profile, "crl");
5983
6767
 
5984
6768
  var ingested = _ingestCrl(input);
5985
6769
  var report;
@@ -6058,8 +6842,9 @@ function _ingestOcsp(input) {
6058
6842
  * is `pki.ocsp.verify`'s, and the certificates in `certs` are linted with `pki.lint.certificate`.
6059
6843
  *
6060
6844
  * @opts profile `"rfc6960"` or `"rfc5019"` (default runs the rfc6960 rows and marks the rfc5019
6061
- * rows not applicable). A certificate or CRL profile name is refused rather than
6062
- * run against a response.
6845
+ * rows not applicable), or `"cnsa-2.0"` for the one rule that profile states about
6846
+ * a response's signature. A profile name this verb does not run is refused, and the
6847
+ * refusal names the verb that does.
6063
6848
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6064
6849
  * always reflect the complete, unfiltered result.
6065
6850
  * @example
@@ -6089,9 +6874,7 @@ function ocsp(input, opts) {
6089
6874
  var rules_;
6090
6875
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_OCSP_RULES;
6091
6876
  else if (OCSP_PROFILES[profile]) rules_ = OCSP_PROFILES[profile];
6092
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6093
- else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
6094
- else throw _cfg("lint/unknown-profile", "unknown OCSP lint profile \"" + profile + "\" (known: " + Object.keys(OCSP_PROFILES).join(", ") + ")");
6877
+ else throw _profileRefusal(profile, "ocsp");
6095
6878
 
6096
6879
  var ingested = _ingestOcsp(input);
6097
6880
  var report;
@@ -6193,10 +6976,7 @@ function cms(input, opts) {
6193
6976
  var rules_;
6194
6977
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
6195
6978
  else if (CMS_PROFILES[profile]) rules_ = CMS_PROFILES[profile];
6196
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6197
- else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
6198
- else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
6199
- else throw _cfg("lint/unknown-profile", "unknown CMS lint profile \"" + profile + "\" (known: " + Object.keys(CMS_PROFILES).join(", ") + ")");
6979
+ else throw _profileRefusal(profile, "cms");
6200
6980
 
6201
6981
  var ingested = _ingestCms(input);
6202
6982
  var report;
@@ -6283,9 +7063,7 @@ function tsp(input, opts) {
6283
7063
  var rules_;
6284
7064
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
6285
7065
  else if (TSP_PROFILES[profile]) rules_ = TSP_PROFILES[profile];
6286
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6287
- else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6288
- else throw _cfg("lint/unknown-profile", "unknown timestamp lint profile \"" + profile + "\" (known: " + Object.keys(TSP_PROFILES).join(", ") + ")");
7066
+ else throw _profileRefusal(profile, "tsp");
6289
7067
 
6290
7068
  var ingested = _ingestTsp(input);
6291
7069
  var report;
@@ -6374,9 +7152,7 @@ function attrcert(input, opts) {
6374
7152
  var rules_;
6375
7153
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
6376
7154
  else if (AC_PROFILES[profile]) rules_ = AC_PROFILES[profile];
6377
- else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
6378
- else if (CMS_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CMS profile; lint a message with pki.lint.cms");
6379
- else throw _cfg("lint/unknown-profile", "unknown attribute certificate lint profile \"" + profile + "\" (known: " + Object.keys(AC_PROFILES).join(", ") + ")");
7155
+ else throw _profileRefusal(profile, "attrcert");
6380
7156
 
6381
7157
  var ingested = _ingestAttrCert(input);
6382
7158
  var report;
@@ -6442,10 +7218,9 @@ function rules(profile, artifact) {
6442
7218
  .concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
6443
7219
  } else if (artifact != null) {
6444
7220
  set = BY_ARTIFACT[artifact][profile];
6445
- if (!set) {
6446
- throw _cfg("lint/unknown-profile", "\"" + profile + "\" is not a " + artifact +
6447
- " lint profile (known: " + Object.keys(BY_ARTIFACT[artifact]).join(", ") + ")");
6448
- }
7221
+ /** @internal The same refusal the VERB for this artifact gives, so asking the enumerator where a
7222
+ * profile lives and asking the verb to run it are answered by one sentence rather than two. */
7223
+ if (!set) throw _profileRefusal(profile, artifact);
6449
7224
  } else {
6450
7225
  set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
6451
7226
  CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
@@ -6476,8 +7251,9 @@ function rules(profile, artifact) {
6476
7251
  * @example
6477
7252
  * pki.lint.profiles();
6478
7253
  * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "cabf-smime", "cabf-cs",
6479
- * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root", "rfc2986",
6480
- * // "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc3161", "rfc5755"]
7254
+ * // "etsi-qc", "mozilla-root", "chrome-root", "microsoft-root", "apple-root",
7255
+ * // "nist-pqc-transition", "cnsa-2.0", "rfc2986", "rfc5280-crl", "rfc6960", "rfc5019",
7256
+ * // "rfc5652", "rfc3161", "rfc5755"]
6481
7257
  */
6482
7258
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
6483
7259
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
@@ -6497,6 +7273,126 @@ function profiles() {
6497
7273
  return out;
6498
7274
  }
6499
7275
 
7276
+ /** @internal The rules the build-time gate does not refuse on because the SPEC does not place the
7277
+ * requirement on the issuer, keyed by the last segment of the rule id so one entry answers for every
7278
+ * profile carrying the rule: a certificate reports lint/rfc5280/unknown-critical-extension and a
7279
+ * revocation list reports lint/rfc5280-crl/unknown-critical-extension, and the two state one
7280
+ * requirement. The value is the reason, read by the gate's conformance vectors rather than restated.
7281
+ *
7282
+ * The other reason a rule cannot apply, that it reads a signature the gate has not produced yet, is
7283
+ * not a name here: those rules declare `readsSignatureValue` where they are defined, and
7284
+ * SIGNATURE_VALUE_RULES below derives the set from the same registries `rules()` walks. A rule that
7285
+ * starts reading the signature value therefore exempts itself, and a vector measures the declared set
7286
+ * against the rules whose verdict actually moves with the signature. */
7287
+ var BUILD_GATE_EXEMPT = Object.assign(Object.create(null), {
7288
+ "unknown-critical-extension": "RFC 5280 sec. 4.2 places this requirement on \"a certificate-using system\", so the rule reports what a relying party owes rather than what an issuer does. It covers two artifacts an issuer may conform in emitting: one carrying a critical extension the specification does not identify, which the same section permits a conforming CA to support, and one carrying an extension the specification does identify but this toolkit's own path validation does not process, such as a critical policyMappings",
7289
+ });
7290
+
7291
+ /** @internal Every rule id whose verdict reads the signature VALUE, derived at load from the rule rows
7292
+ * themselves rather than listed. The gate runs on an artifact carrying a placeholder signature, so one
7293
+ * of these answers about the placeholder and not about what the caller is asking to emit. */
7294
+ var SIGNATURE_VALUE_RULES = (function () {
7295
+ var out = Object.create(null);
7296
+ Object.keys(BY_ARTIFACT).forEach(function (artifact) {
7297
+ Object.keys(BY_ARTIFACT[artifact]).forEach(function (name) {
7298
+ BY_ARTIFACT[artifact][name].forEach(function (r) {
7299
+ if (r.readsSignatureValue === true) out[r.id] = true;
7300
+ });
7301
+ });
7302
+ });
7303
+ return out;
7304
+ })();
7305
+
7306
+ /** @internal The profile whose rows an artifact is held to whatever the caller names. RFC 5280 states
7307
+ * the contract a certificate and a revocation list are written to and RFC 2986 the one a request is,
7308
+ * so these run on every authoring call and `opts.profile` names rows beside them. */
7309
+ var BUILD_GATE_ALWAYS = Object.assign(Object.create(null), {
7310
+ certificate: "rfc5280", csr: "rfc2986", crl: "rfc5280-crl",
7311
+ });
7312
+
7313
+ var BUILD_GATE_VERB = Object.assign(Object.create(null), {
7314
+ certificate: certificate, csr: csr, crl: crl,
7315
+ });
7316
+
7317
+ /** @internal The profile name that runs no rules. A toolkit that cannot emit a non-conforming artifact
7318
+ * cannot build the corpus its own linter is tested against, and an operator reproducing a real
7319
+ * misissuance needs the same. The escape is a name the caller writes rather than an absence, so a
7320
+ * non-conforming artifact is one somebody asked for: the gate is closed by default and every refusal
7321
+ * names this among the profiles it would accept. */
7322
+ var BUILD_GATE_NONE = "none";
7323
+
7324
+ /** @internal The exemption table, copied so a caller cannot edit the gate's own line. The gate's
7325
+ * vectors read the reasons from here, which is why it is reachable at all. */
7326
+ function buildGateExemptions() {
7327
+ return Object.assign(Object.create(null), BUILD_GATE_EXEMPT);
7328
+ }
7329
+
7330
+ /** @internal The rule ids that declared they read the signature value, for the vector that measures
7331
+ * this set against the rules whose verdict actually moves when the signature does. */
7332
+ function buildGateSignatureRules() {
7333
+ return Object.assign(Object.create(null), SIGNATURE_VALUE_RULES);
7334
+ }
7335
+
7336
+ /** @internal The profile a build-time gate always runs for this artifact, for a vector that derives
7337
+ * the expected rule set from the same name the gate uses. */
7338
+ function buildGateProfile(artifact) {
7339
+ return BUILD_GATE_ALWAYS[artifact] || null;
7340
+ }
7341
+
7342
+ function _buildGateTail(id) {
7343
+ var cut = id.lastIndexOf("/");
7344
+ return cut === -1 ? id : id.slice(cut + 1);
7345
+ }
7346
+
7347
+ /**
7348
+ * @internal Refuse a profile name an authoring verb cannot run, under the CALLER's code, so
7349
+ * `pki.x509.sign` reports an unknown profile in its own domain as it reports every other bad option.
7350
+ * The sentence is the one `pki.lint.rules` gives for the same name, so asking the linter where a
7351
+ * profile lives and asking a builder to hold an artifact to it are answered alike.
7352
+ */
7353
+ function assertKnownBuildProfile(artifact, profile, E, code) {
7354
+ if (profile == null || profile === BUILD_GATE_NONE) return;
7355
+ if (typeof profile !== "string") {
7356
+ throw E(code, "profile must be the name of a lint profile (known: " +
7357
+ Object.keys(BY_ARTIFACT[artifact]).join(", ") + ", " + BUILD_GATE_NONE + ")");
7358
+ }
7359
+ if (!Object.prototype.hasOwnProperty.call(BY_ARTIFACT[artifact], profile)) {
7360
+ throw E(code, _profileRefusal(profile, artifact).message + ", or \"" + BUILD_GATE_NONE +
7361
+ "\" to emit what the spec describes without holding it to a profile");
7362
+ }
7363
+ }
7364
+
7365
+ /**
7366
+ * @internal Refuse to emit an artifact this toolkit's own linter grades `error` or worse. The caller
7367
+ * passes the artifact it is about to sign, carrying a placeholder signature, and the gate runs the
7368
+ * always-on profile for that artifact plus the one the caller named. A finding whose rule id is in
7369
+ * BUILD_GATE_EXEMPT is passed over, and every other one throws under the caller's code naming the
7370
+ * rule and its citation.
7371
+ *
7372
+ * Standards are the contract, so this runs before the signing key is reached: a certificate that was
7373
+ * signed and then discarded is still one a key signed and an audit log recorded. A caller naming the
7374
+ * profile "none" gets no rules and whatever the spec describes.
7375
+ */
7376
+ function assertBuildClean(artifact, der, profile, E, code) {
7377
+ if (profile === BUILD_GATE_NONE) return;
7378
+ var verb = BUILD_GATE_VERB[artifact];
7379
+ var reports = [verb(der, { profile: BUILD_GATE_ALWAYS[artifact] })];
7380
+ if (profile != null && profile !== BUILD_GATE_ALWAYS[artifact]) {
7381
+ reports.push(verb(der, { profile: profile }));
7382
+ }
7383
+ for (var r = 0; r < reports.length; r++) {
7384
+ var found = reports[r].findings;
7385
+ for (var i = 0; i < found.length; i++) {
7386
+ var f = found[i];
7387
+ if (f.severity !== "error" && f.severity !== "fatal") continue;
7388
+ if (SIGNATURE_VALUE_RULES[f.id]) continue;
7389
+ if (BUILD_GATE_EXEMPT[_buildGateTail(f.id)]) continue;
7390
+ throw E(code, "this spec describes " + _article(ARTIFACT_NOUN[artifact]) + " " +
7391
+ ARTIFACT_NOUN[artifact] + " that violates " + f.id + " (" + f.citation + "): " + f.message);
7392
+ }
7393
+ }
7394
+ }
7395
+
6500
7396
  module.exports = {
6501
7397
  certificate: certificate,
6502
7398
  csr: csr,
@@ -6507,4 +7403,9 @@ module.exports = {
6507
7403
  attrcert: attrcert,
6508
7404
  rules: rules,
6509
7405
  profiles: profiles,
7406
+ assertBuildClean: assertBuildClean,
7407
+ assertKnownBuildProfile: assertKnownBuildProfile,
7408
+ buildGateExemptions: buildGateExemptions,
7409
+ buildGateSignatureRules: buildGateSignatureRules,
7410
+ buildGateProfile: buildGateProfile,
6510
7411
  };