@blamejs/pki 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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.9.5 — 2026-10-12
8
+
9
+ Refuse a non-boolean switch option instead of reading it as the state the caller did not ask for.
10
+
11
+ ### Changed
12
+
13
+ - For every one of these switches, `true` and `false` pass, `null` and an omitted option select the documented default, and any other value is refused with that module's own `domain/reason` code naming the option. For every value that was already valid the emitted bytes are unchanged. A call that passes a non-boolean to one of these options now throws instead of building a message the other way: pass the boolean, or omit the option to take its default.
14
+ - `opts.pem` is unchanged and still takes `true` or a PEM label string where a verb documents one, as do `signingTime` (a time or `false`) and `pki.pkcs12.build`'s `mac` (a descriptor or `false`). These carry a value as well as an off form, and each is held to the type its own value needs.
15
+
16
+ ### Fixed
17
+
18
+ - `pki.cms.sign` refuses a non-boolean `detached`. The option decided the message shape in one place and whether the content was carried in another, so `detached: "false"` took the attached branch and then omitted the content from it, emitting an attached SignedData with nothing in it.
19
+ - `pki.cms.authenticate` refuses a non-boolean `authenticatedAttributes`. With `false` the MAC covers the content octets directly (RFC 6211 sec. 2), so the two values are structurally different messages and a mistyped one produced the message the caller had declined.
20
+ - `pki.cms.digest` refuses a non-boolean `detached`; `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `signedAttributes`; `pki.ocsp.sign` refuses a non-boolean `embedCert` or `extendedRevoke`; `pki.tsp.sign` refuses a non-boolean `ordering`; `pki.pkcs12.build` refuses a non-boolean `integrity.certificates`; `pki.path.crlChecker` and `pki.path.fetchingChecker` refuse a non-boolean `useDeltas`.
21
+ - `pki.est.serverkeygen` reads a `null` `requestedEncryption` as the absent option it documents rather than as a stated `false`. It tested whether the caller had stated an override by presence alone, so a `null` counted as stated and coerced to `false`, and a request against a CSR that does advertise key encryption was refused for contradicting a CSR the caller never disagreed with.
22
+
23
+ ### Security
24
+
25
+ - `pki.smime.sign` and `pki.smime.encrypt` refuse a non-boolean `protectHeaders`. Read by comparison to `true`, a value of any other type left RFC 9788 header protection off and the message went out unprotected with no error, so a caller that asked for the protection did not get it and was not told.
26
+ - The `pki.est` and `pki.scep` verbs refuse a non-boolean `allowCrossOriginRedirect`. Read by truthiness, `"false"` allowed the cross-origin redirect on an unsafe method that the check exists to refuse (RFC 7030 sec. 3.2.1), which is the opposite of what that value states.
27
+ - `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `certificates`, the switch that withholds the signer chain. A value of any other type embedded the certificates the caller asked to leave out.
28
+ - `pki.x509.sign` and `pki.csr.sign` refuse a non-boolean criticality flag (`keyUsageCritical`, `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`, `tlsFeatureCritical`). A value of any other type could emit an extension critical that the caller asked to be non-critical, and RFC 5280 sec. 6.1 requires a relying party that does not recognize a critical extension to reject the certificate.
29
+
30
+ ## v0.9.4 — 2026-10-12
31
+
32
+ Refuse to issue an elliptic curve certificate asserting a key usage its own algorithm forbids.
33
+
34
+ ### Added
35
+
36
+ - `rfc5480` joins `pki.lint.profiles()`, naming the elliptic curve rows on their own so a caller can ask for them alone. They also ride with the `rfc5280` profile, because that is the profile whose own clause delegates the question and the one every authoring call is held to.
37
+
38
+ ### Security
39
+
40
+ - `pki.x509.sign` refuses a spec whose elliptic curve subject key asserts `keyEncipherment` or `dataEncipherment`, and `pki.lint.certificate` reports it as `lint/rfc5480/ec-key-usage-forbidden`. RFC 5280 sec. 4.2.1.3 delegates the per-algorithm key usage values ("appropriate values for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and [RFC4491]"), RFC 5480 updates RFC 3279 for elliptic curve, and RFC 8813 sec. 3 states the prohibition outright for a certificate indicating `id-ecPublicKey`. Measured before this: both bits were emitted and no error-severity finding was reported, so a certificate violating a MUST NOT left the signer unremarked. One finding names both bits where both are set, since an operator clearing them clears them together. The RSA side has no such row: RFC 3279 sec. 2.3.1 states a MAY list and no prohibition, so an error-severity finding there would promote a MAY-list omission to a MUST NOT and stop the signer emitting a certificate a caller may want. `opts.profile: "none"` still emits what a spec describes, which is what reproducing a real misissuance needs.
41
+
7
42
  ## v0.9.3 — 2026-10-09
8
43
 
9
44
  Carry out the signature a caller asked for, compare a name by its own matching rule, and answer a proof of possession with which check failed.
package/README.md CHANGED
@@ -275,7 +275,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
275
275
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
276
276
  | `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
277
277
  | `pki.identity` | Service identity (RFC 9525, which obsoletes RFC 6125). `match(cert, references, opts)` says whether a certificate presents a name the client was trying to reach, which is a separate answer from whether it chains: §1.2 has an application need both, and `pki.path.validate` takes `opts.identity` to run them together and report each. A reference identity is a domain name (DNS-ID), a textual IP address or a 4- or 16-octet `BufferSource` (IP-ID), `{ type: "srv", service: "_imap", value: "example.com" }` (SRV-ID, RFC 4985), or `{ type: "uri", scheme: "https", value: "www.example.com" }` (URI-ID). A record carries its service type and its domain together, so no comparison can pair the service of one reference with the domain of another (§6.5). The subject `commonName` is never read, under any option or fallback, including when the certificate carries no subjectAltName at all (§2). Names are compared label by label under an ASCII-only fold with one trailing dot normalized on both sides; an A-label is compared as the ASCII it is and never decoded, so a reference carrying a character outside ASCII is refused rather than guessed at, this toolkit shipping no IDNA implementation. An IP-ID is compared octet for octet against an `iPAddress` entry, never against a `dNSName` holding the same address as text, and no prefix or mask applies (§6.2, §6.4). Wildcards are on and `opts.wildcards: false` turns them off, which §3 requires stating either way: one wildcard, the whole of the left-most label, reaching exactly one reference label, and a presented name failing those rules is ignored with the search continuing rather than refusing the certificate (§6.3). A wildcard spanning an administrative boundary such as `*.co.uk` is out of scope as it is in §7.1, since no public-suffix list ships here. The verdict names which reference matched, so a caller can use it as the validated identity (§6.6), and lists every entry that was skipped with the reason — `match` |
278
- | `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. A name string that opens as a distinguished name is refused rather than certified as one commonName holding the whole text, so `subject: "CN=Example CA, O=Example"` names the two forms that do work instead of issuing `CN=CN=Example CA\, O=Example`; a common name that contains an `=` further along, such as `Example CA (v=2)`, is unaffected. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
278
+ | `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. A name string that opens as a distinguished name is refused rather than certified as one commonName holding the whole text, so `subject: "CN=Example CA, O=Example"` names the two forms that do work instead of issuing `CN=CN=Example CA\, O=Example`; a common name that contains an `=` further along, such as `Example CA (v=2)`, is unaffected. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, which is what refuses an EC subject key asserting `keyEncipherment` or `dataEncipherment` (RFC 8813 §3), so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
279
279
  | `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985). `sign(spec, key, opts)` builds and signs a `CertificationRequest` from a `spec` of subject (which may be empty), the public key being certified, an optional `extensionRequest` carrying the requested v3 extensions a CA copies into the issued certificate (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
280
280
  | `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest; a `baseCertificateID` names its issuer as one non-empty directoryName, which sec. 4.2.2 requires in the same words sec. 4.2.3 uses for the AC issuer, and `pki.schema.attrcert.parse` refuses any other shape), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
281
281
  | `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. A key that cannot sign, which is where an ML-KEM enrollment sits, takes a `POPOPrivKey` proof instead (RFC 4211 sec. 4.2, sec. 4.3): `pop.type` of `keyEncipherment` or `keyAgreement` with `pop.method: 'subsequentMessage'` declares whether an encrypted certificate (`encrCert`) or a challenge (`challengeResp`) completes the proof over the enrolling protocol's next exchange, and `pop.method: 'encryptedKey'` encloses the requester's private key for the CA in a CMS `EnvelopedData` under `id-ct-encKeyWithID`, requiring `pop.privateKey`, `pop.recipients`, the `pop.identifier` sec. 4.2.1 makes mandatory, and `pop.archive: true` because RFC 9810 sec. 5.2.8.3.1 permits the method only where archival is intended. The enclosed key must be the private half of `certTemplate.publicKey`, or the proof demonstrates possession of a key the request never asked to have certified. The `thisMessage` and `dhMAC` alternatives the specification deprecates are refused with their successors named. `pop.method: 'agreeMAC'` completes the four: `pop.key` is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM `PRIVATE KEY` block or a `KeyObject`, and `pop.caCert` the authority's certificate it already holds; the two agree a static secret (RFC 2875 sec. 3, obsoleted by RFC 6955), and the key derived from it MACs the DER `certReq` under `id-dhPop-static-HMAC-SHA1`. Either key may name its group in the PKCS#3 or the X9.42 encoding. `pop.key` must be the private half of `certTemplate.publicKey`, and the template must name both a subject and a public key. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. `verifyPop(messages)` is the receiving side: for each `CertReqMsg` it verifies the `POPOSigningKey` signature over the bytes RFC 4211 sec. 4.1 names — the DER of `poposkInput` when present, of `certReq` when not — through the one path-validation signature engine, and answers `{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }` with the fields re-derived from the message's own bytes. Each verdict reports only what its preimage covers: `publicKey` is the key possession was proven for, and `subject` is the requested name when the `certReq` was signed. A `poposkInput` preimage covers the key and the sender alone, so any subject beside it is unsigned and is withheld with `subjectBound: false`. A `raVerified` proof is an RA's out-of-band assertion and a `keyEncipherment` or `keyAgreement` proof completes over a later exchange, so each is reported unverified with its `method` named instead of guessed. Parsing stays at `pki.schema.crmf.parse` — `build`, `verifyPop` |
@@ -303,7 +303,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
303
303
  the checkpoint read through `pki.tlog` and verified under the key the entry names in `logId.keyId`, so a signed tree root from one pinned log cannot satisfy an entry claiming another; the size and root the proof folds against come from that verified checkpoint rather than from the `inclusionProof` fields no signature covers, 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. A Rekor v2 `hashedrekord` v0.0.2 entry is read on either arm: Rekor v2 supports no DSSE entry type, so a DSSE signature is logged as a `hashedrekord` over the hash of its signing preimage, and the entry is bound by the signature it records, the certificate that made it, and that digest. Where the entry states `keyDetails`, that statement is held to the leaf key rather than read in its place. Rekor v2 issues no signed entry timestamp and writes `integratedTime` as zero, so it is ignored and `opts.tsaRoots` pins the timestamp-authority anchors for an RFC 3161 token over the signature bytes; with neither that nor `opts.time` the verdict is `sigstore/no-attested-time`, because a certificate that lives about ten minutes cannot be validated against an unattested instant. `timestampSource` names which of the three dated it, beside `logOrigin`, `treeSize` and `checkpointKeyId` from the verified checkpoint. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
304
304
  | `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. Every format `pki.schema.all()` detects has a report and its own named verb, so a PKCS#8 key, a PKCS#12 store, a CRMF request, a CMP message, an EST CSR-attributes response, an RFC 5914 trust anchor list, an OCSP request or response, an RFC 3161 timestamp response and an RFC 5755 attribute certificate each read the same way a certificate does. No report prints private key material: a PKCS#8 and a PKCS#12 name the algorithm and say a private key is present, never its bytes, and a PKCS#12's encrypted safes are named rather than decrypted since these verbs take no password. `asn1(input)` is the structural dump below the reports, shaped after `openssl asn1parse`: one line per value carrying the byte offset, the nesting depth, the header and content lengths, the form, the tag name, and a primitive's rendered content, with an OBJECT IDENTIFIER named from the registry and a time rendered as the bytes on the wire. A conforming input is read by the same strict decoder the parsers use; when that decode refuses the bytes, the report names the refusal and a header walk renders the prefix it can read and the offset where it stopped, so a truncated, trailing-byte, indefinite-length or non-minimal-length file still reads. It withholds no value: where the other verbs here name a key and print none of it, this one prints bytes, private key material included, so a report that must not print a key is `any` or `pkcs8` — `certificate`, `crl`, `csr`, `cms`, `pkcs8`, `pkcs12`, `crmf`, `cmp`, `csrattrs`, `trustanchor`, `ocspRequest`, `ocspResponse`, `tsp`, `attrcert`, `asn1`, `any` |
305
305
  | `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` |
306
- | `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, 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), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
306
+ | `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. The RFC 5280 profile carries the rows that document's own clauses delegate: §4.2.1.3 states that "appropriate values for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and [RFC4491]", and RFC 8813 §3 is where the elliptic-curve answer now is, so an EC certificate asserting keyEncipherment or dataEncipherment is `lint/rfc5480/ec-key-usage-forbidden` there and under `rfc5480` alone. The RFC 5280 rows themselves: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the CA/Browser Forum subscriber certificate profile of §7.1.2.7 (the extension presence and criticality table, the authorityInformationAccess accessMethod and accessLocation tables, basicConstraints cA and pathLenConstraint, certificatePolicies against anyPolicy and the exactly-one reserved identifier rule and the permitted-qualifier table, the forbidden extended key purposes, the two Key Usage tables that differ by key type, and the subjectAltName GeneralName table with its criticality rule in both directions), the CA/Browser Forum S/MIME BR subscriber profile of §7.1.2.3 (a) through (n) as `cabf-smime`, whose requirements follow the generation a certificate declares through its reserved policy identifier (the distribution-point and accessMethod URI scheme rules, the policy qualifier rules for an id-qt-cps URL and an id-qt-unotice explicitText and noticeRef, the key-usage table for the certificate's own key algorithm including a bit set past the nine named positions, the extended key purposes forbidden at every generation and the ones Strict alone forbids, the authorityKeyIdentifier field rules, a required mailbox in the subjectAltName as an rfc822Name or an id-on-SmtpUTF8Mailbox otherName, the criticality each extension must carry, the subjectDirectoryAttributes and Adobe extensions the Strict generation prohibits, the Legal Entity Identifier extensions keyed on the certificate type rather than the generation, and the Legacy generation retired on 15 July 2025), the CA/Browser Forum Code Signing BR subscriber profile of §7.1.2.3 as `cabf-cs`, whose rows follow the kind a certificate declares through one of the three reserved identifiers of §7.1.6.1 (the extension presence and criticality rules at the strengths this document states them, a HTTP URL required in the distribution points and in each accessMethod location, a critical keyUsage asserting digitalSignature with `keyCertSign` and `cRLSign` forbidden and every other bit discouraged, the extended key purpose each kind must carry with the critical extKeyUsage a timestamp certificate needs, the §7.1.6.4 exactly-one-identifier rule gated to 15 September 2026, the §6.3.2 ceilings of 39 months, 460 days and 135 months, and the §6.1.5.2 floors of a 3072-bit RSA modulus, NIST P-256, P-384 or P-521, or DSA parameters of 2048 bits with a 224- or 256-bit modulus length, and the §7.1.4.2 subject distinguished name rows: a required commonName, a prohibited domainComponent, a subject attribute made only of the metadata characters the clause names, a required organizationName and two-letter countryName, one of localityName and stateOrProvinceName, and on an EV certificate a businessCategory from the four strings the clause names, a jurisdictionCountryName and a serialNumber), the ETSI qualified-certificate profile of EN 319 412-5 as `etsi-qc` (a non-critical qcStatements extension, a QcType naming one and only one of the three purposes the clause defines, a QcPDS providing an https URL and, on an EU qualified certificate, a document in English and no more than one per language, plus the EN 319 412-1 semantics identifiers holding a subject serialNumber or organizationIdentifier to a three-character identity type reference, a two-character country code, a hyphen-minus and an identifier, with XG required of an LEI value and a name registration authority URI required of a locally defined reference), the Mozilla Root Store Policy as `mozilla-root`, which is named rather than detected because a root program's requirements apply by virtue of the store a certificate chains to (the §5.1 AlgorithmIdentifier encodings compared as DER, an RSA modulus of at least 2048 bits and a whole number of octets, a public exponent above 1, an EdDSA key only beside id-kp-emailProtection, the §5.1.3 SHA-1 prohibitions on their own dates, a serial greater than zero with a short magnitude reported as a warning, an extKeyUsage on every end entity without anyExtendedKeyUsage, and the §5.3 intermediate rules from 2019, which pass over a certificate issued in its own name), the Chrome Root Program Policy v1.8 as `chrome-root`, named on the same grounds (the §1.3.2 dedicated-hierarchy rules holding a subordinate CA certificate's extKeyUsage to id-kp-serverAuth with id-kp-clientAuth reported as a warning, scoped to the unexpired certificates the clause names by passing over a notAfter before 15 June 2026, the same rules over a subscriber certificate issued on or after 15 March 2027 where id-kp-serverAuth stands alone and an extKeyUsage naming id-kp-OCSPSigning and nothing else is a delegated OCSP responder rather than a subscriber, the §1.3.1.3 three-year subordinate CA validity ceiling, and the §1.3.1.2 root term limit, which reports a self-signed CA certificate whose validity runs past the removal date its own notBefore fixes and passes over a CA reissued in its own name whose authorityKeyIdentifier names a key other than its own subjectKeyIdentifier), the Microsoft Trusted Root Program requirements as `microsoft-root`, named on the same grounds (the §3.A.1 root commonName and the keyUsage that must be present, critical and set keyCertSign and cRLSign, the §3.A.3 term of eight to 25 years, the §3.A.5 revocation pointer and §3.A.8 separation of uses on an issuing CA, the §3.A.10 seventeen reserved policy identifiers on an end entity, the §3.A.14 exclusivity of id-kp-OCSPSigning, the §3.B signature and key table including its code signing column, and the §3.E.2 extended key usages the program enables on a root), the Apple Root Program Policy as `apple-root`, named on the same grounds (its Appendix A trust purposes read as extended key usage sets, the §1.5 root key floor of RSA 4096 or NIST P-384, the §1.7 subordinate CA rules gated to 1 August 2026 and 1 July 2027, the §2.3 rfc822Name an S/MIME subscriber needs from 1 February 2027, and the subscriber purpose scoping that binds when its permission expires on 1 July 2027), the two algorithm-policy profiles, named for the same reason a root program's are (which policy a certificate is held to is the caller's assertion): `nist-pqc-transition` carries NIST IR 8547's two transitions, an error for a classical subject key or signature algorithm in a certificate issued from 2036 and a warning for a subject key at or below the 112-bit security strength issued from 2031, with the strength read off SP 800-57 Part 1 Rev 5 Table 2 and a composite signature passed over as the dual-algorithm operation the guidance endorses; `rfc7633` reads the values of a TLS Feature extension, reporting a feature outside the 16-bit width of the IANA registry RFC 7633 §4.1 points at, a repeated feature, the transparency_info feature RFC 9162 §7.2 asks a CA not to issue, and the status_request_v2 feature RFC 9846 leaves out of its ExtensionType enum, while a critical TLS Feature extension is reported by the default set; and `cnsa-2.0` carries the suite profile of `draft-jenkins-cnsa2-pkix-profile-05` (ML-DSA-87 or ML-KEM-1024 as the subject public key, ML-DSA-87 as the signature, and the extension, criticality and key-usage rules each of the three certificate kinds carries, where the key-usage set follows the kind the subject key declares), the same signature rule reaching `crl` and `ocsp` under that name because the document states it once for all three, 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), `cms` (the RFC 5652 profile: a signing-time naming a year from 1950 to 2049 encoded as GeneralizedTime where §11.3 requires UTCTime, a signer digest the digestAlgorithms set does not list, a version 1 attribute certificate, and notices for a SignedData with no signers and for a detached signature), `tsp` (the RFC 3161 profile: a tsa hint naming a subject the token's own certificate does not carry, a serial wider than 160 bits, and a token stating no accuracy), `attrcert` (the RFC 5755 profile: the §4.3 criticality table, a critical extension the profile does not define, a Holder naming more than one option, and an issuerUniqueID), each short because the strict parser behind it already refuses most of what its document requires and those clauses arrive as `lint/unparseable` carrying the parser's own code, `rules`, `profiles` |
307
307
  | `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
308
308
  | `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 |
309
309
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <file> [--asn1]`, `pki keygen --out <key>`, `pki csr --key <k> --subject <dn>`, `pki issue (--key <k> --subject <dn> \| --csr <req>)`, `pki fetch <https-url>`, `pki lint <file>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>`. `pki <verb> --help` prints that verb's usage on stdout and exits 0. Issuing from a request answers what the request asks for: `--copy-requested-san` writes the requested subjectAltName into the certificate, `--ignore-requested-extensions` issues without it, and a request asking for anything neither accounts for is refused rather than certified short |
package/lib/cms-digest.js CHANGED
@@ -78,7 +78,8 @@ function _digestName(opts) {
78
78
  * @opts
79
79
  * - `digestAlgorithm` (string) -- `sha256` (default), `sha384` or `sha512`.
80
80
  * - `contentType` (string) -- the encapsulated content type by OID name, default `data`.
81
- * - `detached` (boolean) -- omit the content, leaving the digest to travel alone.
81
+ * - `detached` (boolean) -- omit the content, leaving the digest to travel alone. A value other
82
+ * than `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
82
83
  * - `pem` (boolean) -- return a PEM `CMS` block instead of DER.
83
84
  * @example
84
85
  * var der = await pki.cms.digest(Buffer.from("hello"));
@@ -94,6 +95,10 @@ async function _digest(content, opts) {
94
95
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.digest options");
95
96
  guard.identifier.assertKnownKeys(opts, _DIGEST_OPTS, _err, "cms/bad-input",
96
97
  "pki.cms.digest has an unknown option ");
98
+ /** @internal Whether the content rides in the message is decided below by comparing this option
99
+ * to `true`, so a value of any other type attached a content the caller asked to leave out. */
100
+ var detached = guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input",
101
+ "pki.cms.digest opts.detached");
97
102
  var raw = guard.bytes.source(content, CmsError, "cms/bad-input", "content");
98
103
  var alg = _digestName(opts);
99
104
  var ctName = opts.contentType === undefined ? "data" : opts.contentType;
@@ -101,7 +106,7 @@ async function _digest(content, opts) {
101
106
  if (!ctOid) throw _err("cms/bad-input", "opts.contentType is not a known OID name: " + guard.text.showValue(ctName));
102
107
 
103
108
  var value = guard.crypto.digest(NODE_DIGEST[alg], raw);
104
- var eciChildren = opts.detached === true
109
+ var eciChildren = detached
105
110
  ? [b.oid(ctOid)]
106
111
  : [b.oid(ctOid), b.explicit(0, b.octetString(raw))];
107
112
  var dd = b.sequence([
@@ -649,7 +649,12 @@ function authenticate(content, recipients, opts) {
649
649
 
650
650
  async function _authenticateImpl(content, recipients, opts) {
651
651
  opts = opts || intrinsic.create(null);
652
- var withAttrs = opts.authenticatedAttributes !== false;
652
+ /** @internal Typed before it is used, because this switch picks the option set the door below
653
+ * checks against as well as the message that gets built: with `authenticatedAttributes: false`
654
+ * the MAC covers the content octets directly, so a value of any other type selected a
655
+ * structurally different message than the caller asked for and said nothing. */
656
+ var withAttrs = guard.identifier.booleanOptionDefaultOn(opts.authenticatedAttributes, _err,
657
+ "cms/bad-input", "pki.cms.authenticate opts.authenticatedAttributes");
653
658
  guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
654
659
  _err, "cms/bad-input", function (k) {
655
660
  return "pki.cms.authenticate has an unknown option " + JSON.stringify(k) +
package/lib/cms-sign.js CHANGED
@@ -186,6 +186,19 @@ function _algorithmProtectionPair(attrOid, digestAlgId, armAlgId, armTag) {
186
186
  * The emitter below reads the same option again, which is sound because the options door refuses
187
187
  * one supplied through an accessor, own or inherited, before any value is read: two reads of a
188
188
  * plain field are one answer. */
189
+ /** @internal The three switches these verbs document, typed where the options arrive. Each is read
190
+ * further down by comparing it to `false`, which answers for a value of any other type as the
191
+ * state the caller did not ask for: measured, `certificates: "false"` embedded the signer
192
+ * certificate the caller asked to withhold, and `detached: "false"` is read as "not detached" by
193
+ * the shape test and as "detached" by the content test, emitting an attached SignedData with no
194
+ * content in it. Typed here, those comparisons read a value already known to be a boolean.
195
+ * `detached` is not an option every one of these verbs accepts; an absent one types as absent. */
196
+ function _assertSignSwitches(opts, verb) {
197
+ guard.identifier.booleanOptionDefaultOn(opts.certificates, _err, "cms/bad-input", verb + " opts.certificates");
198
+ guard.identifier.booleanOptionDefaultOn(opts.signedAttributes, _err, "cms/bad-input", verb + " opts.signedAttributes");
199
+ guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input", verb + " opts.detached");
200
+ }
201
+
189
202
  function _assertProtectionPlaceable(opts) {
190
203
  var wanted = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
191
204
  if (opts.signedAttributes === false && wanted) {
@@ -373,6 +386,7 @@ function _signDispatch(content, signers, opts) {
373
386
  async function _signStream(stream, signers, opts) {
374
387
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
375
388
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
389
+ _assertSignSwitches(opts, "pki.cms.sign");
376
390
  _assertProtectionPlaceable(opts);
377
391
  if (opts.detached !== true) {
378
392
  throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
@@ -413,6 +427,7 @@ async function _signStream(stream, signers, opts) {
413
427
  function _sign(content, signers, opts) {
414
428
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
415
429
  guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
430
+ _assertSignSwitches(opts, "pki.cms.sign");
416
431
  _assertProtectionPlaceable(opts);
417
432
  var contentBuf = _toBuf(content, "content");
418
433
  var list = Array.isArray(signers) ? _b.reqDenseArray(signers, "the signer list") : [signers];
@@ -680,6 +695,7 @@ function countersign(cmsInput, signers, opts) {
680
695
  function _countersign(cmsInput, signers, opts) {
681
696
  opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
682
697
  guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
698
+ _assertSignSwitches(opts, "pki.cms.countersign");
683
699
  _assertProtectionPlaceable(opts);
684
700
  var list = Array.isArray(signers) ? signers : [signers];
685
701
  if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
package/lib/cms-verify.js CHANGED
@@ -1275,16 +1275,20 @@ function _addCert(out, der) {
1275
1275
  * the same content as the buffered form.
1276
1276
  *
1277
1277
  * @opts detached Omit the encapsulated content (a detached signature; the verifier
1278
- * supplies the content). Default false.
1278
+ * supplies the content). Default false. A value other than `true`,
1279
+ * `false`, `null` or an absent option is refused `cms/bad-input`.
1279
1280
  * @opts eContentType The encapsulated content type (an OID name). Default `data`.
1280
1281
  * @opts signedAttributes Include signed attributes (content-type, message-digest, signing-time);
1281
- * false signs the content directly. Default true.
1282
+ * false signs the content directly. Default true. A value other than
1283
+ * `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
1282
1284
  * @opts additionalSignedAttributes Extra signed attributes to add to every SignerInfo, each
1283
1285
  * `{ type, values }` (`type` an OID name or dotted string, `values` an
1284
1286
  * array of DER value `Buffer`s). Covered by the signature.
1285
1287
  * @opts signingTime A `Date` for the signing-time attribute, or false to omit it.
1286
1288
  * @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
1287
- * @opts certificates Embed the signer certificates in the output. Default true.
1289
+ * @opts certificates Embed the signer certificates in the output. Default true. A value other
1290
+ * than `true`, `false`, `null` or an absent option is refused
1291
+ * `cms/bad-input`.
1288
1292
  * @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
1289
1293
  * @opts unsignedAttributes Unsigned attributes for every SignerInfo, each `{ type, values }`
1290
1294
  * (`type` an OID name or dotted string, `values` an array of DER value
@@ -1341,12 +1345,15 @@ var sign = cmsSign.sign;
1341
1345
  * instead of the primary signature (a nested countersignature).
1342
1346
  * @opts signedAttributes Include signed attributes (message-digest + signing-time; never a
1343
1347
  * content-type). Default true; false signs the target signature octets
1344
- * directly.
1348
+ * directly. A value other than `true`, `false`, `null` or an absent
1349
+ * option is refused `cms/bad-input`.
1345
1350
  * @opts signingTime A `Date` for the countersignature's signing-time, or false to omit it.
1346
1351
  * @opts additionalSignedAttributes Extra signed attributes on the countersignature (a content-type
1347
1352
  * is rejected, RFC 5652 sec. 11.4).
1348
1353
  * @opts sid `"issuerAndSerial"` (default) or `"ski"`.
1349
- * @opts certificates Embed the countersigner certificate(s) in the SignedData. Default true.
1354
+ * @opts certificates Embed the countersigner certificate(s) in the SignedData. Default
1355
+ * true. A value other than `true`, `false`, `null` or an absent option
1356
+ * is refused `cms/bad-input`.
1350
1357
  * @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` attribute on the
1351
1358
  * countersignature, copying ITS own digest and signature algorithm
1352
1359
  * identifiers rather than the countersigned signature's. Off by
@@ -1440,7 +1447,8 @@ var encrypt = cmsEncrypt.encrypt;
1440
1447
  * @opts digestAlgorithm The authenticated-attributes message-digest hash (defaults to the MAC hash).
1441
1448
  * @opts authenticatedAttributes Include the content-type + message-digest authenticated attributes
1442
1449
  * (the default, and required for a non-`data` content type); false MACs the
1443
- * content directly.
1450
+ * content directly. A value other than `true`, `false`, `null` or an absent
1451
+ * option is refused `cms/bad-input`.
1444
1452
  * @opts authAttrs Extra authenticated attributes appended to the content-type + message-digest pair.
1445
1453
  * @opts contentType The encapsulated content type (an OID name). Default `data`.
1446
1454
  * @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
package/lib/csr-sign.js CHANGED
@@ -172,7 +172,11 @@ function _relatedCertRequest(spec) {
172
172
  * subject-owned set `pki.x509.sign` takes, or an array of pre-encoded Extension DER) and
173
173
  * `challengePassword`. An extension the issuing CA assigns (authorityKeyIdentifier, the
174
174
  * certificate-transparency pair, the CA version and previous-certificate hash, ocspNoCheck) is not a
175
- * request and is refused by name. `key` (or
175
+ * request and is refused by name. The per-extension criticality flags `pki.x509.sign` takes
176
+ * (`keyUsageCritical`, default true, and `extendedKeyUsageCritical`,
177
+ * `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`,
178
+ * `tlsFeatureCritical`, each default false) apply here too; each is a boolean, and a value other
179
+ * than `true`, `false`, `null` or an absent flag is refused `csr/bad-input`. `key` (or
176
180
  * `{ key }`) is the subject's own PKCS#8 private key, WebCrypto CryptoKey, or signer
177
181
  * `{ algorithm, publicKey, sign }`, so the request is self-signed
178
182
  * to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
package/lib/est.js CHANGED
@@ -173,6 +173,17 @@ function _knownOpts(opts, known, verb) {
173
173
  Object.keys(known).sort().join(", ");
174
174
  });
175
175
  _assertTransport(settled);
176
+ /** @internal The two switches these verbs document, typed here because this is the one door every
177
+ * one of them passes through and it runs before any request goes out. Both are read further down
178
+ * by truthiness: `allowCrossOriginRedirect: "false"` is truthy, so it ALLOWED the cross-origin
179
+ * redirect on an unsafe method that the check exists to refuse (sec. 3.2.1), and
180
+ * `requestedEncryption: "false"` is read as "encryption was requested", which skips the
181
+ * sec. 4.4.2 check that an encrypted key part was advertised. A verb that does not accept one of
182
+ * them never sees a value for it. */
183
+ guard.identifier.booleanOption(settled.allowCrossOriginRedirect, E, "est/bad-input",
184
+ "pki.est." + verb + " opts.allowCrossOriginRedirect");
185
+ guard.identifier.booleanOption(settled.requestedEncryption, E, "est/bad-input",
186
+ "pki.est." + verb + " opts.requestedEncryption");
176
187
  return settled;
177
188
  }
178
189
 
@@ -1643,6 +1654,8 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
1643
1654
  * There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
1644
1655
  * at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
1645
1656
  * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
1657
+ * A value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
1658
+ * before the request goes out.
1646
1659
  * @example
1647
1660
  * // a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
1648
1661
  * var r = await pki.est.cacerts("https://ca.example",
@@ -1695,7 +1708,9 @@ function cacerts(baseUrl, opts) {
1695
1708
  * - `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `proxy` / `now`, as in pki.est.cacerts.
1696
1709
  * - `strict`: reject an enroll response that carries more than the single issued certificate.
1697
1710
  * - `username` / `password` / `auth` -- HTTP credentials: top-level `username` / `password` mean Basic; `auth` (`{ scheme: "basic" | "digest", username, password, ... }`, as in pki.est.cacerts) selects Basic or Digest. Answered only after server authorization (empty username allowed).
1698
- * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST.
1711
+ * - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST. A
1712
+ * value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
1713
+ * before the request goes out.
1699
1714
  * @example
1700
1715
  * var req = await pki.csr.sign({ subject: "device.example", subjectPublicKey: signerSpki }, { key: signerKeyPkcs8 });
1701
1716
  * // a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
@@ -1919,6 +1934,8 @@ async function _serverkeygenResult(res, opts, derived) {
1919
1934
  * @opts
1920
1935
  * - `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
1921
1936
  * overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
1937
+ * `requestedEncryption` is a boolean: a value other than `true`, `false`, `null` or an absent
1938
+ * option is refused `est/bad-input`.
1922
1939
  * - every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
1923
1940
  * @example
1924
1941
  * var r = await pki.est.serverkeygen("https://ca.example", csrDer,
@@ -1933,7 +1950,11 @@ function serverkeygen(baseUrl, csrInput, opts) {
1933
1950
  var checkedTransport = opts.transport;
1934
1951
  var csrDer = _csrDer(csrInput);
1935
1952
  var derived = _serverkeygenEncryptionFromCsr(csrDer);
1936
- if (opts.requestedEncryption !== undefined && !!opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + !!opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
1953
+ /** @internal An override is a value the caller stated, and `null` states nothing: it is the
1954
+ * absent form the switch door accepts, so the CSR's own answer stands. Read as present it
1955
+ * coerced to `false` and contradicted a CSR the caller never disagreed with. The value is a
1956
+ * boolean by the time it reaches here, so it is compared rather than coerced. */
1957
+ if (opts.requestedEncryption != null && opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
1937
1958
  if (opts.expectedRecipientKeyId !== undefined) {
1938
1959
  if (!Buffer.isBuffer(opts.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId must be a Buffer");
1939
1960
  if (derived.expectedRecipientKeyId && !opts.expectedRecipientKeyId.equals(derived.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId contradicts the key identifier the CSR advertised (RFC 7030 sec. 4.4.1)");
@@ -743,7 +743,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
743
743
  // @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
744
744
  // @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
745
745
  function booleanOption(value, E, code, label) {
746
- if (value === undefined || value === null) return false;
746
+ return _switchValue(value, false, E, code, label);
747
+ }
748
+
749
+ /** @internal The same read for a switch whose absent form is ON: an option the caller turns OFF,
750
+ * where the documented default is the state an omitted option selects. The two differ only in what
751
+ * absence means, so the type rule is written once and each boundary names the default it documents.
752
+ * A boundary comparing such an option to `false` answers for every other value as the default
753
+ * state, which is how `certificates: "false"` embedded the certificate the caller asked to
754
+ * withhold; once the value has been through here, that comparison reads a boolean.
755
+ *
756
+ * @enforced-by behavioral -- the per-verb vectors in coercion-safe-options.test.js drive each
757
+ * @enforced-by behavioral -- shipped verb with a non-boolean and assert the typed refusal names the
758
+ * @enforced-by behavioral -- option, and with true, false and absence to pin the three accepted
759
+ * @enforced-by behavioral -- forms. The comparison this replaces has its own codebase-wide
760
+ * @enforced-by behavioral -- detector, optout-switch-read-by-coercion.
761
+ */
762
+ function booleanOptionDefaultOn(value, E, code, label) {
763
+ return _switchValue(value, true, E, code, label);
764
+ }
765
+
766
+ /** @internal A switch is the boolean it documents, an absent one is the stated default, and every
767
+ * other value is refused. The value is rendered from its own type rather than coerced, so a caller
768
+ * value with a throwing `toString` reports the typed refusal. `null` is read as absence, the same
769
+ * reading the rest of this module gives it. */
770
+ function _switchValue(value, whenAbsent, E, code, label) {
771
+ if (value === undefined || value === null) return whenAbsent;
747
772
  if (value === true || value === false) return value;
748
773
  throw E(code, label + " must be true or false, got " + _showSwitch(value));
749
774
  }
@@ -776,6 +801,7 @@ function declaresOwn(obj, name) {
776
801
  module.exports = intrinsic.freeze({
777
802
  assertCallable: assertCallable,
778
803
  booleanOption: booleanOption,
804
+ booleanOptionDefaultOn: booleanOptionDefaultOn,
779
805
  declaresOwn: declaresOwn,
780
806
  assertCallableOption: assertCallableOption,
781
807
  assertCanonicalOid: assertCanonicalOid,
package/lib/lint.js CHANGED
@@ -3901,6 +3901,43 @@ function _isMlKem(cert) {
3901
3901
  var spki = cert.subjectPublicKeyInfo;
3902
3902
  return !!(spki && spki.algorithm && ML_KEM_EK_LEN[spki.algorithm.name] !== undefined);
3903
3903
  }
3904
+ /** @internal RFC 5280 sec. 4.2.1.3 states no per-algorithm bit set of its own: "appropriate values
3905
+ * for keyUsage extensions for particular algorithms are specified in [RFC3279], [RFC4055], and
3906
+ * [RFC4491]". RFC 5480 updates RFC 3279 for elliptic curve, and RFC 8813 sec. 3 adds the
3907
+ * prohibition: for a certificate indicating id-ecPublicKey, keyEncipherment and dataEncipherment
3908
+ * MUST NOT be present. The row runs in the rfc5280 profile because that is the profile whose own
3909
+ * clause delegates the question, and the profile every authoring call is held to.
3910
+ *
3911
+ * Only the id-ecPublicKey arm is here. Sec. 3's other two arms govern id-ecDH and id-ecMQV, which
3912
+ * this registry does not name, so a row keyed on them could never fire; registering them as
3913
+ * subject-key algorithms is adding surface rather than closing this. There is no RSA row: RFC 3279
3914
+ * sec. 2.3.1 states a MAY list and no prohibition, so an error-severity finding there would promote
3915
+ * a MAY-list omission to a MUST NOT and stop the signer emitting a certificate a caller may want. */
3916
+ var EC_FORBIDDEN_KU_BITS = ["keyEncipherment", "dataEncipherment"];
3917
+ var RFC5480_RULES = [
3918
+ {
3919
+ id: "lint/rfc5480/ec-key-usage-forbidden", severity: "error", source: "rfc5480",
3920
+ citation: "RFC 8813 3 (updating RFC 5480 3)",
3921
+ message: "an elliptic curve certificate's keyUsage must not assert keyEncipherment or dataEncipherment",
3922
+ /** @internal The same classifier the CABF key-usage tables select on, so one answer to "is this
3923
+ * an ECC key" serves both and they cannot diverge on a subject key. */
3924
+ appliesTo: _isEccKey,
3925
+ check: function (cert, ctx) {
3926
+ var ku = ctx.decode("keyUsage");
3927
+ if (!ku || !ku.value) return null;
3928
+ var set = [];
3929
+ for (var i = 0; i < EC_FORBIDDEN_KU_BITS.length; i++) {
3930
+ if (ku.value[EC_FORBIDDEN_KU_BITS[i]] === true) _push(set, EC_FORBIDDEN_KU_BITS[i]);
3931
+ }
3932
+ /** @internal One finding naming both bits rather than one each: the fault is the pair the
3933
+ * clause forbids, and an operator clearing them clears them together. The join is the
3934
+ * captured one, since this file is mid-conversion and a live `Array.prototype.join` is a
3935
+ * read a replacement decides. */
3936
+ return set.length ? { context: { bits: intrinsic.join(set, ", ") } } : null;
3937
+ },
3938
+ },
3939
+ ];
3940
+
3904
3941
  var RFC9935_RULES = [
3905
3942
  {
3906
3943
  id: "lint/rfc9935/kem-key-usage", severity: "error", source: "rfc9935", citation: "RFC 9935 5",
@@ -6650,7 +6687,12 @@ var RFC5280_EXTENSION_SYNTAX_RULES = RFC5280_RULES.filter(function (r) {
6650
6687
  return r.id === "lint/rfc5280/extension-undecodable";
6651
6688
  });
6652
6689
  var PROFILES = Object.assign(Object.create(null), {
6653
- "rfc5280": RFC5280_RULES,
6690
+ /** @internal The elliptic curve rows ride with rfc5280 rather than only under their own name,
6691
+ * because sec. 4.2.1.3 of that document delegates the per-algorithm key usage values and RFC 8813
6692
+ * is where the delegate's answer now is. Under their own name as well, so a caller can ask for
6693
+ * them alone and `pki.lint.profiles()` names the document that states the rule. */
6694
+ "rfc5280": intrinsic.concatList(RFC5280_RULES, RFC5480_RULES),
6695
+ "rfc5480": intrinsic.concatList(RFC5480_RULES, RFC5280_EXTENSION_SYNTAX_RULES),
6654
6696
  "rfc9881": RFC9881_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6655
6697
  "rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
6656
6698
  "rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
@@ -6685,6 +6727,11 @@ var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat
6685
6727
  * inert under the default set because each one asks whether the caller named the profile. */
6686
6728
  .concat(MOZILLA_RULES).concat(CHROME_RULES).concat(MICROSOFT_RULES).concat(APPLE_RULES)
6687
6729
  .concat(NIST_TRANSITION_RULES).concat(CNSA2_RULES);
6730
+ /** @internal Appended through the capture rather than a fourth `.concat`: this file is
6731
+ * mid-conversion and the live `Array.prototype.concat` reads `Symbol.isConcatSpreadable` off each
6732
+ * operand and builds through ArraySpeciesCreate, so a replacement decides which rows the default
6733
+ * set holds. The pre-existing chain above is counted in the conversion budget. */
6734
+ ALL_RULES = intrinsic.concatList(ALL_RULES, RFC5480_RULES);
6688
6735
 
6689
6736
  var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
6690
6737
  return CABF_TLS_CSR_RULE_IDS.indexOf(r.id) !== -1;
@@ -7701,11 +7748,11 @@ function rules(profile, artifact) {
7701
7748
  * and `"default"`, are not among them.
7702
7749
  * @example
7703
7750
  * pki.lint.profiles();
7704
- * // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633", "x509-altsig",
7705
- * // "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root", "chrome-root",
7706
- * // "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0", "rfc2986",
7707
- * // "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc8933", "rfc3161",
7708
- * // "rfc5755"]
7751
+ * // -> ["rfc5280", "rfc5480", "rfc9881", "rfc9909", "rfc9935", "rfc9763", "rfc7633",
7752
+ * // "x509-altsig", "cabf-tls", "cabf-smime", "cabf-cs", "etsi-qc", "mozilla-root",
7753
+ * // "chrome-root", "microsoft-root", "apple-root", "nist-pqc-transition", "cnsa-2.0",
7754
+ * // "rfc2986", "rfc9883", "rfc5280-crl", "rfc6960", "rfc5019", "rfc5652", "rfc8933",
7755
+ * // "rfc3161", "rfc5755"]
7709
7756
  */
7710
7757
  /** @internal `cabf-tls` names the certificate profile in this list and is not repeated for the
7711
7758
  * request form: `pki.lint.csr` takes the same name and runs those rows against what a request
package/lib/ocsp.js CHANGED
@@ -316,6 +316,8 @@ function _normCertDer(cert, what) {
316
316
  * extendedRevoke emit the id-pkix-ocsp-extended-revoke extension (RFC 6960 sec. 4.4.8); implied
317
317
  * by a non-issued response, where `false` is refused.
318
318
  * embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
319
+ * For both switches, a value other than `true`, `false`, `null` or an absent
320
+ * option is refused `ocsp/bad-input`.
319
321
  * pem emit a PEM `OCSP RESPONSE` string instead of DER.
320
322
  * @example
321
323
  * var ca = await pki.key.generate("Ed25519");
@@ -350,6 +352,12 @@ function _sign(responseData, responder, opts) {
350
352
  opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.sign options");
351
353
  guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "ocsp/bad-input",
352
354
  "pki.ocsp.sign has an unknown option (the nonce to echo back is `nonce`): ");
355
+ /** @internal Both switches are read below against one boolean each, which answered for a value
356
+ * of any other type as the opposite of what the caller asked: `embedCert: "false"` carried the
357
+ * responder certificate a direct-CA response leaves out, and `extendedRevoke: "false"` placed
358
+ * the extension while escaping the sec. 2.2 refusal that reads the same option. */
359
+ guard.identifier.booleanOptionDefaultOn(opts.embedCert, _err, "ocsp/bad-input", "pki.ocsp.sign opts.embedCert");
360
+ guard.identifier.booleanOption(opts.extendedRevoke, _err, "ocsp/bad-input", "pki.ocsp.sign opts.extendedRevoke");
353
361
  responseData = responseData || _ocspIntrinsic.create(null);
354
362
  if (typeof responseData !== "object") throw _err("ocsp/bad-input", "the responseData must be an object { responderID, responses, producedAt? }");
355
363
  guard.identifier.assertKnownKeys(responseData, _RESPONSE_DATA_KEYS, _err, "ocsp/bad-input", "unknown responseData field (accepted: responderID, responses, producedAt): ");
@@ -2147,7 +2147,9 @@ function selectDelta(candidates) {
2147
2147
  * revocationDate.
2148
2148
  *
2149
2149
  * @opts
2150
- * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
2150
+ * useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)). Default `true`;
2151
+ * a value other than `true`, `false`, `null` or an absent option is refused
2152
+ * `path/bad-input`.
2151
2153
  * Default true. When false a delta is never merged; it is
2152
2154
  * still consulted for revocation.
2153
2155
  *
@@ -2171,7 +2173,10 @@ function crlChecker(crls, opts) {
2171
2173
  return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
2172
2174
  "crlChecker: crls[" + ci + "]");
2173
2175
  });
2174
- var useDeltas = !(opts && opts.useDeltas === false);
2176
+ /** @internal The sec. 6.3.1(b) merge, read as the boolean it documents: the comparison this
2177
+ * replaces answered for a value of any other type by merging deltas the caller turned off. */
2178
+ var useDeltas = guard.identifier.booleanOptionDefaultOn(opts && opts.useDeltas, E,
2179
+ "path/bad-input", "crlChecker: opts.useDeltas");
2175
2180
  return {
2176
2181
  check: async function (cert, issuer, ctx) {
2177
2182
  var time = ctx.time;
@@ -532,7 +532,7 @@ function _normalizeSpec(spec, opts, form) {
532
532
  * @opts
533
533
  * - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
534
534
  * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, password? }`. `password` overrides the shared integrity password for the MAC. `keyLength` is a `pbmac1` field: the classic Appendix B derivation produces a key at the hash's own output length, and naming one under `hmac` is refused.
535
- * - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
535
+ * - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected. `certificates` embeds the signer certificates in the authSafe, default true; a value other than `true`, `false`, `null` or an absent option is refused `pkcs12/bad-input`.
536
536
  * - `recipientCerts` -- (public-key privacy convenience) `[certDer|PEM, ...]`; the convenience-form cert + key are placed in one recipient-enveloped safe (a plaintext keyBag). For the full form, set per-`safeContents` `recipients: [{ cert }, ...]` -- CERTIFICATE recipients only (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM, dispatched off the cert key); a password or KEK recipient is not public-key privacy and is rejected -- with an optional `contentEncryptionAlgorithm` (`aes-128|192|256-cbc`, default 256; GCM/AEAD rejected). `encrypt` (password) and `recipients` (public-key) on the same safe is rejected; privacy is independent of integrity.
537
537
  * - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
538
538
  * @example
@@ -572,6 +572,11 @@ async function _build(spec, opts) {
572
572
  if (opts.integrity != null) {
573
573
  if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
574
574
  guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
575
+ /** @internal Forwarded to `pki.cms.sign` below by comparing it to `false`, which answered for a
576
+ * value of any other type by embedding the signer certificates the caller asked to leave out
577
+ * of the authSafe. */
578
+ guard.identifier.booleanOptionDefaultOn(opts.integrity.certificates, _err, "pkcs12/bad-input",
579
+ "opts.integrity.certificates");
575
580
  if (opts.integrity.mode !== "public-key") {
576
581
  throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + guard.text.showValue(opts.integrity.mode));
577
582
  }
package/lib/pki-build.js CHANGED
@@ -648,6 +648,36 @@ function makeBuilder(ctx) {
648
648
  if (bc.pathLen != null) pathLen(bc.pathLen);
649
649
  guard.identifier.assertKnownKeys(bc, BC_KEYS, E, "bad-input", "unknown basicConstraints field ");
650
650
  }
651
+
652
+ /** @internal The criticality a caller states for an extension, read as the boolean each flag
653
+ * documents and resolved to the default an omitted flag selects. RFC 5280 sec. 4.2 fixes
654
+ * keyUsage critical and leaves the other five non-critical, which is why the defaults differ.
655
+ * `basicConstraints` carries its own flag inside the value and is typed by `validateBcSpec`.
656
+ *
657
+ * The flags are RETURNED, so the emitters below read criticality from this record rather than
658
+ * from the caller's spec a second time. A flag read off the spec at the point of emission is a
659
+ * read of a value that was never typed: measured, `extendedKeyUsageCritical: "false"` emitted
660
+ * that extension critical, and an extension a relying party does not recognize as critical is
661
+ * one RFC 5280 sec. 6.1 requires it to reject. */
662
+ var CRITICALITY_FLAGS = [
663
+ ["keyUsageCritical", true],
664
+ ["extendedKeyUsageCritical", false],
665
+ ["certificatePoliciesCritical", false],
666
+ ["policyMappingsCritical", false],
667
+ ["qcStatementsCritical", false],
668
+ ["tlsFeatureCritical", false],
669
+ ];
670
+ function criticality(extSpec, label) {
671
+ var out = intrinsic.create(null);
672
+ var where = label || "extensions";
673
+ for (var i = 0; i < CRITICALITY_FLAGS.length; i++) {
674
+ var k = CRITICALITY_FLAGS[i][0];
675
+ out[k] = CRITICALITY_FLAGS[i][1]
676
+ ? guard.identifier.booleanOptionDefaultOn(extSpec[k], E, "bad-input", where + "." + k)
677
+ : guard.identifier.booleanOption(extSpec[k], E, "bad-input", where + "." + k);
678
+ }
679
+ return out;
680
+ }
651
681
  function extBasicConstraints(spec) {
652
682
  var children = [];
653
683
  if (spec.cA === true) _push(children,b.boolean(true));
@@ -1368,16 +1398,17 @@ function makeBuilder(ctx) {
1368
1398
  guard.identifier.assertKnownKeys(extSpec, REQ_EXT_KEYS, E, "bad-input", function (k) {
1369
1399
  return "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension";
1370
1400
  });
1401
+ var crit = criticality(extSpec, "requested extensions");
1371
1402
  var out = [];
1372
1403
  if (extSpec.subjectKeyIdentifier != null) {
1373
1404
  if (extSpec.subjectKeyIdentifier === true && spki == null) throw E("bad-input", "subjectKeyIdentifier auto-derive (true) requires the public key -- supply a Buffer key id, or include the public key");
1374
1405
  _push(out,ext(O("subjectKeyIdentifier"), false, extSki(skiKeyId(extSpec.subjectKeyIdentifier, spki))));
1375
1406
  }
1376
- if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"), extSpec.keyUsageCritical !== false, extKeyUsage(extSpec.keyUsage)));
1377
- if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
1407
+ if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"), crit.keyUsageCritical, extKeyUsage(extSpec.keyUsage)));
1408
+ if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
1378
1409
  if (extSpec.basicConstraints != null) { validateBcSpec(extSpec.basicConstraints); _push(out,ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, extBasicConstraints(extSpec.basicConstraints))); }
1379
1410
  if (extSpec.subjectAltName != null) _push(out,ext(O("subjectAltName"), false, extSan(extSpec.subjectAltName)));
1380
- if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
1411
+ if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"), crit.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
1381
1412
  /** @internal RFC 5280 sec. 4.2.1.10 fixes this extension critical, in a request as in the certificate. */
1382
1413
  if (extSpec.nameConstraints != null) guard.list.append(out, ext(O("nameConstraints"), true, extNameConstraints(extSpec.nameConstraints)));
1383
1414
  if (extSpec.authorityInfoAccess != null) guard.list.append(out, ext(O("authorityInfoAccess"), false, extAuthorityInfoAccess(extSpec.authorityInfoAccess)));
@@ -1385,14 +1416,14 @@ function makeBuilder(ctx) {
1385
1416
  if (extSpec.freshestCRL != null) guard.list.append(out, ext(O("freshestCRL"), false, extCrlDistributionPoints(extSpec.freshestCRL, "freshestCRL")));
1386
1417
  if (extSpec.policyConstraints != null) guard.list.append(out, ext(O("policyConstraints"), true, extPolicyConstraints(extSpec.policyConstraints)));
1387
1418
  if (extSpec.inhibitAnyPolicy != null) guard.list.append(out, ext(O("inhibitAnyPolicy"), true, extInhibitAnyPolicy(extSpec.inhibitAnyPolicy)));
1388
- if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), !!extSpec.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
1419
+ if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), crit.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
1389
1420
  if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
1390
- if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), !!extSpec.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
1421
+ if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), crit.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
1391
1422
  /** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
1392
1423
  * sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
1393
1424
  * belongs in this table. The criticality knob is here too, because a request carries the
1394
1425
  * Extension shape whole and the CA decides what it issues. */
1395
- if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
1426
+ if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), crit.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
1396
1427
  if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
1397
1428
  if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
1398
1429
  if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
@@ -1457,6 +1488,7 @@ function makeBuilder(ctx) {
1457
1488
  encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
1458
1489
  requestedExtensions: requestedExtensions,
1459
1490
  extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
1491
+ criticality: criticality,
1460
1492
  extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki, AKI_KEYS: AKI_KEYS,
1461
1493
  extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
1462
1494
  extSubjectInfoAccess: extSubjectInfoAccess, extSubjectDirectoryAttributes: extSubjectDirectoryAttributes,
@@ -670,7 +670,9 @@ function _deltaUrlsFor(parsedCrl, state) {
670
670
  * of the same shape. A transport that does not declare that it blocks
671
671
  * private addresses is refused a hostname.
672
672
  * @opts allowPlaintextHttp Whether an `http` URL is fetched. Default `true`.
673
- * @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`.
673
+ * @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`. A
674
+ * value other than `true`, `false`, `null` or an absent option is refused
675
+ * `path/bad-input`.
674
676
  * @opts nonce Whether an OCSP request carries a 32-octet nonce. `true` or `false`,
675
677
  * and anything else is `path/bad-input` rather than the default, since a
676
678
  * control read as off is one an operator asked for and did not get.
@@ -709,7 +711,11 @@ function fetchingChecker(opts) {
709
711
  * door is the value the rule is applied on. */
710
712
  net: intrinsic.freeze({ allowHttp: allowPlaintextHttp, guardsAddresses: _declaresAddressGuard(transport) }),
711
713
  allowPlaintextHttp: allowPlaintextHttp,
712
- useDeltas: !(opts.useDeltas === false),
714
+ /** @internal Read the way `nonce` on the next line has been read since it was written: the
715
+ * comparison this replaces answered for a value of any other type by fetching the deltas the
716
+ * caller declined. */
717
+ useDeltas: guard.identifier.booleanOptionDefaultOn(opts.useDeltas, E, "path/bad-input",
718
+ "fetchingChecker: opts.useDeltas"),
713
719
  nonce: _booleanOption(opts.nonce, "fetchingChecker: opts.nonce"),
714
720
  tls: opts.tls,
715
721
  maxFetches: guard.limits.cap(opts.maxFetches, "fetchingChecker: opts.maxFetches", LIMITS.REVOCATION_MAX_FETCHES,
package/lib/scep.js CHANGED
@@ -28,6 +28,12 @@
28
28
  * the message layer, transport-agnostic; a live SCEP CA is driven over
29
29
  * pki.transport separately.
30
30
  *
31
+ * Every verb that reaches a CA accepts the same transport bounds, listed on
32
+ * each of them. One of those bounds is a switch: `allowCrossOriginRedirect`
33
+ * opts in to following a cross-origin redirect on an unsafe method. It is a
34
+ * boolean, and a value other than `true`, `false`, `null` or an absent option
35
+ * is refused `scep/bad-input` before the first request goes out.
36
+ *
31
37
  * @card
32
38
  * Build and read SCEP (RFC 8894) messages: a PKCSReq, RenewalReq, CertPoll,
33
39
  * GetCert, or GetCRL request, or a CA-side CertRep response, wrapped in the
@@ -731,7 +737,11 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
731
737
  var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
732
738
  var proxySnap = _snapshotProxy(opts.proxy);
733
739
  var tlsBase = _tlsForRequest(opts);
734
- var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
740
+ /** @internal Read as the boolean it documents, on the one path every network verb here takes and
741
+ * before the first request. Taken by truthiness, `allowCrossOriginRedirect: "false"` ALLOWED the
742
+ * cross-origin redirect on an unsafe method that the check below exists to refuse. */
743
+ var allowCrossOrigin = guard.identifier.booleanOption(opts.allowCrossOriginRedirect, _err,
744
+ "scep/bad-input", "opts.allowCrossOriginRedirect");
735
745
  function _hopTls(current) {
736
746
  var t = tlsBase;
737
747
  if (_urlOrigin(current) !== initialOrigin) {
package/lib/smime.js CHANGED
@@ -76,12 +76,23 @@ var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
76
76
  /** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
77
77
  * decide whether a rule applies and reads it again for the value the rule uses, and an option
78
78
  * reached through an accessor answers each read separately. Callers assign the return value. */
79
+ /** @internal Header protection is turned on below by comparing this option to `true`, so a value
80
+ * of any other type sent the message with the RFC 9788 protection the caller asked for left off.
81
+ * Typed at the one door every verb here passes through, which is before the entity is built. */
82
+ function _assertSmimeSwitches(opts, verb) {
83
+ guard.identifier.booleanOption(opts.protectHeaders, _err, "smime/bad-input",
84
+ "pki.smime." + verb + " opts.protectHeaders");
85
+ }
86
+
79
87
  function _knownOpts(opts, known, verb) {
80
88
  guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
81
89
  return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
82
90
  Object.keys(known).sort().join(", ");
83
91
  });
84
- return guard.identifier.snapshotOptions(opts, known);
92
+ /** @internal Typed on the snapshot, which is the object the verb goes on to read. */
93
+ var settled = guard.identifier.snapshotOptions(opts, known);
94
+ _assertSmimeSwitches(settled, verb);
95
+ return settled;
85
96
  }
86
97
 
87
98
  function _isPkcs7(type, kind) {
@@ -654,7 +665,7 @@ function _base64Body(der) {
654
665
  * @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
655
666
  * @opts contentType the wrapped entity's Content-Type (default `text/plain; charset=utf-8`).
656
667
  * @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
657
- * @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers.
668
+ * @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
658
669
  * @opts headers the Non-Structural fields to protect + display: an object `{ Name: value }` or an array `[{ name, value }]` (Subject / From / To / Date / ...); used with `protectHeaders`.
659
670
  * @opts hcp the Header Confidentiality Policy applied to the OUTER display copies: `"hcp_baseline"` (default) or `"hcp_no_confidentiality"`. A signed message's payload is not encrypted, so this governs presentation, not secrecy; used with `protectHeaders`.
660
671
  * @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
@@ -875,7 +886,7 @@ function _cmsEncryptOpts(opts) {
875
886
  *
876
887
  * @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
877
888
  * @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
878
- * @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside.
889
+ * @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
879
890
  * @opts headers the Non-Structural fields to protect (object `{ Name: value }` or array `[{ name, value }]`); the real values, hidden by the HCP.
880
891
  * @opts hcp the Header Confidentiality Policy: `"hcp_baseline"` (the default, obscuring Subject and removing Comments/Keywords) or `"hcp_no_confidentiality"` (leave all outer values visible). Per RFC 9788 sec. 3.2.1 / sec. 11.4, `hcp_baseline` deliberately does not strip `Bcc` (removing it can break deliverability to a Bcc'd recipient); to keep a blind recipient out of the plaintext outer headers, omit `Bcc` from `opts.headers`.
881
892
  * @opts contentEncryptionAlgorithm forwarded to cms.encrypt: `"aes-256-gcm"` (default) / `"aes-128-gcm"` / `"aes-256-cbc"` / `"aes-128-cbc"`.
package/lib/tsp-sign.js CHANGED
@@ -123,7 +123,9 @@ function _signingCertV2(certDer, hashName) {
123
123
  * @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
124
124
  * @opts accuracy The genTime +/- accuracy, as `{ seconds?, millis?, micros? }`, naming at
125
125
  * least one field; omit it to make no accuracy claim.
126
- * @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
126
+ * @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean). A value
127
+ * other than `true`, `false`, `null` or an absent option is refused
128
+ * `tsp/bad-input`.
127
129
  * @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
128
130
  * @opts digestAlgorithm / pss Passed through to `pki.cms.sign` (the signer digest algorithm, RSA-PSS padding).
129
131
  * @opts additionalSignedAttributes Extra signed attributes to carry on the token, as `[{ type, values }]` (forwarded to `pki.cms.sign`).
@@ -163,6 +165,11 @@ function _sign(messageImprint, tsa, opts) {
163
165
  }
164
166
  guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
165
167
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
168
+ /** @internal The ordering claim is emitted below only for the boolean `true`, so a value of any
169
+ * other type left the TSTInfo saying the opposite of what the caller asked, which is what RFC
170
+ * 3161 sec. 2.4.2 has a relying party read to compare two tokens from this TSA. `certReq` on the
171
+ * request verb has been typed since it was written; this is the same read. */
172
+ guard.identifier.booleanOption(opts.ordering, _err, "tsp/bad-input", "pki.tsp.sign opts.ordering");
166
173
  if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
167
174
  guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
168
175
  return "unknown TSA field " + JSON.stringify(k) +
package/lib/x509-sign.js CHANGED
@@ -228,6 +228,10 @@ function _buildExtensions(extSpec, ctx) {
228
228
  return "unknown extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form for a custom extension";
229
229
  });
230
230
 
231
+ /** @internal The criticality flags, typed at this door and read from the record below: the
232
+ * emitters took them off the caller's spec, where a value that was never a boolean selected a
233
+ * criticality nobody asked for. */
234
+ var crit = _b.criticality(extSpec);
231
235
  var bc = extSpec.basicConstraints;
232
236
  if (bc != null) _validateBcSpec(bc);
233
237
  var caTrue = !!(bc && bc.cA === true);
@@ -257,14 +261,14 @@ function _buildExtensions(extSpec, ctx) {
257
261
  var akiF = _akiFields(akiVal, ctx);
258
262
  _push(out,_ext(O("authorityKeyIdentifier"), false, _extAki(akiF[0], akiF[1], akiF[2])));
259
263
  }
260
- if (ku != null) _push(out,_ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _extKeyUsage(ku)));
261
- if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
264
+ if (ku != null) _push(out,_ext(O("keyUsage"), crit.keyUsageCritical, _extKeyUsage(ku)));
265
+ if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
262
266
  if (bc != null) {
263
267
  if (bc.cA === true && bc.critical === false) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
264
268
  _push(out,_ext(O("basicConstraints"), bc.critical !== false, _extBasicConstraints(bc)));
265
269
  }
266
270
  if (extSpec.subjectAltName != null) _push(out,_ext(O("subjectAltName"), ctx.subjectEmpty, _extSan(extSpec.subjectAltName)));
267
- if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
271
+ if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), crit.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
268
272
  /** @internal Sections 4.2.2.1 and 4.2.1.15 fix these non-critical; sec. 4.2.1.13 states a SHOULD
269
273
  * and the toolkit issues the conforming form. */
270
274
  if (extSpec.authorityInfoAccess != null) guard.list.append(out, _ext(O("authorityInfoAccess"), false, _extAia(extSpec.authorityInfoAccess)));
@@ -279,15 +283,15 @@ function _buildExtensions(extSpec, ctx) {
279
283
  * does not process the extension, so a critical one is a certificate this toolkit's own validator
280
284
  * must reject and pki.lint.certificate grades `unknown-critical-extension`. The default is the
281
285
  * usable form and the knob reaches the SHOULD-conforming one, as certificatePolicies does. */
282
- if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), !!extSpec.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
286
+ if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), crit.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
283
287
  if (extSpec.issuerAltName != null) guard.list.append(out, _ext(O("issuerAltName"), false, _encodeGeneralNames(_reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
284
288
  /** @internal pki.path.validate does not process qcStatements, so a critical one is rejected as an
285
289
  * unrecognized critical extension; the default is the form that validates and the knob reaches
286
290
  * the other, as policyMappings and certificatePolicies do. */
287
- if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), !!extSpec.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
291
+ if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), crit.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
288
292
  /** @internal RFC 7633 sec. 4: SHOULD NOT be critical, and the clause names the case where critical
289
293
  * is the desired behavior, so the knob exists and the default is the compatible form. */
290
- if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
294
+ if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), crit.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
291
295
  if (extSpec.nameConstraints != null) {
292
296
  if (!caTrue) throw _err("x509/bad-input", "nameConstraints appears only in a CA certificate, so it requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.10)");
293
297
  guard.list.append(out, _ext(O("nameConstraints"), true, _extNameConstraints(extSpec.nameConstraints)));
@@ -565,6 +569,14 @@ function _hasCriticalSan(extSpec) {
565
569
  * (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions
566
570
  * (v1 tag, `critical=FALSE`, `cA=FALSE`), and the CA cross-field rules (keyCertSign and
567
571
  * pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed `CertificateError`.
572
+ *
573
+ * Criticality is stated per extension: `keyUsageCritical` (default true), and
574
+ * `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`,
575
+ * `qcStatementsCritical` and `tlsFeatureCritical` (each default false). `basicConstraints` carries
576
+ * its own `critical` inside the value. Each is a boolean: a value other than `true`, `false`,
577
+ * `null` or an absent flag is refused `x509/bad-input`. The extensions RFC 5280 fixes one way
578
+ * (`nameConstraints` critical, `authorityInfoAccess` and `cRLDistributionPoints` non-critical) take
579
+ * no flag.
568
580
  * Where the spec carries raw DER (a `Name` Buffer, a pre-encoded `Extension`, an issuer `publicKey`
569
581
  * SPKI), a structural fault throws `CertificateError`, while a malformed leaf inside those bytes
570
582
  * throws `Asn1Error`, the same two-error contract the parsers present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -87,6 +87,6 @@
87
87
  "devDependencies": {
88
88
  "c8": "12.0.0",
89
89
  "esbuild": "0.28.2",
90
- "eslint": "10.11.0"
90
+ "eslint": "10.12.0"
91
91
  }
92
92
  }
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:a3e510f1-a551-49fe-aabf-02db532556c6",
5
+ "serialNumber": "urn:uuid:e1f0a0ad-8621-4fc9-a62a-62df66fe4d21",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-09T01:08:02.322Z",
8
+ "timestamp": "2026-10-12T15:58:58.900Z",
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.9.3",
22
+ "bom-ref": "@blamejs/pki@0.9.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.9.3",
25
+ "version": "0.9.5",
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.9.3",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.9.5",
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.9.3",
57
+ "ref": "@blamejs/pki@0.9.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]