@blamejs/pki 0.8.24 → 0.8.25

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,23 @@ 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.25 — 2026-09-24
8
+
9
+ A dated permission that lapses is a requirement, and a certificate's own extended key usages name the purpose it was issued for.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.certificate(der, { profile: "apple-root" })` runs the Apple Root Program Policy v2.0 profile. Naming it is the only way to reach these rows, and passing no profile leaves every one of them inert.
14
+ - Appendix A's seven Trust Purposes, each a required extended key usage set: Server Authentication, Legacy TLS, Secure Email, Legacy S/MIME, Client Authentication, Time Stamping and VMC. A certificate is dedicated to a purpose when its extKeyUsage is not empty and names nothing outside that purpose's set, which is how the appendix states it: a subordinate CA "MUST contain only the EKU(s) specified for that Trust Purpose".
15
+ - The section 1.7 rules over a subordinate CA certificate, each gated on the signing date the clause defines for itself, which it says a renewal, a re-key and a cross-sign each produce afresh. From 1 August 2026 the extKeyUsage extension is required and `anyExtendedKeyUsage` is not permitted. In the window that runs to 1 July 2027 the clause admits either dedication to one purpose or three conditions together, of which one is in the certificate: that it names no extended key usage belonging to a purpose. So a certificate naming one of those identifiers has already failed that arm, and if its set spans two purposes it has failed the other, which is what a single certificate settles. From 1 July 2027 the second arm is gone and dedication is required outright, so a certificate for a use the appendix does not list is reported then and passed over before.
16
+ - Section 2.3's rule that a subscriber certificate carrying `id-kp-emailProtection` and signed on or after 1 February 2027 names at least one rfc822Name in its subjectAltName, and Appendix A's last note read as what it is: a permission for a subscriber certificate signed before 1 July 2027 to carry extended key usages spanning more than one purpose. A permission bounded by a date binds once the date passes. `anyExtendedKeyUsage` is in scope for that rule even though the appendix's table does not list it, because RFC 5280 section 4.2.1.12 makes it a certificate not restricted to any specific key purpose: it names every purpose rather than none, and conforms to no single one. That is what separates it from a purpose the table does not list at all, such as code signing, which leaves a certificate under no trust purpose and outside the rule.
17
+ - Section 1.5's floor for a root certificate, an RSA modulus of at least 4096 bits or a curve of at least NIST P-384. The clause names those two families and no others, so a key outside both is reported as well.
18
+ - `id-kp-BrandIndicatorforMessageIdentification` (`1.3.6.1.5.5.7.3.31`) joins the OID registry, under IANA's own spelling for it, so the VMC purpose resolves by name like the other six.
19
+
20
+ ### Changed
21
+
22
+ - `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.
23
+
7
24
  ## v0.8.24 — 2026-09-24
8
25
 
9
26
  A requirement this toolkit already refuses at parse is not one a profile owes a rule.
package/README.md CHANGED
@@ -290,7 +290,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
290
290
  | `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
291
291
  | `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
292
292
  | `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
293
- | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the 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 Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `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
@@ -2527,6 +2527,219 @@ var MICROSOFT_RULES = [
2527
2527
  },
2528
2528
  ];
2529
2529
 
2530
+ /** @internal The seven Trust Purposes appendix A defines, each as the extended key usage set it
2531
+ * requires. Dedication is a SUBSET test rather than equality, which the appendix states twice: a
2532
+ * subordinate CA "MUST contain only the EKU(s) specified for that Trust Purpose", and sec. 2.1.3
2533
+ * repeats it. So {id-kp-serverAuth} is dedicated under Server Authentication and under Legacy TLS
2534
+ * alike, and a set spanning two purposes is under neither. */
2535
+ var APPLE_TRUST_PURPOSES = [
2536
+ ["serverAuth"],
2537
+ ["serverAuth", "clientAuth"],
2538
+ ["emailProtection"],
2539
+ ["emailProtection", "clientAuth"],
2540
+ ["clientAuth"],
2541
+ ["timeStamping"],
2542
+ ["brandIndicatorforMessageIdentification"],
2543
+ ].map(function (ekus) {
2544
+ return { ekus: ekus.map(function (n) { return oid.byName(n); }) };
2545
+ });
2546
+ /** @internal Every identifier the table names, across all seven purposes. */
2547
+ var APPLE_PURPOSE_EKUS = [];
2548
+ APPLE_TRUST_PURPOSES.forEach(function (p) {
2549
+ p.ekus.forEach(function (d) {
2550
+ if (APPLE_PURPOSE_EKUS.indexOf(d) === -1) APPLE_PURPOSE_EKUS.push(d);
2551
+ });
2552
+ });
2553
+ /** @internal Sec. 1.7 and the change log: an extended key usage extension and the prohibition on
2554
+ * anyExtendedKeyUsage from this date, dedication to a single Trust Purpose from the later one.
2555
+ * Sec. 2's note fixes the comparison at "00:00:00 UTC" and sec. 1.7 defines the signing date as
2556
+ * the date of issuance, including for a renewal, a re-key or a cross-sign. */
2557
+ var APPLE_EKU_FROM = new Date("2026-08-01T00:00:00Z");
2558
+ var APPLE_SINGLE_PURPOSE_FROM = new Date("2027-07-01T00:00:00Z");
2559
+ var APPLE_SMIME_SAN_FROM = new Date("2027-02-01T00:00:00Z");
2560
+ var APPLE_ROOT_RSA_MIN_BITS = 4096;
2561
+ /** @internal "a minimum key size of ... ECDSA 384-bit", so P-384 and above. */
2562
+ var APPLE_ROOT_CURVES = ["secp384r1", "secp521r1"];
2563
+
2564
+ function _isApple(cert, ctx) { return ctx.profile === "apple-root"; }
2565
+ function _isAppleRoot(cert, ctx) {
2566
+ return _isApple(cert, ctx) && ctx.isCaCert && _sameName(cert.issuer, cert.subject) &&
2567
+ _selfSignedIdentifiers(cert, ctx);
2568
+ }
2569
+ function _isAppleSubordinate(cert, ctx) {
2570
+ return _isApple(cert, ctx) && ctx.isCaCert && !_sameName(cert.issuer, cert.subject);
2571
+ }
2572
+ function _isAppleSubscriber(cert, ctx) { return _isApple(cert, ctx) && !ctx.isCaCert; }
2573
+ function _applePurposes(ctx) {
2574
+ var d = ctx.decode("extKeyUsage");
2575
+ return (d && Array.isArray(d.value)) ? d.value : null;
2576
+ }
2577
+ /** @internal Whether an extended key usage set is dedicated to one Trust Purpose: non-empty, and
2578
+ * contained in at least one purpose's required set. WHICH purpose is not returned, and cannot
2579
+ * honestly be: {id-kp-clientAuth} sits inside Client Authentication, Legacy TLS and Legacy S/MIME
2580
+ * alike, which is why the appendix has to say in prose that such a certificate belongs under a
2581
+ * Client Authentication CP/CPS. No finding names a purpose, so the rows ask only the question the
2582
+ * bytes answer. The subset form is the document's, and under
2583
+ * the CURRENT table it decides nothing an equality test would not: every proper non-empty subset
2584
+ * of the two purposes with two identifiers is itself another purpose's whole set. No vector
2585
+ * separates the two readings, and that is a fact about the table rather than about the rule, so a
2586
+ * purpose added later is what the subset form is written for. */
2587
+ function _appleIsDedicated(purposes) {
2588
+ if (!purposes || !purposes.length) return false;
2589
+ for (var i = 0; i < APPLE_TRUST_PURPOSES.length; i++) {
2590
+ var p = APPLE_TRUST_PURPOSES[i];
2591
+ var within = true;
2592
+ for (var j = 0; j < purposes.length; j++) {
2593
+ if (p.ekus.indexOf(purposes[j]) === -1) { within = false; break; }
2594
+ }
2595
+ if (within) return true;
2596
+ }
2597
+ return false;
2598
+ }
2599
+ /** @internal Whether the set is under a Trust Purpose at all, which is what separates a certificate
2600
+ * the scoping reaches from one it does not. anyExtendedKeyUsage is not in the appendix's table and
2601
+ * is still in scope: RFC 5280 sec. 4.2.1.12 makes it a certificate "not restricted to any specific
2602
+ * key purpose", so it names EVERY purpose rather than none, and it conforms to no single one. That
2603
+ * is what separates it from id-kp-codeSigning, which names a use the table does not list and
2604
+ * leaves the certificate under no purpose. */
2605
+ function _appleNamesAPurpose(purposes) {
2606
+ if (!purposes) return false;
2607
+ if (purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1) return true;
2608
+ for (var i = 0; i < purposes.length; i++) {
2609
+ if (APPLE_PURPOSE_EKUS.indexOf(purposes[i]) !== -1) return true;
2610
+ }
2611
+ return false;
2612
+ }
2613
+ function _appleSignedBefore(cert, when) {
2614
+ var nb = cert.validity && cert.validity.notBefore;
2615
+ if (!guard.time.isDate(nb)) return false;
2616
+ return guard.time.instantOf(nb) < guard.time.instantOf(when);
2617
+ }
2618
+ function _appleEkuLabels(purposes) {
2619
+ return purposes.map(function (d) { return oid.name(d) || d; });
2620
+ }
2621
+
2622
+ /** @internal The Apple Root Program Policy v2.0 profile. */
2623
+ var APPLE_RULES = [
2624
+ {
2625
+ id: "lint/apple-root/root-key-too-small", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.5",
2626
+ /** @internal "Root Inclusion Requests MUST only contain Root CA Certificates that have a
2627
+ * minimum key size of RSA 4096-bit or ECDSA 384-bit." The clause names two families and no
2628
+ * others, so a key outside both is outside it. */
2629
+ message: "a root certificate needs an RSA modulus of at least 4096 bits or a curve of at least NIST P-384",
2630
+ appliesTo: _isAppleRoot,
2631
+ check: function (cert) {
2632
+ var spki = cert.subjectPublicKeyInfo;
2633
+ var name = spki && spki.algorithm && spki.algorithm.name;
2634
+ if (name && RSA_KEY_ALGS[name]) {
2635
+ var bits = _rsaModulusBits(spki);
2636
+ if (bits === null) return { context: { rsaBits: null } };
2637
+ return bits < APPLE_ROOT_RSA_MIN_BITS ? { context: { rsaBits: bits } } : null;
2638
+ }
2639
+ if (name === "ecPublicKey") {
2640
+ var curve = _ecCurveName(spki);
2641
+ return APPLE_ROOT_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
2642
+ }
2643
+ return { context: { algorithm: name || null } };
2644
+ },
2645
+ },
2646
+ {
2647
+ id: "lint/apple-root/subordinate-ca-eku-missing", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2648
+ message: "a subordinate CA certificate must carry an extKeyUsage extension",
2649
+ appliesTo: _isAppleSubordinate,
2650
+ effectiveDate: APPLE_EKU_FROM,
2651
+ check: function (cert, ctx) { return ctx.raw("extKeyUsage") ? null : true; },
2652
+ },
2653
+ {
2654
+ id: "lint/apple-root/subordinate-ca-eku-any", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2655
+ message: "a subordinate CA certificate must not assert anyExtendedKeyUsage",
2656
+ appliesTo: _isAppleSubordinate,
2657
+ effectiveDate: APPLE_EKU_FROM,
2658
+ check: function (cert, ctx) {
2659
+ var purposes = _applePurposes(ctx);
2660
+ if (purposes === null) return null;
2661
+ return purposes.indexOf(oid.byName("anyExtendedKeyUsage")) !== -1 ? true : null;
2662
+ },
2663
+ },
2664
+ {
2665
+ id: "lint/apple-root/subordinate-ca-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2666
+ /** @internal In this window the clause admits (a) dedication to a single Trust Purpose, or (b)
2667
+ * three conditions together, of which one is in the bytes: "it contains no Extended Key Usage
2668
+ * associated with a Trust Purpose defined in Appendix A". A certificate naming one of those
2669
+ * identifiers has failed (b) already, and if its set is not within a single purpose it has
2670
+ * failed (a) as well, so both arms fail on the bytes alone. A certificate naming none of them
2671
+ * is passed over here, because (b)'s other two conditions are the issuing root's configuration
2672
+ * and a CP/CPS, and the row below picks it up once (b) is gone. */
2673
+ message: "a subordinate CA certificate names extended key usages spanning more than one trust purpose",
2674
+ appliesTo: _isAppleSubordinate,
2675
+ effectiveDate: APPLE_EKU_FROM,
2676
+ check: function (cert, ctx) {
2677
+ if (!_appleSignedBefore(cert, APPLE_SINGLE_PURPOSE_FROM)) return null;
2678
+ var purposes = _applePurposes(ctx);
2679
+ if (!_appleNamesAPurpose(purposes)) return null;
2680
+ return _appleIsDedicated(purposes)
2681
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2682
+ },
2683
+ },
2684
+ {
2685
+ id: "lint/apple-root/subordinate-ca-not-single-purpose", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 1.7",
2686
+ /** @internal "A Subordinate CA Certificate signed on or after 2027-07-01 MUST be dedicated to a
2687
+ * single Trust Purpose." Arm (b) is gone, so the set must be within one purpose whatever it
2688
+ * names, and the certificate the row above passes over is reported here. Two rows rather than
2689
+ * one, because the two windows differ in what fixes them. */
2690
+ message: "a subordinate CA certificate must be dedicated to a single trust purpose",
2691
+ appliesTo: _isAppleSubordinate,
2692
+ effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
2693
+ check: function (cert, ctx) {
2694
+ var purposes = _applePurposes(ctx);
2695
+ if (purposes === null) return null;
2696
+ return _appleIsDedicated(purposes)
2697
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2698
+ },
2699
+ },
2700
+ {
2701
+ id: "lint/apple-root/smime-subscriber-without-rfc822-name", severity: "error", source: "apple-root", citation: "Apple Root Program Policy 2.3",
2702
+ /** @internal "Effective 2027-02-01, all newly signed Subscriber certificates that contain the
2703
+ * id-kp-emailProtection EKU MUST include at least one rfc822Name in the subjectAltName
2704
+ * extension." */
2705
+ message: "a subscriber certificate carrying id-kp-emailProtection must name at least one rfc822Name in its subjectAltName",
2706
+ appliesTo: function (cert, ctx) {
2707
+ var purposes = _applePurposes(ctx);
2708
+ return _isAppleSubscriber(cert, ctx) && !!purposes &&
2709
+ purposes.indexOf(oid.byName("emailProtection")) !== -1;
2710
+ },
2711
+ effectiveDate: APPLE_SMIME_SAN_FROM,
2712
+ check: function (cert, ctx) {
2713
+ var san = ctx.decode("subjectAltName");
2714
+ var names = san && san.value && san.value.names;
2715
+ if (!Array.isArray(names)) return true;
2716
+ for (var i = 0; i < names.length; i++) {
2717
+ if (names[i] && names[i].tagNumber === 1 && typeof names[i].value === "string" &&
2718
+ names[i].value.length) { return null; }
2719
+ }
2720
+ return true;
2721
+ },
2722
+ },
2723
+ {
2724
+ id: "lint/apple-root/subscriber-mixes-trust-purposes", severity: "error", source: "apple-root", citation: "Apple Root Program Policy appendix A",
2725
+ /** @internal The appendix's last note is a dated PERMISSION rather than a prohibition:
2726
+ * "Subscriber certificates issued under a Trust Purpose ... and signed before 2027-07-01 MAY
2727
+ * contain EKU combinations that do not conform to the single Trust Purpose scoping defined in
2728
+ * this Appendix". The permission is bounded by its date, so the scoping binds once it passes.
2729
+ * A subscriber naming none of the appendix's identifiers is under no Trust Purpose and is
2730
+ * passed over, which is the reading sec. 1.7 arm (b) takes of the same shape. */
2731
+ message: "a subscriber certificate names extended key usages spanning more than one trust purpose",
2732
+ appliesTo: _isAppleSubscriber,
2733
+ effectiveDate: APPLE_SINGLE_PURPOSE_FROM,
2734
+ check: function (cert, ctx) {
2735
+ var purposes = _applePurposes(ctx);
2736
+ if (!_appleNamesAPurpose(purposes)) return null;
2737
+ return _appleIsDedicated(purposes)
2738
+ ? null : { context: { extKeyUsage: _appleEkuLabels(purposes) } };
2739
+ },
2740
+ },
2741
+ ];
2742
+
2530
2743
  /** @internal The three purposes ETSI EN 319 412-5 clause 4.2.3 names, of which a QcType declares
2531
2744
  * "one and only one". The ASN.1 carries an extension marker, so an identifier outside the three
2532
2745
  * is admitted and is not one of the purposes being counted. */
@@ -4970,12 +5183,13 @@ var PROFILES = Object.assign(Object.create(null), {
4970
5183
  "mozilla-root": MOZILLA_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4971
5184
  "chrome-root": CHROME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4972
5185
  "microsoft-root": MICROSOFT_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
5186
+ "apple-root": APPLE_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
4973
5187
  });
4974
5188
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
4975
5189
  .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
4976
5190
  /** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
4977
5191
  * inert under the default set because each one asks whether the caller named the profile. */
4978
- .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES);
5192
+ .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES);
4979
5193
 
4980
5194
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
4981
5195
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
package/lib/oid.js CHANGED
@@ -180,7 +180,8 @@ var FAMILIES = Object.assign(Object.create(null), {
180
180
 
181
181
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
182
182
  serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9,
183
- secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32, bundleSecurity: 35 } },
183
+ secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32,
184
+ brandIndicatorforMessageIdentification: 31, bundleSecurity: 35 } },
184
185
 
185
186
  pkinitKp: { base: [1, 3, 6, 1, 5, 2, 3], of: { pkinitClientAuth: 4, pkinitKdc: 5 } },
186
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.24",
3
+ "version": "0.8.25",
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:cdbd209f-fdc3-4d13-8b62-96f8d9222905",
5
+ "serialNumber": "urn:uuid:34f30267-88dd-4bb6-b35e-f48345fa4997",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T16:24:07.579Z",
8
+ "timestamp": "2026-09-24T17:21:10.581Z",
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.24",
22
+ "bom-ref": "@blamejs/pki@0.8.25",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.24",
25
+ "version": "0.8.25",
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.24",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.25",
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.24",
57
+ "ref": "@blamejs/pki@0.8.25",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]