@blamejs/pki 0.8.19 → 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 +19 -0
- package/README.md +1 -1
- package/lib/lint.js +193 -18
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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
|
+
|
|
7
26
|
## v0.8.19 — 2026-09-24
|
|
8
27
|
|
|
9
28
|
A code signing certificate is linted against the kind 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 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` |
|
|
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/lint.js
CHANGED
|
@@ -224,13 +224,25 @@ function _sanDnsNames(ctx) {
|
|
|
224
224
|
if (!d || !d.value || !Array.isArray(d.value.names)) return [];
|
|
225
225
|
return d.value.names.filter(function (n) { return n && n.tagNumber === 2; }).map(function (n) { return n.value; });
|
|
226
226
|
}
|
|
227
|
-
|
|
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) {
|
|
228
230
|
var out = [];
|
|
229
231
|
((cert.subject && cert.subject.rdns) || []).forEach(function (rdn) {
|
|
230
|
-
rdn.forEach(function (a) {
|
|
232
|
+
rdn.forEach(function (a) { out.push(a); });
|
|
231
233
|
});
|
|
232
234
|
return out;
|
|
233
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"); }
|
|
234
246
|
|
|
235
247
|
/** @internal The criticality RFC 5280 fixes for a certificate extension, read from the shared table
|
|
236
248
|
* the certificate signer refuses to write against. The profile has its own row for each extension
|
|
@@ -739,10 +751,9 @@ function _smimeKuMatchesAMode(modes, asserted) {
|
|
|
739
751
|
* kind, because sec. 7.1.2.3 (f) asks one question of both; only the timestamping identifier takes
|
|
740
752
|
* the other arm. */
|
|
741
753
|
var CS_KIND_BY_POLICY = Object.create(null);
|
|
742
|
-
[
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (dotted) CS_KIND_BY_POLICY[dotted] = row[1];
|
|
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;
|
|
746
757
|
});
|
|
747
758
|
/** @internal "Effective September 15, 2026 a Certificate issued to a Subscriber MUST contain
|
|
748
759
|
* exactly one of the reserved policy OIDs specified in Section 7.1.6.1" (sec. 7.1.6.4). Dated, so
|
|
@@ -785,30 +796,87 @@ var CS_PERMITTED_EXTRA_EKU = ["ms-lifetime-signing", "emailProtection", "ms-docu
|
|
|
785
796
|
/** @internal Sec. 7.1.2.3 (e) names these two as MUST NOT, and every other bit beside
|
|
786
797
|
* digitalSignature as SHOULD NOT, so one sentence becomes two rows at two strengths. */
|
|
787
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. */
|
|
788
809
|
|
|
789
|
-
/** @internal
|
|
790
|
-
|
|
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);
|
|
791
842
|
var d = ctx.decode("certificatePolicies");
|
|
792
|
-
if (!d || !Array.isArray(d.value)) return
|
|
793
|
-
var found = null;
|
|
843
|
+
if (!d || !Array.isArray(d.value)) return out;
|
|
794
844
|
for (var i = 0; i < d.value.length; i++) {
|
|
795
845
|
var k = CS_KIND_BY_POLICY[d.value[i].policyIdentifier];
|
|
796
|
-
if (k
|
|
846
|
+
if (k) out[k] = true;
|
|
797
847
|
}
|
|
798
|
-
return
|
|
848
|
+
return out;
|
|
799
849
|
}
|
|
800
850
|
/** @internal Sec. 7.1.6.3 gives a subordinate CA these identifiers too, so detection asks for the
|
|
801
851
|
* identifier AND that the certificate is not a CA, as the S/MIME profile does and for the same
|
|
802
852
|
* reason. Naming the profile overrides, which is how a malformed subscriber is checked. */
|
|
803
853
|
function _isCs(cert, ctx) {
|
|
804
854
|
if (ctx.explicitCsProfile) return true;
|
|
805
|
-
|
|
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;
|
|
806
859
|
}
|
|
807
860
|
/** @internal A certificate declaring no kind is read as a code signing one, which is the arm the
|
|
808
861
|
* section states first and the only arm a caller who named the profile without a reserved
|
|
809
862
|
* identifier can have meant. */
|
|
810
|
-
function _isCsTimestamp(cert, ctx) {
|
|
811
|
-
|
|
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; }
|
|
812
880
|
/** @internal The accessLocation URIs of the two accessMethods sec. 7.1.2.3 (c) names, flat: unlike
|
|
813
881
|
* the S/MIME clause this one states one rule for both, so they are not grouped by method. A
|
|
814
882
|
* location that is not a uniformResourceIdentifier is kept as null, since the method is still
|
|
@@ -1796,10 +1864,12 @@ var CABF_CS_RULES = [
|
|
|
1796
1864
|
check: function (cert, ctx) {
|
|
1797
1865
|
var d = ctx.decode("extKeyUsage");
|
|
1798
1866
|
if (!d || !Array.isArray(d.value)) return null;
|
|
1799
|
-
var required =
|
|
1867
|
+
var required = [];
|
|
1868
|
+
if (_isCsTimestamp(cert, ctx)) required.push(oid.byName("timeStamping"));
|
|
1869
|
+
if (_isCsCodeSigning(cert, ctx)) required.push(oid.byName("codeSigning"));
|
|
1800
1870
|
var out = [];
|
|
1801
1871
|
d.value.forEach(function (dotted) {
|
|
1802
|
-
if (dotted
|
|
1872
|
+
if (required.indexOf(dotted) !== -1) return;
|
|
1803
1873
|
if (CS_PERMITTED_EXTRA_EKU.indexOf(dotted) !== -1) return;
|
|
1804
1874
|
if (CS_FORBIDDEN_EKU.indexOf(dotted) !== -1) return;
|
|
1805
1875
|
out.push({ context: { purpose: dotted, name: oid.name(dotted) || null } });
|
|
@@ -1835,7 +1905,9 @@ var CABF_CS_RULES = [
|
|
|
1835
1905
|
var v = cert.validity;
|
|
1836
1906
|
if (!guard.time.isDate(v.notBefore) || !guard.time.isDate(v.notAfter)) return null;
|
|
1837
1907
|
var notAfter = guard.time.instantOf(v.notAfter);
|
|
1838
|
-
|
|
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)) {
|
|
1839
1911
|
var tsCeiling = guard.time.addMonths(v.notBefore, 135, _cfg, "lint/bad-input", "notBefore");
|
|
1840
1912
|
return notAfter > guard.time.instantOf(tsCeiling)
|
|
1841
1913
|
? { context: { maxMonths: 135 } } : null;
|
|
@@ -1874,6 +1946,109 @@ var CABF_CS_RULES = [
|
|
|
1874
1946
|
return { context: { keyAlgorithm: name || null } };
|
|
1875
1947
|
},
|
|
1876
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
|
+
},
|
|
1877
2052
|
];
|
|
1878
2053
|
|
|
1879
2054
|
var CABF_TLS_RULES = [
|
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
|
]
|