@blamejs/pki 0.8.18 → 0.8.20
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 +40 -0
- package/README.md +1 -1
- package/lib/guard-time.js +50 -0
- package/lib/lint.js +616 -3
- package/lib/oid.js +13 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ 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.20 — 2026-09-24
|
|
8
|
+
|
|
9
|
+
An extended validation code signing certificate is held to the subject its own section requires.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The subject fields both arms share (section 7.1.4.2.2): a `commonName` is required, a `domainComponent` is prohibited, and a subject attribute carrying only the metadata characters the clause names, a full stop, a hyphen and a space, is reported. An empty value states the same absence and is read the same way.
|
|
14
|
+
- The subject fields a code signing certificate must carry (sections 7.1.4.2.3 and 7.1.4.2.4): an `organizationName`, a `countryName` that is a two-letter code, and at least one of `localityName` and `stateOrProvinceName`, since each of those two is required exactly when the other is absent. Section 7.1.4.2.4 carries the address fields onto an extended validation certificate, so both arms answer for them.
|
|
15
|
+
- The subject fields an extended validation certificate adds (section 7.1.4.2.4): a `businessCategory` that is one of the four strings the clause names, a `jurisdictionCountryName`, and a `serialNumber`. The clause also bounds the jurisdiction fields against the level the incorporating or registration agency operates at, and requires the state or province at the locality level only where the state or province regulates registration there. A certificate carries neither fact, and a country with a locality and no state is conformant for such an agency, so no row grades the relationship and a valid certificate is not failed for it.
|
|
16
|
+
- The three reserved policy identifiers are read apart rather than together, so `2.23.140.1.3` selects the extended validation arm and `2.23.140.1.4.1` the ordinary one. The extension rules of section 7.1.2.3 ask the same question of both, and the subject rules do not.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- A subject attribute is read through one reader shared by every rule that asks for one, flattened across the relative distinguished names, because a distinguished name says the same thing whether it carries one multi-valued element or several single-valued ones.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Which rules a certificate answers for no longer depends on the order its policies sit in. A certificate asserting two reserved identifiers claims both, and the profile read only the first, so one asserting the ordinary code signing identifier ahead of the extended validation one escaped every extended validation rule while the same certificate with those two reversed did not. Section 7.1.6.4 reports more than one identifier, but only for a certificate issued on or after 15 September 2026, so nothing else caught it. A certificate claiming both kinds is now held to both, and to the tighter of the two validity ceilings.
|
|
25
|
+
|
|
26
|
+
## v0.8.19 — 2026-09-24
|
|
27
|
+
|
|
28
|
+
A code signing certificate is linted against the kind it declares itself to be.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `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.
|
|
33
|
+
- 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.
|
|
34
|
+
- cRLDistributionPoints (b): the extension is required, must not be critical, and must carry a HTTP URL for the CRL service.
|
|
35
|
+
- 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.
|
|
36
|
+
- basicConstraints (d) must not assert `cA`. authorityKeyIdentifier (g) is required and must not be critical.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
7
47
|
## v0.8.18 — 2026-09-24
|
|
8
48
|
|
|
9
49
|
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, 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 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); },
|
|
@@ -221,13 +224,25 @@ function _sanDnsNames(ctx) {
|
|
|
221
224
|
if (!d || !d.value || !Array.isArray(d.value.names)) return [];
|
|
222
225
|
return d.value.names.filter(function (n) { return n && n.tagNumber === 2; }).map(function (n) { return n.value; });
|
|
223
226
|
}
|
|
224
|
-
|
|
227
|
+
/** @internal Every subject attribute, flattened across the RDNs, since a distinguished name says
|
|
228
|
+
* the same thing whether it carries one multi-valued RDN or several single-valued ones. */
|
|
229
|
+
function _subjectAttributes(cert) {
|
|
225
230
|
var out = [];
|
|
226
231
|
((cert.subject && cert.subject.rdns) || []).forEach(function (rdn) {
|
|
227
|
-
rdn.forEach(function (a) {
|
|
232
|
+
rdn.forEach(function (a) { out.push(a); });
|
|
228
233
|
});
|
|
229
234
|
return out;
|
|
230
235
|
}
|
|
236
|
+
/** @internal The string values of one subject attribute type, named through the registry. */
|
|
237
|
+
function _subjectValuesOf(cert, name) {
|
|
238
|
+
var dotted = oid.byName(name);
|
|
239
|
+
var out = [];
|
|
240
|
+
_subjectAttributes(cert).forEach(function (a) {
|
|
241
|
+
if (a.type === dotted && typeof a.value === "string") out.push(a.value);
|
|
242
|
+
});
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function _subjectCNs(cert) { return _subjectValuesOf(cert, "commonName"); }
|
|
231
246
|
|
|
232
247
|
/** @internal The criticality RFC 5280 fixes for a certificate extension, read from the shared table
|
|
233
248
|
* the certificate signer refuses to write against. The profile has its own row for each extension
|
|
@@ -731,6 +746,154 @@ function _smimeKuMatchesAMode(modes, asserted) {
|
|
|
731
746
|
return false;
|
|
732
747
|
}
|
|
733
748
|
|
|
749
|
+
/** @internal Code Signing BR sec. 7.1.6.1 reserves three policy identifiers, keyed here to the KIND
|
|
750
|
+
* of certificate each declares. The EV and the non-EV code signing identifiers declare the same
|
|
751
|
+
* kind, because sec. 7.1.2.3 (f) asks one question of both; only the timestamping identifier takes
|
|
752
|
+
* the other arm. */
|
|
753
|
+
var CS_KIND_BY_POLICY = Object.create(null);
|
|
754
|
+
["code-signing", "code-signing-ev", "code-signing-timestamping"].forEach(function (name) {
|
|
755
|
+
var dotted = oid.byName(name);
|
|
756
|
+
if (dotted) CS_KIND_BY_POLICY[dotted] = name;
|
|
757
|
+
});
|
|
758
|
+
/** @internal "Effective September 15, 2026 a Certificate issued to a Subscriber MUST contain
|
|
759
|
+
* exactly one of the reserved policy OIDs specified in Section 7.1.6.1" (sec. 7.1.6.4). Dated, so
|
|
760
|
+
* a certificate issued before it is not held to a rule that did not yet apply. */
|
|
761
|
+
var CS_RESERVED_POLICY_REQUIRED = new Date("2026-09-15T00:00:00Z");
|
|
762
|
+
/** @internal Sec. 6.3.2 states three ceilings: 39 months for a code signing certificate issued
|
|
763
|
+
* before this date, 460 days for one issued on or after it, and 135 months for a timestamp
|
|
764
|
+
* certificate whichever side of it. */
|
|
765
|
+
var CS_VALIDITY_DAYS_START = new Date("2026-03-01T00:00:00Z");
|
|
766
|
+
/** @internal Sec. 6.1.5.2, the curves, the modulus floor and the two DSA parameter options for a
|
|
767
|
+
* subscriber key. The TLS profile names the same three curves from its own document; these are
|
|
768
|
+
* this one's. The section names RSA, ECDSA and DSA, so each family is read on its own terms and a
|
|
769
|
+
* family it does not name is what the row reports. */
|
|
770
|
+
var CS_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
|
|
771
|
+
var CS_RSA_MIN_BITS = 3072;
|
|
772
|
+
var CS_DSA_L = 2048;
|
|
773
|
+
var CS_DSA_N = [224, 256];
|
|
774
|
+
/** @internal The DSA key length L and modulus length N, read as the bit lengths of p and q in the
|
|
775
|
+
* Dss-Parms the AlgorithmIdentifier carries. */
|
|
776
|
+
function _dsaParamBits(spki) {
|
|
777
|
+
try {
|
|
778
|
+
var params = spki.algorithm && spki.algorithm.parameters;
|
|
779
|
+
if (!Buffer.isBuffer(params)) return null;
|
|
780
|
+
var seq = asn1.decode(params);
|
|
781
|
+
if (!seq.children || seq.children.length !== 3) return null;
|
|
782
|
+
var p = asn1.read.integer(seq.children[0]);
|
|
783
|
+
var q = asn1.read.integer(seq.children[1]);
|
|
784
|
+
asn1.read.integer(seq.children[2]);
|
|
785
|
+
if (p <= 0n || q <= 0n) return null;
|
|
786
|
+
return { L: p.toString(2).length, N: q.toString(2).length };
|
|
787
|
+
} catch (_e) { return null; }
|
|
788
|
+
}
|
|
789
|
+
/** @internal Sec. 7.1.2.3 (c) names these two accessMethods and no other. */
|
|
790
|
+
var CS_AIA_METHODS = ["ocsp", "caIssuers"].map(function (n) { return oid.byName(n); });
|
|
791
|
+
/** @internal Sec. 7.1.2.3 (f): forbidden at every kind, and the three the clause names as MAY
|
|
792
|
+
* beside id-kp-codeSigning, which the discouraged-purpose row passes over. */
|
|
793
|
+
var CS_FORBIDDEN_EKU = ["anyExtendedKeyUsage", "serverAuth"].map(function (n) { return oid.byName(n); });
|
|
794
|
+
var CS_PERMITTED_EXTRA_EKU = ["ms-lifetime-signing", "emailProtection", "ms-document-signing",
|
|
795
|
+
"ms-document-signing-as-printed"].map(function (n) { return oid.byName(n); });
|
|
796
|
+
/** @internal Sec. 7.1.2.3 (e) names these two as MUST NOT, and every other bit beside
|
|
797
|
+
* digitalSignature as SHOULD NOT, so one sentence becomes two rows at two strengths. */
|
|
798
|
+
var CS_FORBIDDEN_KU = ["keyCertSign", "cRLSign"];
|
|
799
|
+
/** @internal Sec. 7.1.4.2.4 (b): the field MUST contain one of these four strings, which the clause
|
|
800
|
+
* fixes exactly, so a differing case is a differing string. */
|
|
801
|
+
var CS_BUSINESS_CATEGORIES = ["Private Organization", "Government Entity", "Business Entity",
|
|
802
|
+
"Non-Commercial Entity"];
|
|
803
|
+
/** @internal Sec. 7.1.4.2.4 (c) bounds the jurisdiction fields against the LEVEL the incorporating
|
|
804
|
+
* or registration agency operates at, which the certificate does not carry, so no row grades the
|
|
805
|
+
* relationship between them. The clause requires the state or province at the locality level only
|
|
806
|
+
* "where the state or province regulates the registration of the entities at the locality level",
|
|
807
|
+
* so a country with a locality and no state is conformant for such an agency and reporting it
|
|
808
|
+
* would fail a valid certificate. The country is required outright and has its own row. */
|
|
809
|
+
|
|
810
|
+
/** @internal Whether a subject attribute value carries nothing but the metadata sec. 7.1.4.2.2 (d)
|
|
811
|
+
* names, which is a full stop, a hyphen and a space. An empty value is the same statement that
|
|
812
|
+
* the value is absent, so it is read the same way. The clause also covers "any other indication
|
|
813
|
+
* that the value is absent, incomplete, or not applicable", which names no further characters, so
|
|
814
|
+
* the row reports the ones it does name. */
|
|
815
|
+
function _isMetadataOnlyValue(s) {
|
|
816
|
+
if (typeof s !== "string") return false;
|
|
817
|
+
for (var i = 0; i < s.length; i++) {
|
|
818
|
+
var c = s.charCodeAt(i);
|
|
819
|
+
if (c !== 46 && c !== 45 && c !== 32) return false;
|
|
820
|
+
}
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
/** @internal Whether a value is the two-letter form sec. 7.1.4.2.3 (f) requires of a country code.
|
|
824
|
+
* The code's membership of ISO 3166-1 is not decided here, because the section names no list and
|
|
825
|
+
* the toolkit carries none; the builders hold their own country inputs to the same two-letter
|
|
826
|
+
* shape. */
|
|
827
|
+
function _isTwoLetterCountry(s) {
|
|
828
|
+
if (typeof s !== "string" || s.length !== 2) return false;
|
|
829
|
+
for (var i = 0; i < 2; i++) {
|
|
830
|
+
var c = s.charCodeAt(i);
|
|
831
|
+
if (c < 65 || c > 90) return false;
|
|
832
|
+
}
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** @internal Which reserved identifiers the certificate ASSERTS, as a set. Order in the extension
|
|
837
|
+
* carries no meaning, and a certificate asserting two of them claims both, so reading only the
|
|
838
|
+
* first would let the DER order decide which rules it answers for. Sec. 7.1.6.4 reports more than
|
|
839
|
+
* one, but only for a certificate issued on or after the date it states. */
|
|
840
|
+
function _csAsserted(ctx) {
|
|
841
|
+
var out = Object.create(null);
|
|
842
|
+
var d = ctx.decode("certificatePolicies");
|
|
843
|
+
if (!d || !Array.isArray(d.value)) return out;
|
|
844
|
+
for (var i = 0; i < d.value.length; i++) {
|
|
845
|
+
var k = CS_KIND_BY_POLICY[d.value[i].policyIdentifier];
|
|
846
|
+
if (k) out[k] = true;
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
/** @internal Sec. 7.1.6.3 gives a subordinate CA these identifiers too, so detection asks for the
|
|
851
|
+
* identifier AND that the certificate is not a CA, as the S/MIME profile does and for the same
|
|
852
|
+
* reason. Naming the profile overrides, which is how a malformed subscriber is checked. */
|
|
853
|
+
function _isCs(cert, ctx) {
|
|
854
|
+
if (ctx.explicitCsProfile) return true;
|
|
855
|
+
var a = _csAsserted(ctx);
|
|
856
|
+
var any = a["code-signing"] === true || a["code-signing-ev"] === true ||
|
|
857
|
+
a["code-signing-timestamping"] === true;
|
|
858
|
+
return any && !ctx.isCaCert;
|
|
859
|
+
}
|
|
860
|
+
/** @internal A certificate declaring no kind is read as a code signing one, which is the arm the
|
|
861
|
+
* section states first and the only arm a caller who named the profile without a reserved
|
|
862
|
+
* identifier can have meant. */
|
|
863
|
+
function _isCsTimestamp(cert, ctx) {
|
|
864
|
+
return _isCs(cert, ctx) && _csAsserted(ctx)["code-signing-timestamping"] === true;
|
|
865
|
+
}
|
|
866
|
+
/** @internal A certificate asserting a code signing identifier is one, and so is a certificate
|
|
867
|
+
* asserting none, which is the arm the section states first and the only arm a caller who named
|
|
868
|
+
* the profile without an identifier can have meant. A certificate asserting the timestamping
|
|
869
|
+
* identifier alone is not. */
|
|
870
|
+
function _isCsCodeSigning(cert, ctx) {
|
|
871
|
+
if (!_isCs(cert, ctx)) return false;
|
|
872
|
+
var a = _csAsserted(ctx);
|
|
873
|
+
if (a["code-signing"] === true || a["code-signing-ev"] === true) return true;
|
|
874
|
+
return a["code-signing-timestamping"] !== true;
|
|
875
|
+
}
|
|
876
|
+
/** @internal Sec. 7.1.4.2.4 covers EV code signing certificates alone, where sec. 7.1.4.2.3 covers
|
|
877
|
+
* non-EV ones and sec. 7.1.4.2.2 covers both. None of the three names a Timestamp Certificate, so
|
|
878
|
+
* the subject rows are scoped by the arm rather than by the profile. */
|
|
879
|
+
function _isCsEv(cert, ctx) { return _isCs(cert, ctx) && _csAsserted(ctx)["code-signing-ev"] === true; }
|
|
880
|
+
/** @internal The accessLocation URIs of the two accessMethods sec. 7.1.2.3 (c) names, flat: unlike
|
|
881
|
+
* the S/MIME clause this one states one rule for both, so they are not grouped by method. A
|
|
882
|
+
* location that is not a uniformResourceIdentifier is kept as null, since the method is still
|
|
883
|
+
* provided and still carries no HTTP URL. */
|
|
884
|
+
function _csAiaLocations(ctx) {
|
|
885
|
+
var d = ctx.decode("authorityInfoAccess");
|
|
886
|
+
if (!d || !Array.isArray(d.value)) return [];
|
|
887
|
+
var out = [];
|
|
888
|
+
for (var i = 0; i < d.value.length; i++) {
|
|
889
|
+
var ad = d.value[i];
|
|
890
|
+
if (CS_AIA_METHODS.indexOf(ad.accessMethod) === -1) continue;
|
|
891
|
+
var loc = ad.accessLocation;
|
|
892
|
+
out.push(loc && loc.tag === 6 && typeof loc.value === "string" ? loc.value : null);
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
|
|
734
897
|
/** @internal Sec. 7.1.2.3 (c) gives a URI scheme table for each of these two accessMethods and
|
|
735
898
|
* none for any other, so a scheme is measured on these and an unrecognized accessMethod has no
|
|
736
899
|
* table to be held to. */
|
|
@@ -1439,6 +1602,455 @@ var CABF_SMIME_RULES = [
|
|
|
1439
1602
|
},
|
|
1440
1603
|
];
|
|
1441
1604
|
|
|
1605
|
+
/** @internal The CA/Browser Forum Code Signing BR v3.11.0 subscriber profile. Every severity is
|
|
1606
|
+
* graded against the clause the row cites, never against the same-named row of another profile:
|
|
1607
|
+
* this document says MUST where the S/MIME one says SHOULD for cRLDistributionPoints criticality,
|
|
1608
|
+
* authorityInformationAccess presence and keyUsage criticality. */
|
|
1609
|
+
var CABF_CS_RULES = [
|
|
1610
|
+
{
|
|
1611
|
+
id: "lint/cabf-cs/policies-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
|
|
1612
|
+
message: "a subscriber certificate must carry a certificatePolicies extension",
|
|
1613
|
+
appliesTo: _isCs,
|
|
1614
|
+
check: function (cert, ctx) { return ctx.raw("certificatePolicies") ? null : true; },
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
id: "lint/cabf-cs/policies-critical", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
|
|
1618
|
+
message: "certificatePolicies should not be marked critical",
|
|
1619
|
+
appliesTo: _isCs,
|
|
1620
|
+
check: function (cert, ctx) {
|
|
1621
|
+
var e = ctx.raw("certificatePolicies");
|
|
1622
|
+
return (e && e.critical === true) ? true : null;
|
|
1623
|
+
},
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
id: "lint/cabf-cs/policies-reserved-identifier-count", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.6.4",
|
|
1627
|
+
/** @internal "a Certificate issued to a Subscriber MUST contain exactly one of the reserved
|
|
1628
|
+
* policy OIDs specified in Section 7.1.6.1", so both none and two are reported. */
|
|
1629
|
+
message: "a subscriber certificate must assert exactly one reserved code signing policy identifier",
|
|
1630
|
+
appliesTo: _isCs,
|
|
1631
|
+
effectiveDate: CS_RESERVED_POLICY_REQUIRED,
|
|
1632
|
+
check: function (cert, ctx) {
|
|
1633
|
+
var d = ctx.decode("certificatePolicies");
|
|
1634
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1635
|
+
var found = d.value.filter(function (pi) { return !!CS_KIND_BY_POLICY[pi.policyIdentifier]; });
|
|
1636
|
+
return found.length === 1 ? null : { context: { count: found.length } };
|
|
1637
|
+
},
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
id: "lint/cabf-cs/policies-cps-not-http-url", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (a)",
|
|
1641
|
+
/** @internal "HTTP URL for the Subordinate CA's Certification Practice Statement". This clause
|
|
1642
|
+
* names one scheme where the S/MIME clause for the same qualifier names two, so an HTTPS
|
|
1643
|
+
* qualifier is conformant there and reported here. */
|
|
1644
|
+
message: "an id-qt-cps policy qualifier must be a HTTP URL",
|
|
1645
|
+
appliesTo: _isCs,
|
|
1646
|
+
check: function (cert, ctx) {
|
|
1647
|
+
var out = [];
|
|
1648
|
+
_eachPolicyQualifier(ctx, function (qid, value, pi) {
|
|
1649
|
+
if (qid !== OID_CPS) return;
|
|
1650
|
+
var uri = null;
|
|
1651
|
+
// 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
|
|
1652
|
+
try {
|
|
1653
|
+
uri = asn1.read.string(value);
|
|
1654
|
+
} catch (_e) {
|
|
1655
|
+
void 0;
|
|
1656
|
+
}
|
|
1657
|
+
if (pkiBuild.httpUrlScheme(uri) !== "http") {
|
|
1658
|
+
out.push({ context: { policyIdentifier: pi.policyIdentifier, cps: uri } });
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
return out.length ? out : null;
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
id: "lint/cabf-cs/crldp-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
|
|
1666
|
+
message: "a subscriber certificate must carry a cRLDistributionPoints extension",
|
|
1667
|
+
appliesTo: _isCs,
|
|
1668
|
+
check: function (cert, ctx) { return ctx.raw("cRLDistributionPoints") ? null : true; },
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
id: "lint/cabf-cs/crldp-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
|
|
1672
|
+
/** @internal "It MUST NOT be marked critical", where the S/MIME clause for the same extension
|
|
1673
|
+
* says SHOULD NOT, so the same certificate draws an error here and a warning there. */
|
|
1674
|
+
message: "cRLDistributionPoints must not be marked critical",
|
|
1675
|
+
appliesTo: _isCs,
|
|
1676
|
+
check: function (cert, ctx) {
|
|
1677
|
+
var e = ctx.raw("cRLDistributionPoints");
|
|
1678
|
+
return (e && e.critical === true) ? true : null;
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
id: "lint/cabf-cs/crldp-no-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (b)",
|
|
1683
|
+
/** @internal "it MUST contain the HTTP URL of the CA's CRL service". One HTTP URL is required
|
|
1684
|
+
* and the clause forbids no other scheme beside it. */
|
|
1685
|
+
message: "cRLDistributionPoints must contain a HTTP URL",
|
|
1686
|
+
appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("cRLDistributionPoints"); },
|
|
1687
|
+
check: function (cert, ctx) {
|
|
1688
|
+
var uris = _crldpUris(ctx);
|
|
1689
|
+
return uris.some(_isHttpUri) ? null : { context: { uris: uris } };
|
|
1690
|
+
},
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
id: "lint/cabf-cs/aia-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
|
|
1694
|
+
/** @internal "This extension MUST be present", where the S/MIME clause asks at SHOULD. */
|
|
1695
|
+
message: "a subscriber certificate must carry an authorityInformationAccess extension",
|
|
1696
|
+
appliesTo: _isCs,
|
|
1697
|
+
check: function (cert, ctx) { return ctx.raw("authorityInfoAccess") ? null : true; },
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
id: "lint/cabf-cs/aia-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
|
|
1701
|
+
message: "authorityInformationAccess must not be marked critical",
|
|
1702
|
+
appliesTo: _isCs,
|
|
1703
|
+
check: function (cert, ctx) {
|
|
1704
|
+
var e = ctx.raw("authorityInfoAccess");
|
|
1705
|
+
return (e && e.critical === true) ? true : null;
|
|
1706
|
+
},
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
id: "lint/cabf-cs/aia-ca-issuers-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
|
|
1710
|
+
/** @internal "It MUST contain the HTTP URL of the Issuing CA's certificate", so the method is
|
|
1711
|
+
* required rather than recommended, unlike the S/MIME clause's SHOULD. */
|
|
1712
|
+
message: "authorityInformationAccess must carry an id-ad-caIssuers accessMethod",
|
|
1713
|
+
appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
|
|
1714
|
+
check: function (cert, ctx) {
|
|
1715
|
+
var d = ctx.decode("authorityInfoAccess");
|
|
1716
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1717
|
+
var wanted = oid.byName("caIssuers");
|
|
1718
|
+
for (var i = 0; i < d.value.length; i++) {
|
|
1719
|
+
if (d.value[i].accessMethod === wanted) return null;
|
|
1720
|
+
}
|
|
1721
|
+
return true;
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
id: "lint/cabf-cs/aia-non-http-uri", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (c)",
|
|
1726
|
+
/** @internal The clause states one rule for both accessMethods it names, so unlike the S/MIME
|
|
1727
|
+
* clause there is no table per method and every location is held to the same reading. */
|
|
1728
|
+
message: "an authorityInformationAccess accessLocation must be a HTTP URL",
|
|
1729
|
+
appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("authorityInfoAccess"); },
|
|
1730
|
+
check: function (cert, ctx) {
|
|
1731
|
+
var locs = _csAiaLocations(ctx);
|
|
1732
|
+
var bad = locs.filter(function (u) { return !_isHttpUri(u); });
|
|
1733
|
+
if (!bad.length) return null;
|
|
1734
|
+
return { context: { uris: bad.filter(function (u) { return u !== null; }),
|
|
1735
|
+
nonUriLocations: bad.filter(function (u) { return u === null; }).length } };
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
id: "lint/cabf-cs/basic-constraints-ca-true", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (d)",
|
|
1740
|
+
message: "a subscriber certificate's basicConstraints cA field must not be true",
|
|
1741
|
+
appliesTo: _isCs,
|
|
1742
|
+
check: function (cert, ctx) {
|
|
1743
|
+
var d = ctx.decode("basicConstraints");
|
|
1744
|
+
return (d && d.value && d.value.cA === true) ? true : null;
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
id: "lint/cabf-cs/key-usage-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
|
|
1749
|
+
message: "a subscriber certificate must carry a keyUsage extension",
|
|
1750
|
+
appliesTo: _isCs,
|
|
1751
|
+
check: function (cert, ctx) { return ctx.raw("keyUsage") ? null : true; },
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
id: "lint/cabf-cs/key-usage-not-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
|
|
1755
|
+
/** @internal "MUST be marked critical", where the S/MIME clause says SHOULD. */
|
|
1756
|
+
message: "keyUsage must be marked critical",
|
|
1757
|
+
appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
|
|
1758
|
+
check: function (cert, ctx) {
|
|
1759
|
+
var e = ctx.raw("keyUsage");
|
|
1760
|
+
return (e && e.critical === true) ? null : true;
|
|
1761
|
+
},
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
id: "lint/cabf-cs/key-usage-missing-digital-signature", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
|
|
1765
|
+
message: "keyUsage must assert digitalSignature",
|
|
1766
|
+
appliesTo: function (cert, ctx) { return _isCs(cert, ctx) && !!ctx.raw("keyUsage"); },
|
|
1767
|
+
check: function (cert, ctx) {
|
|
1768
|
+
var d = ctx.decode("keyUsage");
|
|
1769
|
+
if (!d || !d.value) return null;
|
|
1770
|
+
return d.value.digitalSignature === true ? null : true;
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
id: "lint/cabf-cs/key-usage-forbidden-bit", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
|
|
1775
|
+
/** @internal "Bit positions for keyCertSign and cRLSign MUST NOT be set." The next sentence
|
|
1776
|
+
* covers every other bit at SHOULD NOT, so the two lists are two rows at two strengths. */
|
|
1777
|
+
message: "keyUsage must not assert keyCertSign or cRLSign",
|
|
1778
|
+
appliesTo: _isCs,
|
|
1779
|
+
check: function (cert, ctx) {
|
|
1780
|
+
var d = ctx.decode("keyUsage");
|
|
1781
|
+
if (!d || !d.value) return null;
|
|
1782
|
+
var out = [];
|
|
1783
|
+
CS_FORBIDDEN_KU.forEach(function (bit) {
|
|
1784
|
+
if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
|
|
1785
|
+
});
|
|
1786
|
+
return out.length ? out : null;
|
|
1787
|
+
},
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
id: "lint/cabf-cs/key-usage-discouraged-bit", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (e)",
|
|
1791
|
+
/** @internal "All other bit positions SHOULD NOT be set", which reaches every bit other than
|
|
1792
|
+
* the required digitalSignature and the two the row above forbids, including one set past the
|
|
1793
|
+
* nine RFC 5280 names. */
|
|
1794
|
+
message: "keyUsage should not assert a bit beyond digitalSignature",
|
|
1795
|
+
appliesTo: _isCs,
|
|
1796
|
+
check: function (cert, ctx) {
|
|
1797
|
+
var d = ctx.decode("keyUsage");
|
|
1798
|
+
if (!d || !d.value) return null;
|
|
1799
|
+
var out = [];
|
|
1800
|
+
KU_BIT_NAMES.forEach(function (bit) {
|
|
1801
|
+
if (bit === "digitalSignature" || CS_FORBIDDEN_KU.indexOf(bit) !== -1) return;
|
|
1802
|
+
if (d.value[bit] === true) out.push({ context: { keyUsage: bit } });
|
|
1803
|
+
});
|
|
1804
|
+
if (d.value.reservedBitsSet === true) out.push({ context: { reservedBitSet: true } });
|
|
1805
|
+
return out.length ? out : null;
|
|
1806
|
+
},
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
id: "lint/cabf-cs/eku-missing-code-signing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
|
|
1810
|
+
message: "a code signing certificate must carry id-kp-codeSigning",
|
|
1811
|
+
appliesTo: _isCsCodeSigning,
|
|
1812
|
+
check: function (cert, ctx) {
|
|
1813
|
+
var d = ctx.decode("extKeyUsage");
|
|
1814
|
+
if (!d || !Array.isArray(d.value)) return true;
|
|
1815
|
+
return d.value.indexOf(oid.byName("codeSigning")) === -1 ? true : null;
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
id: "lint/cabf-cs/eku-missing-time-stamping", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
|
|
1820
|
+
message: "a timestamp certificate must carry id-kp-timeStamping",
|
|
1821
|
+
appliesTo: _isCsTimestamp,
|
|
1822
|
+
check: function (cert, ctx) {
|
|
1823
|
+
var d = ctx.decode("extKeyUsage");
|
|
1824
|
+
if (!d || !Array.isArray(d.value)) return true;
|
|
1825
|
+
return d.value.indexOf(oid.byName("timeStamping")) === -1 ? true : null;
|
|
1826
|
+
},
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
id: "lint/cabf-cs/eku-not-critical-timestamp", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
|
|
1830
|
+
/** @internal "id-kp-timeStamping MUST be present and MUST be marked critical", the one place
|
|
1831
|
+
* this section requires a critical extKeyUsage. */
|
|
1832
|
+
message: "a timestamp certificate's extKeyUsage must be marked critical",
|
|
1833
|
+
appliesTo: function (cert, ctx) { return _isCsTimestamp(cert, ctx) && !!ctx.raw("extKeyUsage"); },
|
|
1834
|
+
check: function (cert, ctx) {
|
|
1835
|
+
var e = ctx.raw("extKeyUsage");
|
|
1836
|
+
return (e && e.critical === true) ? null : true;
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
id: "lint/cabf-cs/eku-forbidden-purpose", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
|
|
1841
|
+
message: "extKeyUsage must not carry anyExtendedKeyUsage or id-kp-serverAuth",
|
|
1842
|
+
appliesTo: _isCs,
|
|
1843
|
+
check: function (cert, ctx) {
|
|
1844
|
+
var d = ctx.decode("extKeyUsage");
|
|
1845
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1846
|
+
var out = [];
|
|
1847
|
+
CS_FORBIDDEN_EKU.forEach(function (dotted) {
|
|
1848
|
+
if (d.value.indexOf(dotted) !== -1) {
|
|
1849
|
+
out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
return out.length ? out : null;
|
|
1853
|
+
},
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
id: "lint/cabf-cs/eku-discouraged-purpose", severity: "warn", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (f)",
|
|
1857
|
+
/** @internal "Other values SHOULD NOT be present." The clause continues that a CA presenting
|
|
1858
|
+
* one must hold a business agreement with a platform vendor, which is a fact about the CA
|
|
1859
|
+
* rather than about the certificate, so this row reports the value and says nothing about the
|
|
1860
|
+
* agreement. The purpose each kind requires, the three the clause names as MAY, and the two
|
|
1861
|
+
* the row above forbids are all passed over here. */
|
|
1862
|
+
message: "extKeyUsage should not carry a purpose beyond the ones this profile names",
|
|
1863
|
+
appliesTo: _isCs,
|
|
1864
|
+
check: function (cert, ctx) {
|
|
1865
|
+
var d = ctx.decode("extKeyUsage");
|
|
1866
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
1867
|
+
var required = [];
|
|
1868
|
+
if (_isCsTimestamp(cert, ctx)) required.push(oid.byName("timeStamping"));
|
|
1869
|
+
if (_isCsCodeSigning(cert, ctx)) required.push(oid.byName("codeSigning"));
|
|
1870
|
+
var out = [];
|
|
1871
|
+
d.value.forEach(function (dotted) {
|
|
1872
|
+
if (required.indexOf(dotted) !== -1) return;
|
|
1873
|
+
if (CS_PERMITTED_EXTRA_EKU.indexOf(dotted) !== -1) return;
|
|
1874
|
+
if (CS_FORBIDDEN_EKU.indexOf(dotted) !== -1) return;
|
|
1875
|
+
out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
|
|
1876
|
+
});
|
|
1877
|
+
return out.length ? out : null;
|
|
1878
|
+
},
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
id: "lint/cabf-cs/aki-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
|
|
1882
|
+
message: "a subscriber certificate must carry an authorityKeyIdentifier extension",
|
|
1883
|
+
appliesTo: _isCs,
|
|
1884
|
+
check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
id: "lint/cabf-cs/aki-critical", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.2.3 (g)",
|
|
1888
|
+
message: "authorityKeyIdentifier must not be marked critical",
|
|
1889
|
+
appliesTo: _isCs,
|
|
1890
|
+
check: function (cert, ctx) {
|
|
1891
|
+
var e = ctx.raw("authorityKeyIdentifier");
|
|
1892
|
+
return (e && e.critical === true) ? true : null;
|
|
1893
|
+
},
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
id: "lint/cabf-cs/validity-too-long", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.3.2",
|
|
1897
|
+
/** @internal Three ceilings, keyed on the kind and on the issuance date: 39 months for a code
|
|
1898
|
+
* signing certificate issued before 1 March 2026, 460 days for one issued on or after it, and
|
|
1899
|
+
* 135 months for a timestamp certificate either side of that date. A ceiling stated in months
|
|
1900
|
+
* is measured in months, through guard.time.addMonths, since how many days 39 months hold
|
|
1901
|
+
* depends on which 39. */
|
|
1902
|
+
message: "the validity period exceeds the maximum this profile gives for the certificate's kind and issuance date",
|
|
1903
|
+
appliesTo: _isCs,
|
|
1904
|
+
check: function (cert, ctx) {
|
|
1905
|
+
var v = cert.validity;
|
|
1906
|
+
if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
|
|
1907
|
+
var notAfter = guard.time.instantOf(v.notAfter);
|
|
1908
|
+
/** @internal A certificate claiming both kinds is held to the code signing ceiling, the
|
|
1909
|
+
* tighter of the two, since it must satisfy every ceiling it claims. */
|
|
1910
|
+
if (!_isCsCodeSigning(cert, ctx)) {
|
|
1911
|
+
var tsCeiling = guard.time.addMonths(v.notBefore, 135, _cfg, "lint/bad-input", "notBefore");
|
|
1912
|
+
return notAfter > guard.time.instantOf(tsCeiling)
|
|
1913
|
+
? { context: { maxMonths: 135 } } : null;
|
|
1914
|
+
}
|
|
1915
|
+
if (guard.time.instantOf(v.notBefore) < guard.time.instantOf(CS_VALIDITY_DAYS_START)) {
|
|
1916
|
+
var monthCeiling = guard.time.addMonths(v.notBefore, 39, _cfg, "lint/bad-input", "notBefore");
|
|
1917
|
+
return notAfter > guard.time.instantOf(monthCeiling)
|
|
1918
|
+
? { context: { maxMonths: 39 } } : null;
|
|
1919
|
+
}
|
|
1920
|
+
var days = (notAfter - guard.time.instantOf(v.notBefore)) / MS_PER_DAY;
|
|
1921
|
+
return days > 460 ? { context: { days: Math.round(days), maxDays: 460 } } : null;
|
|
1922
|
+
},
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
id: "lint/cabf-cs/weak-key", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 6.1.5.2",
|
|
1926
|
+
/** @internal The section names RSA, ECDSA and DSA and no other family, so a key outside the
|
|
1927
|
+
* three is reported: a certificate the profile covers must carry one it names. */
|
|
1928
|
+
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)",
|
|
1929
|
+
appliesTo: _isCs,
|
|
1930
|
+
check: function (cert) {
|
|
1931
|
+
var spki = cert.subjectPublicKeyInfo, name = spki && spki.algorithm && spki.algorithm.name;
|
|
1932
|
+
if (RSA_KEY_ALGS[name]) {
|
|
1933
|
+
var bits = _rsaModulusBits(spki);
|
|
1934
|
+
return (bits === null || bits < CS_RSA_MIN_BITS) ? { context: { rsaBits: bits } } : null;
|
|
1935
|
+
}
|
|
1936
|
+
if (name === "ecPublicKey") {
|
|
1937
|
+
var curve = _ecCurveName(spki);
|
|
1938
|
+
return CS_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null;
|
|
1939
|
+
}
|
|
1940
|
+
if (name === "dsa") {
|
|
1941
|
+
var dsa = _dsaParamBits(spki);
|
|
1942
|
+
if (dsa === null) return { context: { keyAlgorithm: "dsa", dsaL: null, dsaN: null } };
|
|
1943
|
+
return (dsa.L === CS_DSA_L && CS_DSA_N.indexOf(dsa.N) !== -1)
|
|
1944
|
+
? null : { context: { keyAlgorithm: "dsa", dsaL: dsa.L, dsaN: dsa.N } };
|
|
1945
|
+
}
|
|
1946
|
+
return { context: { keyAlgorithm: name || null } };
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
id: "lint/cabf-cs/subject-common-name-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.2 (a)",
|
|
1951
|
+
message: "a code signing certificate's subject must carry a commonName",
|
|
1952
|
+
appliesTo: _isCsCodeSigning,
|
|
1953
|
+
check: function (cert) { return _subjectValuesOf(cert, "commonName").length ? null : true; },
|
|
1954
|
+
},
|
|
1955
|
+
{
|
|
1956
|
+
id: "lint/cabf-cs/subject-domain-component-prohibited", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.2 (c)",
|
|
1957
|
+
message: "a code signing certificate's subject must not carry a domainComponent",
|
|
1958
|
+
appliesTo: _isCsCodeSigning,
|
|
1959
|
+
check: function (cert) {
|
|
1960
|
+
var n = _subjectValuesOf(cert, "domainComponent").length;
|
|
1961
|
+
return n ? { context: { count: n } } : null;
|
|
1962
|
+
},
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
id: "lint/cabf-cs/subject-attribute-metadata-only", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.2 (d)",
|
|
1966
|
+
/** @internal "Subject attributes MUST NOT contain only metadata such as '.', '-', and ' '
|
|
1967
|
+
* (i.e. space) characters, and/or any other indication that the value is absent, incomplete,
|
|
1968
|
+
* or not applicable." Read over every attribute, since the clause names none in particular. */
|
|
1969
|
+
message: "a subject attribute carries only metadata characters",
|
|
1970
|
+
appliesTo: _isCsCodeSigning,
|
|
1971
|
+
check: function (cert) {
|
|
1972
|
+
var out = [];
|
|
1973
|
+
_subjectAttributes(cert).forEach(function (a) {
|
|
1974
|
+
if (typeof a.value !== "string" || !_isMetadataOnlyValue(a.value)) return;
|
|
1975
|
+
out.push({ context: { attribute: oid.name(a.type) || a.type, value: a.value } });
|
|
1976
|
+
});
|
|
1977
|
+
return out.length ? out : null;
|
|
1978
|
+
},
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
id: "lint/cabf-cs/subject-organization-name-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.3 (a) / 7.1.4.2.4 (a)",
|
|
1982
|
+
/** @internal Required by the non-EV subsection and again by the EV one, so the row covers both
|
|
1983
|
+
* arms and cites each clause. */
|
|
1984
|
+
message: "a code signing certificate's subject must carry an organizationName",
|
|
1985
|
+
appliesTo: _isCsCodeSigning,
|
|
1986
|
+
check: function (cert) { return _subjectValuesOf(cert, "organizationName").length ? null : true; },
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
id: "lint/cabf-cs/subject-locality-and-state-absent", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.3 (c) and (d)",
|
|
1990
|
+
/** @internal localityName is "Required if the stateOrProvinceName field is absent" and the
|
|
1991
|
+
* state row says the same in the other direction, so one of the two must be present and
|
|
1992
|
+
* either alone satisfies both. Sec. 7.1.4.2.4 (e) carries these rows onto an EV certificate. */
|
|
1993
|
+
message: "a code signing certificate's subject must carry a localityName or a stateOrProvinceName",
|
|
1994
|
+
appliesTo: _isCsCodeSigning,
|
|
1995
|
+
check: function (cert) {
|
|
1996
|
+
return (_subjectValuesOf(cert, "localityName").length ||
|
|
1997
|
+
_subjectValuesOf(cert, "stateOrProvinceName").length) ? null : true;
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
id: "lint/cabf-cs/subject-country-name-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.3 (f)",
|
|
2002
|
+
message: "a code signing certificate's subject must carry a countryName",
|
|
2003
|
+
appliesTo: _isCsCodeSigning,
|
|
2004
|
+
check: function (cert) { return _subjectValuesOf(cert, "countryName").length ? null : true; },
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
id: "lint/cabf-cs/subject-country-name-bad-syntax", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.3 (f)",
|
|
2008
|
+
/** @internal "MUST contain the two-letter ISO 3166-1 country code", with the user-assigned XX
|
|
2009
|
+
* admitted by the same clause, which is itself two letters. */
|
|
2010
|
+
message: "a subject countryName must be a two-letter code",
|
|
2011
|
+
appliesTo: _isCsCodeSigning,
|
|
2012
|
+
check: function (cert) {
|
|
2013
|
+
var out = [];
|
|
2014
|
+
_subjectValuesOf(cert, "countryName").forEach(function (v) {
|
|
2015
|
+
if (!_isTwoLetterCountry(v)) out.push({ context: { countryName: v } });
|
|
2016
|
+
});
|
|
2017
|
+
return out.length ? out : null;
|
|
2018
|
+
},
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
id: "lint/cabf-cs/subject-business-category-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.4 (b)",
|
|
2022
|
+
message: "an EV code signing certificate's subject must carry a businessCategory",
|
|
2023
|
+
appliesTo: _isCsEv,
|
|
2024
|
+
check: function (cert) { return _subjectValuesOf(cert, "businessCategory").length ? null : true; },
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
id: "lint/cabf-cs/subject-business-category-invalid", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.4 (b)",
|
|
2028
|
+
message: "a subject businessCategory must be one of the four strings this clause names",
|
|
2029
|
+
appliesTo: _isCsEv,
|
|
2030
|
+
check: function (cert) {
|
|
2031
|
+
var out = [];
|
|
2032
|
+
_subjectValuesOf(cert, "businessCategory").forEach(function (v) {
|
|
2033
|
+
if (CS_BUSINESS_CATEGORIES.indexOf(v) === -1) out.push({ context: { businessCategory: v } });
|
|
2034
|
+
});
|
|
2035
|
+
return out.length ? out : null;
|
|
2036
|
+
},
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
id: "lint/cabf-cs/subject-jurisdiction-country-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.4 (c)",
|
|
2040
|
+
/** @internal The clause lists the locality and the state as required only where the agency
|
|
2041
|
+
* operates at that level, and the country without that qualification. */
|
|
2042
|
+
message: "an EV code signing certificate's subject must carry a jurisdictionCountryName",
|
|
2043
|
+
appliesTo: _isCsEv,
|
|
2044
|
+
check: function (cert) { return _subjectValuesOf(cert, "jurisdictionCountryName").length ? null : true; },
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
id: "lint/cabf-cs/subject-serial-number-missing", severity: "error", source: "cabf-cs", citation: "CABF Code Signing BR 7.1.4.2.4 (d)",
|
|
2048
|
+
message: "an EV code signing certificate's subject must carry a serialNumber",
|
|
2049
|
+
appliesTo: _isCsEv,
|
|
2050
|
+
check: function (cert) { return _subjectValuesOf(cert, "serialNumber").length ? null : true; },
|
|
2051
|
+
},
|
|
2052
|
+
];
|
|
2053
|
+
|
|
1442
2054
|
var CABF_TLS_RULES = [
|
|
1443
2055
|
{
|
|
1444
2056
|
id: "lint/cabf-tls/aia-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
|
|
@@ -3089,9 +3701,10 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
3089
3701
|
"rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
3090
3702
|
"cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
3091
3703
|
"cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
3704
|
+
"cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
3092
3705
|
});
|
|
3093
3706
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
3094
|
-
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES);
|
|
3707
|
+
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES);
|
|
3095
3708
|
|
|
3096
3709
|
var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
3097
3710
|
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
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:8eac5f1d-fefa-406c-937a-2a4c3d5e7164",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-24T11:09:13.708Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.20",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.20",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.20",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.8.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.20",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|