@blamejs/pki 0.8.21 → 0.8.23
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 +36 -0
- package/README.md +1 -1
- package/lib/lint.js +561 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ 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.23 — 2026-09-24
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.lint.certificate(der, { profile: "chrome-root" })` runs the Chrome Root Program Policy v1.8 profile. Naming it is the only way to reach these rows, and passing no profile leaves every one of them inert.
|
|
14
|
+
- The section 1.3.2 rules for a subordinate CA certificate, which the policy states as two admitted forms, `id-kp-serverAuth` alone or `id-kp-serverAuth` with `id-kp-clientAuth`. Both forms require the extKeyUsage extension and require `id-kp-serverAuth` in it, and neither admits any other purpose, so those three are errors and `anyExtendedKeyUsage` is reported among the others. Each offending purpose draws its own finding naming that purpose. The second form is open only where the certificate was disclosed to the CCADB before 15 June 2026, which a certificate does not carry, so `id-kp-clientAuth` is reported as a warning whose message names the date that settles it.
|
|
15
|
+
- Those four rows reach the unexpired subordinate CA certificates the clause names, so a certificate whose notAfter precedes 15 June 2026 is passed over: the phase-out begins that day and the certificate was not among the unexpired ones then. They do reach a branch serving another purpose, which is the point of the section rather than an overreach, since it governs every subordinate CA certificate beneath a root in the store and exists to phase out the multi-purpose hierarchies. Whether a certificate is revoked is not in the bytes and stays outside the reading.
|
|
16
|
+
- The same section's rule for a subscriber certificate, which admits `id-kp-serverAuth` alone and is gated on issuance rather than disclosure. The three rows are silent on a certificate whose notBefore precedes 15 March 2027 and report on the same bytes after it, and `id-kp-clientAuth` is an error there rather than the warning it draws on a subordinate CA. A subscriber certificate is not every end entity: a delegated OCSP responder has its own profile in the Baseline Requirements that section 1.1.1 binds, asking for `id-kp-OCSPSigning` and no other purpose, so a certificate whose extKeyUsage names that purpose and nothing else is passed over. One naming it beside others is not a responder under that profile and is read here as the subscriber certificate it is.
|
|
17
|
+
- The section 1.3.1.3 ceiling of three years on a subordinate CA certificate's validity period, reported as a warning at the strength the clause states it. A ceiling given in years is measured in months, so a 29 February notBefore clamps to 28 February rather than moving into March.
|
|
18
|
+
- The section 1.3.1.2 root CA term limit. The clause determines the age of key material from the earliest certificate that carries the key, and a self-signed root's own notBefore is at or after that date. The schedule is monotone, so the removal date computed from notBefore is the latest one the schedule can give, and a notAfter past it runs beyond the term whatever earlier certificate exists. The finding names a self-signed CA certificate rather than a root, which is as far as the bytes reach: a CA outside any store issues itself one of these during its own key rollover and it matches a root in every field. What the finding says is that if this certificate is a root in the store the caller named, its term ends before its validity does, and the notice grade is that conditional. It carries the removal date.
|
|
19
|
+
- Matching issuer and subject names make a certificate self-issued, which is not the same as self-signed: a CA reissued in its own name under a new key carries the same two names and was signed by its predecessor. The term limit governs a root, so it reads the key identifiers as well, and passes over a certificate whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier. A certificate that omits the extension, or names its own key in it, is held to the clause. A keyIdentifier with no readable subjectKeyIdentifier beside it settles nothing, and the row rests on settling it, so that reads as not a root too. What remains outside that reading is a reissued CA carrying no authorityKeyIdentifier keyIdentifier at all, which RFC 5280 section 4.2.1.1 permits only in a self-signed certificate and which the `rfc5280` profile reports on its own row.
|
|
20
|
+
- A subordinate CA is a role in a hierarchy rather than something a certificate states, so the rows that govern one read the certificate's own issuer and subject names and pass over a certificate issued in its own name. Section 1.3.1.1 requires a cross-certificate from a root to its replacement, which carries one name in both fields, so those rows under-report rather than reporting a succession certificate for the extended key usage a root does not need.
|
|
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
|
+
## v0.8.22 — 2026-09-24
|
|
27
|
+
|
|
28
|
+
An algorithm identifier is compared as the bytes a root program fixed, not as the algorithm it names.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `pki.lint.certificate(der, { profile: "mozilla-root" })` runs the Mozilla Root Store Policy v3.1 profile. Naming it is the only way to reach these rows, and passing no profile leaves every one of them inert.
|
|
33
|
+
- The section 5.1 algorithm encodings, compared as DER: a subjectPublicKeyInfo AlgorithmIdentifier must be one of the four the policy writes out, for RSA and for NIST P-256, P-384 and P-521, and a signature AlgorithmIdentifier one of the ten, covering RSASSA-PKCS1-v1_5, RSASSA-PSS at its matching mask and salt lengths, and ECDSA. Only one of a certificate's two algorithm fields is read, because the parser already refuses a certificate whose outer `signatureAlgorithm` differs from `tbsCertificate.signature`.
|
|
34
|
+
- An RSA modulus must be at least 2048 bits and a whole number of octets, and a public exponent of 1 is reported as the unusable key the policy names. An EdDSA key is admitted only in a certificate carrying `id-kp-emailProtection`, and is exempt from the encoding rows, since the policy gives no EdDSA encoding to hold it to. An EdDSA signature is exempt for the same reason and on the issuer's behalf: a permitted EdDSA intermediate signs with that key, and reporting the certificates it signs would fail them for a key they do not carry.
|
|
35
|
+
- The section 5.1.3 SHA-1 prohibitions, each gated to the date it carries: from 1 July 2022 over an end entity certificate carrying `id-kp-emailProtection`, and from 1 July 2023 over a certificate carrying `id-kp-OCSPSigning` or over an intermediate certificate. That clause names intermediates rather than certificate authorities, so a root's own self-signature is not one of the four things it lists. The clause also allows SHA-1 over an end entity only where a list of conditions all hold, so a certificate carrying no extKeyUsage, or one naming `id-kp-serverAuth` or `anyExtendedKeyUsage`, is reported as outside that allowance. Its remaining conditions name the issuing certificate and the Baseline Requirements scope, which are not in these bytes.
|
|
36
|
+
- The section 5.2 practices a certificate's own bytes answer for: an extKeyUsage extension on every end entity certificate with no `anyExtendedKeyUsage` in it, and a serial number greater than zero. The rest of that sentence asks for at least 64 bits of CSPRNG output, which constrains the draw rather than the integer, and a DER INTEGER drops the leading zero octets a draw produces: eight random octets beginning with zero encode to seven of magnitude, which a conforming certificate does about once in 256. So a short magnitude is reported as a warning and the positive-serial half as an error, each at the strength its own clause supports.
|
|
37
|
+
- The section 5.3 rules for intermediate certificates created after 1 January 2019, gated to that date: an extKeyUsage extension is required, `anyExtendedKeyUsage` is not permitted, and `id-kp-serverAuth` and `id-kp-emailProtection` may not appear together. Being an intermediate is a role in a hierarchy that a certificate does not state, so these three read the one thing the bytes give and pass over a certificate issued in its own name. That is every root, which the clause does not govern, and it is also a rollover intermediate reissued in its own name, which the clause does govern, so the three under-report rather than reporting a root for carrying no extended key usage.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `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.
|
|
42
|
+
|
|
7
43
|
## v0.8.21 — 2026-09-24
|
|
8
44
|
|
|
9
45
|
A qualified certificate is linted against the statements it carries, not just their syntax.
|
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 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 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
|
@@ -272,6 +272,17 @@ function _rsaModulusBits(spki) {
|
|
|
272
272
|
return mod.toString(2).length;
|
|
273
273
|
} catch (_e) { return null; }
|
|
274
274
|
}
|
|
275
|
+
/** @internal The RSA public exponent, read from the same RSAPublicKey the modulus comes from, or
|
|
276
|
+
* null where the key does not decode as one. */
|
|
277
|
+
function _rsaExponent(spki) {
|
|
278
|
+
try {
|
|
279
|
+
var pk = spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
|
|
280
|
+
if (!Buffer.isBuffer(pk)) return null;
|
|
281
|
+
var seq = asn1.decode(pk);
|
|
282
|
+
if (!seq.children || seq.children.length !== 2) return null;
|
|
283
|
+
return asn1.read.integer(seq.children[1]);
|
|
284
|
+
} catch (_e) { return null; }
|
|
285
|
+
}
|
|
275
286
|
var RSA_KEY_ALGS = Object.assign(Object.create(null), { rsaEncryption: 1, rsassaPss: 1, rsaesOaep: 1 });
|
|
276
287
|
var APPROVED_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
|
|
277
288
|
function _ecCurveName(spki) {
|
|
@@ -1602,6 +1613,550 @@ var CABF_SMIME_RULES = [
|
|
|
1602
1613
|
},
|
|
1603
1614
|
];
|
|
1604
1615
|
|
|
1616
|
+
/** @internal Mozilla Root Store Policy v3.1 sec. 5.1.1 and 5.1.2 give the permitted
|
|
1617
|
+
* AlgorithmIdentifier encodings as hex-encoded BYTES, so these are compared as DER rather than as
|
|
1618
|
+
* a decoded triple: the point of the clause is the encoding, and two AlgorithmIdentifiers naming
|
|
1619
|
+
* the same algorithm can differ in whether the parameters are an explicit NULL or absent. */
|
|
1620
|
+
var MOZILLA_SPKI_ALGORITHMS = [
|
|
1621
|
+
"300d06092a864886f70d0101010500",
|
|
1622
|
+
"301306072a8648ce3d020106082a8648ce3d030107",
|
|
1623
|
+
"301006072a8648ce3d020106052b81040022",
|
|
1624
|
+
"301006072a8648ce3d020106052b81040023",
|
|
1625
|
+
];
|
|
1626
|
+
var MOZILLA_SIGNATURE_ALGORITHMS = [
|
|
1627
|
+
"300d06092a864886f70d0101050500",
|
|
1628
|
+
"300d06092a864886f70d01010b0500",
|
|
1629
|
+
"300d06092a864886f70d01010c0500",
|
|
1630
|
+
"300d06092a864886f70d01010d0500",
|
|
1631
|
+
"304106092a864886f70d01010a3034a00f300d060960864801650304020105" +
|
|
1632
|
+
"00a11c301a06092a864886f70d010108300d06096086480165030402010500a203020120",
|
|
1633
|
+
"304106092a864886f70d01010a3034a00f300d060960864801650304020205" +
|
|
1634
|
+
"00a11c301a06092a864886f70d010108300d06096086480165030402020500a203020130",
|
|
1635
|
+
"304106092a864886f70d01010a3034a00f300d060960864801650304020305" +
|
|
1636
|
+
"00a11c301a06092a864886f70d010108300d06096086480165030402030500a203020140",
|
|
1637
|
+
"300a06082a8648ce3d040302",
|
|
1638
|
+
"300a06082a8648ce3d040303",
|
|
1639
|
+
"300a06082a8648ce3d040304",
|
|
1640
|
+
];
|
|
1641
|
+
/** @internal The RSASSA-PKCS1-v1_5 with SHA-1 encoding, which sec. 5.1.1 admits and sec. 5.1.3
|
|
1642
|
+
* then restricts by date and by key purpose. */
|
|
1643
|
+
var MOZILLA_SHA1_RSA = "300d06092a864886f70d0101050500";
|
|
1644
|
+
var MOZILLA_SHA1_ECDSA = "300906072a8648ce3d0401";
|
|
1645
|
+
var MOZILLA_SHA1_EMAIL_FROM = new Date("2022-07-01T00:00:00Z");
|
|
1646
|
+
var MOZILLA_SHA1_OCSP_CA_FROM = new Date("2023-07-01T00:00:00Z");
|
|
1647
|
+
/** @internal "Intermediate certificates created after January 1, 2019" (sec. 5.3). */
|
|
1648
|
+
var MOZILLA_INTERMEDIATE_EKU_FROM = new Date("2019-01-01T00:00:00Z");
|
|
1649
|
+
/** @internal "at least 64 bits of output from a CSPRNG" in the serial (sec. 5.2), of which the
|
|
1650
|
+
* eight octets are what a certificate's bytes can answer for. */
|
|
1651
|
+
var MOZILLA_SERIAL_MIN_OCTETS = 8;
|
|
1652
|
+
var MOZILLA_RSA_MIN_BITS = 2048;
|
|
1653
|
+
|
|
1654
|
+
/** @internal The SubjectPublicKeyInfo's AlgorithmIdentifier as DER hex. The parsed record carries
|
|
1655
|
+
* the decoded triple, and this clause is about the encoding, so the bytes are read back out. */
|
|
1656
|
+
function _mozillaSpkiAlgorithmHex(cert) {
|
|
1657
|
+
try {
|
|
1658
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
1659
|
+
if (!spki || !Buffer.isBuffer(spki.bytes)) return null;
|
|
1660
|
+
var node = asn1.decode(spki.bytes);
|
|
1661
|
+
if (!node.children || !node.children.length) return null;
|
|
1662
|
+
return node.children[0].bytes.toString("hex");
|
|
1663
|
+
} catch (_e) { return null; }
|
|
1664
|
+
}
|
|
1665
|
+
/** @internal The signature AlgorithmIdentifier as DER hex, read from the TBSCertificate. Only one
|
|
1666
|
+
* of the two algorithm fields needs reading: schema-x509 refuses a certificate whose outer
|
|
1667
|
+
* signatureAlgorithm differs byte-for-byte from tbsCertificate.signature, so by the time a rule
|
|
1668
|
+
* sees one they cannot disagree. */
|
|
1669
|
+
function _mozillaSignatureAlgorithmHex(cert) {
|
|
1670
|
+
// allow:swallow-unverified the ingest door has already produced a parsed certificate, so this reads bytes the parser accepted and a fault answers null
|
|
1671
|
+
try {
|
|
1672
|
+
var kids = asn1.decode(cert.tbsBytes).children;
|
|
1673
|
+
if (!kids || kids.length < 2) return null;
|
|
1674
|
+
var idx = (kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 2 : 1;
|
|
1675
|
+
return kids[idx] ? kids[idx].bytes.toString("hex") : null;
|
|
1676
|
+
} catch (_e) { return null; }
|
|
1677
|
+
}
|
|
1678
|
+
function _mozillaIsEdDsa(cert) {
|
|
1679
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
1680
|
+
var name = spki && spki.algorithm && spki.algorithm.name;
|
|
1681
|
+
return name === "Ed25519" || name === "Ed448";
|
|
1682
|
+
}
|
|
1683
|
+
function _mozillaIsSha1Signature(cert) {
|
|
1684
|
+
var hex = _mozillaSignatureAlgorithmHex(cert);
|
|
1685
|
+
return hex === MOZILLA_SHA1_RSA || hex === MOZILLA_SHA1_ECDSA;
|
|
1686
|
+
}
|
|
1687
|
+
/** @internal The profile is NAMED, never detected. Every other profile here is selected by
|
|
1688
|
+
* something the certificate declares; a root program's policy applies because the certificate
|
|
1689
|
+
* chains to a root in that store, which the certificate does not state and a single-certificate
|
|
1690
|
+
* lint cannot know. */
|
|
1691
|
+
function _isMozilla(cert, ctx) { return ctx.profile === "mozilla-root"; }
|
|
1692
|
+
/** @internal An INTERMEDIATE, which is what sec. 5.3 governs. Being an intermediate is a role in a
|
|
1693
|
+
* hierarchy and a certificate does not state it, so this reads the one thing the bytes give: a
|
|
1694
|
+
* certificate issued in its own name is passed over. That is every root, which the clause does not
|
|
1695
|
+
* govern, and it is also a self-issued rollover intermediate, which the clause DOES govern, so the
|
|
1696
|
+
* rows under-report rather than reporting a root for carrying no extended key usage. The
|
|
1697
|
+
* cross-certificate the clause exempts need not carry matching names, which is why each message
|
|
1698
|
+
* names that exemption for the operator to apply. */
|
|
1699
|
+
function _isMozillaIntermediate(cert, ctx) {
|
|
1700
|
+
return _isMozilla(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
|
|
1701
|
+
}
|
|
1702
|
+
function _isMozillaEndEntity(cert, ctx) { return _isMozilla(cert, ctx) && !ctx.isCaCert; }
|
|
1703
|
+
|
|
1704
|
+
/** @internal The Mozilla Root Store Policy v3.1 profile. */
|
|
1705
|
+
var MOZILLA_RULES = [
|
|
1706
|
+
{
|
|
1707
|
+
id: "lint/mozilla-root/spki-algorithm-encoding", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.1 and 5.1.2",
|
|
1708
|
+
/** @internal An EdDSA key is exempt here and answered by its own row: sec. 5.1 admits one in a
|
|
1709
|
+
* certificate carrying id-kp-emailProtection and gives no EdDSA encoding, so holding it to
|
|
1710
|
+
* this list would report every conforming certificate of that kind. */
|
|
1711
|
+
message: "the subjectPublicKeyInfo AlgorithmIdentifier is not one of the encodings this policy names",
|
|
1712
|
+
appliesTo: function (cert, ctx) { return _isMozilla(cert, ctx) && !_mozillaIsEdDsa(cert); },
|
|
1713
|
+
check: function (cert) {
|
|
1714
|
+
var hex = _mozillaSpkiAlgorithmHex(cert);
|
|
1715
|
+
return MOZILLA_SPKI_ALGORITHMS.indexOf(hex) === -1 ? { context: { algorithmIdentifier: hex } } : null;
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
id: "lint/mozilla-root/eddsa-without-email-protection", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1",
|
|
1720
|
+
/** @internal "EdDSA keys MAY be included in certificates that chain to a root certificate in
|
|
1721
|
+
* our root store if the certificate contains id-kp-emailProtection in the EKU extension.
|
|
1722
|
+
* Otherwise, EdDSA keys MUST NOT be included." */
|
|
1723
|
+
message: "an EdDSA key needs id-kp-emailProtection in the extKeyUsage extension",
|
|
1724
|
+
appliesTo: function (cert, ctx) { return _isMozilla(cert, ctx) && _mozillaIsEdDsa(cert); },
|
|
1725
|
+
check: function (cert, ctx) {
|
|
1726
|
+
var d = ctx.decode("extKeyUsage");
|
|
1727
|
+
if (!d || !Array.isArray(d.value)) return true;
|
|
1728
|
+
return d.value.indexOf(oid.byName("emailProtection")) === -1 ? true : null;
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
id: "lint/mozilla-root/signature-algorithm-encoding", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.1 and 5.1.2",
|
|
1733
|
+
/** @internal Membership of the ten, and nothing more: sec. 5.1.2 binds a curve to a digest for
|
|
1734
|
+
* the SIGNING key, which is the issuer's and is not in this certificate. Matching
|
|
1735
|
+
* distinguished names would not prove the subject key made the signature, which is the
|
|
1736
|
+
* cross-certificate shape sec. 5.2 calls out.
|
|
1737
|
+
* An EdDSA signature is outside these clauses rather than in breach of them: sec. 5.1.1 and
|
|
1738
|
+
* sec. 5.1.2 fix the encodings for a signing RSA key and a signing ECDSA key, and sec. 5.1
|
|
1739
|
+
* admits an EdDSA key in a certificate carrying id-kp-emailProtection, which can be an
|
|
1740
|
+
* intermediate and sign with it. Reporting the certificates it signs would fail them for the
|
|
1741
|
+
* issuer's key. */
|
|
1742
|
+
message: "the signature AlgorithmIdentifier is not one of the encodings this policy names",
|
|
1743
|
+
appliesTo: function (cert, ctx) {
|
|
1744
|
+
var name = cert.signatureAlgorithm && cert.signatureAlgorithm.name;
|
|
1745
|
+
return _isMozilla(cert, ctx) && name !== "Ed25519" && name !== "Ed448";
|
|
1746
|
+
},
|
|
1747
|
+
check: function (cert) {
|
|
1748
|
+
var hex = _mozillaSignatureAlgorithmHex(cert);
|
|
1749
|
+
return MOZILLA_SIGNATURE_ALGORITHMS.indexOf(hex) === -1 ? { context: { algorithmIdentifier: hex } } : null;
|
|
1750
|
+
},
|
|
1751
|
+
},
|
|
1752
|
+
{
|
|
1753
|
+
id: "lint/mozilla-root/rsa-modulus-invalid", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1",
|
|
1754
|
+
/** @internal "RSA keys whose modulus size in bits is divisible by 8, and is at least 2048
|
|
1755
|
+
* bits". The AlgorithmIdentifier row above cannot answer for this, since the modulus lives in
|
|
1756
|
+
* the subjectPublicKey rather than in the algorithm field. */
|
|
1757
|
+
message: "an RSA modulus must be at least 2048 bits and a whole number of octets",
|
|
1758
|
+
appliesTo: function (cert, ctx) {
|
|
1759
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
1760
|
+
return _isMozilla(cert, ctx) && !!(spki && spki.algorithm && RSA_KEY_ALGS[spki.algorithm.name]);
|
|
1761
|
+
},
|
|
1762
|
+
check: function (cert) {
|
|
1763
|
+
var bits = _rsaModulusBits(cert.subjectPublicKeyInfo);
|
|
1764
|
+
if (bits === null) return { context: { rsaBits: null } };
|
|
1765
|
+
return (bits < MOZILLA_RSA_MIN_BITS || bits % 8 !== 0) ? { context: { rsaBits: bits } } : null;
|
|
1766
|
+
},
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
id: "lint/mozilla-root/rsa-exponent-one", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
|
|
1770
|
+
/** @internal "invalid public keys (e.g., RSA certificates with public exponent equal to 1)",
|
|
1771
|
+
* the one example the clause gives and the one a certificate's bytes settle. */
|
|
1772
|
+
message: "an RSA public exponent of 1 is not a usable key",
|
|
1773
|
+
appliesTo: function (cert, ctx) {
|
|
1774
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
1775
|
+
return _isMozilla(cert, ctx) && !!(spki && spki.algorithm && RSA_KEY_ALGS[spki.algorithm.name]);
|
|
1776
|
+
},
|
|
1777
|
+
check: function (cert) {
|
|
1778
|
+
var e = _rsaExponent(cert.subjectPublicKeyInfo);
|
|
1779
|
+
return e !== null && e <= 1n ? { context: { exponent: e.toString() } } : null;
|
|
1780
|
+
},
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
id: "lint/mozilla-root/serial-number-not-positive", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
|
|
1784
|
+
/** @internal "all new certificates MUST have a serial number greater than zero", the half of
|
|
1785
|
+
* the sentence a certificate's bytes settle outright. */
|
|
1786
|
+
message: "a serial number must be greater than zero",
|
|
1787
|
+
appliesTo: _isMozilla,
|
|
1788
|
+
check: function (cert) {
|
|
1789
|
+
if (typeof cert.serialNumber !== "bigint") return null;
|
|
1790
|
+
return cert.serialNumber > 0n ? null : { context: { serialNumber: cert.serialNumber.toString() } };
|
|
1791
|
+
},
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
id: "lint/mozilla-root/serial-number-short", severity: "warn", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
|
|
1795
|
+
/** @internal "containing at least 64 bits of output from a CSPRNG" constrains the DRAW, and a
|
|
1796
|
+
* DER INTEGER does not carry the leading zero octets a draw can produce: eight random octets
|
|
1797
|
+
* whose first is zero encode to seven of magnitude, which happens to a conforming certificate
|
|
1798
|
+
* about once in 256. So the magnitude is evidence rather than a verdict and the row is graded
|
|
1799
|
+
* below the sentence's other half, which is its own row above. */
|
|
1800
|
+
message: "a serial number carries fewer than eight octets of magnitude, which 64 bits of CSPRNG output rarely produces",
|
|
1801
|
+
appliesTo: _isMozilla,
|
|
1802
|
+
check: function (cert) {
|
|
1803
|
+
var octets = _serialOctets(cert);
|
|
1804
|
+
return octets < MOZILLA_SERIAL_MIN_OCTETS ? { context: { octets: octets } } : null;
|
|
1805
|
+
},
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
id: "lint/mozilla-root/end-entity-eku-missing", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
|
|
1809
|
+
message: "an end entity certificate must carry an extKeyUsage extension",
|
|
1810
|
+
appliesTo: _isMozillaEndEntity,
|
|
1811
|
+
check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
id: "lint/mozilla-root/end-entity-eku-any", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.2",
|
|
1815
|
+
message: "an end entity certificate's extKeyUsage must not name anyExtendedKeyUsage",
|
|
1816
|
+
appliesTo: _isMozillaEndEntity,
|
|
1817
|
+
check: function (cert, ctx) {
|
|
1818
|
+
var d = ctx.decode("extKeyUsage");
|
|
1819
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1820
|
+
return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
|
|
1821
|
+
},
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
id: "lint/mozilla-root/sha1-email-protection", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
|
|
1825
|
+
/** @internal "Effective July 1, 2022, CAs SHALL NOT sign SHA-1 hashes over end entity
|
|
1826
|
+
* certificates with an EKU extension containing the id-kp-emailProtection key purpose." */
|
|
1827
|
+
message: "a SHA-1 signature over an end entity certificate carrying id-kp-emailProtection",
|
|
1828
|
+
appliesTo: _isMozillaEndEntity,
|
|
1829
|
+
effectiveDate: MOZILLA_SHA1_EMAIL_FROM,
|
|
1830
|
+
check: function (cert, ctx) {
|
|
1831
|
+
if (!_mozillaIsSha1Signature(cert)) return null;
|
|
1832
|
+
var d = ctx.decode("extKeyUsage");
|
|
1833
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1834
|
+
return d.value.indexOf(oid.byName("emailProtection")) !== -1 ? true : null;
|
|
1835
|
+
},
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
id: "lint/mozilla-root/sha1-end-entity-outside-allowance", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
|
|
1839
|
+
/** @internal SHA-1 over an end entity is permitted "only if all the following are true", and
|
|
1840
|
+
* the conditions this certificate answers for are that it "contains an EKU extension that does
|
|
1841
|
+
* not contain the id-kp-serverAuth, id-kp-emailProtection, or anyExtendedKeyUsage key
|
|
1842
|
+
* purposes". The remaining conditions name the issuing certificate and the BR scope, which are
|
|
1843
|
+
* not in these bytes. The id-kp-emailProtection arm has its own dated row, so this one passes
|
|
1844
|
+
* over it and a certificate draws one finding rather than two. */
|
|
1845
|
+
message: "a SHA-1 signature over an end entity certificate outside the allowance this clause gives",
|
|
1846
|
+
appliesTo: _isMozillaEndEntity,
|
|
1847
|
+
check: function (cert, ctx) {
|
|
1848
|
+
if (!_mozillaIsSha1Signature(cert)) return null;
|
|
1849
|
+
var d = ctx.decode("extKeyUsage");
|
|
1850
|
+
if (!d || !Array.isArray(d.value)) return { context: { reason: "no extKeyUsage extension" } };
|
|
1851
|
+
var out = [];
|
|
1852
|
+
["serverAuth", "anyExtendedKeyUsage"].forEach(function (name) {
|
|
1853
|
+
if (d.value.indexOf(oid.byName(name)) !== -1) out.push({ context: { purpose: name } });
|
|
1854
|
+
});
|
|
1855
|
+
return out.length ? out : null;
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
id: "lint/mozilla-root/sha1-ocsp-or-ca", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.1.3",
|
|
1860
|
+
/** @internal "Effective July 1, 2023, CAs SHALL NOT sign SHA-1 hashes over: certificates with
|
|
1861
|
+
* an EKU extension containing the id-kp-ocspSigning key purpose; intermediate certificates
|
|
1862
|
+
* that chain up to roots in Mozilla's program; OCSP responses; or CRLs." The two the
|
|
1863
|
+
* certificate answers for are the key purpose and being an INTERMEDIATE. The clause names
|
|
1864
|
+
* intermediate certificates rather than CA certificates, so a root's own self-signature is not
|
|
1865
|
+
* one of the four things it lists, and the intermediate reading is the one the sec. 5.3 rows
|
|
1866
|
+
* use with the limit it carries. */
|
|
1867
|
+
message: "a SHA-1 signature over an OCSP signing certificate or an intermediate certificate",
|
|
1868
|
+
appliesTo: _isMozilla,
|
|
1869
|
+
effectiveDate: MOZILLA_SHA1_OCSP_CA_FROM,
|
|
1870
|
+
check: function (cert, ctx) {
|
|
1871
|
+
if (!_mozillaIsSha1Signature(cert)) return null;
|
|
1872
|
+
if (_isMozillaIntermediate(cert, ctx)) return { context: { reason: "intermediate certificate" } };
|
|
1873
|
+
var d = ctx.decode("extKeyUsage");
|
|
1874
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1875
|
+
return d.value.indexOf(oid.byName("ocspSigning")) !== -1
|
|
1876
|
+
? { context: { reason: "id-kp-OCSPSigning" } } : null;
|
|
1877
|
+
},
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
id: "lint/mozilla-root/intermediate-eku-missing", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
|
|
1881
|
+
/** @internal The clause exempts "cross-certificates that share a private key with a
|
|
1882
|
+
* corresponding root certificate", which names a role in a hierarchy that a certificate does
|
|
1883
|
+
* not state, so the message carries the exemption for the operator to apply. */
|
|
1884
|
+
message: "an intermediate certificate must carry an extKeyUsage extension, unless it is a cross-certificate sharing a private key with a root",
|
|
1885
|
+
appliesTo: _isMozillaIntermediate,
|
|
1886
|
+
effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
|
|
1887
|
+
check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
id: "lint/mozilla-root/intermediate-eku-any", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
|
|
1891
|
+
message: "an intermediate certificate's extKeyUsage must not name anyExtendedKeyUsage, unless it is a cross-certificate sharing a private key with a root",
|
|
1892
|
+
appliesTo: _isMozillaIntermediate,
|
|
1893
|
+
effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
|
|
1894
|
+
check: function (cert, ctx) {
|
|
1895
|
+
var d = ctx.decode("extKeyUsage");
|
|
1896
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1897
|
+
return d.value.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
id: "lint/mozilla-root/intermediate-eku-server-and-email", severity: "error", source: "mozilla-root", citation: "Mozilla Root Store Policy 5.3",
|
|
1902
|
+
message: "an intermediate certificate must not name both id-kp-serverAuth and id-kp-emailProtection, unless it is a cross-certificate sharing a private key with a root",
|
|
1903
|
+
appliesTo: _isMozillaIntermediate,
|
|
1904
|
+
effectiveDate: MOZILLA_INTERMEDIATE_EKU_FROM,
|
|
1905
|
+
check: function (cert, ctx) {
|
|
1906
|
+
var d = ctx.decode("extKeyUsage");
|
|
1907
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1908
|
+
var server = d.value.indexOf(oid.byName("serverAuth")) !== -1;
|
|
1909
|
+
var email = d.value.indexOf(oid.byName("emailProtection")) !== -1;
|
|
1910
|
+
return (server && email) ? true : null;
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
];
|
|
1914
|
+
|
|
1915
|
+
/** @internal The two purposes sec. 1.3.2 admits in a subordinate CA certificate, as (a)
|
|
1916
|
+
* id-kp-serverAuth alone or (b) id-kp-serverAuth with id-kp-clientAuth. */
|
|
1917
|
+
var CHROME_TLS_PURPOSES = ["serverAuth", "clientAuth"].map(function (n) { return oid.byName(n); });
|
|
1918
|
+
/** @internal "All corresponding subscriber certificates issued on or after March 15, 2027"
|
|
1919
|
+
* (sec. 1.3.2). Issuance is a date the certificate carries, unlike the CCADB disclosure date the
|
|
1920
|
+
* subordinate CA half of the same clause turns on. */
|
|
1921
|
+
var CHROME_SUBSCRIBER_EKU_FROM = new Date("2027-03-15T00:00:00Z");
|
|
1922
|
+
/** @internal "Beginning June 15, 2026, the Chrome Root Program will phase-out PKI hierarchies found
|
|
1923
|
+
* in violation of the below requirements" (sec. 1.3.2). */
|
|
1924
|
+
var CHROME_DEDICATED_FROM = new Date("2026-06-15T00:00:00Z");
|
|
1925
|
+
var CHROME_SUBORDINATE_MAX_MONTHS = 36;
|
|
1926
|
+
/** @internal The sec. 1.3.1.2 removal schedule, keyed on the date the key material was generated.
|
|
1927
|
+
* Each row holds the exclusive upper bound of its bracket; a date at or after the last bound takes
|
|
1928
|
+
* the clause's tail, fifteen years from generation. */
|
|
1929
|
+
var CHROME_TERM_SCHEDULE = [
|
|
1930
|
+
{ until: new Date("2008-01-01T00:00:00Z"), removal: new Date("2026-04-15T00:00:00Z") },
|
|
1931
|
+
{ until: new Date("2010-01-01T00:00:00Z"), removal: new Date("2027-04-15T00:00:00Z") },
|
|
1932
|
+
{ until: new Date("2012-01-01T00:00:00Z"), removal: new Date("2028-04-15T00:00:00Z") },
|
|
1933
|
+
{ until: new Date("2014-04-15T00:00:00Z"), removal: new Date("2029-04-15T00:00:00Z") },
|
|
1934
|
+
];
|
|
1935
|
+
var CHROME_TERM_MONTHS = 180;
|
|
1936
|
+
|
|
1937
|
+
function _chromeRemoval(notBefore) {
|
|
1938
|
+
var at = guard.time.instantOf(notBefore);
|
|
1939
|
+
for (var i = 0; i < CHROME_TERM_SCHEDULE.length; i++) {
|
|
1940
|
+
if (at < guard.time.instantOf(CHROME_TERM_SCHEDULE[i].until)) return CHROME_TERM_SCHEDULE[i].removal;
|
|
1941
|
+
}
|
|
1942
|
+
return guard.time.addMonths(notBefore, CHROME_TERM_MONTHS, _cfg, "lint/bad-input", "notBefore");
|
|
1943
|
+
}
|
|
1944
|
+
/** @internal The profile is NAMED, never detected, for the reason mozilla-root is. */
|
|
1945
|
+
function _isChrome(cert, ctx) { return ctx.profile === "chrome-root"; }
|
|
1946
|
+
/** @internal A SUBORDINATE CA, which is what sec. 1.3.2 and sec. 1.3.1.3 govern. Being one is a
|
|
1947
|
+
* role in a hierarchy that a certificate does not state, so this reads the one thing the bytes
|
|
1948
|
+
* give and passes over a certificate issued in its own name. That is every root, which those
|
|
1949
|
+
* clauses do not govern, and it is also a rollover CA reissued in its own name, which they do, so
|
|
1950
|
+
* the rows under-report rather than reporting a root for carrying no extended key usage. */
|
|
1951
|
+
function _isChromeSubordinate(cert, ctx) {
|
|
1952
|
+
return _isChrome(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
|
|
1953
|
+
}
|
|
1954
|
+
/** @internal Sec. 1.3.2 reaches "all corresponding UNEXPIRED and unrevoked subordinate CA
|
|
1955
|
+
* certificates", and its phase-out begins on 15 June 2026. A certificate whose notAfter precedes
|
|
1956
|
+
* that day was not among the unexpired ones the clause names when it began to bite, so the four
|
|
1957
|
+
* extended key usage rows pass over it. Whether a certificate is revoked is not in these bytes and
|
|
1958
|
+
* stays outside the reading. Sec. 2.3 states the same requirement for an Applicant hierarchy with
|
|
1959
|
+
* no date on it, and a certificate does not say whether its hierarchy is an Applicant's, so the
|
|
1960
|
+
* date sec. 1.3.2 carries is the one used.
|
|
1961
|
+
* Expiry is an EXEMPTION from a clause the names have already brought the certificate under, so a
|
|
1962
|
+
* notAfter that cannot be read leaves the rows applying: an exemption nothing establishes is not
|
|
1963
|
+
* granted. That is the opposite default from `_selfSignedIdentifiers`, where the unreadable field
|
|
1964
|
+
* is what the row's own premise rests on. */
|
|
1965
|
+
function _isChromeTlsSubordinate(cert, ctx) {
|
|
1966
|
+
if (!_isChromeSubordinate(cert, ctx)) return false;
|
|
1967
|
+
var notAfter = cert.validity && cert.validity.notAfter;
|
|
1968
|
+
if (!guard.time.isDate(notAfter)) return true;
|
|
1969
|
+
return guard.time.instantOf(notAfter) >= guard.time.instantOf(CHROME_DEDICATED_FROM);
|
|
1970
|
+
}
|
|
1971
|
+
/** @internal A SUBSCRIBER certificate, which is not every end entity beneath a root. A delegated
|
|
1972
|
+
* OCSP responder has its own profile in the Baseline Requirements that sec. 1.1.1 binds, and that
|
|
1973
|
+
* profile asks for id-kp-OCSPSigning and NO other purpose, so reading one as a subscriber would
|
|
1974
|
+
* report a conforming responder for the id-kp-serverAuth it must not carry. The exemption is the
|
|
1975
|
+
* purpose standing ALONE rather than merely present: a certificate naming it beside others is not
|
|
1976
|
+
* a responder under that profile, and exempting on presence would let any certificate leave every
|
|
1977
|
+
* row here by adding the purpose. Naming this profile does not run the cabf-tls set, so no other
|
|
1978
|
+
* row would catch it. */
|
|
1979
|
+
function _isChromeSubscriber(cert, ctx) {
|
|
1980
|
+
if (!_isChrome(cert, ctx) || ctx.isCaCert) return false;
|
|
1981
|
+
var purposes = _chromePurposes(ctx);
|
|
1982
|
+
if (!purposes || purposes.length !== 1) return true;
|
|
1983
|
+
return purposes[0] !== oid.byName("ocspSigning");
|
|
1984
|
+
}
|
|
1985
|
+
/** @internal Matching issuer and subject names make a certificate self-ISSUED, which is not the
|
|
1986
|
+
* same as self-signed: a CA reissued in its own name under a new key carries the same two names
|
|
1987
|
+
* and was signed by its predecessor. The key identifiers separate the two without a signature
|
|
1988
|
+
* check, which this synchronous surface cannot make. A certificate whose authorityKeyIdentifier
|
|
1989
|
+
* names a key other than its own subjectKeyIdentifier was signed by a different key and is not a
|
|
1990
|
+
* root; one that omits the extension, or names its own, is consistent with a self-signature. A
|
|
1991
|
+
* keyIdentifier present with no readable subjectKeyIdentifier beside it settles nothing, and the
|
|
1992
|
+
* row rests on settling it, so that reads as not a root too. The residual is a rollover CA that
|
|
1993
|
+
* carries no authorityKeyIdentifier keyIdentifier at all, which RFC 5280 sec. 4.2.1.1 permits
|
|
1994
|
+
* only in a self-signed certificate and which the rfc5280 profile reports on its own row. */
|
|
1995
|
+
function _selfSignedIdentifiers(cert, ctx) {
|
|
1996
|
+
var aki = ctx.decode("authorityKeyIdentifier");
|
|
1997
|
+
var kid = aki && aki.value ? aki.value.keyIdentifier : null;
|
|
1998
|
+
if (!Buffer.isBuffer(kid)) return true;
|
|
1999
|
+
var ski = ctx.decode("subjectKeyIdentifier");
|
|
2000
|
+
return Buffer.isBuffer(ski && ski.value) && kid.equals(ski.value);
|
|
2001
|
+
}
|
|
2002
|
+
function _isChromeRoot(cert, ctx) {
|
|
2003
|
+
return _isChrome(cert, ctx) && ctx.isCaCert && _sameName(cert.issuer, cert.subject) &&
|
|
2004
|
+
_selfSignedIdentifiers(cert, ctx);
|
|
2005
|
+
}
|
|
2006
|
+
function _chromePurposes(ctx) {
|
|
2007
|
+
var d = ctx.decode("extKeyUsage");
|
|
2008
|
+
return (d && Array.isArray(d.value)) ? d.value : null;
|
|
2009
|
+
}
|
|
2010
|
+
function _chromePurposeLabel(dotted) { return oid.name(dotted) || dotted; }
|
|
2011
|
+
|
|
2012
|
+
/** @internal The Chrome Root Program Policy v1.8 profile. */
|
|
2013
|
+
var CHROME_RULES = [
|
|
2014
|
+
{
|
|
2015
|
+
id: "lint/chrome-root/subordinate-ca-eku-missing", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2016
|
+
/** @internal Both admitted forms include the extension, so its absence breaks the clause on
|
|
2017
|
+
* either side of the CCADB disclosure date that decides between them. */
|
|
2018
|
+
message: "a subordinate CA certificate must carry an extKeyUsage extension",
|
|
2019
|
+
appliesTo: _isChromeTlsSubordinate,
|
|
2020
|
+
check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
id: "lint/chrome-root/subordinate-ca-eku-server-auth-missing", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2024
|
+
/** @internal Both forms require the purpose, so its absence needs no date either. A missing
|
|
2025
|
+
* extension is the row above, which is why this one reads a present extension only. */
|
|
2026
|
+
message: "a subordinate CA certificate's extKeyUsage must name id-kp-serverAuth",
|
|
2027
|
+
appliesTo: _isChromeTlsSubordinate,
|
|
2028
|
+
check: function (cert, ctx) {
|
|
2029
|
+
var purposes = _chromePurposes(ctx);
|
|
2030
|
+
if (purposes === null) return null;
|
|
2031
|
+
return purposes.indexOf(oid.byName("serverAuth")) === -1 ? true : null;
|
|
2032
|
+
},
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
id: "lint/chrome-root/subordinate-ca-eku-not-tls", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2036
|
+
/** @internal Neither form admits a purpose outside the two, and anyExtendedKeyUsage is such a
|
|
2037
|
+
* purpose. Each offending purpose draws its own finding, since a certificate naming two is
|
|
2038
|
+
* two breaches and an operator fixes them one at a time.
|
|
2039
|
+
* A non-TLS branch is inside this row rather than outside it. The clause governs "all
|
|
2040
|
+
* corresponding unexpired and unrevoked subordinate CA certificates operated beneath an
|
|
2041
|
+
* existing root included in the Chrome Root Store", and it exists "to align all PKI
|
|
2042
|
+
* hierarchies included in the Chrome Root Store on the principle of serving only TLS server
|
|
2043
|
+
* authentication use cases", so a code signing intermediate beneath such a root is the
|
|
2044
|
+
* multi-purpose hierarchy the section phases out. */
|
|
2045
|
+
message: "a subordinate CA certificate's extKeyUsage must name no purpose other than id-kp-serverAuth and id-kp-clientAuth",
|
|
2046
|
+
appliesTo: _isChromeTlsSubordinate,
|
|
2047
|
+
check: function (cert, ctx) {
|
|
2048
|
+
var purposes = _chromePurposes(ctx);
|
|
2049
|
+
if (purposes === null) return null;
|
|
2050
|
+
var out = [];
|
|
2051
|
+
purposes.forEach(function (dotted) {
|
|
2052
|
+
if (CHROME_TLS_PURPOSES.indexOf(dotted) === -1) {
|
|
2053
|
+
out.push({ context: { purpose: _chromePurposeLabel(dotted) } });
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
return out.length ? out : null;
|
|
2057
|
+
},
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
id: "lint/chrome-root/subordinate-ca-eku-client-auth", severity: "warn", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2061
|
+
/** @internal Form (b) is open only to a subordinate CA disclosed to the CCADB before 15 June
|
|
2062
|
+
* 2026, and sec. 2.3 gives an Applicant hierarchy the same form with 15 June 2025. Neither
|
|
2063
|
+
* date is in these bytes, so the row reports below error and its message names what decides
|
|
2064
|
+
* it. */
|
|
2065
|
+
message: "a subordinate CA certificate naming id-kp-clientAuth is admitted only where it was disclosed to the CCADB before 15 June 2026, which a certificate does not carry",
|
|
2066
|
+
appliesTo: _isChromeTlsSubordinate,
|
|
2067
|
+
check: function (cert, ctx) {
|
|
2068
|
+
var purposes = _chromePurposes(ctx);
|
|
2069
|
+
if (purposes === null) return null;
|
|
2070
|
+
return purposes.indexOf(oid.byName("clientAuth")) !== -1 ? true : null;
|
|
2071
|
+
},
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
id: "lint/chrome-root/subscriber-eku-missing", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2075
|
+
message: "a subscriber certificate must carry an extKeyUsage extension",
|
|
2076
|
+
appliesTo: _isChromeSubscriber,
|
|
2077
|
+
effectiveDate: CHROME_SUBSCRIBER_EKU_FROM,
|
|
2078
|
+
check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
id: "lint/chrome-root/subscriber-eku-server-auth-missing", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2082
|
+
message: "a subscriber certificate's extKeyUsage must name id-kp-serverAuth",
|
|
2083
|
+
appliesTo: _isChromeSubscriber,
|
|
2084
|
+
effectiveDate: CHROME_SUBSCRIBER_EKU_FROM,
|
|
2085
|
+
check: function (cert, ctx) {
|
|
2086
|
+
var purposes = _chromePurposes(ctx);
|
|
2087
|
+
if (purposes === null) return null;
|
|
2088
|
+
return purposes.indexOf(oid.byName("serverAuth")) === -1 ? true : null;
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
id: "lint/chrome-root/subscriber-eku-not-server-auth", severity: "error", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.2",
|
|
2093
|
+
/** @internal The subscriber half admits id-kp-serverAuth alone, so id-kp-clientAuth is
|
|
2094
|
+
* reported here rather than warned as it is on a subordinate CA: the same purpose, graded by
|
|
2095
|
+
* which sentence of the clause reads it. */
|
|
2096
|
+
message: "a subscriber certificate's extKeyUsage must name no purpose other than id-kp-serverAuth",
|
|
2097
|
+
appliesTo: _isChromeSubscriber,
|
|
2098
|
+
effectiveDate: CHROME_SUBSCRIBER_EKU_FROM,
|
|
2099
|
+
check: function (cert, ctx) {
|
|
2100
|
+
var purposes = _chromePurposes(ctx);
|
|
2101
|
+
if (purposes === null) return null;
|
|
2102
|
+
var out = [];
|
|
2103
|
+
purposes.forEach(function (dotted) {
|
|
2104
|
+
if (dotted !== oid.byName("serverAuth")) {
|
|
2105
|
+
out.push({ context: { purpose: _chromePurposeLabel(dotted) } });
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
return out.length ? out : null;
|
|
2109
|
+
},
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
id: "lint/chrome-root/subordinate-ca-validity-over-three-years", severity: "warn", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.1.3",
|
|
2113
|
+
/** @internal "Subordinate CA certificates SHOULD have a maximum validity period of three (3)
|
|
2114
|
+
* years." A ceiling stated in years is measured in months, through guard.time.addMonths, since
|
|
2115
|
+
* how many days three years hold depends on which three. The clause's other sentence asks that
|
|
2116
|
+
* CA owners deploy a new subordinate CA each year, which is a schedule rather than anything
|
|
2117
|
+
* one certificate answers for. */
|
|
2118
|
+
message: "a subordinate CA certificate's validity period should not exceed three years",
|
|
2119
|
+
appliesTo: _isChromeSubordinate,
|
|
2120
|
+
check: function (cert) {
|
|
2121
|
+
var v = cert.validity;
|
|
2122
|
+
if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
|
|
2123
|
+
var ceiling = guard.time.addMonths(v.notBefore, CHROME_SUBORDINATE_MAX_MONTHS, _cfg,
|
|
2124
|
+
"lint/bad-input", "notBefore");
|
|
2125
|
+
return guard.time.instantOf(v.notAfter) > guard.time.instantOf(ceiling)
|
|
2126
|
+
? { context: { maxMonths: CHROME_SUBORDINATE_MAX_MONTHS } } : null;
|
|
2127
|
+
},
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
id: "lint/chrome-root/self-signed-ca-term-limit-outlived", severity: "notice", source: "chrome-root", citation: "Chrome Root Program Policy 1.3.1.2",
|
|
2131
|
+
/** @internal The clause determines the age of key material by the earlier of a key generation
|
|
2132
|
+
* report, which is not in these bytes, or "the validity date of the earliest appearing
|
|
2133
|
+
* certificate that contains the corresponding public key". A self-signed root's own notBefore
|
|
2134
|
+
* is the validity date of one such certificate, so the determined date is at or before it. The
|
|
2135
|
+
* schedule is monotone, so the removal date computed from notBefore is the LATEST the removal
|
|
2136
|
+
* can fall, and reporting only a notAfter past that date holds whatever earlier certificate
|
|
2137
|
+
* carries the key.
|
|
2138
|
+
* The table opens on 1 January 2006 and gives no row for an older key; an older key is removed
|
|
2139
|
+
* no later than a 2006 one, so the first bracket takes it. Its fourth bracket ends on 14 April
|
|
2140
|
+
* 2014 and its tail begins after 15 April 2014, and the two agree on the day between them,
|
|
2141
|
+
* since 15 April 2014 plus fifteen years is 15 April 2029.
|
|
2142
|
+
* The row names a SELF-SIGNED CA certificate rather than a root, which is as far as the bytes
|
|
2143
|
+
* reach. A CA outside the store issues itself one of these during its own key rollover, and it
|
|
2144
|
+
* is identical in every field to a root's. What the finding says is that IF this certificate
|
|
2145
|
+
* is a root in the store the caller named, its term ends before its validity does, and the
|
|
2146
|
+
* notice grade is that conditional. The CA's other certificate, the one its parent issued,
|
|
2147
|
+
* carries different names and is read by the sec. 1.3.2 rows. */
|
|
2148
|
+
message: "a self-signed CA certificate whose validity runs past the removal date this clause fixes for key material of its age",
|
|
2149
|
+
appliesTo: _isChromeRoot,
|
|
2150
|
+
check: function (cert) {
|
|
2151
|
+
var v = cert.validity;
|
|
2152
|
+
if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
|
|
2153
|
+
var removal = _chromeRemoval(v.notBefore);
|
|
2154
|
+
return guard.time.instantOf(v.notAfter) > guard.time.instantOf(removal)
|
|
2155
|
+
? { context: { removal: removal.toISOString().slice(0, 10) } } : null;
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
];
|
|
2159
|
+
|
|
1605
2160
|
/** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
|
|
1606
2161
|
* "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
|
|
1607
2162
|
* is admitted and is not one of the purposes being counted. */
|
|
@@ -4042,9 +4597,14 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
4042
4597
|
"cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4043
4598
|
"cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4044
4599
|
"etsi-qc": ETSI_QC_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4600
|
+
"mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4601
|
+
"chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
4045
4602
|
});
|
|
4046
4603
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
4047
|
-
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
|
|
4604
|
+
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
|
|
4605
|
+
/** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
|
|
4606
|
+
* inert under the default set because each one asks whether the caller named the profile. */
|
|
4607
|
+
.concat(MOZILLA_RULES).concat(CHROME_RULES);
|
|
4048
4608
|
|
|
4049
4609
|
var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
4050
4610
|
return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:3b5edc83-2243-4232-883c-f9c904da9bbb",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-24T15:20:10.358Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.23",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.23",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.23",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|