@blamejs/pki 0.8.26 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.8.27 — 2026-09-28
8
+
9
+ Two algorithm-policy profiles, and a two-digit year read against the clock rather than a constant.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.certificate(der, { profile: "nist-pqc-transition" })` reports NIST IR 8547's two transitions. A classical subject public key or signature algorithm in a certificate issued from 2036 is an error, which Table 2 and Table 4 make disallowed at every parameter set they list. A classical subject key at or below the 112-bit security strength issued from 2031 is a warning, which is the strength those tables deprecate and which SP 800-57 Part 1 Rev 5 Table 2 maps onto an RSA modulus of 2048 bits, a curve whose field is 224 to 255 bits, and a finite-field group stated as a pair, L of 2048 with N of 224. Either finite-field parameter caps the strength, so a group with a 3072-bit prime and a 224-bit subgroup order is reported, and a group whose encoding states no subgroup order is read on its prime. "After 2030" and "after 2035" name whole years, so each transition begins on 1 January of the year that follows and is compared against the certificate's own notBefore. IR 8547 is an initial public draft, so these are a draft's dates and the citation says so.
14
+ - Ed25519 is deliberately outside the 2031 row. Its field is 255 bits, which SP 800-57's ECC column places at the 112-bit strength, so reading the two documents together would deprecate it after 2030; IR 8547's own EdDSA row carries a 128-bit entry alone, with no 112-bit entry and no 2030 date, and the profile follows the document it cites. A composite signature or subject key is passed over in both rows, because the tables list pure families and the same guidance endorses dual-algorithm operation through the transition.
15
+ - `pki.lint.certificate(der, { profile: "cnsa-2.0" })` reports the CNSA 2.0 certificate profile of `draft-jenkins-cnsa2-pkix-profile-05`, which states what a certificate must carry where the NSA algorithm notice lists algorithms. Seventeen rows: the certificate must be X.509 version 3, the subject public key must be ML-DSA-87 or ML-KEM-1024 and the signature must be id-ml-dsa-87, which also answers the clause forbidding HashML-DSA, since a HashML-DSA identifier is not that one; a keyUsage extension must be critical in all three scopes; a self-signed CA must carry a subjectKeyIdentifier, a keyUsage and a basicConstraints, a non-self-signed CA an authorityKeyIdentifier, a keyUsage and a basicConstraints, where a certificate whose keyUsage asserts keyCertSign is read as a CA whether or not it carries basicConstraints, since RFC 5280 section 4.2.1.3 requires the cA boolean of any certificate asserting that bit and a certificate omitting the extension is the one this requirement is about; a CA reissued in its own name under a new key is read as the second of the two CA kinds, since its authorityKeyIdentifier names a key other than its own subjectKeyIdentifier and the section governing it permits the pathLenConstraint the self-signed section forbids; and an end entity an authorityKeyIdentifier and a keyUsage, with a missing subjectKeyIdentifier on an end entity reported as a warning where that clause says SHOULD; a CA's basicConstraints must be critical and must set the cA boolean, and a self-signed CA's must carry no pathLenConstraint, which the non-self-signed section calls optional; a CA's keyUsage must set keyCertSign and cRLSign, may set digitalSignature and nonRepudiation, and must set nothing else; an end entity's extKeyUsage must be present and must not name anyExtendedKeyUsage; and a certificatePolicies extension must be non-critical and should carry no policy qualifiers. Each departure from a closed key-usage set is its own finding, so a certificate setting two forbidden bits reports both.
16
+ - The end entity key-usage set follows the kind the subject public key declares, which is what makes that clause readable from the bytes: an ML-DSA-87 certificate must set digitalSignature and may set nonRepudiation, an ML-KEM-1024 certificate must set keyEncipherment alone, and a key outside the suite has no set to be held to and is reported as the key instead.
17
+ - `pki.lint.crl` and `pki.lint.ocsp` take the same `cnsa-2.0` name for the one rule the document states about them: the signature on a CRL and on an OCSP response follows the same rule as a certificate's. Both halves of it reach them, the algorithm the artifact was signed with and the length FIPS 204 fixes for a signature of that algorithm, the second of which a certificate is told by the RFC 9881 row its own profile runs.
18
+ - The profile also runs the RFC 9881 and RFC 9935 subject-key rows, which section 4 of the draft names for "the algorithm identifier, and the syntax and semantics for the Subject Public Key Information field" of the two keys the suite fixes. So a subject key whose algorithm says ML-KEM-1024 or ML-DSA-87 while its bytes are not a key of the size FIPS 203 or FIPS 204 gives that parameter set is reported, where reading the algorithm identifier alone called it conforming.
19
+ - `pki.lint.certificate` gains `lint/rfc9881/mldsa-key-length` and `lint/rfc9881/mldsa-signature-length` for that second half, under every profile that carries the RFC 9881 rows. FIPS 204 fixes both an ML-DSA public key's length and a signature's per parameter set, and RFC 9881 section 4 carries each directly in its BIT STRING, so a value of any other length is not what the identifier names. The ML-KEM row beside them has read its own sizes since that profile shipped.
20
+ - Every clause of that profile which this toolkit's parsers settle before a row could read it has a conformance vector asserting the parser's verdict instead: a present parameters field on either suite algorithm arrives as `x509/bad-algorithm-parameters`, and an inner TBSCertificate or TBSCertList signature identifier that differs from the outer signatureAlgorithm as `x509/bad-signature-algorithm` or `crl/bad-signature-algorithm`, so the row reading one of those two fields answers for both. Four further clauses are recorded without a row, because the bytes do not settle them: which extended key purposes are consistent with which key-usage bits is enumerated nowhere in the document, a certificatePolicies extension that is present asserts a policy, the responder authorization an OCSP response also owes is a trust decision over a chain that `pki.ocsp.verify` makes, and a CA reissued in its own name that carries no authorityKeyIdentifier at all cannot be told from a root, since a root omits that extension legitimately and RFC 5280 section 4.2.1.1 permits its absence only there, which the default rule set reports.
21
+
22
+ ### Changed
23
+
24
+ - `pki.lint.profiles()` lists `nist-pqc-transition` and `cnsa-2.0` beside the others. `cnsa-2.0` appears once although three verbs run it, because that call enumerates profile names rather than the places they are registered.
25
+
26
+ ### Fixed
27
+
28
+ - `pki.lint.certificate`, `pki.lint.crl`, `pki.lint.ocsp`, `pki.lint.csr`, `pki.lint.cms`, `pki.lint.tsp` and `pki.lint.attrcert` all answer the same way about where a profile name lives. A verb handed a profile it does not run refuses it with `lint/unknown-profile` naming every verb that does run it, read from the registry rather than from a list written into each verb. Before, each verb carried its own chain of checks in its own order, so a name carried by more than one artifact drew a different answer depending on which verb was asked, and each chain omitted the artifacts nobody had added a branch for: a CMS profile name reached `pki.lint.crl` as simply unknown. `pki.lint.rules(profile, artifact)` gives that same sentence, so asking where a profile lives and asking a verb to run it are one answer.
29
+ - An HTTP `Retry-After` header carrying an rfc850-date, the obsolete format with a two-digit year, is read against the time the value arrives. RFC 7231 section 7.1.1.1 requires a recipient to read a year more than 50 years in the future as the most recent past year with the same two digits, which depends on when the value is received. A caller that supplied a reference time already got that; one that did not fell back to a fixed cutoff at 70, which is the same rule evaluated once for a reference year around 2020 and which misreads a band of years that widens by one every year. At a 2026 receipt the years 70 through 76 were read a century early. `pki.est`'s retry handling is the reachable caller, since its own `now` option is optional. The four-digit formats are unaffected.
30
+ - `pki.rfc3339`'s description of why it validates the calendar names the case that needs it. `2026-02-30T00:00:00Z` is the one the runtime accepts and reads as 2 March, two days from the date the value names; month 13, hour 25 and a `+25:00` offset are refused by the runtime as well, so the check agrees with it there. A `:60` leap second, which the description gave as its example, is one of those, and refusing it is a decision rather than a malformation: RFC 3339 section 5.7 permits the value and JavaScript cannot represent it. Each of those statements now has a vector asserting it against the runtime, so the reason the check exists cannot quietly stop being true. No behavior changed.
31
+
7
32
  ## v0.8.26 — 2026-09-28
8
33
 
9
34
  Three lint verbs, and each is short because the parser that feeds it is strict.
package/README.md CHANGED
@@ -290,7 +290,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
290
290
  | `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
291
291
  | `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
292
292
  | `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
293
- | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
293
+ | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
294
294
  | `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
295
295
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
296
296
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert\|crl\|ocsp-response>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
@@ -160,6 +160,11 @@ function _signTradComponent(trad, pkcs8, mprime) {
160
160
  }
161
161
 
162
162
  module.exports = {
163
+ /** @internal The FIPS 204 public-key and signature sizes of each ML-DSA parameter set, keyed by the
164
+ * name the engine knows it by and carrying the identifier the registry names it with. The composite
165
+ * arms are built from it, and a caller holding a certificate to those sizes reads them here rather
166
+ * than restating a figure that must agree with the one the arms use. */
167
+ MLDSA_COMPONENT: MLDSA_COMPONENT,
163
168
  COMPOSITE_ALGS: COMPOSITE_ALGS,
164
169
  compositeVerify: compositeVerify,
165
170
  resolveCompositeDescriptor: resolveCompositeDescriptor,
@@ -4,6 +4,7 @@
4
4
  // @internal
5
5
 
6
6
  var constants = require("./constants");
7
+ var guard = require("./guard-all");
7
8
 
8
9
  var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(1);
9
10
 
@@ -28,12 +29,27 @@ function _isAllDigits(s) {
28
29
  return true;
29
30
  }
30
31
 
31
- function _rfc850Year(yy, refMs) {
32
- if (typeof refMs !== "number" || !isFinite(refMs)) return yy + (yy < 70 ? 2000 : 1900);
33
- var refYear = new Date(refMs).getUTCFullYear();
34
- var full = Math.floor(refYear / 100) * 100 + yy;
35
- if (full - refYear > 50) full -= 100;
36
- return full;
32
+ /** @internal RFC 7231 sec. 7.1.1.1: a recipient "MUST interpret a timestamp that appears to be more
33
+ * than 50 years in the future as representing the most recent year in the past that had the same last
34
+ * two digits". The reading therefore depends on when the value is received, and a caller that does not
35
+ * say when that was is receiving it now. A fixed cutoff instead is this rule evaluated once, for the
36
+ * year the cutoff was chosen in, and it misreads a widening band of two-digit years for every year
37
+ * that passes after it.
38
+ * THE CLAUSE COMPARES TIMESTAMPS, so this compares timestamps, which is why it runs once the month, day
39
+ * and time are read. A comparison of years alone reads a timestamp in the fiftieth year ahead as inside
40
+ * the window whatever its month: received on 28 September 2026, `31-Dec-76` is fifty years and three
41
+ * months ahead, which is more than fifty, and names 1976. */
42
+ function _rfc850Year(yy, mon, day, hh, mi, ss, refMs) {
43
+ var ref = (typeof refMs === "number" && isFinite(refMs)) ? refMs : Date.now();
44
+ var refDate = new Date(ref);
45
+ var refInstant = guard.time.instantOf(refDate);
46
+ if (isNaN(refInstant)) return yy + (yy < 70 ? 2000 : 1900);
47
+ var full = Math.floor(refDate.getUTCFullYear() / 100) * 100 + yy;
48
+ var horizon = new Date(refInstant);
49
+ horizon.setUTCFullYear(horizon.getUTCFullYear() + 50);
50
+ var horizonInstant = guard.time.instantOf(horizon);
51
+ if (isNaN(horizonInstant)) return full;
52
+ return Date.UTC(full, mon, day, hh, mi, ss) > horizonInstant ? full - 100 : full;
37
53
  }
38
54
 
39
55
  function _scanImf(s) {
@@ -52,7 +68,12 @@ function _scanRfc850(s, refMs) {
52
68
  var mo = _monthAt(s, p); if (!mo || s.charAt(p + 3) !== "-") return null; p += 4;
53
69
  var yy = _digits2(s, p); if (yy < 0 || s.charAt(p + 2) !== " ") return null; p += 3;
54
70
  var t = _timeAt(s, p); if (!t || s.slice(t.next, t.next + 4) !== " GMT" || t.next + 4 !== s.length) return null;
55
- return { day: dd, monName: mo, year: _rfc850Year(yy, refMs), hh: t.hh, mi: t.mi, ss: t.ss };
71
+ /** @internal The century is resolved once the whole timestamp is read, since the clause compares the
72
+ * timestamp rather than its year. A month the calendar does not hold is refused by the round-trip
73
+ * check in `httpDateMs`, which reads the resolved year this returns. */
74
+ var mon = MONTHS.indexOf(mo);
75
+ var year = _rfc850Year(yy, mon, dd, t.hh, t.mi, t.ss, refMs);
76
+ return { day: dd, monName: mo, year: year, hh: t.hh, mi: t.mi, ss: t.ss };
56
77
  }
57
78
  function _scanAsctime(s) {
58
79
  var p = _matchName(s, 0, DOW_SHORT);
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
 
@@ -3913,8 +3914,59 @@ function _pqcSigKeyUsageRule(id, source, citation, family, appliesTo) {
3913
3914
  check: _pqcSigKeyUsage,
3914
3915
  };
3915
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
+
3916
3928
  var RFC9881_RULES = [
3917
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
+ },
3918
3970
  ];
3919
3971
  var RFC9909_RULES = [
3920
3972
  _pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
@@ -4615,8 +4667,72 @@ var RFC5280_CRL_RULES = [
4615
4667
  },
4616
4668
  ];
4617
4669
 
4618
- var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
4619
- 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);
4620
4736
 
4621
4737
  /** @internal The RFC 5652 profile is SHORT, and the reason is worth stating where the rows are:
4622
4738
  * this toolkit's CMS parser already refuses nearly everything sections 5 and 11 require, so those
@@ -5420,8 +5536,28 @@ var RFC5019_OCSP_RULES = [
5420
5536
  },
5421
5537
  ];
5422
5538
 
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);
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);
5425
5561
 
5426
5562
  /** @internal The extensions an issuer determines, which a request may carry and a CA does not take
5427
5563
  * from one. subjectKeyIdentifier and authorityKeyIdentifier are computed from the keys, and the
@@ -5580,6 +5716,602 @@ var CABF_TLS_CSR_RULE_IDS = [
5580
5716
  "lint/cabf-tls/weak-key",
5581
5717
  ];
5582
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
+
5583
6315
  /** @internal RFC 5280 sec. 4.2 fixes each extension's syntax wherever it is carried. Every row in
5584
6316
  * every profile below asks for an extension BY NAME, and the shared decoder returns null both for
5585
6317
  * one that is absent and for one whose value does not decode, so without this row a malformed
@@ -5602,12 +6334,25 @@ var PROFILES = Object.assign(Object.create(null), {
5602
6334
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5603
6335
  "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5604
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),
5605
6349
  });
5606
6350
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
5607
6351
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
5608
6352
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
5609
6353
  * 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);
6354
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES)
6355
+ .concat(NIST_TRANSITION_RULES).concat(CNSA2_RULES);
5611
6356
 
5612
6357
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
5613
6358
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
@@ -5731,13 +6476,50 @@ function _ingestCsr(input) {
5731
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 } } }; }
5732
6477
  }
5733
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
+
5734
6520
  function _selectRules(profile) {
5735
6521
  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
- }
6522
+ if (!PROFILES[profile]) throw _profileRefusal(profile, "certificate");
5741
6523
  return PROFILES[profile];
5742
6524
  }
5743
6525
 
@@ -5773,7 +6555,9 @@ function _applyThreshold(report, severity) {
5773
6555
  * a CA certificate reports that CA against subscriber requirements. The CA
5774
6556
  * profiles of section 7.1.2.10 are a separate set this does not yet carry. Left
5775
6557
  * unnamed, a certificate is held to those rows only where it asserts
5776
- * 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.
5777
6561
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
5778
6562
  * `worst` always reflect the complete, unfiltered result.
5779
6563
  * @example
@@ -5872,8 +6656,10 @@ function _ingestCrl(input) {
5872
6656
  * extension repeated on the CRL or on an entry (section 4.2) each arrive as `lint/unparseable`
5873
6657
  * carrying the parser's own code.
5874
6658
  *
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.
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.
5877
6663
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
5878
6664
  * always reflect the complete, unfiltered result.
5879
6665
  * @example
@@ -5946,10 +6732,7 @@ function csr(input, opts) {
5946
6732
  var rules_;
5947
6733
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CSR_RULES;
5948
6734
  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(", ") + ")");
6735
+ else throw _profileRefusal(profile, "csr");
5953
6736
 
5954
6737
  var ingested = _ingestCsr(input);
5955
6738
  var report;
@@ -5977,9 +6760,7 @@ function crl(input, opts) {
5977
6760
  var rules_;
5978
6761
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CRL_RULES;
5979
6762
  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(", ") + ")");
6763
+ else throw _profileRefusal(profile, "crl");
5983
6764
 
5984
6765
  var ingested = _ingestCrl(input);
5985
6766
  var report;
@@ -6058,8 +6839,9 @@ function _ingestOcsp(input) {
6058
6839
  * is `pki.ocsp.verify`'s, and the certificates in `certs` are linted with `pki.lint.certificate`.
6059
6840
  *
6060
6841
  * @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.
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.
6063
6845
  * @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
6064
6846
  * always reflect the complete, unfiltered result.
6065
6847
  * @example
@@ -6089,9 +6871,7 @@ function ocsp(input, opts) {
6089
6871
  var rules_;
6090
6872
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_OCSP_RULES;
6091
6873
  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(", ") + ")");
6874
+ else throw _profileRefusal(profile, "ocsp");
6095
6875
 
6096
6876
  var ingested = _ingestOcsp(input);
6097
6877
  var report;
@@ -6193,10 +6973,7 @@ function cms(input, opts) {
6193
6973
  var rules_;
6194
6974
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CMS_RULES;
6195
6975
  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(", ") + ")");
6976
+ else throw _profileRefusal(profile, "cms");
6200
6977
 
6201
6978
  var ingested = _ingestCms(input);
6202
6979
  var report;
@@ -6283,9 +7060,7 @@ function tsp(input, opts) {
6283
7060
  var rules_;
6284
7061
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_TSP_RULES;
6285
7062
  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(", ") + ")");
7063
+ else throw _profileRefusal(profile, "tsp");
6289
7064
 
6290
7065
  var ingested = _ingestTsp(input);
6291
7066
  var report;
@@ -6374,9 +7149,7 @@ function attrcert(input, opts) {
6374
7149
  var rules_;
6375
7150
  if (profile == null || profile === "all" || profile === "default") rules_ = ALL_AC_RULES;
6376
7151
  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(", ") + ")");
7152
+ else throw _profileRefusal(profile, "attrcert");
6380
7153
 
6381
7154
  var ingested = _ingestAttrCert(input);
6382
7155
  var report;
@@ -6442,10 +7215,9 @@ function rules(profile, artifact) {
6442
7215
  .concat(ALL_CMS_RULES).concat(ALL_TSP_RULES).concat(ALL_AC_RULES);
6443
7216
  } else if (artifact != null) {
6444
7217
  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
- }
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);
6449
7221
  } else {
6450
7222
  set = PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile] ||
6451
7223
  CSR_PROFILES[profile] || CMS_PROFILES[profile] || TSP_PROFILES[profile] ||
@@ -6476,8 +7248,9 @@ function rules(profile, artifact) {
6476
7248
  * @example
6477
7249
  * pki.lint.profiles();
6478
7250
  * // -> ["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"]
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"]
6481
7254
  */
6482
7255
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
6483
7256
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
package/lib/rfc3339.js CHANGED
@@ -6,11 +6,18 @@
6
6
  * lib/rfc3339.js: RFC 3339 date-time parsing + validation, a fail-closed engine primitive shared by
7
7
  * pki.acme (a boolean field-validator over server-supplied JSON) and pki.ct (parse a CT log-list
8
8
  * timestamp / temporal-interval bound to a comparable Date). Beyond the grammar it enforces CALENDAR
9
- * validity: a syntactically well-formed but impossible instant (month 13, February 30, hour 25, a
10
- * :60 leap second, a +25:00 offset) is rejected, so a downstream expiry / window comparison never runs
11
- * on a value JS `Date` would silently roll over or `Date.parse` would NaN. Like the byte-reader / guard
12
- * family, `parse` takes the caller's `(code, message) -> error` factory so every consumer keeps its own
13
- * domain fault code.
9
+ * validity: a syntactically well-formed but impossible instant is rejected, so a downstream expiry or
10
+ * window comparison never runs on a value JS `Date` reads as a different instant.
11
+ * `2026-02-30T00:00:00Z` is the case that earns the check its keep: `new Date` reads it as
12
+ * `2026-03-02T00:00:00.000Z`, two days forward, and returns that instant without complaint. The
13
+ * runtime refuses month 13, hour 25 and a `+25:00` offset on its own, so the check agrees with it
14
+ * there and adds nothing.
15
+ * A `:60` second is well-formed: RFC 3339 sec. 5.7 permits `time-second` to be 60 at the end of a
16
+ * month in which a leap second occurs. Refusing it here is a decision, taken because JS cannot
17
+ * represent a leap second, so a caller that accepted one would hold either an invalid Date or an
18
+ * instant one second off; a typed error says so instead. Like the byte-reader / guard family, `parse`
19
+ * takes the caller's `(code, message) -> error` factory so every consumer keeps its own domain fault
20
+ * code.
14
21
  */
15
22
 
16
23
  function _digitsAt(v, start, count) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.26",
3
+ "version": "0.8.27",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -86,6 +86,6 @@
86
86
  "devDependencies": {
87
87
  "c8": "12.0.0",
88
88
  "esbuild": "0.28.2",
89
- "eslint": "10.10.0"
89
+ "eslint": "10.11.0"
90
90
  }
91
91
  }
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:cbd1318b-e7fc-430e-9a9c-ec7c99161ac6",
5
+ "serialNumber": "urn:uuid:0dc52d70-7641-471f-98fe-c06068d7838f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-28T05:44:25.737Z",
8
+ "timestamp": "2026-09-28T19:32:41.570Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.8.26",
22
+ "bom-ref": "@blamejs/pki@0.8.27",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.26",
25
+ "version": "0.8.27",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.8.26",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.27",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.8.26",
57
+ "ref": "@blamejs/pki@0.8.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]