@blamejs/pki 0.8.18 → 0.8.19

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,27 @@ 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.19 — 2026-09-24
8
+
9
+ A code signing certificate is linted against the kind it declares itself to be.
10
+
11
+ ### Added
12
+
13
+ - `pki.lint.certificate(der, { profile: "cabf-cs" })` runs the Code Signing BR v3.11.0 subscriber profile. The rows reach a certificate unprompted too, because one asserting a reserved identifier has declared itself to be one. Detection reads the decoded extension, so a certificate whose certificatePolicies does not decode declares nothing and is reported for the encoding instead. A certificate issued to a subordinate CA carries a reserved identifier as well under section 7.1.6.3, so it is not read as a subscriber; naming the profile in the call still lints it as one.
14
+ - certificatePolicies (a): the extension is required and should not be critical, and an `id-qt-cps` qualifier must be a HTTP URL. Section 7.1.6.4 requires exactly one reserved identifier for a certificate issued on or after 15 September 2026, gated to the issuance date the certificate states.
15
+ - cRLDistributionPoints (b): the extension is required, must not be critical, and must carry a HTTP URL for the CRL service.
16
+ - authorityInformationAccess (c): the extension is required, must not be critical, must carry an `id-ad-caIssuers` accessMethod, and each location the clause names must be a HTTP URL. Unlike the S/MIME profile, one rule covers both accessMethods, because this clause states one.
17
+ - basicConstraints (d) must not assert `cA`. authorityKeyIdentifier (g) is required and must not be critical.
18
+ - keyUsage (e): the extension is required, must be critical, and must assert `digitalSignature`. `keyCertSign` and `cRLSign` are reported as errors and every other bit as a warning, because the clause states the two lists at different strengths in consecutive sentences.
19
+ - extKeyUsage (f): a code signing certificate must carry `id-kp-codeSigning` and a timestamp certificate `id-kp-timeStamping`, and a timestamp certificate's extKeyUsage must be marked critical, which is the one place this profile requires a critical extKeyUsage. `anyExtendedKeyUsage` and `id-kp-serverAuth` are forbidden. Any other purpose is reported as a warning, leaving the three the clause names as permitted: Lifetime Signing, `id-kp-emailProtection` and Document Signing. Document Signing is read under both identifiers, the `1.3.6.1.4.1.311.10.3.12` Microsoft assigns it and the `1.3.6.1.4.1.311.3.10.3.12` the clause prints, so a certificate carrying either is passed over.
20
+ - The section 6.3.2 ceilings: 39 months for a code signing certificate issued before 1 March 2026, 460 days for one issued on or after it, and 135 months for a timestamp certificate. A ceiling stated in months is measured in months, since how many days 39 months hold depends on which 39.
21
+ - The section 6.1.5.2 floors: an RSA modulus of at least 3072 bits, one of NIST P-256, P-384 and P-521, or a DSA key whose parameters are a length of 2048 bits with a modulus length of 224 or 256. Each family the section names is read on its own terms, and a family it names none of is what the row reports.
22
+
23
+ ### Changed
24
+
25
+ - The three reserved code signing policy identifiers of section 7.1.6.1 resolve through `pki.oid`, named for the kind each one declares, along with the Microsoft extended key purposes the section 7.1.2.3 (f) clause names as permitted.
26
+ - Severities in this profile are graded against its own clauses, so a certificate can draw an error here and a warning under `cabf-smime` for the same shape: this document requires a non-critical cRLDistributionPoints, a present authorityInformationAccess and a critical keyUsage where the S/MIME one recommends them.
27
+
7
28
  ## v0.8.18 — 2026-09-24
8
29
 
9
30
  An S/MIME certificate is linted against the generation it declares itself to be.
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 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), 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/guard-time.js CHANGED
@@ -12,6 +12,16 @@ var _Date = Date;
12
12
 
13
13
  var _dateGetTime = require("./guard-intrinsic").uncurry(Date.prototype.getTime);
14
14
  var _dateGetUTCFullYear = require("./guard-intrinsic").uncurry(Date.prototype.getUTCFullYear);
15
+ var _dateGetUTCMonth = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMonth);
16
+ var _dateGetUTCDate = require("./guard-intrinsic").uncurry(Date.prototype.getUTCDate);
17
+ var _dateGetUTCHours = require("./guard-intrinsic").uncurry(Date.prototype.getUTCHours);
18
+ var _dateGetUTCMinutes = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMinutes);
19
+ var _dateGetUTCSeconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCSeconds);
20
+ var _dateGetUTCMilliseconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMilliseconds);
21
+ var _dateSetUTCFullYear = require("./guard-intrinsic").uncurry(Date.prototype.setUTCFullYear);
22
+ var _dateUTC = Date.UTC;
23
+ var _floor = Math.floor;
24
+ var _ceil = Math.ceil;
15
25
  /** @internal A DER GeneralizedTime carries a four-digit year and a UTCTime a narrower one, so a Date
16
26
  * outside 0000..9999 has no PKI encoding. */
17
27
  var DER_YEAR_MIN = 0, DER_YEAR_MAX = 9999;
@@ -59,6 +69,45 @@ function isDate(value) {
59
69
  return util.types.isDate(value);
60
70
  }
61
71
 
72
+ // @enforced-by behavioral -- reading a month and adding to it is also how a date renderer prints one, so the arithmetic has no rename-proof shape a detector can tell from `getUTCMonth() + 1`; the clamping and year-borrow vectors in guard-time.test.js are the guard
73
+ /** @internal The instant `months` calendar months after a valid Date, clamped to the last day of
74
+ * the target month where that month is shorter, so 31 January plus one month gives 28 or 29
75
+ * February. A ceiling stated in months has no reading in days, since how many days 39 months hold
76
+ * depends on which 39. Time of day is carried through unchanged. */
77
+ function addMonths(value, months, E, code, label) {
78
+ assertValid(value, E, code, label);
79
+ var year = _dateGetUTCFullYear(value);
80
+ var month = _dateGetUTCMonth(value) + months;
81
+ var day = _dateGetUTCDate(value);
82
+ var carry = _floorDiv(month, 12);
83
+ var targetYear = year + carry;
84
+ var targetMonth = month - carry * 12;
85
+ var lastDay = _daysInMonth(targetYear, targetMonth);
86
+ /** @internal Date.UTC reads a year of 0..99 as 1900..1999, and a DER time carries 0000..9999, so
87
+ * the date is built at a safe year and the real one is set with the month and day together. One
88
+ * call sets all three, where setting the year alone would roll 29 February out of a leap year the
89
+ * remap had landed on. */
90
+ var out = new _Date(_dateUTC(2000, 0, 1,
91
+ _dateGetUTCHours(value), _dateGetUTCMinutes(value), _dateGetUTCSeconds(value),
92
+ _dateGetUTCMilliseconds(value)));
93
+ _dateSetUTCFullYear(out, targetYear, targetMonth, day < lastDay ? day : lastDay);
94
+ return out;
95
+ }
96
+ var _MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
97
+ /** @internal The proleptic Gregorian leap rule the Date object follows, so year 0 is a leap year
98
+ * and year 1900 is not. */
99
+ function _daysInMonth(year, month) {
100
+ if (month !== 1) return _MONTH_DAYS[month];
101
+ return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 29 : 28;
102
+ }
103
+ /** @internal Integer division that rounds toward negative infinity, so a month index before
104
+ * January of the starting year borrows a year rather than truncating toward zero. */
105
+ function _floorDiv(a, b) {
106
+ var q = a / b;
107
+ var f = q < 0 ? -_ceil(-q) : _floor(q);
108
+ return f;
109
+ }
110
+
62
111
  // @enforced-by behavioral -- a coercion-safe Date converter has no rename-proof code shape; the RED vector (a caller value with no numeric or string form, a BigInt or a throwing Symbol.toPrimitive, becomes an invalid Date the downstream isNaN check rejects, never a native TypeError out of the Date constructor) is the guard.
63
112
  function toDate(value) {
64
113
  if (util.types.isDate(value)) return value;
@@ -69,6 +118,7 @@ function toDate(value) {
69
118
  module.exports = _freeze({
70
119
  assertValid: assertValid,
71
120
  assertEncodable: assertEncodable,
121
+ addMonths: addMonths,
72
122
  instantOf: instantOf,
73
123
  isDate: isDate,
74
124
  toDate: toDate,
package/lib/lint.js CHANGED
@@ -154,6 +154,9 @@ function _makeCtx(cert, profile) {
154
154
  * policy identifier, so unlike the TLS case the rows detect it from the certificate itself and
155
155
  * the flag only records that the caller also named it. */
156
156
  explicitSmimeProfile: profile === "cabf-smime",
157
+ /** @internal A code signing subscriber certificate names its kind through a reserved policy
158
+ * identifier the same way, so this flag only records that the caller also named the profile. */
159
+ explicitCsProfile: profile === "cabf-cs",
157
160
  isCaCert: isCa,
158
161
  isTlsServerCert: explicitTls || (_hasEku(cert, "serverAuth") && !isCa),
159
162
  decode: function (name) { return _decodeOrNull(cert, name); },
@@ -731,6 +734,98 @@ function _smimeKuMatchesAMode(modes, asserted) {
731
734
  return false;
732
735
  }
733
736
 
737
+ /** @internal Code Signing BR sec. 7.1.6.1 reserves three policy identifiers, keyed here to the KIND
738
+ * of certificate each declares. The EV and the non-EV code signing identifiers declare the same
739
+ * kind, because sec. 7.1.2.3 (f) asks one question of both; only the timestamping identifier takes
740
+ * the other arm. */
741
+ var CS_KIND_BY_POLICY = Object.create(null);
742
+ [["code-signing", "code-signing"], ["code-signing-ev", "code-signing"],
743
+ ["code-signing-timestamping", "timestamping"]].forEach(function (row) {
744
+ var dotted = oid.byName(row[0]);
745
+ if (dotted) CS_KIND_BY_POLICY[dotted] = row[1];
746
+ });
747
+ /** @internal "Effective September 15, 2026 a Certificate issued to a Subscriber MUST contain
748
+ * exactly one of the reserved policy OIDs specified in Section 7.1.6.1" (sec. 7.1.6.4). Dated, so
749
+ * a certificate issued before it is not held to a rule that did not yet apply. */
750
+ var CS_RESERVED_POLICY_REQUIRED = new Date("2026-09-15T00:00:00Z");
751
+ /** @internal Sec. 6.3.2 states three ceilings: 39 months for a code signing certificate issued
752
+ * before this date, 460 days for one issued on or after it, and 135 months for a timestamp
753
+ * certificate whichever side of it. */
754
+ var CS_VALIDITY_DAYS_START = new Date("2026-03-01T00:00:00Z");
755
+ /** @internal Sec. 6.1.5.2, the curves, the modulus floor and the two DSA parameter options for a
756
+ * subscriber key. The TLS profile names the same three curves from its own document; these are
757
+ * this one's. The section names RSA, ECDSA and DSA, so each family is read on its own terms and a
758
+ * family it does not name is what the row reports. */
759
+ var CS_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
760
+ var CS_RSA_MIN_BITS = 3072;
761
+ var CS_DSA_L = 2048;
762
+ var CS_DSA_N = [224, 256];
763
+ /** @internal The DSA key length L and modulus length N, read as the bit lengths of p and q in the
764
+ * Dss-Parms the AlgorithmIdentifier carries. */
765
+ function _dsaParamBits(spki) {
766
+ try {
767
+ var params = spki.algorithm && spki.algorithm.parameters;
768
+ if (!Buffer.isBuffer(params)) return null;
769
+ var seq = asn1.decode(params);
770
+ if (!seq.children || seq.children.length !== 3) return null;
771
+ var p = asn1.read.integer(seq.children[0]);
772
+ var q = asn1.read.integer(seq.children[1]);
773
+ asn1.read.integer(seq.children[2]);
774
+ if (p <= 0n || q <= 0n) return null;
775
+ return { L: p.toString(2).length, N: q.toString(2).length };
776
+ } catch (_e) { return null; }
777
+ }
778
+ /** @internal Sec. 7.1.2.3 (c) names these two accessMethods and no other. */
779
+ var CS_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
780
+ /** @internal Sec. 7.1.2.3 (f): forbidden at every kind, and the three the clause names as MAY
781
+ * beside id-kp-codeSigning, which the discouraged-purpose row passes over. */
782
+ var CS_FORBIDDEN_EKU = ["anyExtendedKeyUsage", "serverAuth"].map(function (n) { return oid.byName(n); });
783
+ var CS_PERMITTED_EXTRA_EKU = ["ms-lifetime-signing", "emailProtection", "ms-document-signing",
784
+ "ms-document-signing-as-printed"].map(function (n) { return oid.byName(n); });
785
+ /** @internal Sec. 7.1.2.3 (e) names these two as MUST NOT, and every other bit beside
786
+ * digitalSignature as SHOULD NOT, so one sentence becomes two rows at two strengths. */
787
+ var CS_FORBIDDEN_KU = ["keyCertSign", "cRLSign"];
788
+
789
+ /** @internal The kind the asserted reserved identifier declares, or null where none is asserted. */
790
+ function _csKind(ctx) {
791
+ var d = ctx.decode("certificatePolicies");
792
+ if (!d || !Array.isArray(d.value)) return null;
793
+ var found = null;
794
+ for (var i = 0; i < d.value.length; i++) {
795
+ var k = CS_KIND_BY_POLICY[d.value[i].policyIdentifier];
796
+ if (k && found === null) found = k;
797
+ }
798
+ return found;
799
+ }
800
+ /** @internal Sec. 7.1.6.3 gives a subordinate CA these identifiers too, so detection asks for the
801
+ * identifier AND that the certificate is not a CA, as the S/MIME profile does and for the same
802
+ * reason. Naming the profile overrides, which is how a malformed subscriber is checked. */
803
+ function _isCs(cert, ctx) {
804
+ if (ctx.explicitCsProfile) return true;
805
+ return _csKind(ctx) !== null && !ctx.isCaCert;
806
+ }
807
+ /** @internal A certificate declaring no kind is read as a code signing one, which is the arm the
808
+ * section states first and the only arm a caller who named the profile without a reserved
809
+ * identifier can have meant. */
810
+ function _isCsTimestamp(cert, ctx) { return _isCs(cert, ctx) && _csKind(ctx) === "timestamping"; }
811
+ function _isCsCodeSigning(cert, ctx) { return _isCs(cert, ctx) && _csKind(ctx) !== "timestamping"; }
812
+ /** @internal The accessLocation URIs of the two accessMethods sec. 7.1.2.3 (c) names, flat: unlike
813
+ * the S/MIME clause this one states one rule for both, so they are not grouped by method. A
814
+ * location that is not a uniformResourceIdentifier is kept as null, since the method is still
815
+ * provided and still carries no HTTP URL. */
816
+ function _csAiaLocations(ctx) {
817
+ var d = ctx.decode("authorityInfoAccess");
818
+ if (!d || !Array.isArray(d.value)) return [];
819
+ var out = [];
820
+ for (var i = 0; i < d.value.length; i++) {
821
+ var ad = d.value[i];
822
+ if (CS_AIA_METHODS.indexOf(ad.accessMethod) === -1) continue;
823
+ var loc = ad.accessLocation;
824
+ out.push(loc && loc.tag === 6 && typeof loc.value === "string" ? loc.value : null);
825
+ }
826
+ return out;
827
+ }
828
+
734
829
  /** @internal Sec. 7.1.2.3 (c) gives a URI scheme table for each of these two accessMethods and
735
830
  * none for any other, so a scheme is measured on these and an unrecognized accessMethod has no
736
831
  * table to be held to. */
@@ -1439,6 +1534,348 @@ var CABF_SMIME_RULES = [
1439
1534
  },
1440
1535
  ];
1441
1536
 
1537
+ /** @internal The CA/Browser Forum Code Signing BR v3.11.0 subscriber profile. Every severity is
1538
+ * graded against the clause the row cites, never against the same-named row of another profile:
1539
+ * this document says MUST where the S/MIME one says SHOULD for cRLDistributionPoints criticality,
1540
+ * authorityInformationAccess presence and keyUsage criticality. */
1541
+ var CABF_CS_RULES = [
1542
+ {
1543
+ id: "lint/cabf-cs/policies-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1544
+ message: "a subscriber certificate must carry a certificatePolicies extension",
1545
+ appliesTo: _isCs,
1546
+ check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
1547
+ },
1548
+ {
1549
+ id: "lint/cabf-cs/policies-critical", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1550
+ message: "certificatePolicies should not be marked critical",
1551
+ appliesTo: _isCs,
1552
+ check: function (cert, ctx) {
1553
+ var e = ctx.raw("certificatePolicies");
1554
+ return (e && e.critical === true) ? true : null;
1555
+ },
1556
+ },
1557
+ {
1558
+ id: "lint/cabf-cs/policies-reserved-identifier-count", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.6.4",
1559
+ /** @internal "a Certificate issued to a Subscriber MUST contain exactly one of the reserved
1560
+ * policy OIDs specified in Section 7.1.6.1", so both none and two are reported. */
1561
+ message: "a subscriber certificate must assert exactly one reserved code signing policy identifier",
1562
+ appliesTo: _isCs,
1563
+ effectiveDate: CS_RESERVED_POLICY_REQUIRED,
1564
+ check: function (cert, ctx) {
1565
+ var d = ctx.decode("certificatePolicies");
1566
+ if (!d || !Array.isArray(d.value)) return null;
1567
+ var found = d.value.filter(function (pi) { return !!CS_KIND_BY_POLICY[pi.policyIdentifier]; });
1568
+ return found.length === 1 ? null : { context: { count: found.length } };
1569
+ },
1570
+ },
1571
+ {
1572
+ id: "lint/cabf-cs/policies-cps-not-http-url", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
1573
+ /** @internal "HTTP URL for the Subordinate CA's Certification Practice Statement". This clause
1574
+ * names one scheme where the S/MIME clause for the same qualifier names two, so an HTTPS
1575
+ * qualifier is conformant there and reported here. */
1576
+ message: "an id-qt-cps policy qualifier must be a HTTP URL",
1577
+ appliesTo: _isCs,
1578
+ check: function (cert, ctx) {
1579
+ var out = [];
1580
+ _eachPolicyQualifier(ctx, function (qid, value, pi) {
1581
+ if (qid !== OID_CPS) return;
1582
+ var uri = null;
1583
+ // allow:swallow-unverified a CPSuri that will not read as a string is not a URL either, so it falls to the report below rather than out of the rule
1584
+ try {
1585
+ uri = asn1.read.string(value);
1586
+ } catch (_e) {
1587
+ void 0;
1588
+ }
1589
+ if (pkiBuild.httpUrlScheme(uri) !== "http") {
1590
+ out.push({ context: { policyIdentifier: pi.policyIdentifier, cps: uri } });
1591
+ }
1592
+ });
1593
+ return out.length ? out : null;
1594
+ },
1595
+ },
1596
+ {
1597
+ id: "lint/cabf-cs/crldp-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1598
+ message: "a subscriber certificate must carry a cRLDistributionPoints extension",
1599
+ appliesTo: _isCs,
1600
+ check: function (cert, ctx) { return ctx.raw("cRLDistributionPoints") ? null : true; },
1601
+ },
1602
+ {
1603
+ id: "lint/cabf-cs/crldp-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1604
+ /** @internal "It MUST NOT be marked critical", where the S/MIME clause for the same extension
1605
+ * says SHOULD NOT, so the same certificate draws an error here and a warning there. */
1606
+ message: "cRLDistributionPoints must not be marked critical",
1607
+ appliesTo: _isCs,
1608
+ check: function (cert, ctx) {
1609
+ var e = ctx.raw("cRLDistributionPoints");
1610
+ return (e && e.critical === true) ? true : null;
1611
+ },
1612
+ },
1613
+ {
1614
+ id: "lint/cabf-cs/crldp-no-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
1615
+ /** @internal "it MUST contain the HTTP URL of the CA's CRL service". One HTTP URL is required
1616
+ * and the clause forbids no other scheme beside it. */
1617
+ message: "cRLDistributionPoints must contain a HTTP URL",
1618
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("cRLDistributionPoints"); },
1619
+ check: function (cert, ctx) {
1620
+ var uris = _crldpUris(ctx);
1621
+ return uris.some(_isHttpUri) ? null : { context: { uris: uris } };
1622
+ },
1623
+ },
1624
+ {
1625
+ id: "lint/cabf-cs/aia-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1626
+ /** @internal "This extension MUST be present", where the S/MIME clause asks at SHOULD. */
1627
+ message: "a subscriber certificate must carry an authorityInformationAccess extension",
1628
+ appliesTo: _isCs,
1629
+ check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
1630
+ },
1631
+ {
1632
+ id: "lint/cabf-cs/aia-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1633
+ message: "authorityInformationAccess must not be marked critical",
1634
+ appliesTo: _isCs,
1635
+ check: function (cert, ctx) {
1636
+ var e = ctx.raw("authorityInfoAccess");
1637
+ return (e && e.critical === true) ? true : null;
1638
+ },
1639
+ },
1640
+ {
1641
+ id: "lint/cabf-cs/aia-ca-issuers-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1642
+ /** @internal "It MUST contain the HTTP URL of the Issuing CA's certificate", so the method is
1643
+ * required rather than recommended, unlike the S/MIME clause's SHOULD. */
1644
+ message: "authorityInformationAccess must carry an id-ad-caIssuers accessMethod",
1645
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1646
+ check: function (cert, ctx) {
1647
+ var d = ctx.decode("authorityInfoAccess");
1648
+ if (!d || !Array.isArray(d.value)) return null;
1649
+ var wanted = oid.byName("caIssuers");
1650
+ for (var i = 0; i < d.value.length; i++) {
1651
+ if (d.value[i].accessMethod === wanted) return null;
1652
+ }
1653
+ return true;
1654
+ },
1655
+ },
1656
+ {
1657
+ id: "lint/cabf-cs/aia-non-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
1658
+ /** @internal The clause states one rule for both accessMethods it names, so unlike the S/MIME
1659
+ * clause there is no table per method and every location is held to the same reading. */
1660
+ message: "an authorityInformationAccess accessLocation must be a HTTP URL",
1661
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
1662
+ check: function (cert, ctx) {
1663
+ var locs = _csAiaLocations(ctx);
1664
+ var bad = locs.filter(function (u) { return !_isHttpUri(u); });
1665
+ if (!bad.length) return null;
1666
+ return { context: { uris: bad.filter(function (u) { return u !== null; }),
1667
+ nonUriLocations: bad.filter(function (u) { return u === null; }).length } };
1668
+ },
1669
+ },
1670
+ {
1671
+ id: "lint/cabf-cs/basic-constraints-ca-true", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (d)",
1672
+ message: "a subscriber certificate's basicConstraints cA field must not be true",
1673
+ appliesTo: _isCs,
1674
+ check: function (cert, ctx) {
1675
+ var d = ctx.decode("basicConstraints");
1676
+ return (d && d.value && d.value.cA === true) ? true : null;
1677
+ },
1678
+ },
1679
+ {
1680
+ id: "lint/cabf-cs/key-usage-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1681
+ message: "a subscriber certificate must carry a keyUsage extension",
1682
+ appliesTo: _isCs,
1683
+ check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
1684
+ },
1685
+ {
1686
+ id: "lint/cabf-cs/key-usage-not-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1687
+ /** @internal "MUST be marked critical", where the S/MIME clause says SHOULD. */
1688
+ message: "keyUsage must be marked critical",
1689
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
1690
+ check: function (cert, ctx) {
1691
+ var e = ctx.raw("keyUsage");
1692
+ return (e && e.critical === true) ? null : true;
1693
+ },
1694
+ },
1695
+ {
1696
+ id: "lint/cabf-cs/key-usage-missing-digital-signature", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1697
+ message: "keyUsage must assert digitalSignature",
1698
+ appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
1699
+ check: function (cert, ctx) {
1700
+ var d = ctx.decode("keyUsage");
1701
+ if (!d || !d.value) return null;
1702
+ return d.value.digitalSignature === true ? null : true;
1703
+ },
1704
+ },
1705
+ {
1706
+ id: "lint/cabf-cs/key-usage-forbidden-bit", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1707
+ /** @internal "Bit positions for keyCertSign and cRLSign MUST NOT be set." The next sentence
1708
+ * covers every other bit at SHOULD NOT, so the two lists are two rows at two strengths. */
1709
+ message: "keyUsage must not assert keyCertSign or cRLSign",
1710
+ appliesTo: _isCs,
1711
+ check: function (cert, ctx) {
1712
+ var d = ctx.decode("keyUsage");
1713
+ if (!d || !d.value) return null;
1714
+ var out = [];
1715
+ CS_FORBIDDEN_KU.forEach(function (bit) {
1716
+ if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
1717
+ });
1718
+ return out.length ? out : null;
1719
+ },
1720
+ },
1721
+ {
1722
+ id: "lint/cabf-cs/key-usage-discouraged-bit", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
1723
+ /** @internal "All other bit positions SHOULD NOT be set", which reaches every bit other than
1724
+ * the required digitalSignature and the two the row above forbids, including one set past the
1725
+ * nine RFC 5280 names. */
1726
+ message: "keyUsage should not assert a bit beyond digitalSignature",
1727
+ appliesTo: _isCs,
1728
+ check: function (cert, ctx) {
1729
+ var d = ctx.decode("keyUsage");
1730
+ if (!d || !d.value) return null;
1731
+ var out = [];
1732
+ KU_BIT_NAMES.forEach(function (bit) {
1733
+ if (bit === "digitalSignature" || CS_FORBIDDEN_KU.indexOf(bit) !== -1) return;
1734
+ if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
1735
+ });
1736
+ if (d.value.reservedBitsSet === true) out.push({ context: { reservedBitSet: true } });
1737
+ return out.length ? out : null;
1738
+ },
1739
+ },
1740
+ {
1741
+ id: "lint/cabf-cs/eku-missing-code-signing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1742
+ message: "a code signing certificate must carry id-kp-codeSigning",
1743
+ appliesTo: _isCsCodeSigning,
1744
+ check: function (cert, ctx) {
1745
+ var d = ctx.decode("extKeyUsage");
1746
+ if (!d || !Array.isArray(d.value)) return true;
1747
+ return d.value.indexOf(oid.byName("codeSigning")) === -1 ? true : null;
1748
+ },
1749
+ },
1750
+ {
1751
+ id: "lint/cabf-cs/eku-missing-time-stamping", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1752
+ message: "a timestamp certificate must carry id-kp-timeStamping",
1753
+ appliesTo: _isCsTimestamp,
1754
+ check: function (cert, ctx) {
1755
+ var d = ctx.decode("extKeyUsage");
1756
+ if (!d || !Array.isArray(d.value)) return true;
1757
+ return d.value.indexOf(oid.byName("timeStamping")) === -1 ? true : null;
1758
+ },
1759
+ },
1760
+ {
1761
+ id: "lint/cabf-cs/eku-not-critical-timestamp", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1762
+ /** @internal "id-kp-timeStamping MUST be present and MUST be marked critical", the one place
1763
+ * this section requires a critical extKeyUsage. */
1764
+ message: "a timestamp certificate's extKeyUsage must be marked critical",
1765
+ appliesTo: function (cert, ctx) { return _isCsTimestamp(cert, ctx) && !!ctx.raw("extKeyUsage"); },
1766
+ check: function (cert, ctx) {
1767
+ var e = ctx.raw("extKeyUsage");
1768
+ return (e && e.critical === true) ? null : true;
1769
+ },
1770
+ },
1771
+ {
1772
+ id: "lint/cabf-cs/eku-forbidden-purpose", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1773
+ message: "extKeyUsage must not carry anyExtendedKeyUsage or id-kp-serverAuth",
1774
+ appliesTo: _isCs,
1775
+ check: function (cert, ctx) {
1776
+ var d = ctx.decode("extKeyUsage");
1777
+ if (!d || !Array.isArray(d.value)) return null;
1778
+ var out = [];
1779
+ CS_FORBIDDEN_EKU.forEach(function (dotted) {
1780
+ if (d.value.indexOf(dotted) !== -1) {
1781
+ out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
1782
+ }
1783
+ });
1784
+ return out.length ? out : null;
1785
+ },
1786
+ },
1787
+ {
1788
+ id: "lint/cabf-cs/eku-discouraged-purpose", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
1789
+ /** @internal "Other values SHOULD NOT be present." The clause continues that a CA presenting
1790
+ * one must hold a business agreement with a platform vendor, which is a fact about the CA
1791
+ * rather than about the certificate, so this row reports the value and says nothing about the
1792
+ * agreement. The purpose each kind requires, the three the clause names as MAY, and the two
1793
+ * the row above forbids are all passed over here. */
1794
+ message: "extKeyUsage should not carry a purpose beyond the ones this profile names",
1795
+ appliesTo: _isCs,
1796
+ check: function (cert, ctx) {
1797
+ var d = ctx.decode("extKeyUsage");
1798
+ if (!d || !Array.isArray(d.value)) return null;
1799
+ var required = _isCsTimestamp(cert, ctx) ? oid.byName("timeStamping") : oid.byName("codeSigning");
1800
+ var out = [];
1801
+ d.value.forEach(function (dotted) {
1802
+ if (dotted === required) return;
1803
+ if (CS_PERMITTED_EXTRA_EKU.indexOf(dotted) !== -1) return;
1804
+ if (CS_FORBIDDEN_EKU.indexOf(dotted) !== -1) return;
1805
+ out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
1806
+ });
1807
+ return out.length ? out : null;
1808
+ },
1809
+ },
1810
+ {
1811
+ id: "lint/cabf-cs/aki-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
1812
+ message: "a subscriber certificate must carry an authorityKeyIdentifier extension",
1813
+ appliesTo: _isCs,
1814
+ check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
1815
+ },
1816
+ {
1817
+ id: "lint/cabf-cs/aki-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
1818
+ message: "authorityKeyIdentifier must not be marked critical",
1819
+ appliesTo: _isCs,
1820
+ check: function (cert, ctx) {
1821
+ var e = ctx.raw("authorityKeyIdentifier");
1822
+ return (e && e.critical === true) ? true : null;
1823
+ },
1824
+ },
1825
+ {
1826
+ id: "lint/cabf-cs/validity-too-long", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.3.2",
1827
+ /** @internal Three ceilings, keyed on the kind and on the issuance date: 39 months for a code
1828
+ * signing certificate issued before 1 March 2026, 460 days for one issued on or after it, and
1829
+ * 135 months for a timestamp certificate either side of that date. A ceiling stated in months
1830
+ * is measured in months, through guard.time.addMonths, since how many days 39 months hold
1831
+ * depends on which 39. */
1832
+ message: "the validity period exceeds the maximum this profile gives for the certificate's kind and issuance date",
1833
+ appliesTo: _isCs,
1834
+ check: function (cert, ctx) {
1835
+ var v = cert.validity;
1836
+ if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
1837
+ var notAfter = guard.time.instantOf(v.notAfter);
1838
+ if (_isCsTimestamp(cert, ctx)) {
1839
+ var tsCeiling = guard.time.addMonths(v.notBefore, 135, _cfg, "lint/bad-input", "notBefore");
1840
+ return notAfter > guard.time.instantOf(tsCeiling)
1841
+ ? { context: { maxMonths: 135 } } : null;
1842
+ }
1843
+ if (guard.time.instantOf(v.notBefore) < guard.time.instantOf(CS_VALIDITY_DAYS_START)) {
1844
+ var monthCeiling = guard.time.addMonths(v.notBefore, 39, _cfg, "lint/bad-input", "notBefore");
1845
+ return notAfter > guard.time.instantOf(monthCeiling)
1846
+ ? { context: { maxMonths: 39 } } : null;
1847
+ }
1848
+ var days = (notAfter - guard.time.instantOf(v.notBefore)) / MS_PER_DAY;
1849
+ return days > 460 ? { context: { days: Math.round(days), maxDays: 460 } } : null;
1850
+ },
1851
+ },
1852
+ {
1853
+ id: "lint/cabf-cs/weak-key", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.1.5.2",
1854
+ /** @internal The section names RSA, ECDSA and DSA and no other family, so a key outside the
1855
+ * three is reported: a certificate the profile covers must carry one it names. */
1856
+ message: "the subject public key is below the Code Signing BR minimum (RSA < 3072 bits, or a curve or family the section does not name)",
1857
+ appliesTo: _isCs,
1858
+ check: function (cert) {
1859
+ var spki = cert.subjectPublicKeyInfo, name = spki && spki.algorithm && spki.algorithm.name;
1860
+ if (RSA_KEY_ALGS[name]) {
1861
+ var bits = _rsaModulusBits(spki);
1862
+ return (bits === null || bits < CS_RSA_MIN_BITS) ? { context: { rsaBits: bits } } : null;
1863
+ }
1864
+ if (name === "ecPublicKey") {
1865
+ var curve = _ecCurveName(spki);
1866
+ return CS_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
1867
+ }
1868
+ if (name === "dsa") {
1869
+ var dsa = _dsaParamBits(spki);
1870
+ if (dsa === null) return { context: { keyAlgorithm: "dsa", dsaL: null, dsaN: null } };
1871
+ return (dsa.L === CS_DSA_L && CS_DSA_N.indexOf(dsa.N) !== -1)
1872
+ ? null : { context: { keyAlgorithm: "dsa", dsaL: dsa.L, dsaN: dsa.N } };
1873
+ }
1874
+ return { context: { keyAlgorithm: name || null } };
1875
+ },
1876
+ },
1877
+ ];
1878
+
1442
1879
  var CABF_TLS_RULES = [
1443
1880
  {
1444
1881
  id: "lint/cabf-tls/aia-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
@@ -3089,9 +3526,10 @@ var PROFILES = Object.assign(Object.create(null), {
3089
3526
  "rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3090
3527
  "cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3091
3528
  "cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3529
+ "cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
3092
3530
  });
3093
3531
  var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
3094
- .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES);
3532
+ .concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES);
3095
3533
 
3096
3534
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
3097
3535
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
package/lib/oid.js CHANGED
@@ -127,7 +127,8 @@ var FAMILIES = Object.assign(Object.create(null), {
127
127
  "smime-mailbox-legacy": [5, 1, 1], "smime-mailbox-multipurpose": [5, 1, 2], "smime-mailbox-strict": [5, 1, 3],
128
128
  "smime-organization-legacy": [5, 2, 1], "smime-organization-multipurpose": [5, 2, 2], "smime-organization-strict": [5, 2, 3],
129
129
  "smime-sponsor-legacy": [5, 3, 1], "smime-sponsor-multipurpose": [5, 3, 2], "smime-sponsor-strict": [5, 3, 3],
130
- "smime-individual-legacy": [5, 4, 1], "smime-individual-multipurpose": [5, 4, 2], "smime-individual-strict": [5, 4, 3] } },
130
+ "smime-individual-legacy": [5, 4, 1], "smime-individual-multipurpose": [5, 4, 2], "smime-individual-strict": [5, 4, 3],
131
+ "code-signing-ev": 3, "code-signing": [4, 1], "code-signing-timestamping": [4, 2] } },
131
132
 
132
133
  idCp: { base: [1, 3, 6, 1, 5, 5, 7, 14], of: { "id-cp-ipAddr-asNumber": 2, "id-cp-ipAddr-asNumber-v2": 3 } },
133
134
 
@@ -139,6 +140,17 @@ var FAMILIES = Object.assign(Object.create(null), {
139
140
  * generation. */
140
141
  adobe: { base: [1, 2, 840, 113583, 1, 1, 9], of: { "adobe-timestamp": 1, "adobe-archive-rev-info": 2 } },
141
142
 
143
+ /** @internal The extended key purposes Code Signing BR sec. 7.1.2.3 (f) names as MAY beside
144
+ * id-kp-codeSigning, so a row reporting any other purpose can exclude them by name. Document
145
+ * Signing is registered twice: Microsoft assigns it 1.3.6.1.4.1.311.10.3.12, the sibling of the
146
+ * Lifetime Signing arc printed two lines above it in that clause, and the clause itself prints
147
+ * 1.3.6.1.4.1.311.3.10.3.12. Both resolve, so a certificate carrying either is read as the
148
+ * purpose the clause permits. */
149
+ msEku: { base: [1, 3, 6, 1, 4, 1, 311], of: {
150
+ "ms-lifetime-signing": [10, 3, 13],
151
+ "ms-document-signing": [10, 3, 12],
152
+ "ms-document-signing-as-printed": [3, 10, 3, 12] } },
153
+
142
154
  gsmaRspRole: { base: [2, 23, 146, 1, 2, 1], of: {
143
155
  "id-rspRole-ci": 0, "id-rspRole-euicc-v2": 1, "id-rspRole-euicc": [0, 0, 0, 0, 0],
144
156
  "id-rspRole-eum-v2": 2, "id-rspRole-eum": [0, 0, 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
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:d298e2df-b5dc-4670-a00b-3f63a3d1a174",
5
+ "serialNumber": "urn:uuid:226c63e5-3f86-43a6-a5bd-ad58867e4706",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T09:13:14.788Z",
8
+ "timestamp": "2026-09-24T10:24:04.955Z",
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.18",
22
+ "bom-ref": "@blamejs/pki@0.8.19",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.18",
25
+ "version": "0.8.19",
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.18",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.19",
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.18",
57
+ "ref": "@blamejs/pki@0.8.19",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]