@blamejs/pki 0.9.6 → 0.9.7
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 +29 -0
- package/README.md +3 -3
- package/lib/asn1-der.js +6 -5
- package/lib/cmp-session.js +10 -3
- package/lib/cmp-verify.js +2 -3
- package/lib/compromised-keys.js +114 -0
- package/lib/crmf-sign.js +2 -0
- package/lib/csr-sign.js +4 -0
- package/lib/framework-error.js +20 -7
- package/lib/guard-distrust.js +128 -8
- package/lib/guard-verdict.js +16 -3
- package/lib/http-transport.js +128 -20
- package/lib/lint.js +25 -43
- package/lib/path-validate.js +23 -4
- package/lib/revocation-fetch.js +3 -2
- package/lib/trust.js +40 -2
- package/lib/webauthn-mds.js +3 -2
- package/lib/x509-sign.js +8 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.7 — 2026-10-21
|
|
8
|
+
|
|
9
|
+
Refuse a key whose private key is public wherever it appears, with no option to turn the refusal off.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.trust.knownCompromisedKeys()`: the built-in list, one frozen `{ spkiSha256, name, reason, sources }` record per key, in a new array on every call. It holds the eight eMudhra keys; the Superfish root and the twelve other roots installed by products built on the Komodia interception library; the PrivDog root; the ten Adguard CA keys; eDellRoot and DSDTestProvider; the bealocalhost.de root of the German Federal Bar's beA client; and the AMI test UEFI Secure Boot platform key (PKfail, CVE-2024-8105). Each private key was obtained from a published source and checked against the public key.
|
|
14
|
+
- `lint/key-compromise/known-compromised-key` reports a certificate or request whose subject key is on that list. It runs in the default certificate and request rule sets and in the `key-compromise` profile.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `lint/key-compromise/small-ec-private-key` also reports a key whose algorithm identifier is id-ecDH or id-ecMQV.
|
|
19
|
+
- A test fixture that certifies a small-scalar elliptic curve key through `pki.x509.sign` or requests one through `pki.csr.sign` now fails with `x509/compromised-key` or `csr/compromised-key`. Build such a fixture outside the signing verbs.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Every `pki.errors` error now defines its `name`, `code`, `isPkiError`, `permanent`, class flag and `cause` as own properties instead of assigning them. A setter for one of those names inherited from `Object.prototype` ran on every construction, and a setter that threw turned the construction of any toolkit error into that throw; inside the TLS identity check it escaped as an uncaught exception. The descriptors those defines use, and the ones the verdict objects use, carry no prototype, so an inherited `get` no longer makes them throw.
|
|
24
|
+
- A `tls.checkServerIdentity` hook that throws now refuses the server with `transport/server-auth-failed`. Node calls the hook without a `try`, so the throw escaped as an uncaught exception and a process with no `uncaughtException` handler exited.
|
|
25
|
+
- A CMP `rootCaKeyUpdate` whose old root key is listed in `distrustedKeys` is now refused with `cmp/bad-info-value`. Only the new root key was checked, and the old root's signature on `newWithOld` is what authorizes the new key.
|
|
26
|
+
- `pki.asn1.build.oid`, and every verb that encodes an OID through it, split the dotted form with the live `String.prototype.split` and assembled the arcs with a concat that reads `Symbol.isConcatSpreadable`. A replaced `split` changed the OID encoded (`1.2.840.10045.4.3.3`, ecdsa-with-SHA384, was encoded as `2.5.4.3`), and a false spreading hook threw an untyped `TypeError`. The encoder now scans the dotted form and copies the arcs itself.
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
|
|
30
|
+
- `pki.path.validate` and `pki.path.build` fail a path whose trust anchor, intermediate or target carries a compromised key with `path/compromised-key`, in a `compromisedKey` row on the certificate that carries it or a `compromisedIssuerKey` row on the certificate the anchor issued, and ask no revocation status at or below it. A key is compromised when it is listed in `pki.trust.knownCompromisedKeys()`, or when it is an elliptic curve key on P-256, P-384, P-521 or secp256k1 whose private key is an integer below 256 or within 256 of the group order. A listed key is matched however a certificate encodes it, the way a `distrustedKeys` entry is. The check runs with or without a `distrustedKeys` list, and no option turns it off.
|
|
31
|
+
- The verbs that validate a chain on a caller's behalf refuse a compromised key through the same check: `pki.cms.verify`, `pki.smime.verify`, `pki.tsp.verify`, `pki.cmp`, `pki.possession.verifyRequest`, `pki.webauthn`, `pki.sigstore.verifyBundle` and `pki.ocsp.verify`. The routes that validate no chain refuse it too: a FIDO metadata BLOB whose signing chain is the trust anchor itself (`webauthn/metadata-untrusted`), a CMP `rootCaKeyUpdate` whose new root key or old root key is compromised (`cmp/bad-info-value`), a certificate a CMP session with no trust anchors is issued for a compromised key (`cmp/bad-cert-response`), and a CMP key generation authority accepted under `authorizedBySharedSecret` (`cmp/unauthorized-kga`). A compromised issuing CA authorizes no OCSP responder, a delegated responder with a compromised key is not an authorized one, and `pki.path.crlChecker` and `pki.path.fetchingChecker` answer `"unknown"` for a compromised issuer without fetching.
|
|
32
|
+
- `pki.transport.https` and the ACME, EST, SCEP, CMP and CT clients refuse a server whose certificate chain carries a compromised key, on every connection and hop, with `transport/server-key-compromised` (`acme/server-key-compromised` and so on from a client), and a proxy with `transport/proxy-key-compromised`. With no `tls.distrustedKeys`, the check reads the chain node reports and refuses on a hit; it does not prove the links of that chain. It reads the chain before a `tls.checkServerIdentity` hook runs, and refuses a chain longer than `C.LIMITS.PATH_MAX_CERTS` with the same code. A non-empty `tls.distrustedKeys` turns on the proven walk, which refuses a compromised key as well as a listed one. A `tls.anchors` or `proxy.tls.anchors` certificate with a compromised key is refused with `transport/bad-input` or `transport/bad-proxy` before any connection, and under `useSystemStore` such a certificate in the platform store is left out of the certificates the connection trusts.
|
|
33
|
+
- `pki.trust.systemAnchors` skips a certificate with a compromised key with the reason `trust/compromised-key`, and does not return it as an intermediate.
|
|
34
|
+
- `pki.x509.sign` refuses to certify a compromised subject key, issue under a compromised issuer key, or bind a compromised alternative key, with `x509/compromised-key`. `pki.csr.sign` refuses a compromised subject key or possession-statement certificate key with `csr/compromised-key`, and `pki.crmf.build` refuses a compromised `certTemplate.publicKey` or statement-of-possession key with `crmf/compromised-key`. CA/Browser Forum TLS BR 6.1.1.3 forbids a CA to issue a certificate for such a key.
|
|
35
|
+
|
|
7
36
|
## v0.9.6 — 2026-10-21
|
|
8
37
|
|
|
9
38
|
Refuse a distrusted CA key wherever it appears, and trust only the platform store under useSystemStore.
|
package/README.md
CHANGED
|
@@ -273,7 +273,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
273
273
|
| `pki.cmc` | Build and interpret CMC messages (RFC 5272, obsoleted by RFC 10002). `build(spec, signer)` assembles a Full PKI Request across all three request arms (PKCS#10, CRMF, other) and carries it either way RFC 5272 §3.2 allows: `{ cert, key }` signs it, while `{ mac: { identifier, secret } }` authenticates it under a shared secret for a client that holds no key yet, using the PasswordRecipientInfo that section requires and deriving the key from the identifier followed by the secret. A key that cannot sign for itself proves possession another way: `spec.popChallenge` answers the authority's encrypted challenge (RFC 5272 §6.7), decrypting the proof value, confirming it hashes to the witness the challenge carries, and returning the MAC the authority asked for, with `cmc/pop-failed` when the value is not the one issued. A signed request goes through `pki.cms.sign` under `id-cct-PKIData`. A PKCS#10 (`tcr`) request is verified against its own proof-of-possession before signing, so a request whose self-signature does not verify under its subject public key is refused rather than signed into a message a CA would reject. Body-part identifiers are unique across the whole message and never the reserved 0; a caller's clash is refused rather than renumbered, since a control may already reference it. An Identity Proof V2 witness is computed over the `reqSequence` bytes exactly as emitted (§6.2.1 step 1) rather than a re-serialization, a POP Link Witness is emitted only alongside the POP Link Random control §6.3.1.1 requires beside it, and a renewal carries neither Identification nor Identity Proof in either version. `verify(response, sent)` takes what the CA returned plus the state the client retained and reduces it to one terminal outcome: `issued`, `pending`, `confirm-required`, `pop-required`, or `rejected`. It binds the exchange first — Transaction Identifier, the Sender and Recipient Nonce echo compared in constant time and by full value so a truncation cannot match, and the Data Return echo — with each check applying only if the client sent that half, and, once sent, an absent or differing echo being a refusal, which is the replay defense. `bodyPartIDs` extends the same rule to what the response is about: a status reporting on a body part the request never sent is refused, which the transaction and nonce cannot catch, because a server can echo both correctly while answering about a different message. Several status controls are permitted and the worst governs, so a failure cannot hide behind an earlier success; the absence of any status control is success, per §6.1.2. The carrier's signature must verify (§3.2.1.3.4): a conforming SignedData carries its own signer certificate, so the ordinary build-then-verify flow needs nothing extra and the verdict reports `signatureVerified: true`. Where the signer is found nowhere the posture is fail-closed with a named opt-out — supply `certs` with the responder's certificate, or `allowUnverified: true`, in which case the verdict reports `signatureVerified: false`. Doing neither is refused, the opt-out never excuses a signature that is present and wrong, and a carrier with no signer at all is refused outright. The response's own `cmsSequence` and `otherMsgs` come back raw, since a request whose only arm was the other-message form has no certificate to return and §4.1 puts its answer there. Nothing is trusted: issued certificates are read from the CMS certificate bag (§4.2) and surfaced raw for `pki.path.validate`, and a Publish Trust Anchors control is surfaced with `trusted: false` rather than added to a store — `build`, `verify` |
|
|
274
274
|
| `pki.schema.cmc` | Decode CMC messages (RFC 5272 as updated by RFC 6402, the pair obsoleted by RFC 10002 / 10003 / 10004): a Full PKI Request (`PKIData`) or Full PKI Response (`PKIResponse`) riding inside a CMS SignedData, reached by the encapsulated content type (`id-cct-PKIData` / `id-cct-PKIResponse`). Controls are surfaced in wire order with their values raw, so an unrecognized one is data rather than a fault. Tagged requests decode across all three arms. The status verdicts (`CMCStatusInfo` v1 and `CMCStatusInfoV2`) are collected as an ordered list, and the RFC 6402 two-module `OtherStatusInfo` ambiguity — `pendInfo` and `extendedFailInfo` are both untagged SEQUENCEs in the 1988 module, told apart only by their first element — is resolved by inspection and refused when it cannot be told apart. Body-part identity is unique across the whole message rather than per sequence, 0 is reserved as the reference to the enclosing PKIData, and the `reqSequence` bytes are surfaced exactly as they appeared so an Identity Proof witness is computed over the wire bytes. A companion decoder for CMS content rather than an auto-routed format — `parse`, `parsePkiData`, `parsePkiResponse` |
|
|
275
275
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
|
|
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. `distrustedKeys` lists SubjectPublicKeyInfo SHA-256 digests that `validate`, `build` and the OCSP responder checks refuse wherever they appear on a path, as the anchor, an intermediate, the target or a delegated responder (`path/distrusted-key`); one entry covers every cross-certificate issued for the key. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
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. `distrustedKeys` lists SubjectPublicKeyInfo SHA-256 digests that `validate`, `build` and the OCSP responder checks refuse wherever they appear on a path, as the anchor, an intermediate, the target or a delegated responder (`path/distrusted-key`); one entry covers every cross-certificate issued for the key. A key whose private key is public (`pki.trust.knownCompromisedKeys`, or an elliptic curve private key below 256) is refused in the same places with `path/compromised-key`, with or without a list. 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
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` |
|
|
@@ -295,7 +295,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
295
295
|
| `pki.possession` | Statement of possession (RFC 9883): how a key that cannot sign gets certified. PKCS#10 proves possession of the key being certified by signing the request with it, so an ML-KEM, X25519 or X448 key could not be certified through a certification request at all. RFC 9883 signs the request with a different key the requester already holds a certificate for, and names that certificate in an attribute. `pki.csr.sign` takes `spec.privateKeyPossessionStatement` and signs with the key given, which is the one case where it accepts a signing key that is not the subject's and a subject key that cannot sign; both stay refused for a request that declares no statement. `includeCertificate: false` emits the compact form the RFC allows, where the CA looks the certificate up. `pki.crmf.build` takes the same structure as `controls.statementOfPossession`, which the RFC gives the attribute's own OID. `pki.possession.verifyRequest` is the CA's side and enforces the two MUSTs: the request's signature is validated with the public key from the signature certificate, and that certificate's certification path is validated, so `opts.trustAnchors` is required and the verb throws without it rather than returning a verdict that skipped a MUST. The two name comparisons the RFC states as SHOULDs are reported and not enforced, because each ends in a question about certificate policy that no library can answer: `subjectMatches` and `subjectAltNamesMatch` carry the comparison and `valid` is `false` with a reason naming the decision. `requestsSignatureCertificate` reports the prohibition on using the attribute to obtain a signature certificate, which `pki.lint.csr` with `profile: "rfc9883"` grades as an error. A statement carrying a certificate its `signer` does not name is refused in both directions — `parse`, `verifyRequest` |
|
|
296
296
|
| `pki.altSig` | Alternative signatures (ITU-T X.509 (2019) clause 9.8), the shape widely called Catalyst: one certificate carrying two signatures, so a PKI migrating between algorithm sets does not have to reissue to every relying party at once. A relying party that has not migrated reads the native signature and ignores the extensions; one that has reads the alternative signature instead. `pki.x509.sign` takes `issuer.altKey` beside the issuing key and `extensions.subjectAltPublicKeyInfo` for the subject's alternative key, and emits all three extensions: clause 7.2.2 requires the alternative signature to be generated first, over the structure without it, and the native signature then to cover it, so the ordering is not the caller's to get right. `pki.crl.sign` takes `issuer.altKey` for the same mechanism on a CRL, which clause 7.10.3 gives the same procedure; `subjectAltPublicKeyInfo` is certificate-only. `signedData` builds the bytes that signature covers: the `PreTBSCertificate` of `draft-truskovsky-lamps-pq-hybrid-x509` section 4, which is the `tbsCertificate` without its `signature` field and without the `altSignatureValue` extension, and the `PreTBSCertList` of section 5 for a CRL. Those are the bytes Bouncy Castle emits and checks, so an alternative signature made here verifies there and one made there verifies here. Clause 7.2.2 requires a verifier to rebuild that encoding, and this is the one place the toolkit re-encodes what it parsed: every retained component keeps the bytes it had and only the two SEQUENCE headers whose lengths change are recomputed, so nothing is re-serialized from a decoded model. `verify` checks the signature under the issuer's alternative public key, which `subjectAltPublicKey` reads out of the issuer's own certificate; `SubjectAltPublicKeyInfo` has the same encoding as `SubjectPublicKeyInfo`, so that value is an SPKI and needs no conversion. The algorithm is not derived: the structure states it in `altSignatureAlgorithm`, and that stated algorithm is bound to the key through the engine path validation uses, so a key that does not match it resolves `false` rather than being read under some other algorithm; a structure stating no alternative signature throws instead, there being nothing to check. `pki.lint.certificate` with `profile: "x509-altsig"` grades the clause 9.8.3 pairing and the clause 7.2.2 full set as errors and a critical extension as a warning, criticality being stated only in a NOTE — `signedData`, `verify`, `subjectAltPublicKey` |
|
|
297
297
|
| `pki.relatedCert` | Related certificates (RFC 9763): the two structures that let one subject hold a classical certificate and a post-quantum one and have each say the other exists. `requestSignedData` builds the bytes a requester signs to prove it holds the certificate it names, which §3.1 fixes as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime` and nothing else; `locationInfo` travels in the attribute outside those bytes, so a verified proof says nothing about where the certificate can be fetched. `pki.csr.sign` takes the whole attribute as `relatedCertRequest` and encodes those two fields from that same function, so the bytes signed and the bytes emitted cannot drift apart; its `locationInfo` is a `SEQUENCE OF` IA5String URIs rather than one URI, and it keeps the order the requester wrote. `verifyRequest` checks the proof through the engine the certificate path uses, so the algorithm it derives is bound to the key, and returns `{ valid, verified, signerMaySign, signatureAlgorithm }` with a `code` and `reason` when it did not pass. The two checks are reported apart because they fail for different reasons: `verified` is the proof's own signature, and `signerMaySign` is whether the named certificate's keyUsage authorizes that signature at all, which §4.2.1.3 of RFC 5280 decides. A key-establishment certificate passes it, RFC 9763 §3.1 having such a key sign once for proof of possession. The key decides the algorithm, resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, so every algorithm those verbs sign with is reached, including an RSASSA-PSS key whose parameters belong to its identifier; the digest comes from the hash the certificate's signature OID indicates, with `digestAlgorithm` or a DER `signatureAlgorithm` naming either outright. A key whose algorithm fixes its own digest refuses `digestAlgorithm` rather than ignoring it, and a key that cannot sign is refused. A certificate other than the one `certID` names throws rather than returning `false`. `certificateHash` and `matchesCertificate` are the extension side, over the whole related certificate: the algorithm defaults as §4.1 directs, to the hash the related certificate's own signature OID indicates, and a certificate that indicates none throws rather than falling back to one the document does not name. `sha1` is refused. `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the certificate itself computes the digest rather than trusting a caller to name the algorithm it used; supplying both a `hashValue` and a certificate is refused, the two being able to disagree. The extension is emitted non-critical, which §4.1 asks at SHOULD NOT, and a critical one still parses: `pki.lint.certificate` with the `rfc9763` profile grades it, beside the MUST that it appear only in a chain's end-entity certificate — `requestSignedData`, `verifyRequest`, `certificateHash`, `matchesCertificate` |
|
|
298
|
-
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Only a self-issued certificate becomes an anchor; the others, such as the intermediate cache a Windows store export includes, come back in `intermediates` for `pki.path.build`. `systemAnchors({ distrustedKeys })` leaves out a certificate whose key is listed, and `spkiDigest(cert)` computes the digest that list takes. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor`, `spkiDigest` |
|
|
298
|
+
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Only a self-issued certificate becomes an anchor; the others, such as the intermediate cache a Windows store export includes, come back in `intermediates` for `pki.path.build`. `systemAnchors({ distrustedKeys })` leaves out a certificate whose key is listed, and `spkiDigest(cert)` computes the digest that list takes. `knownCompromisedKeys()` returns the built-in list of keys whose private key is public, which every verb refuses with no option to turn it off, as it refuses an elliptic curve key whose private key is an integer below 256. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor`, `spkiDigest`, `knownCompromisedKeys` |
|
|
299
299
|
| `pki.shbs` | Stateful hash-based signature verification: HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST SP 800-208 for CNSA 2.0 firmware signing. `verify` checks an HSS signature, where every level must pass, and `verifyLms` a single-tree LMS, over the raw public-key and signature blobs the parsers already surface. Pure public-input SHA-256 and SHAKE256 hashing, a data-driven typecode registry, and bounds-before-slice reads; a malformed blob throws a typed `ShbsError` while a well-formed but wrong signature returns `false`. Verification only by design, since stateful signing needs atomic one-time-key state that belongs in an HSM. `pki.path.validate` verifies an HSS-signed certificate on its own, so a chain reaching one needs no separate call: the signature covers the whole tbsCertificate rather than a digest of it (RFC 9802 §7.1), and a CertificateList naming the algorithm takes the same route. It is proven against the self-signed certificate RFC 9802 Appendix A publishes — `verify`, `verifyLms` |
|
|
300
300
|
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180), the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS` and `setupR` establish a sender or recipient context (KEM encapsulation plus the key schedule); the context's `seal` and `open` AEAD-encrypt with a sequence-counter nonce, and `export` derives further secrets; the module-level `seal` and `open` are single-shot wrappers. DHKEM (P-256, P-384, P-521, X25519, X448) in all four modes, proven against the RFC 9180 Appendix A vectors, the post-quantum ML-KEM-512/768/1024 KEMs (IANA 0x0040 to 0x0042) in the base and psk modes, proven against the draft-ietf-hpke-pq Appendix A vectors, and the PQ/T hybrid KEMs MLKEM768-P256 (0x0050), MLKEM1024-P384 (0x0051) and MLKEM768-X25519 (0x647a, X-Wing), each pairing an ML-KEM with a nominal group under the CFRG C2PRI combiner and proven against the draft-irtf-cfrg-concrete-hybrid-kems Appendix B vectors; the two-stage HKDF-SHA256/SHA384/SHA512 and the single-stage SHAKE128/SHAKE256 key schedules, proven against the draft-ietf-hpke-pq Appendix A.7/A.8/A.11/A.12 vectors, by AES-GCM / ChaCha20Poly1305 / export-only. On a single-stage KDF `info`, `psk`, `psk_id` and an export are each capped at 65535 bytes; TurboSHAKE128/256 are not offered while no released OpenSSL carries the XOF. `deriveKeyPair`, `generateKeyPair`, `encap` and `decap` reach a KEM on its own. Pure composition over `node:crypto` — `suites`, `setupS`, `setupR`, `seal`, `open`, `encap`, `decap` |
|
|
301
301
|
| `pki.kem` | Composite ML-KEM key establishment (draft-ietf-lamps-pq-composite-kem), a post-quantum ML-KEM hybridized with a traditional RSA-OAEP, ECDH, X25519, or X448 so the established secret holds if either component is later broken. `encapsulate` turns a recipient's composite `SubjectPublicKeyInfo` into a 256-bit shared secret and a ciphertext; `decapsulate` recovers the same secret from the ciphertext and the composite PKCS#8 private key. Each component KEM runs independently and the two secrets are mixed through a SHA3-256 combiner that binds the traditional ciphertext, the traditional public key, and a per-algorithm label. The twelve algorithms pair ML-KEM-768 and ML-KEM-1024 with RSA-OAEP 2048/3072/4096, ECDH over P-256/P-384/P-521 and brainpoolP256r1/P384r1, X25519, and X448, each verified against the draft Appendix G known-answer vectors; a malformed key or ciphertext, an unsupported algorithm, or a component decapsulation failure throws a typed `KemError` — `encapsulate`, `decapsulate` |
|
|
@@ -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. 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. The `key-compromise` profile, in the default set for certificates and requests, reports an elliptic curve key whose private scalar is below 256 (`lint/key-compromise/small-ec-private-key`), `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. The `key-compromise` profile, in the default set for certificates and requests, reports an elliptic curve key whose private scalar is below 256 (`lint/key-compromise/small-ec-private-key`) and a key on the built-in compromised-key list (`lint/key-compromise/known-compromised-key`), `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>` (`fetch` and `verify` take `--distrusted-key <hex>[,<hex>...]`), `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/asn1-der.js
CHANGED
|
@@ -997,12 +997,13 @@ function intToDer(v) {
|
|
|
997
997
|
|
|
998
998
|
function encodeOidContent(dotted) {
|
|
999
999
|
guard.identifier.assertCanonicalOid(dotted, _oidErr, "oid/bad-input", "OID", "oid/bad-arc", "oid/too-many-subidentifiers");
|
|
1000
|
-
var arcs =
|
|
1000
|
+
var arcs = intrinsic.mapList(intrinsic.splitChar(dotted, 46), function (p) { return intrinsic.BigInt(p); });
|
|
1001
1001
|
var a1 = arcs[0], a2 = arcs[1];
|
|
1002
|
-
/** @internal The sub-identifier list
|
|
1003
|
-
* `Array.prototype.concat
|
|
1004
|
-
* the OID a signature or an
|
|
1005
|
-
|
|
1002
|
+
/** @internal The sub-identifier list is assembled by `concatList`, which copies own indexes. A
|
|
1003
|
+
* captured `Array.prototype.concat` still reads `Symbol.isConcatSpreadable` on each argument, so an
|
|
1004
|
+
* inherited hook decided which arcs this encodes, and the result is the OID a signature or an
|
|
1005
|
+
* algorithm is named by. The split and the slice are the captured scans for the same reason. */
|
|
1006
|
+
var subids = intrinsic.concatList([a1 * 40n + a2], intrinsic.copyList(arcs, 2));
|
|
1006
1007
|
var out = [];
|
|
1007
1008
|
for (var s = 0; s < subids.length; s++) {
|
|
1008
1009
|
var v = subids[s];
|
package/lib/cmp-session.js
CHANGED
|
@@ -982,6 +982,10 @@ function session(opts) {
|
|
|
982
982
|
}
|
|
983
983
|
|
|
984
984
|
async function _validateLeaf(leaf, caPubs) {
|
|
985
|
+
/** @internal Checked before the anchors are, since a session with none validates no chain and
|
|
986
|
+
* this is then the only check the issued key meets. */
|
|
987
|
+
var leafRefusal = guard.distrust.refusal(_distrusted, x509.parse(leaf).subjectPublicKeyInfo.bytes, "opts.distrustedKeys");
|
|
988
|
+
if (leafRefusal !== null) throw _err("cmp/bad-cert-response", "the issued certificate's key " + leafRefusal);
|
|
985
989
|
if (_engine == null || opts.trustAnchors == null) return;
|
|
986
990
|
var caPubsList = [];
|
|
987
991
|
if (Array.isArray(caPubs)) caPubs.forEach(function (c) { if (Buffer.isBuffer(c)) _push(caPubsList,c); });
|
|
@@ -1702,9 +1706,12 @@ function session(opts) {
|
|
|
1702
1706
|
var newWithOld = _asCertificate(value.newWithOld, "the rootCaKeyUpdate newWithOld");
|
|
1703
1707
|
var oldWithNew = value.oldWithNew == null ? null : _asCertificate(value.oldWithNew, "the rootCaKeyUpdate oldWithNew");
|
|
1704
1708
|
var pNewNew = x509.parse(newWithNew), pNewOld = x509.parse(newWithOld), pOldRoot = x509.parse(oldRootDer);
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1709
|
+
var newRootRefusal = guard.distrust.refusal(_distrusted, pNewNew.subjectPublicKeyInfo.bytes, "opts.distrustedKeys");
|
|
1710
|
+
if (newRootRefusal !== null) throw _err("cmp/bad-info-value", "the rootCaKeyUpdate new root key " + newRootRefusal);
|
|
1711
|
+
/** @internal The old root's signature on newWithOld is what authorizes the new key, and the holder
|
|
1712
|
+
* of a compromised or distrusted old key can make that signature for any key. */
|
|
1713
|
+
var oldRootRefusal = guard.distrust.refusal(_distrusted, pOldRoot.subjectPublicKeyInfo.bytes, "opts.distrustedKeys");
|
|
1714
|
+
if (oldRootRefusal !== null) throw _err("cmp/bad-info-value", "the rootCaKeyUpdate old root key " + oldRootRefusal);
|
|
1708
1715
|
_assertCaCapable(pNewNew, "rootCaKeyUpdate newWithNew");
|
|
1709
1716
|
_assertCaCapable(pNewOld, "rootCaKeyUpdate newWithOld");
|
|
1710
1717
|
if (!cmpBuild.samePublicKey(pNewOld.subjectPublicKeyInfo.bytes, pNewNew.subjectPublicKeyInfo.bytes)) {
|
package/lib/cmp-verify.js
CHANGED
|
@@ -1030,9 +1030,8 @@ async function _openKeyPackage(container, opts) {
|
|
|
1030
1030
|
_authorizeKga(verdict, signerCert, opts);
|
|
1031
1031
|
/** @internal The shared-secret exemption replaces the authority's chain check, which is where the
|
|
1032
1032
|
* list is otherwise applied, so the authority's own key is checked here. */
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1033
|
+
var kgaRefusal = opts.exempt ? guard.distrust.refusal(opts.distrustedKeys, x509.parse(signerCert).subjectPublicKeyInfo.bytes, "opts.distrustedKeys") : null;
|
|
1034
|
+
if (kgaRefusal !== null) throw _err("cmp/unauthorized-kga", "the key generation authority's key " + kgaRefusal);
|
|
1036
1035
|
if (verdict.eContent == null) throw _keyPackageErr("the SignedData carries no encapsulated content, so no key package was delivered");
|
|
1037
1036
|
return guard.verdict.of({ keys: _readKeyPackage(verdict.eContent), kga: _bufferFrom(signerCert), trusted: opts.exempt ? false : true });
|
|
1038
1037
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
// @internal
|
|
5
|
+
|
|
6
|
+
/** @internal The keys every verifier in this toolkit refuses: certificate keys whose private key is
|
|
7
|
+
* public, whether published or derivable from the public key. Each entry is the SHA-256 digest of
|
|
8
|
+
* the key's SubjectPublicKeyInfo in canonical form (the value `pki.trust.spkiDigest` returns), the
|
|
9
|
+
* subject common name of a certificate that carries it (the product's CA name for a key published
|
|
10
|
+
* without a certificate), why the private key is public, and where that is recorded. An entry is added only when its private key has been recovered or obtained and
|
|
11
|
+
* checked against the public key. `pki.trust.knownCompromisedKeys()` returns a copy. */
|
|
12
|
+
var _freeze = require("./guard-intrinsic").freeze;
|
|
13
|
+
|
|
14
|
+
function _entry(digest, name, reason, sources) {
|
|
15
|
+
return _freeze({ spkiSha256: digest, name: name, reason: reason, sources: _freeze(sources) });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var EMUDHRA_SOURCES = [
|
|
19
|
+
"https://bugzilla.mozilla.org/show_bug.cgi?id=2083449",
|
|
20
|
+
"https://blog.cloudflare.com/emudhra-certificate-compromise/",
|
|
21
|
+
];
|
|
22
|
+
var EMUDHRA_REASON = "the P-384 private key is an integer below 256, which anyone can recover from the public " +
|
|
23
|
+
"key by trying each candidate; eMudhra and Sectigo both issued CA certificates for the key";
|
|
24
|
+
function _emudhra(digest, name) { return _entry(digest, name, EMUDHRA_REASON, EMUDHRA_SOURCES); }
|
|
25
|
+
|
|
26
|
+
var KOMODIA_SOURCES = [
|
|
27
|
+
"https://gist.github.com/Wack0/17c56b77a90073be81d3",
|
|
28
|
+
"https://www.kb.cert.org/vuls/id/529496",
|
|
29
|
+
"https://github.com/hannob/superfishy/tree/afbad9c2635bfd5c4bfa2630c27263f59efecd86/certificates",
|
|
30
|
+
];
|
|
31
|
+
function _komodia(digest, name, product) {
|
|
32
|
+
return _entry(digest, name, product + " uses the Komodia interception library and installs this root with one " +
|
|
33
|
+
"private key built into the software; the key was published in February 2015", KOMODIA_SOURCES);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var ADGUARD_SOURCES = [
|
|
37
|
+
"https://blog.hboeck.de/archives/874-More-TLS-Man-in-the-Middle-failures-Adguard,-Privdog-again-and-ProtocolFilters.dll.html",
|
|
38
|
+
"https://github.com/hannob/superfishy/tree/afbad9c2635bfd5c4bfa2630c27263f59efecd86/certificates",
|
|
39
|
+
];
|
|
40
|
+
function _adguard(digest, n) {
|
|
41
|
+
return _entry(digest, n === 3 ? "Adguard CA R73" : "Adguard CA", "Adguard ships ten CA private keys in ProtocolFilters.dll and certifies one of " +
|
|
42
|
+
"them as its root on each installation; this is key " + n + " of the ten, and all ten were published in 2015", ADGUARD_SOURCES);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var DELL_SOURCES = [
|
|
46
|
+
"https://blog.hboeck.de/archives/876-Superfish-2.0-Dangerous-Certificate-on-Dell-Laptops-breaks-encrypted-HTTPS-Connections.html",
|
|
47
|
+
"https://bugzilla.mozilla.org/show_bug.cgi?id=1227258",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
var KEYS = _freeze([
|
|
51
|
+
_emudhra("ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105", "EM DV TLS CA - G3B"),
|
|
52
|
+
_emudhra("43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b", "EM OV TLS CA - G3B"),
|
|
53
|
+
_emudhra("e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41", "EM EV TLS CA - G3B"),
|
|
54
|
+
_emudhra("49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f", "EM DV TLS CA - G3C"),
|
|
55
|
+
_emudhra("63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8", "EM OV TLS CA - G3C"),
|
|
56
|
+
_emudhra("5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0", "EM EV TLS CA - G3C"),
|
|
57
|
+
_emudhra("ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6", "EM OV CS CA - G3B"),
|
|
58
|
+
_emudhra("92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30", "EM OV CS CA - G3C"),
|
|
59
|
+
|
|
60
|
+
_entry("4bb8f35ba1e126f8dde1b0c420625ed86dce61a7bddadbdea9aba578ff13145e", "Superfish, Inc.",
|
|
61
|
+
"Superfish Visual Discovery, preinstalled on Lenovo consumer laptops, installs this root with one private key " +
|
|
62
|
+
"built into the software; the key and its password were published in February 2015", [
|
|
63
|
+
"https://blog.erratasec.com/2015/02/extracting-superfish-certificate.html",
|
|
64
|
+
"https://www.kb.cert.org/vuls/id/529496",
|
|
65
|
+
"https://github.com/hannob/superfishy/tree/afbad9c2635bfd5c4bfa2630c27263f59efecd86/certificates",
|
|
66
|
+
]),
|
|
67
|
+
_komodia("2698846608c82e14834b0a99da116097801ae712b1637897c8d305ee1f29b9ce", "KeepMyFamilySecure", "Keep My Family Secure"),
|
|
68
|
+
_komodia("c35169fdc7781d149081f60d6e81f259bd504ce19b53a0370d1dbabecb195437", "Qustodio", "Qustodio for Windows"),
|
|
69
|
+
_komodia("0ab38aff3afae30102ee3713bade6b83c9d27cd78045a9a0372b92031274b48b", "Qustodio CA", "Qustodio for macOS"),
|
|
70
|
+
_komodia("338124a8fdeef46002656293c677a0f73c69b664f847915d20fd6431851df423", "Kurupira.NET", "Kurupira WebFilter"),
|
|
71
|
+
_komodia("eb6aaaedf0a08c2e20a07f5d97125ba66c97f52994f1791462791f4f98af6baa", "AtomPark Software Inc", "StaffCop"),
|
|
72
|
+
_komodia("163b33dd8ee4f658fbbd681cef3abb091d890b39f0fc78289a1d1f7b8d39000f", "EasyTech", "Easy Hide IP Classic"),
|
|
73
|
+
_komodia("aabf9d9003d73799f3c6cc1577ab2b109dbcbf6371eef1e633b9e36bea563db0", "Lavasoft Limited", "Ad-Aware Web Companion"),
|
|
74
|
+
_komodia("5305c2c7f001e9c18269be5d914bc0c767e953d5f8540f2f0db27244c010ff37", "Sendori, Inc", "Sendori and PureLeads"),
|
|
75
|
+
_komodia("6a743e1ff94f445184971182955d919232085a0276e5c6dd9dfff22b367cb7e5", "InfoWeise", "SecureTeen"),
|
|
76
|
+
_komodia("30f397f1523cdd1c1511144ea2c37638ba779b8c49c30d08f682f878bc0c8cbf", "ImpresX OU", "DiscountCow"),
|
|
77
|
+
_komodia("86d8efafc3ede0edaa22a99ea8bd78e0decbae6585da94b438a2af1632e52a83", "Objectify Media Inc", "WebProtect"),
|
|
78
|
+
_komodia("37f8ccfa97ee243ff7ba4e799899bd6524355ee72a0ec9d90ae7cefb81409174", "Covenant Eyes", "Covenant Eyes"),
|
|
79
|
+
|
|
80
|
+
_entry("10900ceb22cbcef9e431f6801225df3755f7a75eae0e22f164cae1cbbc81424c", "PrivDog Secure Connection Inspector CA",
|
|
81
|
+
"PrivDog installs this root with the same private key on every installation; the key was published in 2015", [
|
|
82
|
+
"https://blog.hboeck.de/archives/874-More-TLS-Man-in-the-Middle-failures-Adguard,-Privdog-again-and-ProtocolFilters.dll.html",
|
|
83
|
+
"https://github.com/hannob/superfishy/tree/afbad9c2635bfd5c4bfa2630c27263f59efecd86/certificates",
|
|
84
|
+
]),
|
|
85
|
+
_adguard("f2971080d115de5d766fcf121561a591b20aa053846dc5a45f201080016c69db", 0),
|
|
86
|
+
_adguard("ddeb6f867663dc1e2d298f4abfad1f5e79a5021a2f8930c28e20b45b4bd1ccb1", 1),
|
|
87
|
+
_adguard("acbfc3190dd8588904410d746ba9f1f26727ea722718c3ffd5999f924d50cecf", 2),
|
|
88
|
+
_adguard("ade08a07590eabeac543e87b992d23f11022023d8ecd29c46a16e4082a0e7d96", 3),
|
|
89
|
+
_adguard("bf10a7a8ecacac2f33f65b255eda3f64633557683b604324cfe04ef0cdfe3e5e", 4),
|
|
90
|
+
_adguard("3bf92d4350bab7f85c7eb94c625d7e4101c3750b1f7ea0ccb2107db542486f78", 5),
|
|
91
|
+
_adguard("14b8d90aa31112c29773362edae7b4815f3af4329fa7780d5fc96eba6c78a0e7", 6),
|
|
92
|
+
_adguard("1fd5924ce5b7565b752a6ce8edfe783e04a9bae76fa60da744ca673b0991f528", 7),
|
|
93
|
+
_adguard("1732ba84e3ec722470e3a5166f04825db6e66c6b85ec2648aedbefbef4ed9d81", 8),
|
|
94
|
+
_adguard("cc9ce8160876312f0b510ab58ebd6782675bf595e8d14e1ffe6ce81efa27b3ea", 9),
|
|
95
|
+
|
|
96
|
+
_entry("cdee9f3305572a677e1a6c82dc1e02a35b11cae6a684338c9f37fe1ac8daec23", "eDellRoot",
|
|
97
|
+
"Dell installed this root with its private key on Dell computers in 2015; the key was extracted and published", DELL_SOURCES),
|
|
98
|
+
_entry("130ad4e0633521290531b6651f5759b0bc7bc656709ff8f365c2143b0389b6f6", "DSDTestProvider",
|
|
99
|
+
"Dell System Detect installs this root with its private key; the key was extracted and published in 2015", DELL_SOURCES),
|
|
100
|
+
_entry("8ebf425f93b2c7ef59edb0fa840aae95f689503fa2e38a447a0d87975ea0ff75", "bealocalhost.de",
|
|
101
|
+
"the German Federal Bar told lawyers to trust this root in December 2017, and its private key shipped inside the " +
|
|
102
|
+
"beA Client Security software", [
|
|
103
|
+
"https://www.feistyduck.com/newsletter/issue_36_private_keys_in_software",
|
|
104
|
+
"https://dserver.bundestag.de/btd/19/008/1900898.pdf",
|
|
105
|
+
]),
|
|
106
|
+
_entry("b72c2308eb6129308900bd53995fdb6025aa72c30e43f3527254b3ec87c236db", "DO NOT TRUST - AMI Test PK",
|
|
107
|
+
"the AMI test UEFI Secure Boot platform key (PKfail, CVE-2024-8105); its private key was committed to a public " +
|
|
108
|
+
"repository with its password, and the certificate shipped in production firmware", [
|
|
109
|
+
"https://github.com/binarly-io/Vulnerability-REsearch/blob/main/PKfail/BRLY-2024-005.md",
|
|
110
|
+
"https://www.cve.org/CVERecord?id=CVE-2024-8105",
|
|
111
|
+
]),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
module.exports = _freeze({ KEYS: KEYS });
|
package/lib/crmf-sign.js
CHANGED
|
@@ -1177,6 +1177,7 @@ function _buildCertReqMsg(spec, key, opts) {
|
|
|
1177
1177
|
|
|
1178
1178
|
var signingKey = (key && typeof key === "object" && !_isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
|
|
1179
1179
|
var template = _encodeCertTemplate(spec.certTemplate);
|
|
1180
|
+
if (template.spki !== null) guard.distrust.refuseCompromised(template.spki, _err, "crmf/compromised-key", "certTemplate.publicKey");
|
|
1180
1181
|
var certReqChildren = [b.integer(_certReqId(spec.certReqId)), template.der];
|
|
1181
1182
|
var encodedControls = [];
|
|
1182
1183
|
if (spec.controls != null) guard.list.append(certReqChildren, _buildAttrTypeAndValues(spec.controls, "crmf/bad-controls", "controls", CONTROL_VALUE, encodedControls));
|
|
@@ -1203,6 +1204,7 @@ function _buildCertReqMsg(spec, key, opts) {
|
|
|
1203
1204
|
* second walk over the same bytes, and read back through the one statement reader. */
|
|
1204
1205
|
possessionSpki = _encodedPossessionSpki(encodedControls);
|
|
1205
1206
|
}
|
|
1207
|
+
if (possessionSpki !== null) guard.distrust.refuseCompromised(possessionSpki, _err, "crmf/compromised-key", "the key of the statementOfPossession certificate");
|
|
1206
1208
|
|
|
1207
1209
|
var popoP = _promiseResolve(_Promise,
|
|
1208
1210
|
_buildProofOfPossession(spec.pop, certReqDer, template, signingKey, opts, possessionSpki));
|
package/lib/csr-sign.js
CHANGED
|
@@ -188,6 +188,8 @@ function _relatedCertRequest(spec) {
|
|
|
188
188
|
*
|
|
189
189
|
* Before the key is used, the request the spec describes is linted against the RFC 2986 profile and
|
|
190
190
|
* refused with `csr/profile-violation` if any rule grades it `error`, naming the rule and its clause.
|
|
191
|
+
* A `subjectPublicKey`, or the key of a possession statement's certificate, whose private key is
|
|
192
|
+
* public (see `pki.trust.knownCompromisedKeys`) is refused with `csr/compromised-key`.
|
|
191
193
|
*
|
|
192
194
|
* @opts
|
|
193
195
|
* - `pem` (boolean) -- return a PEM `CERTIFICATE REQUEST` string instead of DER.
|
|
@@ -225,6 +227,7 @@ function _sign(spec, key, opts) {
|
|
|
225
227
|
|
|
226
228
|
var subjectSpki = _b.reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the requested key)");
|
|
227
229
|
_b.assertValidSpki(subjectSpki, "spec.subjectPublicKey");
|
|
230
|
+
guard.distrust.refuseCompromised(subjectSpki, _err, "csr/compromised-key", "spec.subjectPublicKey");
|
|
228
231
|
var subjectDer = _b.encodeName(spec.subject == null ? [] : spec.subject);
|
|
229
232
|
|
|
230
233
|
var attrs = [], seenAttr = Object.create(null);
|
|
@@ -245,6 +248,7 @@ function _sign(spec, key, opts) {
|
|
|
245
248
|
var statement = _possessionStatement(spec.privateKeyPossessionStatement);
|
|
246
249
|
addAttr(O("statementOfPossession"), b.raw(statement.der));
|
|
247
250
|
possessionCertSpki = x509Schema.parse(statement.certificate).subjectPublicKeyInfo.bytes;
|
|
251
|
+
guard.distrust.refuseCompromised(possessionCertSpki, _err, "csr/compromised-key", "the key of the possession statement's certificate");
|
|
248
252
|
}
|
|
249
253
|
if (spec.attributes != null) {
|
|
250
254
|
if (!Array.isArray(spec.attributes)) throw _err("csr/bad-input", "spec.attributes must be an array of pre-encoded Attribute DER");
|
package/lib/framework-error.js
CHANGED
|
@@ -78,10 +78,23 @@ function _isCodeShape(c) {
|
|
|
78
78
|
* if (e instanceof pki.errors.PkiError) console.error(e.code);
|
|
79
79
|
* }
|
|
80
80
|
*/
|
|
81
|
+
/** @internal A field of an error, defined as an own data property the way an assignment to a plain
|
|
82
|
+
* object creates one. An assignment runs a setter inherited from `Object.prototype`, and a setter
|
|
83
|
+
* that throws turned the construction of every error into that throw. The descriptor has no
|
|
84
|
+
* prototype, since `defineProperty` reads an inherited `get` or `set` off a literal one. */
|
|
85
|
+
function _own(target, key, value) {
|
|
86
|
+
var d = intrinsic.create(null);
|
|
87
|
+
d.value = value;
|
|
88
|
+
d.writable = true;
|
|
89
|
+
d.enumerable = true;
|
|
90
|
+
d.configurable = true;
|
|
91
|
+
_defineProperty(target, key, d);
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
class PkiError extends Error {
|
|
82
95
|
constructor(message, code) {
|
|
83
96
|
super(message);
|
|
84
|
-
this
|
|
97
|
+
_own(this, "name", "PkiError");
|
|
85
98
|
var c = code || "pki/invalid";
|
|
86
99
|
if (typeof c !== "string" || !_isCodeShape(c)) {
|
|
87
100
|
throw new TypeError(
|
|
@@ -89,9 +102,9 @@ class PkiError extends Error {
|
|
|
89
102
|
" (PkiError takes (message, code); defineClass subclasses take (code, message))"
|
|
90
103
|
);
|
|
91
104
|
}
|
|
92
|
-
this
|
|
93
|
-
this
|
|
94
|
-
this
|
|
105
|
+
_own(this, "code", c);
|
|
106
|
+
_own(this, "isPkiError", true);
|
|
107
|
+
_own(this, "permanent", !_TRANSIENT_REASONS[_reasonOf(c)]);
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
|
|
@@ -129,10 +142,10 @@ function defineClass(name, opts) {
|
|
|
129
142
|
var GeneratedError = class extends PkiError {
|
|
130
143
|
constructor(code, message, arg3) {
|
|
131
144
|
super(message, code);
|
|
132
|
-
this
|
|
133
|
-
this
|
|
145
|
+
_own(this, "name", name);
|
|
146
|
+
_own(this, flagKey, true);
|
|
134
147
|
if (withCause) {
|
|
135
|
-
if (arg3 !== undefined) this
|
|
148
|
+
if (arg3 !== undefined) _own(this, "cause", arg3);
|
|
136
149
|
} else if (arg3 !== undefined) {
|
|
137
150
|
throw new TypeError(name + " takes (code, message); to thread a cause, define the class with { withCause: true }");
|
|
138
151
|
}
|