@blamejs/pki 0.8.23 → 0.8.24

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,29 @@ 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.24 — 2026-09-24
8
+
9
+ A requirement this toolkit already refuses at parse is not one a profile owes a rule.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.certificate(der, { profile: "microsoft-root" })` runs the Microsoft Trusted Root Program profile. Naming it is the only way to reach these rows, and passing no profile leaves every one of them inert.
14
+ - Section 3.A.2 states the scope of the root rules outright, that a certificate added to the store is a self-signed root, so those rows read matching issuer and subject names together with the key identifiers. A CA reissued in its own name under a new key carries both names and was signed by its predecessor. The root rows read the key identifiers and pass over it, and the issuing CA rows read the names and pass over it too, because the bytes do not tell it from the succession certificate a root issues to its replacement. That is the reading `mozilla-root` and `chrome-root` already carry, so the three agree.
15
+ - The section 3.A.1.4 keyUsage rules over a root: the extension is required, it must be critical, and `keyCertSign` and `cRLSign` must both be set, each missing bit naming itself. Its remaining sentence conditions `digitalSignature` on the root key signing OCSP responses, which is what the private key is used for rather than anything the certificate carries. Section 3.A.1.1 requires a commonName; that it identifies the publisher and is unique are not settled by one certificate.
16
+ - The section 3.A.3 term of eight to 25 years, measured in months so a day-of-month with no counterpart clamps. The clause measures from the date of submission, which is at or after notBefore, so a term measured from notBefore is the longest the certificate can offer from any submission date: under eight years is under eight years from every one of them and is an error, while over 25 years from notBefore can still meet 25 years from a later submission and is a warning that says so.
17
+ - The section 3.A.5 requirement that an issuing CA certificate carry a distribution point or an authorityInfoAccess naming an OCSP responder at a URI. The accessMethod alone does not settle it, because an accessLocation admits any GeneralName, so an entry naming `id-ad-ocsp` beside a dNSName satisfies the extension's syntax and points at no responder. No URI scheme is asked for, since this clause names one only for the end entity half it does not require. A distribution point is left to its own forms, because a relative name or a cRLIssuer is a pointer the clause admits. Section 3.A.8's separation of uses goes with it. Its sentence that no issuing CA may combine server authentication with S/MIME, code signing or time stamping is an error, naming the purpose found beside it. Its other two sentences are broader, asking that all four uses be separated and that a separate intermediate serve each, so a pair they forbid and that sentence does not is a warning.
18
+ - The section 3.A.10 requirement that an end entity certificate declare one of the seventeen reserved policy identifiers the clause lists, all of which were already in the OID registry, and the section 3.A.14 rule that a certificate naming `id-kp-OCSPSigning` name no other extended key usage.
19
+ - The section 3.B signature and key table: a signature algorithm outside the RSA and ECDSA forms of SHA-256, SHA-384 and SHA-512, an RSA modulus under 2048 bits, an elliptic curve outside NIST P-256, P-384 and P-521, and a key of a family the table does not name. For code signing and time stamping use, which a certificate declares through its own extended key usage, the table adds that elliptic curve keys are not supported, that an RSA modulus above 4096 bits is not, and that a root needs at least 4096.
20
+ - The section 3.E.2 list of extended key usages the program enables on a root, reported as a warning naming the purpose. The document disagrees with itself here: that list omits `id-kp-codeSigning` while all of section 3.D governs code signing roots and states that all code signing certificates are treated equally. Reporting a code signing root would contradict that section, so the admitted set takes the reading that reports neither.
21
+
22
+ ### Changed
23
+
24
+ - `pki.lint.rules()` enumerates the new rows, which stay inert under the default rule set because each one asks whether the caller named the profile.
25
+
26
+ ### Fixed
27
+
28
+ - A CMC conformance vector for a `NoSignatureValue` that is not an OCTET STRING drew its value from a SHA-256 digest, and random bytes read as a complete OCTET STRING whenever the first two are `04 1E`, so about one run in 65536 the vector presented the very shape it asked the reader to refuse. It now uses an INTEGER, which is never an OCTET STRING. Nothing in the published tarball changes.
29
+
7
30
  ## v0.8.23 — 2026-09-24
8
31
 
9
32
  A root program's requirement is graded by the clause that states it, so the same key purpose is a warning in one place and an error in another.
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 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), `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 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), `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>` |
package/lib/lint.js CHANGED
@@ -2157,6 +2157,376 @@ var CHROME_RULES = [
2157
2157
  },
2158
2158
  ];
2159
2159
 
2160
+ /** @internal The seventeen policy identifiers sec. 3.A.10 lists, resolved through the registry
2161
+ * rather than written as OIDs so a name that stops resolving fails the round-trip gate in
2162
+ * oid.test.js instead of silently dropping out of the set. */
2163
+ var MS_RESERVED_POLICIES = ["domain-validated", "organization-validated", "ev-guidelines",
2164
+ "individual-validated", "code-signing",
2165
+ "smime-mailbox-legacy", "smime-mailbox-multipurpose", "smime-mailbox-strict",
2166
+ "smime-organization-legacy", "smime-organization-multipurpose", "smime-organization-strict",
2167
+ "smime-sponsor-legacy", "smime-sponsor-multipurpose", "smime-sponsor-strict",
2168
+ "smime-individual-legacy", "smime-individual-multipurpose", "smime-individual-strict",
2169
+ ].map(function (n) { return oid.byName(n); });
2170
+ /** @internal Sec. 3.E.2 lists the extended key usages the program enables on a root, and
2171
+ * id-kp-codeSigning is NOT among them. All of sec. 3.D governs "Code Signing Root Certificate
2172
+ * Requirements" and D.3 states that "all Code Signing certificates will be treated equally", so
2173
+ * reporting a code signing root would contradict that section. The document disagrees with itself
2174
+ * and the set takes the reading that reports neither, which the row's warn grade also reflects:
2175
+ * "will only enable" describes what the program turns on rather than a property of the bytes. */
2176
+ var MS_ENABLED_EKUS = ["serverAuth", "clientAuth", "emailProtection", "timeStamping",
2177
+ "ms-document-signing", "codeSigning",
2178
+ ].map(function (n) { return oid.byName(n); });
2179
+ /** @internal The uses sec. 3.A.8 requires an issuing CA to keep apart. */
2180
+ var MS_SEPARATE_USES = ["serverAuth", "emailProtection", "codeSigning", "timeStamping"]
2181
+ .map(function (n) { return oid.byName(n); });
2182
+ /** @internal The signature algorithms the sec. 3.B Digest Algorithms row admits, as the RSA and
2183
+ * ECDSA forms of the three digests it names. RSASSA-PSS carries its digest in the parameters and
2184
+ * is read through `_pssDigestName` instead. A membership test rather than a lookup with a
2185
+ * fallback: an algorithm outside the table is reported, which is what the table says. */
2186
+ var MS_SHA2_SIGNATURES = Object.create(null);
2187
+ ["sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption",
2188
+ "ecdsaWithSHA256", "ecdsaWithSHA384", "ecdsaWithSHA512",
2189
+ ].forEach(function (n) { var d = oid.byName(n); if (d) MS_SHA2_SIGNATURES[d] = n; });
2190
+ var MS_SHA2_DIGESTS = ["sha256", "sha384", "sha512"];
2191
+ var MS_RSA_MIN_BITS = 2048;
2192
+ var MS_CODE_SIGNING_ROOT_MIN_BITS = 4096;
2193
+ var MS_ROOT_MIN_MONTHS = 96;
2194
+ var MS_ROOT_MAX_MONTHS = 300;
2195
+
2196
+ function _isMicrosoft(cert, ctx) { return ctx.profile === "microsoft-root"; }
2197
+ /** @internal Sec. 3.A.2 names the scope outright: "Certificates to be added to the Trusted Root
2198
+ * Store MUST be self-signed root certificates." Matching names make a certificate self-ISSUED
2199
+ * rather than self-signed, so the key identifiers are read with them, exactly as `chrome-root`
2200
+ * reads them for the same reason. */
2201
+ function _isMicrosoftRoot(cert, ctx) {
2202
+ return _isMicrosoft(cert, ctx) && ctx.isCaCert && _sameName(cert.issuer, cert.subject) &&
2203
+ _selfSignedIdentifiers(cert, ctx);
2204
+ }
2205
+ /** @internal An ISSUING CA, which sec. 3.A.5 and 3.A.8 name. It reads the NAMES rather than the
2206
+ * negation of the root reading, so a CA reissued in its own name under a new key is passed over
2207
+ * here as well: the bytes do not tell that certificate from the one a root issues to its
2208
+ * replacement, which is not an issuing CA. That is the same under-report `mozilla-root` and
2209
+ * `chrome-root` carry, and the three agree. */
2210
+ function _isMicrosoftIssuingCa(cert, ctx) {
2211
+ return _isMicrosoft(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
2212
+ }
2213
+ function _isMicrosoftEndEntity(cert, ctx) { return _isMicrosoft(cert, ctx) && !ctx.isCaCert; }
2214
+ function _msPurposes(ctx) {
2215
+ var d = ctx.decode("extKeyUsage");
2216
+ return (d && Array.isArray(d.value)) ? d.value : null;
2217
+ }
2218
+ function _msNamesPurpose(ctx, name) {
2219
+ var purposes = _msPurposes(ctx);
2220
+ return !!purposes && purposes.indexOf(oid.byName(name)) !== -1;
2221
+ }
2222
+ /** @internal Code signing and time stamping use, declared the way `cabf-cs` reads it: through the
2223
+ * certificate's own extended key usage. */
2224
+ function _msIsCodeSigningUse(ctx) {
2225
+ return _msNamesPurpose(ctx, "codeSigning") || _msNamesPurpose(ctx, "timeStamping");
2226
+ }
2227
+ function _msKeyFamily(cert) {
2228
+ var spki = cert.subjectPublicKeyInfo;
2229
+ var name = spki && spki.algorithm && spki.algorithm.name;
2230
+ if (!name) return null;
2231
+ if (RSA_KEY_ALGS[name]) return "rsa";
2232
+ return name === "ecPublicKey" ? "ec" : null;
2233
+ }
2234
+ function _msSignatureIsSha2(cert) {
2235
+ var sigAlg = cert.signatureAlgorithm;
2236
+ if (!sigAlg) return false;
2237
+ if (sigAlg.oid === oid.byName("rsassaPss")) {
2238
+ return MS_SHA2_DIGESTS.indexOf(_pssDigestName(sigAlg)) !== -1;
2239
+ }
2240
+ return MS_SHA2_SIGNATURES[sigAlg.oid] !== undefined;
2241
+ }
2242
+ /** @internal Whether the certificate's own term reaches a ceiling stated in years, measured in
2243
+ * months so that a day-of-month with no counterpart clamps rather than rolling forward. */
2244
+ function _msTermReaches(cert, months) {
2245
+ var v = cert.validity;
2246
+ if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
2247
+ var mark = guard.time.addMonths(v.notBefore, months, _cfg, "lint/bad-input", "notBefore");
2248
+ return guard.time.instantOf(v.notAfter) >= guard.time.instantOf(mark);
2249
+ }
2250
+
2251
+ /** @internal The Microsoft Trusted Root Program profile, from the Program Requirements page.
2252
+ * Two of its clauses have no row, because this toolkit already refuses what they forbid and a row
2253
+ * would be a branch no input can drive. Sec. 3.A.1, "Root certificates must be x.509 v3
2254
+ * certificates": schema-x509 refuses every other version with `x509/bad-version`, which surfaces
2255
+ * as `lint/unparseable`. Sec. 3.A.13, an end entity's basicConstraints carrying a
2256
+ * pathLenConstraint: the shared decoder refuses it under RFC 5280 sec. 4.2.1.9, which surfaces as
2257
+ * `lint/rfc5280/extension-undecodable`. */
2258
+ var MICROSOFT_RULES = [
2259
+ {
2260
+ id: "lint/microsoft-root/root-common-name-missing", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.1.1",
2261
+ /** @internal That the commonName IDENTIFIES the publisher, and that it is UNIQUE, are not
2262
+ * settled by one certificate. That it is there is. */
2263
+ message: "a root certificate's subject must carry a commonName",
2264
+ appliesTo: _isMicrosoftRoot,
2265
+ check: function (cert) { return _subjectCNs(cert).length ? null : true; },
2266
+ },
2267
+ {
2268
+ id: "lint/microsoft-root/root-key-usage-missing", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.1.4",
2269
+ message: "a root certificate must carry a keyUsage extension",
2270
+ appliesTo: _isMicrosoftRoot,
2271
+ check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
2272
+ },
2273
+ {
2274
+ id: "lint/microsoft-root/root-key-usage-not-critical", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.1.4",
2275
+ message: "a root certificate's keyUsage extension must be marked critical",
2276
+ appliesTo: _isMicrosoftRoot,
2277
+ check: function (cert, ctx) {
2278
+ var e = ctx.raw("keyUsage");
2279
+ if (!e) return null;
2280
+ return e.critical === true ? null : true;
2281
+ },
2282
+ },
2283
+ {
2284
+ id: "lint/microsoft-root/root-key-usage-bits", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.1.4",
2285
+ /** @internal Two bits are named, so each missing one draws its own finding. The clause's next
2286
+ * sentence conditions digitalSignature on the root key signing OCSP responses, which is what
2287
+ * the private key is used for rather than anything the certificate carries. */
2288
+ message: "a root certificate's keyUsage must set both keyCertSign and cRLSign",
2289
+ appliesTo: _isMicrosoftRoot,
2290
+ check: function (cert, ctx) {
2291
+ var ku = ctx.decode("keyUsage");
2292
+ if (!ku || !ku.value) return null;
2293
+ var out = [];
2294
+ ["keyCertSign", "cRLSign"].forEach(function (bit) {
2295
+ if (ku.value[bit] !== true) out.push({ context: { bit: bit } });
2296
+ });
2297
+ return out.length ? out : null;
2298
+ },
2299
+ },
2300
+ {
2301
+ id: "lint/microsoft-root/root-validity-under-eight-years", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.3",
2302
+ /** @internal The clause measures from the date of SUBMISSION, which is at or after notBefore,
2303
+ * so the term measured from notBefore is the longest the certificate can offer from any
2304
+ * submission date. Under eight years there is under eight years from every one of them, which
2305
+ * is what makes this row sound on these bytes. */
2306
+ message: "a root certificate must be valid for at least eight years",
2307
+ appliesTo: _isMicrosoftRoot,
2308
+ check: function (cert) {
2309
+ var reaches = _msTermReaches(cert, MS_ROOT_MIN_MONTHS);
2310
+ return reaches === false ? { context: { minMonths: MS_ROOT_MIN_MONTHS } } : null;
2311
+ },
2312
+ },
2313
+ {
2314
+ id: "lint/microsoft-root/root-validity-over-25-years", severity: "warn", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.3",
2315
+ /** @internal The maximum runs the other way and is NOT settled by these bytes: a term of 30
2316
+ * years from notBefore still meets 25 years from submission if submission came five years on.
2317
+ * Reported below error, with the message naming what decides it. */
2318
+ message: "a root certificate's validity exceeds 25 years measured from its notBefore, which the clause measures from the date of submission",
2319
+ appliesTo: _isMicrosoftRoot,
2320
+ check: function (cert) {
2321
+ var v = cert.validity;
2322
+ if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
2323
+ var ceiling = guard.time.addMonths(v.notBefore, MS_ROOT_MAX_MONTHS, _cfg, "lint/bad-input", "notBefore");
2324
+ return guard.time.instantOf(v.notAfter) > guard.time.instantOf(ceiling)
2325
+ ? { context: { maxMonths: MS_ROOT_MAX_MONTHS } } : null;
2326
+ },
2327
+ },
2328
+ {
2329
+ id: "lint/microsoft-root/root-eku-not-enabled", severity: "warn", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.E.2",
2330
+ message: "a root certificate names an extended key usage outside the set this program enables",
2331
+ appliesTo: _isMicrosoftRoot,
2332
+ check: function (cert, ctx) {
2333
+ var purposes = _msPurposes(ctx);
2334
+ if (purposes === null) return null;
2335
+ var out = [];
2336
+ purposes.forEach(function (dotted) {
2337
+ if (MS_ENABLED_EKUS.indexOf(dotted) === -1) {
2338
+ out.push({ context: { purpose: oid.name(dotted) || dotted } });
2339
+ }
2340
+ });
2341
+ return out.length ? out : null;
2342
+ },
2343
+ },
2344
+ {
2345
+ id: "lint/microsoft-root/root-code-signing-rsa-under-4096", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2346
+ /** @internal The RSA cell of the code signing and time stamping column reads
2347
+ * "4096 (New roots only)", so the floor is scoped to a root rather than to every certificate
2348
+ * declaring that use. */
2349
+ message: "a root certificate for code signing or time stamping needs an RSA modulus of at least 4096 bits",
2350
+ appliesTo: function (cert, ctx) {
2351
+ return _isMicrosoftRoot(cert, ctx) && _msIsCodeSigningUse(ctx) && _msKeyFamily(cert) === "rsa";
2352
+ },
2353
+ check: function (cert) {
2354
+ var bits = _rsaModulusBits(cert.subjectPublicKeyInfo);
2355
+ if (bits === null) return { context: { rsaBits: null } };
2356
+ return bits < MS_CODE_SIGNING_ROOT_MIN_BITS ? { context: { rsaBits: bits } } : null;
2357
+ },
2358
+ },
2359
+ {
2360
+ id: "lint/microsoft-root/issuing-ca-no-revocation-pointer", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.5",
2361
+ /** @internal Whether the CRL or the responder answers is a fetch; that one of the two pointers
2362
+ * is in the certificate is bytes. The clause's second sentence is permissive about an end
2363
+ * entity and grades nothing.
2364
+ * "an AIA extension to an OCSP responder" is reached by a URL, and the shared decoder admits
2365
+ * any GeneralName in an accessLocation, so the accessMethod alone does not settle it: an entry
2366
+ * naming id-ad-ocsp beside a dNSName satisfies the extension's syntax and points at no
2367
+ * responder. No scheme is asked for here, because this clause names one only for the end entity
2368
+ * half it does not require, and the profiles that state a scheme table carry their own rows for
2369
+ * it. A distributionPoint is left to its own forms, since a relative name or a cRLIssuer is a
2370
+ * pointer the clause admits. */
2371
+ message: "an issuing CA certificate must carry a cRLDistributionPoints extension or an authorityInfoAccess naming an OCSP responder at a URI",
2372
+ appliesTo: _isMicrosoftIssuingCa,
2373
+ check: function (cert, ctx) {
2374
+ if (ctx.raw("cRLDistributionPoints")) return null;
2375
+ var aia = ctx.decode("authorityInfoAccess");
2376
+ if (!aia || !Array.isArray(aia.value)) return true;
2377
+ var ocsp = oid.byName("ocsp");
2378
+ for (var i = 0; i < aia.value.length; i++) {
2379
+ var ad = aia.value[i];
2380
+ if (ad.accessMethod !== ocsp) continue;
2381
+ var loc = ad.accessLocation;
2382
+ if (loc && loc.tag === 6 && typeof loc.value === "string" && loc.value.length) return null;
2383
+ }
2384
+ return true;
2385
+ },
2386
+ },
2387
+ {
2388
+ id: "lint/microsoft-root/issuing-ca-mixes-server-auth", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.8",
2389
+ /** @internal "a single Issuing CA must not combine server authentication with S/MIME, code
2390
+ * signing, or time stamping EKU", the sentence every reading of the clause agrees on. */
2391
+ message: "an issuing CA certificate must not name server authentication beside S/MIME, code signing or time stamping",
2392
+ appliesTo: _isMicrosoftIssuingCa,
2393
+ check: function (cert, ctx) {
2394
+ if (!_msNamesPurpose(ctx, "serverAuth")) return null;
2395
+ var out = [];
2396
+ ["emailProtection", "codeSigning", "timeStamping"].forEach(function (name) {
2397
+ if (_msNamesPurpose(ctx, name)) out.push({ context: { purpose: name } });
2398
+ });
2399
+ return out.length ? out : null;
2400
+ },
2401
+ },
2402
+ {
2403
+ id: "lint/microsoft-root/issuing-ca-mixes-uses", severity: "warn", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.8",
2404
+ /** @internal The clause's first and third sentences are broader than its second: "must separate
2405
+ * Server Authentication, S/MIME, Code Signing, and Time Stamping uses" and "A separate
2406
+ * intermediate must be used for each use case" forbid a pair the second does not name. Both
2407
+ * readings are in the clause, so the pair only two sentences forbid is graded below the pair
2408
+ * all three do, and the row above answers for that one. */
2409
+ message: "an issuing CA certificate names two of the uses this clause asks a separate intermediate for",
2410
+ appliesTo: _isMicrosoftIssuingCa,
2411
+ check: function (cert, ctx) {
2412
+ if (_msNamesPurpose(ctx, "serverAuth")) return null;
2413
+ var purposes = _msPurposes(ctx);
2414
+ if (purposes === null) return null;
2415
+ var named = MS_SEPARATE_USES.filter(function (dotted) {
2416
+ return purposes.indexOf(dotted) !== -1;
2417
+ });
2418
+ return named.length > 1
2419
+ ? { context: { purposes: named.map(function (d) { return oid.name(d) || d; }) } } : null;
2420
+ },
2421
+ },
2422
+ {
2423
+ id: "lint/microsoft-root/end-entity-reserved-policy-missing", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.10",
2424
+ message: "an end entity certificate must declare one of the reserved policy identifiers this clause lists",
2425
+ appliesTo: _isMicrosoftEndEntity,
2426
+ check: function (cert, ctx) {
2427
+ var d = ctx.decode("certificatePolicies");
2428
+ if (!d || !Array.isArray(d.value)) return true;
2429
+ for (var i = 0; i < d.value.length; i++) {
2430
+ if (MS_RESERVED_POLICIES.indexOf(d.value[i].policyIdentifier) !== -1) return null;
2431
+ }
2432
+ return true;
2433
+ },
2434
+ },
2435
+ {
2436
+ id: "lint/microsoft-root/ocsp-responder-extra-eku", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.A.14",
2437
+ /** @internal "the only EKU allowed is OCSP Signing", so the row reads exclusivity and each
2438
+ * other purpose draws its own finding. */
2439
+ message: "a certificate naming id-kp-OCSPSigning must name no other extended key usage",
2440
+ appliesTo: function (cert, ctx) {
2441
+ return _isMicrosoft(cert, ctx) && _msNamesPurpose(ctx, "ocspSigning");
2442
+ },
2443
+ check: function (cert, ctx) {
2444
+ var purposes = _msPurposes(ctx);
2445
+ var signing = oid.byName("ocspSigning");
2446
+ var out = [];
2447
+ purposes.forEach(function (dotted) {
2448
+ if (dotted !== signing) out.push({ context: { purpose: oid.name(dotted) || dotted } });
2449
+ });
2450
+ return out.length ? out : null;
2451
+ },
2452
+ },
2453
+ {
2454
+ id: "lint/microsoft-root/signature-digest-not-sha2", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2455
+ /** @internal The Digest Algorithms row gives SHA-256, SHA-384 and SHA-512 for every use, and
2456
+ * the two rows beside it give RSA and ECDSA, so an algorithm outside those combinations is
2457
+ * outside the table rather than in some unstated part of it. */
2458
+ message: "the signature algorithm is not one the signature table admits",
2459
+ appliesTo: _isMicrosoft,
2460
+ check: function (cert) {
2461
+ if (_msSignatureIsSha2(cert)) return null;
2462
+ var sigAlg = cert.signatureAlgorithm;
2463
+ return { context: { algorithm: (sigAlg && sigAlg.name) || (sigAlg && sigAlg.oid) || null } };
2464
+ },
2465
+ },
2466
+ {
2467
+ id: "lint/microsoft-root/rsa-modulus-under-2048", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2468
+ message: "an RSA modulus must be at least 2048 bits",
2469
+ appliesTo: function (cert, ctx) {
2470
+ return _isMicrosoft(cert, ctx) && _msKeyFamily(cert) === "rsa";
2471
+ },
2472
+ check: function (cert) {
2473
+ var bits = _rsaModulusBits(cert.subjectPublicKeyInfo);
2474
+ if (bits === null) return { context: { rsaBits: null } };
2475
+ return bits < MS_RSA_MIN_BITS ? { context: { rsaBits: bits } } : null;
2476
+ },
2477
+ },
2478
+ {
2479
+ id: "lint/microsoft-root/ec-curve-not-permitted", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2480
+ message: "an elliptic curve key must be on NIST P-256, P-384 or P-521",
2481
+ appliesTo: function (cert, ctx) {
2482
+ return _isMicrosoft(cert, ctx) && _msKeyFamily(cert) === "ec";
2483
+ },
2484
+ check: function (cert) {
2485
+ var curve = _ecCurveName(cert.subjectPublicKeyInfo);
2486
+ return APPROVED_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
2487
+ },
2488
+ },
2489
+ {
2490
+ id: "lint/microsoft-root/key-algorithm-not-in-table", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2491
+ /** @internal The table gives one RSA row and one ECC row and no others, so a key outside both
2492
+ * families is outside the table. The two rows above grade a key inside one of them. */
2493
+ message: "the subject public key is of a family the signature table does not name",
2494
+ appliesTo: _isMicrosoft,
2495
+ check: function (cert) {
2496
+ if (_msKeyFamily(cert) !== null) return null;
2497
+ var spki = cert.subjectPublicKeyInfo;
2498
+ var name = spki && spki.algorithm && spki.algorithm.name;
2499
+ return { context: { algorithm: name || null } };
2500
+ },
2501
+ },
2502
+ {
2503
+ id: "lint/microsoft-root/code-signing-ecc", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2504
+ /** @internal The ECC cell of the code signing and time stamping column reads "Not Supported",
2505
+ * and the note under the table says "Code Signing does not support ECC or keys > 4096". */
2506
+ message: "an elliptic curve key is not supported for code signing or time stamping use",
2507
+ appliesTo: function (cert, ctx) {
2508
+ return _isMicrosoft(cert, ctx) && _msIsCodeSigningUse(ctx) && _msKeyFamily(cert) === "ec";
2509
+ },
2510
+ check: function (cert) {
2511
+ return { context: { curve: _ecCurveName(cert.subjectPublicKeyInfo) } };
2512
+ },
2513
+ },
2514
+ {
2515
+ id: "lint/microsoft-root/code-signing-rsa-over-4096", severity: "error", source: "microsoft-root", citation: "Microsoft Trusted Root Program 3.B",
2516
+ /** @internal "Code Signing does not support ECC or keys > 4096" states a MAXIMUM, which the
2517
+ * rest of this table and the other root-program profiles do not. */
2518
+ message: "an RSA modulus above 4096 bits is not supported for code signing or time stamping use",
2519
+ appliesTo: function (cert, ctx) {
2520
+ return _isMicrosoft(cert, ctx) && _msIsCodeSigningUse(ctx) && _msKeyFamily(cert) === "rsa";
2521
+ },
2522
+ check: function (cert) {
2523
+ var bits = _rsaModulusBits(cert.subjectPublicKeyInfo);
2524
+ if (bits === null) return null;
2525
+ return bits > MS_CODE_SIGNING_ROOT_MIN_BITS ? { context: { rsaBits: bits } } : null;
2526
+ },
2527
+ },
2528
+ ];
2529
+
2160
2530
  /** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
2161
2531
  * "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
2162
2532
  * is admitted and is not one of the purposes being counted. */
@@ -4599,12 +4969,13 @@ var PROFILES = Object.assign(Object.create(null), {
4599
4969
  "etsi-qc": ETSI_QC_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4600
4970
  "mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4601
4971
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4972
+ "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4602
4973
  });
4603
4974
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
4604
4975
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
4605
4976
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
4606
4977
  * inert under the default set because each one asks whether the caller named the profile. */
4607
- .concat(MOZILLA_RULES).concat(CHROME_RULES);
4978
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES);
4608
4979
 
4609
4980
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
4610
4981
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
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",
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:3b5edc83-2243-4232-883c-f9c904da9bbb",
5
+ "serialNumber": "urn:uuid:cdbd209f-fdc3-4d13-8b62-96f8d9222905",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T15:20:10.358Z",
8
+ "timestamp": "2026-09-24T16:24:07.579Z",
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.23",
22
+ "bom-ref": "@blamejs/pki@0.8.24",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.23",
25
+ "version": "0.8.24",
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.23",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.24",
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.23",
57
+ "ref": "@blamejs/pki@0.8.24",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]