@blamejs/pki 0.8.17 → 0.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +1 -1
- package/lib/guard-time.js +50 -0
- package/lib/lint.js +1307 -35
- package/lib/oid.js +29 -1
- package/lib/pki-build.js +76 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.8.19 — 2026-09-24
|
|
8
|
+
|
|
9
|
+
A code signing certificate is linted against the kind it declares itself to be.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.lint.certificate(der, { profile: "cabf-cs" })` runs the Code Signing BR v3.11.0 subscriber profile. The rows reach a certificate unprompted too, because one asserting a reserved identifier has declared itself to be one. Detection reads the decoded extension, so a certificate whose certificatePolicies does not decode declares nothing and is reported for the encoding instead. A certificate issued to a subordinate CA carries a reserved identifier as well under section 7.1.6.3, so it is not read as a subscriber; naming the profile in the call still lints it as one.
|
|
14
|
+
- certificatePolicies (a): the extension is required and should not be critical, and an `id-qt-cps` qualifier must be a HTTP URL. Section 7.1.6.4 requires exactly one reserved identifier for a certificate issued on or after 15 September 2026, gated to the issuance date the certificate states.
|
|
15
|
+
- cRLDistributionPoints (b): the extension is required, must not be critical, and must carry a HTTP URL for the CRL service.
|
|
16
|
+
- authorityInformationAccess (c): the extension is required, must not be critical, must carry an `id-ad-caIssuers` accessMethod, and each location the clause names must be a HTTP URL. Unlike the S/MIME profile, one rule covers both accessMethods, because this clause states one.
|
|
17
|
+
- basicConstraints (d) must not assert `cA`. authorityKeyIdentifier (g) is required and must not be critical.
|
|
18
|
+
- keyUsage (e): the extension is required, must be critical, and must assert `digitalSignature`. `keyCertSign` and `cRLSign` are reported as errors and every other bit as a warning, because the clause states the two lists at different strengths in consecutive sentences.
|
|
19
|
+
- extKeyUsage (f): a code signing certificate must carry `id-kp-codeSigning` and a timestamp certificate `id-kp-timeStamping`, and a timestamp certificate's extKeyUsage must be marked critical, which is the one place this profile requires a critical extKeyUsage. `anyExtendedKeyUsage` and `id-kp-serverAuth` are forbidden. Any other purpose is reported as a warning, leaving the three the clause names as permitted: Lifetime Signing, `id-kp-emailProtection` and Document Signing. Document Signing is read under both identifiers, the `1.3.6.1.4.1.311.10.3.12` Microsoft assigns it and the `1.3.6.1.4.1.311.3.10.3.12` the clause prints, so a certificate carrying either is passed over.
|
|
20
|
+
- The section 6.3.2 ceilings: 39 months for a code signing certificate issued before 1 March 2026, 460 days for one issued on or after it, and 135 months for a timestamp certificate. A ceiling stated in months is measured in months, since how many days 39 months hold depends on which 39.
|
|
21
|
+
- The section 6.1.5.2 floors: an RSA modulus of at least 3072 bits, one of NIST P-256, P-384 and P-521, or a DSA key whose parameters are a length of 2048 bits with a modulus length of 224 or 256. Each family the section names is read on its own terms, and a family it names none of is what the row reports.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- The three reserved code signing policy identifiers of section 7.1.6.1 resolve through `pki.oid`, named for the kind each one declares, along with the Microsoft extended key purposes the section 7.1.2.3 (f) clause names as permitted.
|
|
26
|
+
- Severities in this profile are graded against its own clauses, so a certificate can draw an error here and a warning under `cabf-smime` for the same shape: this document requires a non-critical cRLDistributionPoints, a present authorityInformationAccess and a critical keyUsage where the S/MIME one recommends them.
|
|
27
|
+
|
|
28
|
+
## v0.8.18 — 2026-09-24
|
|
29
|
+
|
|
30
|
+
An S/MIME certificate is linted against the generation it declares itself to be.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `pki.lint.certificate(der, { profile: "cabf-smime" })` runs the S/MIME BR v1.0.15 subscriber profile of section 7.1.2.3. The rows reach a certificate unprompted too, because one that asserts a reserved policy identifier has declared itself to be one; a certificate asserting none is left alone. 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.
|
|
35
|
+
- certificatePolicies (a): the extension is required, should not be critical, and must include exactly one reserved policy identifier, which is also what leaves the generation unambiguous. An `id-qt-cps` qualifier must be a HTTP or HTTPS URL, where the URI scheme tables elsewhere in the section name HTTP alone. This clause asks for a URL rather than a scheme, so a bare `http://` naming no host is reported. An `id-qt-unotice` qualifier must carry `explicitText` and must not carry a `noticeRef`. A certificate issued under a Legacy generation profile on or after 15 July 2025 is reported, gated to the issuance date it states.
|
|
36
|
+
- cRLDistributionPoints (b): the extension is required and should not be critical, and the URI scheme rule follows the generation. Strict and Multipurpose require every `uniformResourceIdentifier` to be HTTP; Legacy requires at least one and permits others beside it. HTTP means HTTP: the profile writes "a HTTP or HTTPS URL" where both are meant and "the URI scheme HTTP" here, so an HTTPS distribution point does not satisfy it. Having that scheme means being a URI of it, which RFC 7230 section 2.7.1 writes with an authority and no empty host, so `http:crl.example/a.crl` and `http://:80/a.crl` do not satisfy it either. An extension carrying no `uniformResourceIdentifier` at all is reported. A distribution point without one is not, where another point in the same extension carries a conforming URI.
|
|
37
|
+
- authorityInformationAccess (c): the extension should be present and must not be critical, and it should carry an `id-ad-caIssuers` accessMethod. `id-ad-ocsp` and `id-ad-caIssuers` get a table each, unlike the distribution points, whose single table covers the whole extension. Strict and Multipurpose require every URI of a method to be HTTP; Legacy requires at least one of that method's own, so an HTTP OCSP URL does not answer for a caIssuers list that carries only an LDAP one. An accessLocation that is not a `uniformResourceIdentifier` carries no scheme, so it is reported rather than passed over. The profile asks for this extension at SHOULD where the TLS profile asks at SHALL, so the two report the same absence at different strengths.
|
|
38
|
+
- basicConstraints (d) must not assert `cA` and must not carry a `pathLenConstraint`.
|
|
39
|
+
- keyUsage (e): the extension is required, should be critical, and a bit outside the cell for the certificate's own key algorithm is reported. The cells differ, so `keyEncipherment` is conformant on an RSA key and reported on an elliptic-curve one, whose cell gives `keyAgreement` instead, and an ML-KEM key permits `keyEncipherment` alone. The RSA cell also differs by generation: Multipurpose and Legacy admit `dataEncipherment` where Strict does not. A bit set past the nine RFC 5280 named positions is outside every cell and is reported for every key algorithm. A key algorithm the section gives no cell for is held to none.
|
|
40
|
+
- keyUsage (e) is read as the usage modes the cell states, not as a flat set of permitted bits. A conforming certificate asserts every bit of one mode and no bit outside that same mode, so bits the cell names can still combine into nothing it allows: `nonRepudiation` alone matches no mode, and on RSA `digitalSignature` with `dataEncipherment` matches none either, because `dataEncipherment` belongs to the key-management modes and those require `keyEncipherment`. On an elliptic curve `encipherOnly` and `decipherOnly` appear only in the modes that require `keyAgreement`, which is how the cell states "only if keyAgreement is set". A bit the cell names nowhere is reported by the row above this one, so one violation draws one finding.
|
|
41
|
+
- extKeyUsage (f): `id-kp-emailProtection` is required, and `id-kp-serverAuth`, `id-kp-codeSigning`, `id-kp-timeStamping` and `anyExtendedKeyUsage` are forbidden at every generation. Under Strict alone, any other purpose is forbidden too.
|
|
42
|
+
- authorityKeyIdentifier (g): the extension is required and must not be critical, its `keyIdentifier` field must be present, and `authorityCertIssuer` and `authorityCertSerialNumber` must not be. An extension naming only an issuer and serial satisfies the presence rule and is reported by the field rules.
|
|
43
|
+
- subjectAltName (h and 7.1.4.2.1): the extension is required, must carry at least one `rfc822Name` or an `otherName` of type id-on-SmtpUTF8Mailbox, and should not be critical unless the subject is an empty sequence, which is the one condition that lifts it. An otherName is read for its type identifier, since every otherName carries the same tag and only the type tells a mailbox from anything else.
|
|
44
|
+
- The optional extensions and the tables that admit them: smimeCapabilities (i), qcStatements (k) and subjectKeyIdentifier (n) must not be critical, and a subjectKeyIdentifier should be present. subjectDirectoryAttributes (j) is prohibited under Strict and Multipurpose and must not be critical under Legacy. The Adobe timestamp and ArchiveRevInfo extensions (m) are prohibited under Strict and must not be critical elsewhere. The Legal Entity Identifier extensions (l) are keyed on the certificate type rather than the generation: both are prohibited on a Mailbox-validated or Individual-validated certificate, the role identifier is prohibited on an Organization-validated one, and neither may be critical where it is admitted.
|
|
45
|
+
- A cell that reads "Prohibited" carries no criticality sentence, so a prohibited extension that is also critical draws the prohibition alone. A certificate that declares no generation names no cell, and every table the section keys on the generation reads it against Strict, the narrowest of the three.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- The twelve reserved S/MIME policy identifiers of section 7.1.6.1 resolve through `pki.oid`, named for the certificate type and generation each one declares, so an S/MIME certificate reads back with the profile it asserts rather than a bare dotted string. The Legal Entity Identifier and Adobe extension identifiers the profile reads resolve there too.
|
|
50
|
+
- The fuzz harness for `pki.lint.certificate` drives every profile a caller can name as well as the default rule set, and `pki.lint.csr` has a harness of its own. Both hold the same contract: hostile bytes return a report carrying a `lint/unparseable` finding, never a throw.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `pki.lint.certificate`, `csr`, `crl` and `ocsp` take any BufferSource, as every parser in the toolkit already did. They took a `Buffer` alone, so bytes read over the network as a `Uint8Array`, or handed over as an `ArrayBuffer`, raised `lint/bad-input` from the one surface whose data path is documented never to throw. The bytes are snapshotted at the door, so a caller cannot neuter the backing buffer between the check and the read, and a source whose buffer was already detached returns a `fatal` finding like any other unreadable input rather than aborting a scan. A value that is neither bytes, a PEM string nor a parsed object still raises `lint/bad-input`, which is caller error rather than a verdict on a certificate.
|
|
55
|
+
- `pki.lint.rules("all")` and `pki.lint.rules("default")` return every row rather than throwing `lint/unknown-profile`. The lint verbs read both names as naming no profile, so a caller who linted under one and then asked which rows had run was refused the answer by the registry that ran them.
|
|
56
|
+
- Naming a profile no longer reads an extension whose value does not decode as one that is absent. Every row asks for extensions by name and the shared decoder answers null for both cases, so `pki.lint.certificate(der, { profile: "cabf-tls" })` returned a clean report for a certificate whose basicConstraints, keyUsage or subjectAltName was malformed, and so did the RFC 9881, RFC 9909 and RFC 9935 profiles. The RFC 5280 row that reports the encoding runs under every named certificate profile now, as it already did for `pki.lint.csr`. Callers who pass no profile were never affected.
|
|
57
|
+
|
|
7
58
|
## v0.8.17 — 2026-09-24
|
|
8
59
|
|
|
9
60
|
The CA/Browser Forum subscriber certificate profile is linted against its own tables, not a subset of them.
|
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 post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `rules`, `profiles` |
|
|
293
|
+
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length), the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `csr` (the artifact a CA reads before it issues: an empty subject naming no identity, an empty subject whose subjectAltName is not critical, a request for an extension the issuer determines, a request asking for a CA certificate, a broken signing digest, and a challengePassword traveling with the request; `opts.profile: "cabf-tls"` adds the certificate profile's own commonName-in-SAN, dNSName syntax, SAN-required and key-strength rows, applied to what the request asks for, and they run only when named because a request carries no extKeyUsage. It does not check the request's signature, which is a cryptographic operation `pki.csr.verify` owns), `rules`, `profiles` |
|
|
294
294
|
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
|
|
295
295
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
|
|
296
296
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert\|crl\|ocsp-response>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
|
package/lib/guard-time.js
CHANGED
|
@@ -12,6 +12,16 @@ var _Date = Date;
|
|
|
12
12
|
|
|
13
13
|
var _dateGetTime = require("./guard-intrinsic").uncurry(Date.prototype.getTime);
|
|
14
14
|
var _dateGetUTCFullYear = require("./guard-intrinsic").uncurry(Date.prototype.getUTCFullYear);
|
|
15
|
+
var _dateGetUTCMonth = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMonth);
|
|
16
|
+
var _dateGetUTCDate = require("./guard-intrinsic").uncurry(Date.prototype.getUTCDate);
|
|
17
|
+
var _dateGetUTCHours = require("./guard-intrinsic").uncurry(Date.prototype.getUTCHours);
|
|
18
|
+
var _dateGetUTCMinutes = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMinutes);
|
|
19
|
+
var _dateGetUTCSeconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCSeconds);
|
|
20
|
+
var _dateGetUTCMilliseconds = require("./guard-intrinsic").uncurry(Date.prototype.getUTCMilliseconds);
|
|
21
|
+
var _dateSetUTCFullYear = require("./guard-intrinsic").uncurry(Date.prototype.setUTCFullYear);
|
|
22
|
+
var _dateUTC = Date.UTC;
|
|
23
|
+
var _floor = Math.floor;
|
|
24
|
+
var _ceil = Math.ceil;
|
|
15
25
|
/** @internal A DER GeneralizedTime carries a four-digit year and a UTCTime a narrower one, so a Date
|
|
16
26
|
* outside 0000..9999 has no PKI encoding. */
|
|
17
27
|
var DER_YEAR_MIN = 0, DER_YEAR_MAX = 9999;
|
|
@@ -59,6 +69,45 @@ function isDate(value) {
|
|
|
59
69
|
return util.types.isDate(value);
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
// @enforced-by behavioral -- reading a month and adding to it is also how a date renderer prints one, so the arithmetic has no rename-proof shape a detector can tell from `getUTCMonth() + 1`; the clamping and year-borrow vectors in guard-time.test.js are the guard
|
|
73
|
+
/** @internal The instant `months` calendar months after a valid Date, clamped to the last day of
|
|
74
|
+
* the target month where that month is shorter, so 31 January plus one month gives 28 or 29
|
|
75
|
+
* February. A ceiling stated in months has no reading in days, since how many days 39 months hold
|
|
76
|
+
* depends on which 39. Time of day is carried through unchanged. */
|
|
77
|
+
function addMonths(value, months, E, code, label) {
|
|
78
|
+
assertValid(value, E, code, label);
|
|
79
|
+
var year = _dateGetUTCFullYear(value);
|
|
80
|
+
var month = _dateGetUTCMonth(value) + months;
|
|
81
|
+
var day = _dateGetUTCDate(value);
|
|
82
|
+
var carry = _floorDiv(month, 12);
|
|
83
|
+
var targetYear = year + carry;
|
|
84
|
+
var targetMonth = month - carry * 12;
|
|
85
|
+
var lastDay = _daysInMonth(targetYear, targetMonth);
|
|
86
|
+
/** @internal Date.UTC reads a year of 0..99 as 1900..1999, and a DER time carries 0000..9999, so
|
|
87
|
+
* the date is built at a safe year and the real one is set with the month and day together. One
|
|
88
|
+
* call sets all three, where setting the year alone would roll 29 February out of a leap year the
|
|
89
|
+
* remap had landed on. */
|
|
90
|
+
var out = new _Date(_dateUTC(2000, 0, 1,
|
|
91
|
+
_dateGetUTCHours(value), _dateGetUTCMinutes(value), _dateGetUTCSeconds(value),
|
|
92
|
+
_dateGetUTCMilliseconds(value)));
|
|
93
|
+
_dateSetUTCFullYear(out, targetYear, targetMonth, day < lastDay ? day : lastDay);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
var _MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
97
|
+
/** @internal The proleptic Gregorian leap rule the Date object follows, so year 0 is a leap year
|
|
98
|
+
* and year 1900 is not. */
|
|
99
|
+
function _daysInMonth(year, month) {
|
|
100
|
+
if (month !== 1) return _MONTH_DAYS[month];
|
|
101
|
+
return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 29 : 28;
|
|
102
|
+
}
|
|
103
|
+
/** @internal Integer division that rounds toward negative infinity, so a month index before
|
|
104
|
+
* January of the starting year borrows a year rather than truncating toward zero. */
|
|
105
|
+
function _floorDiv(a, b) {
|
|
106
|
+
var q = a / b;
|
|
107
|
+
var f = q < 0 ? -_ceil(-q) : _floor(q);
|
|
108
|
+
return f;
|
|
109
|
+
}
|
|
110
|
+
|
|
62
111
|
// @enforced-by behavioral -- a coercion-safe Date converter has no rename-proof code shape; the RED vector (a caller value with no numeric or string form, a BigInt or a throwing Symbol.toPrimitive, becomes an invalid Date the downstream isNaN check rejects, never a native TypeError out of the Date constructor) is the guard.
|
|
63
112
|
function toDate(value) {
|
|
64
113
|
if (util.types.isDate(value)) return value;
|
|
@@ -69,6 +118,7 @@ function toDate(value) {
|
|
|
69
118
|
module.exports = _freeze({
|
|
70
119
|
assertValid: assertValid,
|
|
71
120
|
assertEncodable: assertEncodable,
|
|
121
|
+
addMonths: addMonths,
|
|
72
122
|
instantOf: instantOf,
|
|
73
123
|
isDate: isDate,
|
|
74
124
|
toDate: toDate,
|