@blamejs/pki 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ 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.6.11 — 2026-08-31
8
+
9
+ pki.path.validate and pki.tsp.verify now take the trust store under the option name trustAnchors, the spelling the other verify verbs already use, accepting one anchor or an array; and pki.acme.client gains opts.resignKeys, which re-signs a request under an alternative account-key algorithm when a CA reports badSignatureAlgorithm.
10
+
11
+ ### Added
12
+
13
+ - pki.acme.client accepts opts.resignKeys, an ordered array of { alg, key } alternatives for the account key (RFC 8555 section 6.2). When a CA rejects the account's JWS algorithm with a badSignatureAlgorithm error and lists the algorithms it supports, the client re-signs the request once with the first listed alternative the CA supports and retries. Each key is a CryptoKey for the same account key material under that algorithm, so the account's registered public key still verifies the re-signed request. The caller's order selects the algorithm and the CA's list only filters it, so a spoofed badSignatureAlgorithm cannot force a weaker algorithm (RFC 8555 section 10). Without the option, or when no listed algorithm is a caller alternative, the error surfaces unchanged.
14
+
15
+ ### Changed
16
+
17
+ - pki.path.validate and pki.tsp.verify read the trust anchor from opts.trustAnchors, the same option the other verify verbs take. It accepts a single anchor tuple or root certificate, or a non-empty array of them; when several are supplied, pki.path.validate selects the anchor that issued the path's top certificate, and a one-element array reproduces the previous single-anchor result exactly. The former singular opts.trustAnchor is removed and is refused as an unknown option rather than silently ignored, so a stale caller gets a named error instead of an unanchored valid result. To upgrade, pass { trustAnchors: anchor } (or { trustAnchors: [a, b] }) where you passed { trustAnchor: anchor }.
18
+
19
+ ## v0.6.10 — 2026-08-31
20
+
21
+ pki.est computes a response body's length from a single read of res.body, shared by one helper across every verb, so a caller-supplied transport whose res.body getter changes between reads cannot desync the type check from the measured length.
22
+
23
+ ### Fixed
24
+
25
+ - pki.est reads a response body's length from a single access of res.body rather than re-reading it across the null / string / BufferSource type-check arms. A caller-supplied transport whose res.body getter returns different values across reads could otherwise size the body from a different value than the one the type check classified. The five identical per-verb length computations are consolidated into one shared helper that reads res.body once.
26
+
7
27
  ## v0.6.9 — 2026-08-31
8
28
 
9
29
  pki.cmp.verify holds a received PKIMessage to the RFC 9483 sec. 3.5 header rules, and pki.path.build and pki.est read a response body an injected transport returns as any BufferSource, not only a Node Buffer.
package/MIGRATING.md CHANGED
@@ -14,6 +14,26 @@ The toolkit has no `deprecate()`-marked surface awaiting removal.
14
14
 
15
15
  Listed newest-first.
16
16
 
17
+ ### v0.6.11 — `pki.path.validate(path, opts) and pki.tsp.verify(token, data, opts)`
18
+
19
+ The trust anchor option is now `trustAnchors` (a single anchor or an array); the former singular `trustAnchor` is removed and refused by name.
20
+
21
+ Both verbs read the trust anchor from `opts.trustAnchors`, the spelling pki.path.build,
22
+ pki.cms.verify, pki.cmp.verify, and pki.smime.verify already use. It accepts a single anchor
23
+ tuple or root certificate, or a non-empty array of them.
24
+
25
+ ```js
26
+ var res = await pki.path.validate(path, { time: t, trustAnchor: anchor }); // removed
27
+
28
+ var res = await pki.path.validate(path, { time: t, trustAnchors: anchor }); // one anchor
29
+ var res = await pki.path.validate(path, { time: t, trustAnchors: [rootA, rootB] }); // several
30
+ ```
31
+
32
+ With several anchors, pki.path.validate selects the one that issued the path's top certificate,
33
+ and a one-element array reproduces the previous single-anchor result. The removed `trustAnchor`
34
+ is refused as an unknown option (`path/bad-input` or `tsp/bad-input`) naming it, rather than read
35
+ as absent, so a request that was silently unanchored now fails closed with a named error.
36
+
17
37
  ### v0.5.24 — `pki.acme.client(...).downloadCertificate(url, opts)`
18
38
 
19
39
  The verb needs to be told which certificate the download is allowed to be, and refuses with acme/binding-required when it is not.
package/README.md CHANGED
@@ -225,7 +225,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
225
225
  | `pki.scep` | SCEP pkiMessage codec (RFC 8894), the client side of Simple Certificate Enrollment. `build(spec)` assembles a `PKCSReq` or `RenewalReq` request: it verifies the PKCS#10 certification request's own proof-of-possession, encrypts it to the CA as the inner EnvelopedData (`pkcsPKIEnvelope`, AES-128-CBC), and signs that under the transaction attributes RFC 8894 §3.2 defines, a messageType, a caller-unique transactionId, and a 16-byte senderNonce. The recipient CA certificate must assert `keyEncipherment`. `parse(bytes, opts?)` reads a `PKCSReq`, `RenewalReq`, or `CertRep` back (the `CertPoll`, `GetCert`, and `GetCRL` client queries are refused): it verifies the outer signature first and refuses a message whose signature does not check, so the transaction attributes it returns come only from a verified signer, never alongside a false verdict. Because a valid signature only proves self-consistency with the embedded certificate, pass `opts.signerCert` to authenticate a CA response against the CA certificate you hold (a public-key mismatch is refused, `signerAuthenticated` reports the result); a caller that omits it gets a crypto-only verdict and must authenticate the surfaced `signerCert` itself. It maps the messageType, pkiStatus, failInfo, and the nonces from their RFC 8894 enumerants and refuses an unknown one; given `recipientKey` it decrypts the `pkcsPKIEnvelope` to the messageData, and given `expectedSenderNonce` it refuses a response whose recipientNonce does not echo it. It reads a CA's CertRep in full. `getCACaps(baseUrl, opts?)`, `getCACert(baseUrl, opts?)`, `enroll(baseUrl, opts)`, and `renew(baseUrl, opts)` carry that codec to a live CA over HTTP (RFC 8894 §4) through an injectable transport (`pki.transport.https` by default): `getCACaps` reports the CA's advertised capabilities and never lowers the client's algorithm choice on the unauthenticated response, `getCACert` pins the CA certificate to an out-of-band `expectedFingerprint` a returned certificate must match, and `enroll` and `renew` POST a PKCSReq or RenewalReq, authenticate the CertRep against the CA certificate and its nonce echo, select the issued certificate by public-key match, throw on a FAILURE with the CA's failInfo, and return a PENDING to retry. Issuing a CertRep, `GetNextCACert`, and automatic polling are not yet built — `build`, `parse`, `getCACaps`, `getCACert`, `enroll`, `renew` |
226
226
  | `pki.transport` | The shared, fail-closed `node:https` transport the enrollment clients drive. `pki.transport.https(defaults)` returns a `transport(request) → { status, headers, body, tls }`, where `tls` carries the negotiated `protocol`, `cipher`, and raw `peerCertificate`. This is the toolkit's only socket choke point: an explicit trust anchor (or an opt-in to the system store) is required, `rejectUnauthorized` is always on, TLS is floored at 1.2, the response body is capped while it streams, and a stalled socket times out. The EST, ACME, and CMP clients reuse it verbatim. If you inject your own transport, return `tls` too: `pki.est.serverkeygen` asserts the negotiated cipher can protect the delivered private key, and a transport that reports no cipher is trusted rather than refused, so omitting the field silently skips that check — `https` |
227
227
  | `pki.jose` | Flattened JWS (RFC 7515) and JWK thumbprints (RFC 7638). `sign` and `verify` run a Flattened JWS against declarative profiles (ACME outer, EAB inner, keyChange inner) that carry the required and forbidden header rules as data. `base64url` is the strict RFC 4648 §5 codec, rejecting padding, non-alphabet characters, and non-canonical trailing bits. `parseJson` is a bounded reader that refuses duplicate members at any depth. `thumbprint` is the RFC 7638 / 8037 / 9964 canonical digest. The algorithm registry binds each `alg` to its key type (ES/RS/PS/EdDSA/ML-DSA), leaving no code path for `alg:none`, an RS256→HS256 key confusion, or an all-zero ECDSA signature; `assertPublicJwk` refuses a JWK carrying private material, so an exported private key is never published. `opts.key` names the key a message must be signed under and governs: where the profile also permits an embedded header `jwk`, the two must be the same key — compared as RFC 7638 thumbprints, so member order cannot make equal keys differ — and a disagreement is refused rather than resolved in the message's favor. `keySource` reports which key answered, since a signature checked against a key you named is a different claim from one checked against the key the message carried — `sign`, `verify`, `base64url`, `parseJson`, `thumbprint`, `assertPublicJwk` |
228
- | `pki.acme` | ACME (RFC 8555 / 8737 / 8738 / 9773). `client(directoryUrl, opts)` is a stateful client driving a live CA directory over `pki.transport`: `newAccount`, `newOrder`, `newAuthz`, `getOrder`, `getAuthorization`, `getChallenge`, `respondToChallenge`, `finalize`, `pollOrder`, `pollAuthorization`, and `downloadCertificate` walk the issuance flow, with `newAuthz` pre-authorizing a single identifier (§7.4.1) and `downloadCertificate` binding the issued certificate to the order — its key, its identifiers, or both; at least one is required by default and the result reports `boundToKey` / `boundToIdentifiers` — before choosing among alternate chains (`Link rel="alternate"`, §7.4.2, via a `selectChain` predicate bounded by `maxAlternates`). `revokeCert` (account-key or certificate-key signed), `keyChange`, `updateAccount` (change the account contacts, §7.3.2), `listOrders` (the account's orders list, following the §7.1.2.1 `Link rel="next"` pagination bounded by a page cap and confined to the orders origin), `deactivateAccount`, `deactivateAuthorization`, `renewalInfo` (ARI), and `renewalWindow` (the RFC 9773 §4.2/4.3 renewal decision) complete the lifecycle. Every URL is HTTPS only, an explicit trust anchor is required, each request carries a fresh single-use nonce with a bounded badNonce retry, reads are POST-as-GET, polling is bounded and sleeps on a Retry-After via an injectable sleeper capped by a poll count and a total-wait budget, and every response body is size-capped. The transport is injectable via `opts.transport`. Over the message layer it composes resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, the request builders (newAccount with External Account Binding, newOrder with `replaces`, finalize with a CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns and ip identifier validators, and the ARI certID with serial sign-padding preserved — `client`, `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
228
+ | `pki.acme` | ACME (RFC 8555 / 8737 / 8738 / 9773). `client(directoryUrl, opts)` is a stateful client driving a live CA directory over `pki.transport`: `newAccount`, `newOrder`, `newAuthz`, `getOrder`, `getAuthorization`, `getChallenge`, `respondToChallenge`, `finalize`, `pollOrder`, `pollAuthorization`, and `downloadCertificate` walk the issuance flow, with `newAuthz` pre-authorizing a single identifier (§7.4.1) and `downloadCertificate` binding the issued certificate to the order — its key, its identifiers, or both; at least one is required by default and the result reports `boundToKey` / `boundToIdentifiers` — before choosing among alternate chains (`Link rel="alternate"`, §7.4.2, via a `selectChain` predicate bounded by `maxAlternates`). `revokeCert` (account-key or certificate-key signed), `keyChange`, `updateAccount` (change the account contacts, §7.3.2), `listOrders` (the account's orders list, following the §7.1.2.1 `Link rel="next"` pagination bounded by a page cap and confined to the orders origin), `deactivateAccount`, `deactivateAuthorization`, `renewalInfo` (ARI), and `renewalWindow` (the RFC 9773 §4.2/4.3 renewal decision) complete the lifecycle. Every URL is HTTPS only, an explicit trust anchor is required, each request carries a fresh single-use nonce with a bounded badNonce retry (and, when `opts.resignKeys` supplies caller-approved alternative account-key algorithms, a bounded RFC 8555 §6.2 badSignatureAlgorithm re-sign that the CA's advertised list can only filter, never widen, so it cannot force a weaker algorithm), reads are POST-as-GET, polling is bounded and sleeps on a Retry-After via an injectable sleeper capped by a poll count and a total-wait budget, and every response body is size-capped. The transport is injectable via `opts.transport`. Over the message layer it composes resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, the request builders (newAccount with External Account Binding, newOrder with `replaces`, finalize with a CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns and ip identifier validators, and the ARI certID with serial sign-padding preserved — `client`, `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
229
229
  | `pki.schema.smime` | S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551). `parseSigningCertificate` and `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` and serial); `parseSmimeCapabilities` decodes the ordered capability list; `decodeAttribute` dispatches a CMS attribute by OID, enforcing the single-value rule and deferring on unknown types. A companion decoder for CMS signed attributes rather than an auto-routed format — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
230
230
  | `pki.cmc` | Build and interpret CMC messages (RFC 5272). `build(spec, signer)` assembles a Full PKI Request across all three request arms (PKCS#10, CRMF, other) and signs it 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` |
231
231
  | `pki.schema.cmc` | Decode CMC messages (RFC 5272 as updated by RFC 6402): 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` |
@@ -245,7 +245,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
245
245
  | `pki.ocsp` | Online Certificate Status Protocol (RFC 6960), both the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs, with the CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile or under SHA-2, plus an optional RFC 9654 nonce and an optional requestor signature. `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER, from the issuing CA directly or a delegated responder, under any `pki.cms.sign` key including the post-quantum ML-DSA and SLH-DSA sets, with `good`, `revoked` (reason and time), or `unknown` per-certificate status. `buildErrorResponse(status)` produces the unsigned §2.3 error (`tryLater`, `unauthorized`, and the rest). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA, or a CA-issued delegate bearing id-kp-OCSPSigning and id-pkix-ocsp-nocheck and passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency against `thisUpdate` and `nextUpdate`, and the request-nonce echo. It returns `{ status: "good" / "revoked" / "unknown", ... }` and never silently accepts. `verifyRequest(request, opts)` is the responder-side inverse of `buildRequest`: it verifies a client's signed request (RFC 6960 §4.1.1) under the requestor certificate the request carries, trying each embedded certificate since the field is unordered, and reports `signed`, `signatureValid`, every certificate whose key verified (`signerCerts`, so a key carried under an expired certificate beside its renewal is not hidden behind ordering) with the first one's decoded subject, the full embedded certificate bag (`certs`, including intermediates for path building), and the request's CertIDs; an unsigned request is reported with `signed: false` rather than refused. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify`, `verifyRequest` |
246
246
  | `pki.ct` | Certificate Transparency (RFC 6962). `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries, a TLS-presentation-language payload inside the §3.3 double DER wrap, into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature. `reconstructSignedData` rebuilds the exact `digitally-signed` preimage, and `verifySct` verifies an SCT signature against a log's public key, routing an ECDSA signature through the strict DER-conformance gate and verifying through the crypto engine, resolving true or false and throwing a typed error on a structural fault. On the producing side, `encodeSctList` builds the extension value byte for byte as the exact inverse of `parseSctList`, and `signSct` performs a log's signing step. For trust, `parseLogList` ingests the CT log-list JSON into constraint-carrying trusted logs, recomputing each log's id as SHA-256 of its key and refusing a disagreeing id (a swapped key, §3.2) and decoding the state and temporal-interval constraints; `verifySctWithLogList` resolves the log key from an SCT's log id, enforces the state (usable, qualified, and readonly trusted; retired only before retirement; pending and rejected refused) and the temporal-interval window, then delegates the signature check to `verifySct`. `verifyLogListSignature(json, signature, publicKey)` verifies the detached `log_list.sig` over the raw log-list bytes against a caller-pinned signer key (RSASSA-PKCS1-v1.5/SHA-256 and an EC P-256 arm, with forgeable-key defenses failing closed), cross-checked against `openssl dgst`. `fetchLogList(opts)` turns that chain into a live client: it GETs the `log_list.json` and its detached `log_list.sig` over `pki.transport`, verifies the detached signature over the raw fetched bytes against a caller-pinned distributor key before parsing, so an unverified document is never parsed, read, cached, or surfaced, then ingests the same bytes through `parseLogList` and returns the trusted-log set plus the surfaced `version` and `timestamp`. Above the per-SCT layer, `verifySctList` renders a certificate-level verdict over every SCT a certificate carries (§3.3), reporting how many distinct trusted logs verified an SCT (so a duplicated SCT cannot inflate the count) and from how many distinct trusted operators against a caller's CT policy (`minScts` and `minOperators`, both defaulting to the RFC floor of one), where a policy shortfall is a verdict and a per-SCT failure is a recorded row rather than a thrown call; `x509CertEntry` reconstructs the precertificate log entry from a final certificate by removing the SCT-list extension from its TBSCertificate as byte surgery, so its embedded SCTs verify end to end (§3.2). The toolkit bakes in no vendor URL or key, TLS trust is explicit with `rejectUnauthorized` always on, each response is size-capped before the trust chain, and the transport is injectable so the whole path is testable offline — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct`, `parseLogList`, `verifySctWithLogList`, `verifySctList`, `x509CertEntry`, `verifyLogListSignature`, `fetchLogList` |
247
247
  | `pki.merkle` | Merkle-tree proof verification (RFC 6962 / RFC 9162). `leafHash`, `nodeHash`, and `emptyRootHash` build the domain-separated (0x00 leaf, 0x01 node) SHA-256 tree hashes. `verifyInclusion` folds an audit proof back to a root, and `verifyConsistency` reconstructs both the old and new root, which is the append-only guarantee; each is constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
248
- | `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({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
248
+ | `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 })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
249
249
  | `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 — `verify`, `verifyLms` |
250
250
  | `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 HKDF 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-521, X25519, X448) by HKDF-SHA256/SHA512 by AES-GCM / ChaCha20Poly1305 / export-only, across all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM and X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
251
251
  | `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` |
package/bin/pki.js CHANGED
@@ -190,7 +190,7 @@ function cmdVerify(args) {
190
190
  var spki = anchor.subjectPublicKeyInfo;
191
191
  return pki.path.validate(certs, {
192
192
  time: time,
193
- trustAnchor: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
193
+ trustAnchors: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
194
194
  }).then(function (res) {
195
195
  process.stdout.write(res.valid ? "valid\n" : "invalid\n");
196
196
  if (!res.valid) {
package/lib/acme.js CHANGED
@@ -43,7 +43,8 @@ var csr = require("./schema-csr");
43
43
  var pkix = require("./schema-pkix");
44
44
  var constants = require("./constants");
45
45
  var rfc3339 = require("./rfc3339");
46
- var webcrypto = require("./webcrypto").webcrypto;
46
+ var wcEngine = require("./webcrypto");
47
+ var webcrypto = wcEngine.webcrypto;
47
48
  var subtle = webcrypto.subtle;
48
49
  var frameworkError = require("./framework-error");
49
50
  var httpTransport = require("./http-transport");
@@ -2259,6 +2260,7 @@ function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, bas
2259
2260
  * - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity } for the default transport.
2260
2261
  * - `timeout` / `maxResponseBytes` / `maxRedirects` -- transport budgets; `maxNonceRetries` -- badNonce retry cap (default 1).
2261
2262
  * - `maxPolls` / `maxTotalWait` / `sleep` -- poll-loop budgets + an injectable sleeper; `clock` -- an injectable receipt clock (default Date.now) for a Retry-After HTTP-date.
2263
+ * - `resignKeys` -- optional ordered `[{ alg, key }]` enabling the RFC 8555 sec. 6.2 badSignatureAlgorithm re-sign. When a CA rejects the account `alg` and advertises the algs it supports, the first entry whose `alg` the CA advertised re-signs the request once and retries. Each `key` is a CryptoKey for the SAME account key material under that `alg`: an RSA account key signs RS256 and PS256 from one key, and its registered public JWK verifies either. The caller's order selects and the CA's list only filters, so a spoofed badSignatureAlgorithm cannot force a weaker alg (sec. 10). Absent, or with no advertised match, the badSignatureAlgorithm surfaces unchanged.
2262
2264
  * - `newAuthz(identifier)` -- pre-authorize a single identifier (RFC 8555 sec. 7.4.1) -> { authorization, url }.
2263
2265
  * - `downloadCertificate(url, { expectedSpki, identifiers, requireBinding, selectChain, maxAlternates })` -- bind the issued certificate to this order, then pick among RFC 8555 sec. 7.4.2 alternate chains. `expectedSpki` (the DER SubjectPublicKeyInfo this order's CSR asked to have certified) and `identifiers` (the order's own identifier array) are what the returned end-entity certificate is checked against: a different key is `acme/certificate-key-mismatch`, a different identifier set `acme/certificate-identifier-mismatch`. Only `dns` and `ip` identifiers map to a name a certificate carries, so an order identifier of another registered type, a certificate `subjectAltName` that is neither a dNSName nor an iPAddress, and a subject common name that is neither a dns name nor a canonical IP address, are each refused as `acme/unsupported-identifier-type` rather than dropped from the comparison. The certificate's alternative names are its identity; its common name is read only where it asserts none. At least one is required (`acme/binding-required`) unless `requireBinding: false`, which waives the requirement to supply material and never the check on material that is supplied. The result reports `boundToKey` / `boundToIdentifiers`. `selectChain({certificate, chain, certificates})` returns the first truthy candidate (primary first, then bounded `Link rel="alternate"` chains, confined to the download's own origin); the result adds `alternates` (the resolved alternate URLs).
2264
2266
  * - `renewalWindow(certDer, { random, clock, replaced, previous })` -- the RFC 9773 ARI renewal decision: composes `renewalInfo`, selects a uniform-random instant in the suggested window -> { suggestedWindow, selectedTime, renewNow, retryAfterSeconds, explanationURL }. Pass a prior result back as `previous` to REUSE its selectedTime while the CA's window is unchanged (RFC 9773 sec. 4.2), so repeated refreshes keep one stable renewal instant.
@@ -2272,7 +2274,7 @@ function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, bas
2272
2274
  // defaulted -- a mistyped `maxRedirects` or `transport` would otherwise change the client's behavior with
2273
2275
  // no error. The per-request options each returned method takes are checked in those methods, not here.
2274
2276
  var _CLIENT_OPTS = {
2275
- accountKey: 1, accountJwk: 1, alg: 1, transport: 1, tls: 1, timeout: 1, maxResponseBytes: 1,
2277
+ accountKey: 1, accountJwk: 1, alg: 1, resignKeys: 1, transport: 1, tls: 1, timeout: 1, maxResponseBytes: 1,
2276
2278
  maxRedirects: 1, maxNonceRetries: 1, maxPolls: 1, maxTotalWait: 1, sleep: 1, clock: 1,
2277
2279
  };
2278
2280
  // The per-request options each client method reads. A near-miss spelling in a method's own bag is
@@ -2299,6 +2301,49 @@ function _gateOpts(bag, whitelist, name) {
2299
2301
  guard.identifier.assertKnownKeys(bag, whitelist, E, "acme/bad-input", "unknown " + name + " option ");
2300
2302
  return bag;
2301
2303
  }
2304
+ // Validate + snapshot opts.resignKeys (config-time, fail closed). Absent -> null (the feature is OFF and
2305
+ // a badSignatureAlgorithm surfaces to the caller unchanged). Present -> a non-empty array of { alg, key }
2306
+ // where alg is a JWS alg string and key is a CryptoKey; each pair is read ONCE into a frozen copy so a
2307
+ // caller getter cannot rewrite it between this check and the re-sign, and the caller's order is preserved.
2308
+ function _resignKeysOf(raw) {
2309
+ if (raw == null) return null;
2310
+ if (!_isArrayI(raw) || raw.length === 0) throw E("acme/bad-input", "resignKeys, when set, must be a non-empty array of { alg, key } (RFC 8555 sec. 6.2)");
2311
+ var out = [];
2312
+ for (var i = 0; i < raw.length; i++) {
2313
+ var e = raw[i];
2314
+ if (!_isObject(e)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
2315
+ // Read each field ONCE into the frozen snapshot: a caller getter cannot then answer the validation
2316
+ // and the re-sign with different values (the alg checked is the alg signed, the key checked is signed).
2317
+ var a = e.alg, k = e.key;
2318
+ if (!_isString(a) || !wcEngine.isCryptoKeyLike(k)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
2319
+ out[i] = intrinsic.freeze({ alg: a, key: k }); // index-write, not a live-prototype .push
2320
+ }
2321
+ return intrinsic.freeze(out);
2322
+ }
2323
+ // Pick the re-sign entry to use for a badSignatureAlgorithm: the FIRST caller-listed entry whose alg the
2324
+ // server advertised as supported. Caller order selects and the server's array only filters, so a bogus
2325
+ // badSignatureAlgorithm cannot steer the client to the server's weakest alg (RFC 8555 sec. 10). Returns
2326
+ // null when no caller entry matches (the badSignatureAlgorithm then surfaces, fail closed).
2327
+ function _chooseResign(resignKeys, serverAlgs) {
2328
+ if (!resignKeys || !_isArrayI(serverAlgs)) return null;
2329
+ for (var i = 0; i < resignKeys.length; i++) {
2330
+ for (var j = 0; j < serverAlgs.length; j++) {
2331
+ if (serverAlgs[j] === resignKeys[i].alg) return resignKeys[i];
2332
+ }
2333
+ }
2334
+ return null;
2335
+ }
2336
+ // A copy of a public JWK with its optional `alg` member set to the alg actually being signed. A
2337
+ // WebCrypto-exported RSA JWK carries the key's native alg (e.g. RS256), so embedding it unchanged under a
2338
+ // re-sign alg (PS256) would advertise a JWK whose declared alg contradicts the JWS header, which a strict
2339
+ // ACME server may reject. Own members are copied through captured intrinsics (no live prototype read).
2340
+ function _jwkForAlg(jwk, alg) {
2341
+ var out = {};
2342
+ var keys = intrinsic.ownKeys(jwk);
2343
+ for (var i = 0; i < keys.length; i++) { out[keys[i]] = jwk[keys[i]]; }
2344
+ out.alg = alg;
2345
+ return out;
2346
+ }
2302
2347
  function client(directoryUrl, opts) {
2303
2348
  opts = guard.identifier.optionsObject(opts, E, "acme/bad-input", "pki.acme.client options");
2304
2349
  guard.identifier.assertKnownKeys(opts, _CLIENT_OPTS, E, "acme/bad-input", "unknown pki.acme.client option ");
@@ -2308,6 +2353,12 @@ function client(directoryUrl, opts) {
2308
2353
  jose.assertPublicJwk(opts.accountJwk);
2309
2354
  var dirUrl = _clientUrl(directoryUrl);
2310
2355
  var accountKey = opts.accountKey, accountJwk = opts.accountJwk, alg = opts.alg;
2356
+ // Optional badSignatureAlgorithm re-sign material (RFC 8555 sec. 6.2). Each entry pairs an alternative
2357
+ // JWS alg with a CryptoKey for the SAME account key material under that alg (an RSA public JWK is
2358
+ // padding-agnostic, so the registered account key still verifies a re-signed request). Snapshotted here
2359
+ // so a caller getter cannot rewrite the pairs between validation and use, and ordered by CALLER
2360
+ // preference: on a badSignatureAlgorithm the caller's order selects, never the server's (sec. 10).
2361
+ var resignKeys = _resignKeysOf(opts.resignKeys);
2311
2362
 
2312
2363
  var transport = opts.transport;
2313
2364
  if (!transport) {
@@ -2472,11 +2523,12 @@ function client(directoryUrl, opts) {
2472
2523
  // with the error response's fresh nonce (RFC 8555 sec. 6.5 -- _takeNonce's LIFO pool surfaces it).
2473
2524
  // `accept` is the media type the request advertises (default application/json). This is the single
2474
2525
  // home of the badNonce retry, so every signed POST -- account-key or certificate-key -- inherits it.
2475
- function _postJws(url, makeJws, accept, okStatuses) {
2526
+ function _postJws(url, makeJws, accept, okStatuses, allowResign) {
2476
2527
  var ok = okStatuses || [200];
2477
2528
  var attempt = 0;
2478
- function once() {
2479
- return _takeNonce().then(makeJws).then(function (jws) {
2529
+ var resignAttempt = 0;
2530
+ function once(override) {
2531
+ return _takeNonce().then(function (nonce) { return makeJws(nonce, override); }).then(function (jws) {
2480
2532
  return _send("POST", url, { "content-type": "application/jose+json", "accept": accept || "application/json" }, JSON.stringify(jws));
2481
2533
  }).then(function (res) {
2482
2534
  // An application/problem+json body is an ERROR regardless of the HTTP status -- a CA / proxy
@@ -2486,7 +2538,17 @@ function client(directoryUrl, opts) {
2486
2538
  var prob = null;
2487
2539
  try { prob = jose.parseJson(_jsonInput(res)); }
2488
2540
  catch (_e) { /* allow:swallow-unverified a non-JSON problem body is not a badNonce; prob stays null, falling through to the typed acme/server-problem throw */ }
2489
- if (prob && typeof prob.type === "string" && _endsWith(prob.type, ":badNonce") && attempt < maxNonceRetries) { attempt++; return once(); }
2541
+ // A badNonce retry keeps the CURRENT signing override (a re-sign is not undone by a following
2542
+ // badNonce), so it re-signs with the same alg the request already switched to.
2543
+ if (prob && typeof prob.type === "string" && _endsWith(prob.type, ":badNonce") && attempt < maxNonceRetries) { attempt++; return once(override); }
2544
+ // A badSignatureAlgorithm (RFC 8555 sec. 6.2): if the caller supplied resignKeys and one of its
2545
+ // algs is in the server's advertised `algorithms`, re-sign ONCE with that caller-preferred key.
2546
+ // The caller's order selects (never the server's), so a bogus badSignatureAlgorithm cannot force
2547
+ // a downgrade (sec. 10); no match, or no resignKeys, falls through to the fail-closed throw.
2548
+ if (allowResign && resignKeys && prob && typeof prob.type === "string" && _endsWith(prob.type, ":badSignatureAlgorithm") && resignAttempt < 1) {
2549
+ var pick = _chooseResign(resignKeys, prob.algorithms);
2550
+ if (pick) { resignAttempt++; return once(pick); }
2551
+ }
2490
2552
  }
2491
2553
  throw _serverProblem(res);
2492
2554
  }
@@ -2501,14 +2563,18 @@ function client(directoryUrl, opts) {
2501
2563
  }
2502
2564
  // Build an account-key JWS (kid or jwk mode) with a fresh nonce and POST it through the retrying _postJws.
2503
2565
  function _post(url, builder, extra, mode, accept, okStatuses) {
2504
- return _postJws(url, function (nonce) {
2566
+ return _postJws(url, function (nonce, override) {
2505
2567
  // The client-owned JWS session fields (signing key, alg, single-use nonce, protected url) are applied
2506
2568
  // AFTER the caller's payload options, so a payload carrying key/alg/nonce/url cannot override them and
2507
- // desynchronize the protected header from the actual request (or reuse a nonce).
2508
- var base = Object.assign({}, extra || {}, { key: accountKey, alg: alg, nonce: nonce, url: url });
2509
- if (mode === "jwk") base.jwk = accountJwk; else base.kid = _requireKid();
2569
+ // desynchronize the protected header from the actual request (or reuse a nonce). On a badSignatureAlgorithm
2570
+ // re-sign `override` carries the caller's chosen { alg, key } (the SAME account material under another alg);
2571
+ // the embedded jwk stays accountJwk, which still verifies it (an RSA public JWK is padding-agnostic).
2572
+ var base = Object.assign({}, extra || {}, { key: override ? override.key : accountKey, alg: override ? override.alg : alg, nonce: nonce, url: url });
2573
+ // On a re-sign the embedded jwk's declared alg must follow the alg being signed, not the account
2574
+ // key's native one, so the published JWK and the JWS header agree (jwk mode / newAccount).
2575
+ if (mode === "jwk") base.jwk = override ? _jwkForAlg(accountJwk, override.alg) : accountJwk; else base.kid = _requireKid();
2510
2576
  return builder(base);
2511
- }, accept, okStatuses);
2577
+ }, accept, okStatuses, true);
2512
2578
  }
2513
2579
  function _postAsGet(url) { return _post(url, postAsGet, null, "kid"); }
2514
2580
 
@@ -2863,6 +2929,11 @@ function client(directoryUrl, opts) {
2863
2929
  // sync -- a bodyless or malformed account body must NOT leave the client on the old key while the
2864
2930
  // server holds the new one. The account object is optional + informational: parse it best-effort.
2865
2931
  accountKey = o.newKey; accountJwk = o.newJwk; alg = o.newAlg;
2932
+ // The resignKeys alternatives are CryptoKeys for the OLD account key material, so they no longer
2933
+ // match the rotated account key. Drop them: re-signing with a stale-key alternative under the new
2934
+ // kid would be rejected by the CA. With them cleared, a later badSignatureAlgorithm surfaces (the
2935
+ // conformant default) rather than retrying with obsolete key material.
2936
+ resignKeys = null;
2866
2937
  var acct = null;
2867
2938
  if (_bodyText(res).trim() !== "") {
2868
2939
  try { acct = validate("account", _json(res)); }
package/lib/est.js CHANGED
@@ -196,6 +196,16 @@ function transferDecode(body) {
196
196
  return der;
197
197
  }
198
198
 
199
+ // The byte length of a transport response body for the size-cap and empty-body checks: a null / absent body
200
+ // is 0, a string its UTF-8 byte length, and any BufferSource (a Buffer / Uint8Array / ArrayBuffer / DataView
201
+ // from an injected transport) its byte count through the byte guard -- never a String()-coerced comma list.
202
+ // res.body is read ONCE so a lying getter on an injected response cannot change it between the arms.
203
+ function _bodyLen(res) {
204
+ var body = res.body;
205
+ return body == null ? 0 : (typeof body === "string" ? Buffer.byteLength(body, "utf8")
206
+ : (Buffer.isBuffer(body) ? guard.bytes.lengthOf(body) : guard.bytes.lengthOf(guard.bytes.source(body, EstError, "est/bad-input", "the response body"))));
207
+ }
208
+
199
209
  /**
200
210
  * @primitive pki.est.transferEncode
201
211
  * @signature pki.est.transferEncode(der) -> string
@@ -1152,7 +1162,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
1152
1162
  // Measure an injected string body as UTF-8 -- the byte width the body is decoded/transfer-decoded at,
1153
1163
  // and what the real socket transport counts -- so a non-ASCII body (multi-byte chars are ~half the
1154
1164
  // count under latin1) cannot undercount past the DoS cap and reach body handling.
1155
- var blen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8") : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, EstError, "est/bad-input", "the response body"))));
1165
+ var blen = _bodyLen(res);
1156
1166
  if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
1157
1167
  // The transport contract returns lowercased headers, but the injectable seam only promises
1158
1168
  // { status, headers, body }; normalize here so a redirect Location / WWW-Authenticate from an
@@ -1302,7 +1312,7 @@ function _certsResult(op, res, opts, csrSpki) {
1302
1312
  if (verdict.status !== "ok") throw E("est/http-error", "an EST " + op + " response must be HTTP 200 or 202 (RFC 7030 sec. 4.1.3 / 4.2.3), got " + res.status);
1303
1313
  // Measure the body as UTF-8 for parity with the transport cap above (this check only distinguishes empty
1304
1314
  // from non-empty, where the encoding is immaterial, but the shape is kept uniform so no site undercounts).
1305
- var bodyLen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8") : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, EstError, "est/bad-input", "the response body"))));
1315
+ var bodyLen = _bodyLen(res);
1306
1316
  if (bodyLen === 0) throw E("est/empty-body", "a 200 " + op + " response carried an empty body (RFC 7030 sec. 4.1.3 / 4.2.3)");
1307
1317
  var parsed = parseCertsOnly(transferDecode(res.body));
1308
1318
  if (op === "cacerts") return { certificates: parsed.certificates, crls: parsed.crls };
@@ -1866,7 +1876,7 @@ function _fullcmcResult(res, opts, wanted, sent) {
1866
1876
  "an EST /fullcmc response must be HTTP 200 or 202 (RFC 7030 sec. 4.3.2), got " + res.status);
1867
1877
  }
1868
1878
 
1869
- var bodyLen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8") : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, EstError, "est/bad-input", "the response body"))));
1879
+ var bodyLen = _bodyLen(res);
1870
1880
  if (bodyLen === 0) throw E("est/empty-body", "a 200 /fullcmc response carried an empty body (RFC 7030 sec. 4.3.2)");
1871
1881
  var der = transferDecode(res.body);
1872
1882
 
@@ -2265,7 +2275,7 @@ async function _serverkeygenResult(res, opts, derived) {
2265
2275
  // A 202 is an enrollment posture (RFC 7030 sec. 4.4 treats /serverkeygen as an enroll): surfaced, never slept.
2266
2276
  if (verdict.status === "retry") return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
2267
2277
  if (verdict.status !== "ok") throw E("est/http-error", "an EST serverkeygen response must be HTTP 200 or 202 (RFC 7030 sec. 4.4.2), got " + res.status);
2268
- var bodyLen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8") : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, EstError, "est/bad-input", "the response body"))));
2278
+ var bodyLen = _bodyLen(res);
2269
2279
  if (bodyLen === 0) throw E("est/empty-body", "a 200 serverkeygen response carried an empty body (RFC 7030 sec. 4.4.2)");
2270
2280
  _assertConfidentialCipher(res);
2271
2281
  // parseServerKeygenResponse does the per-part transfer-decode; the recipient coherence is bound to the CSR.
@@ -2367,7 +2377,7 @@ function _csrattrsResult(res, opts) {
2367
2377
  // A 202 is nonconforming for a policy GET (sec. 4.5.2 lists only 200 / 204 / 404).
2368
2378
  if (verdict.status === "retry") throw E("est/http-error", "a /csrattrs response must be HTTP 200, 204, or 404, not 202 (RFC 7030 sec. 4.5.2)");
2369
2379
  if (verdict.status !== "ok") throw E("est/http-error", "an EST csrattrs response must be HTTP 200 / 204 / 404 (RFC 7030 sec. 4.5.2), got " + res.status);
2370
- var bodyLen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8") : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, EstError, "est/bad-input", "the response body"))));
2380
+ var bodyLen = _bodyLen(res);
2371
2381
  // An empty HTTP body is distinct from a valid EMPTY CsrAttrs (`30 00` / base64 `MAA=`), which parses as {items:[]}.
2372
2382
  if (bodyLen === 0) throw E("est/empty-body", "a 200 csrattrs response carried an empty body (RFC 7030 sec. 4.5.2)");
2373
2383
  var attrs = csrattrsFmt.parse(transferDecode(res.body));
@@ -1214,8 +1214,10 @@ function validateCriticalExtensionStructure(cert) {
1214
1214
  * Validate an ordered certification `path` (anchor->target) against a trust
1215
1215
  * anchor per RFC 5280 6.1. `path` is an array of `pki.schema.x509.parse`
1216
1216
  * objects (or DER/PEM the function parses); `opts` carries `time` (the
1217
- * always-on window check), `trustAnchor` ({ name, publicKey, algorithm,
1218
- * parameters? }), the 6.1.1 user-initial inputs (`initialExplicitPolicy`,
1217
+ * always-on window check), `trustAnchors` (a single { name, publicKey,
1218
+ * algorithm, parameters? } anchor or a non-empty array of them; with several,
1219
+ * the one that issued the path terminus is selected), the 6.1.1 user-initial
1220
+ * inputs (`initialExplicitPolicy`,
1219
1221
  * `initialAnyPolicyInhibit`, `initialPolicyMappingInhibit`,
1220
1222
  * `userInitialPolicySet`, and `initialPermittedSubtrees` /
1221
1223
  * `initialExcludedSubtrees` -- arrays of `{ tag, base }` where `tag` is the
@@ -1263,16 +1265,14 @@ function validateCriticalExtensionStructure(cert) {
1263
1265
  * var res = await pki.path.validate([cert], {
1264
1266
  * time: new Date("2020-01-01T00:00:00Z"),
1265
1267
  * // the anchor's own key algorithm, not the algorithm its issuer signed it with
1266
- * trustAnchor: { name: cert.issuer, publicKey: cert.subjectPublicKeyInfo.bytes,
1267
- * algorithm: cert.subjectPublicKeyInfo.algorithm.oid },
1268
+ * trustAnchors: [{ name: cert.issuer, publicKey: cert.subjectPublicKeyInfo.bytes,
1269
+ * algorithm: cert.subjectPublicKeyInfo.algorithm.oid }],
1268
1270
  * });
1269
1271
  * res.valid; // boolean; res.results[0].checks carries the per-check codes
1270
1272
  */
1271
- // Every option pki.path.validate reads. The anchor option here is the singular `trustAnchor`,
1272
- // while pki.path.build in this same module spells it `trustAnchors` and takes a list. A caller
1273
- // moving between the two verbs carries the wrong spelling without leaving the namespace, and
1274
- // before this it bought them no anchoring and no error, just "a trustAnchor is required" about
1275
- // an option they believed they had passed.
1273
+ // Every option pki.path.validate reads. The anchor option is `trustAnchors` (plural), the same
1274
+ // spelling pki.path.build / pki.cms.verify / pki.cmp.verify use, and it takes a single anchor or a
1275
+ // non-empty array of them. A one-element array reproduces the single-anchor behavior exactly.
1276
1276
  // Derived from three sources, because no one of them is complete: the `opts.*` reads in this
1277
1277
  // function, the `params.*` reads in the RFC 5280 6.1.1 state it is passed down to, and the
1278
1278
  // documented prose above, which is the only place `requiredEku` and the three initial-policy
@@ -1282,7 +1282,7 @@ var _VALIDATE_OPTS = {
1282
1282
  checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
1283
1283
  initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
1284
1284
  initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, requireRevocation: 1,
1285
- requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchor: 1,
1285
+ requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
1286
1286
  userInitialPolicySet: 1, verifier: 1
1287
1287
  };
1288
1288
 
@@ -1328,8 +1328,7 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
1328
1328
  async function validate(path, opts) {
1329
1329
  opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
1330
1330
  guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
1331
- "pki.path.validate has an unknown option. The anchor here is `trustAnchor`, singular; " +
1332
- "pki.path.build takes `trustAnchors`. The unknown option was: ");
1331
+ "pki.path.validate has an unknown option (the anchor option is `trustAnchors`, a single anchor or an array). The unknown option was: ");
1333
1332
  // A CT policy without a log-list cannot be enforced -- refuse it at entry rather than silently
1334
1333
  // skipping the CT gate and returning a valid path the caller believes was CT-checked (fail-closed).
1335
1334
  if (opts.ctPolicy != null && opts.ctLogList == null) throw E("path/bad-input", "validate: opts.ctPolicy requires opts.ctLogList (the trusted CT log-list to verify the certificate's SCTs against)");
@@ -1358,11 +1357,52 @@ async function validate(path, opts) {
1358
1357
  var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1359
1358
  var n = certs.length;
1360
1359
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
1361
- if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
1362
- // Normalize the anchor at the door: a parsed root CERTIFICATE becomes a { name, publicKey, algorithm }
1363
- // tuple, and a mis-shaped tuple fails closed with path/bad-input rather than seeding an undefined
1364
- // working key that a self-describing SPKI could still validate against (a soft-verdict fail-open).
1365
- var anchor = toAnchor(opts.trustAnchor, "validate");
1360
+ // trustAnchors is a single anchor OR a non-empty array of them. Normalize each at the door: a parsed
1361
+ // root CERTIFICATE becomes a { name, publicKey, algorithm } tuple, and a mis-shaped tuple fails closed
1362
+ // with path/bad-input rather than seeding an undefined working key a self-describing SPKI could still
1363
+ // validate against (a soft-verdict fail-open). With more than one anchor, select the one whose subject
1364
+ // issued the top path cert (certs[0], anchor->target order); a one-element list reproduces the
1365
+ // single-anchor behavior exactly, and no match falls to the first so the name-chaining check below
1366
+ // fails closed (an untrusted chain is never validated against a non-matching root).
1367
+ var rawAnchors = opts.trustAnchors;
1368
+ var anchorList = intrinsic.isArray(rawAnchors) ? rawAnchors : [rawAnchors];
1369
+ if (rawAnchors == null || anchorList.length === 0) throw E("path/bad-input", "validate: trustAnchors is required (a single anchor or a non-empty array of anchors)");
1370
+ // With more than one anchor, validate the SAME path against each and accept if ANY yields a valid path
1371
+ // (the trust-store "does this chain lead to any of my roots" question). Two anchors can share a subject
1372
+ // DN with different keys (a rotated or cross-signed root), so choosing by name alone would reject a
1373
+ // valid chain when the wrong same-name anchor is tried first: try each, order-independent for validity.
1374
+ // Each recursion carries exactly one anchor, so it runs the single-anchor body below (no further fan-out).
1375
+ if (anchorList.length > 1) {
1376
+ // Normalize the COMPLETE array up front: toAnchor validates each tuple exactly as the single-anchor
1377
+ // path does, so a malformed anchor is refused (path/bad-input) regardless of its position, never
1378
+ // silently accepted because a valid anchor happened to be tried first.
1379
+ var tuples = [];
1380
+ for (var ti = 0; ti < anchorList.length; ti++) { tuples[ti] = toAnchor(anchorList[ti], "validate"); }
1381
+ // One anchor per recursion, so each runs the single-anchor body below (no further fan-out).
1382
+ var singleOpts = function (a) {
1383
+ var keys = intrinsic.ownKeys(opts);
1384
+ var o = {};
1385
+ for (var ki = 0; ki < keys.length; ki++) { o[keys[ki]] = opts[keys[ki]]; }
1386
+ o.trustAnchors = a;
1387
+ return o;
1388
+ };
1389
+ // Only an anchor whose subject issued certs[0] can complete this path, so try each such candidate and
1390
+ // skip unrelated roots: the cost does not scale with a whole trust store. Same-subject anchors with
1391
+ // different keys (a rotated or cross-signed root) ALL match the terminus and are each tried, so validity
1392
+ // is order-independent.
1393
+ var lastMatched = null, anyMatched = false;
1394
+ for (var ai = 0; ai < tuples.length; ai++) {
1395
+ if (!dnEqual(tuples[ai].name.rdns, certs[0].issuer.rdns)) continue;
1396
+ anyMatched = true;
1397
+ var oneRes = await validate(path, singleOpts(tuples[ai]));
1398
+ if (oneRes && oneRes.valid) return oneRes;
1399
+ lastMatched = oneRes;
1400
+ }
1401
+ // No supplied anchor's subject matched the path terminus: the chain leads to none of them. Run one to
1402
+ // produce the fail-closed valid:false verdict (the name-chaining check reports it) rather than null.
1403
+ return anyMatched ? lastMatched : validate(path, singleOpts(tuples[0]));
1404
+ }
1405
+ var anchor = toAnchor(anchorList[0], "validate");
1366
1406
  // The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
1367
1407
  // check date must fail closed, never silently disable it.
1368
1408
  guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
@@ -3214,8 +3254,8 @@ function toAnchor(entry, verb) {
3214
3254
  * @status stable
3215
3255
  * @spec RFC 5280 sec. 6.1.1
3216
3256
  *
3217
- * Turn a parsed certificate into the trust-anchor tuple `pki.path.validate` (`opts.trustAnchor`) and
3218
- * `pki.path.build` (`opts.trustAnchors`) consume, so a root can be pinned directly instead of
3257
+ * Turn a parsed certificate into the trust-anchor tuple `pki.path.validate` and `pki.path.build` consume
3258
+ * as `opts.trustAnchors` (a single anchor or an array), so a root can be pinned directly instead of
3219
3259
  * hand-building `{ name, publicKey, algorithm }`. The subject becomes the anchor name and its
3220
3260
  * SubjectPublicKeyInfo the anchor key and algorithm. A value that is neither a parsed certificate nor a
3221
3261
  * ready anchor tuple is refused with `path/bad-input`. `validate` and `build` already normalize a
@@ -3226,7 +3266,7 @@ function toAnchor(entry, verb) {
3226
3266
  * var rootDer = await pki.x509.sign({ subject: "Root CA", subjectPublicKey: await pki.key.export(pair.publicKey),
3227
3267
  * notBefore: new Date(0), notAfter: new Date("2999-01-01") }, { key: pair.privateKey });
3228
3268
  * var anchor = pki.path.anchorFromCert(pki.schema.x509.parse(rootDer));
3229
- * // anchor is { name, publicKey, algorithm, ... } -- pass it as opts.trustAnchor to pki.path.validate
3269
+ * // anchor is { name, publicKey, algorithm, ... } -- pass it as opts.trustAnchors to pki.path.validate
3230
3270
  */
3231
3271
  function anchorFromCert(cert) { return toAnchor(cert, "anchorFromCert"); }
3232
3272
 
@@ -3489,11 +3529,8 @@ async function _fetchAiaIssuers(current, aia) {
3489
3529
  // pki.path.build takes its own options AND forwards every pki.path.validate option to each
3490
3530
  // internal validate() call. The documented contract above says so, and validateOpts() copies
3491
3531
  // them wholesale. So the accepted set is the union, derived from _VALIDATE_OPTS. Retyping it
3492
- // would let the two lists drift the first time validate gains an option.
3493
- //
3494
- // `trustAnchor` is removed from the union on purpose: build assigns it per candidate anchor,
3495
- // so a caller passing the singular would have it overwritten and silently ignored. Refusing it
3496
- // is what makes the singular/plural message below actionable rather than trivia.
3532
+ // would let the two lists drift the first time validate gains an option. Both verbs now spell the
3533
+ // anchor option `trustAnchors`, so the union carries one key and build applies its own per-candidate.
3497
3534
  var _BUILD_OPTS = (function () {
3498
3535
  var o = {
3499
3536
  aiaTimeout: 1, candidates: 1, fetchAia: 1, intermediates: 1, maxAiaFetches: 1,
@@ -3501,15 +3538,13 @@ var _BUILD_OPTS = (function () {
3501
3538
  maxResponseBytes: 1, time: 1, tls: 1, transport: 1, trustAnchors: 1, validate: 1
3502
3539
  };
3503
3540
  Object.keys(_VALIDATE_OPTS).forEach(function (k) { o[k] = 1; });
3504
- delete o.trustAnchor;
3505
3541
  return o;
3506
3542
  })();
3507
3543
 
3508
3544
  async function build(leaf, opts) {
3509
3545
  opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "build: opts");
3510
3546
  guard.identifier.assertKnownKeys(opts, _BUILD_OPTS, E, "path/bad-input",
3511
- "pki.path.build has an unknown option. The anchors here are `trustAnchors`, plural; " +
3512
- "pki.path.validate takes `trustAnchor`. The unknown option was: ");
3547
+ "pki.path.build has an unknown option (the anchor option is `trustAnchors`). The unknown option was: ");
3513
3548
  var leafCert;
3514
3549
  try { leafCert = coerceCert(leaf); }
3515
3550
  catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
@@ -3635,7 +3670,7 @@ async function build(leaf, opts) {
3635
3670
  // apply the method rule a second time and drop a function-valued option all over again;
3636
3671
  // forwarding less than was validated is worse than refusing the bag.
3637
3672
  Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
3638
- vo.trustAnchor = anchor;
3673
+ vo.trustAnchors = anchor; // one anchor per candidate; validate accepts a single anchor or an array
3639
3674
  return vo;
3640
3675
  }
3641
3676
 
package/lib/sigstore.js CHANGED
@@ -477,7 +477,7 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
477
477
  var res = await pathValidate.validate(path, {
478
478
  time: new Date(timeMs),
479
479
  historicalMode: true,
480
- trustAnchor: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
480
+ trustAnchors: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
481
481
  requiredEku: ["codeSigning"],
482
482
  });
483
483
  if (res.valid) return;
package/lib/trust.js CHANGED
@@ -875,7 +875,7 @@ function parseCcadbCsv(text) {
875
875
  * var entry = pki.trust.parseCcadbCsv(csv).anchors[0];
876
876
  * var anchor = pki.trust.anchor(entry, { purpose: "serverAuth" });
877
877
  * await pki.path.validate([pki.schema.x509.parse(der)],
878
- * { time: new Date("2026-06-01T00:00:00Z"), trustAnchor: anchor, checkPurpose: "serverAuth" });
878
+ * { time: new Date("2026-06-01T00:00:00Z"), trustAnchors: anchor, checkPurpose: "serverAuth" });
879
879
  */
880
880
  // `purpose` is the only option, and it gates the authorization itself, so a near-miss spelling must not
881
881
  // pass silently: a caller who wrote the plural `purposes` (or carried a sibling's word) would otherwise get
package/lib/tsp-sign.js CHANGED
@@ -697,13 +697,13 @@ function _buildTsaChains(leaf, pool) {
697
697
  * serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
698
698
  * when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
699
699
  * RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
700
- * supplied), and, when a `trustAnchor` is supplied, the full out-of-path TSA-cert path
700
+ * supplied), and, when `trustAnchors` is supplied, the full out-of-path TSA-cert path
701
701
  * validation all pass. A conformance / trust failure of a well-formed token is a
702
702
  * `{ valid:false, code }` verdict; malformed or config input throws a typed `TspError`.
703
703
  *
704
704
  * `trusted` is the second claim and is kept apart from the first. `valid` says the token's
705
705
  * signature and structural bindings hold; `trusted` says the timestamp authority chained to an
706
- * anchor this caller named. Without `trustAnchor` there is nothing to chain to and `trusted` is
706
+ * anchor this caller named. Without `trustAnchors` there is nothing to chain to and `trusted` is
707
707
  * `false`: every branch, refusing and accepting alike, carries the field with a definite
708
708
  * value. A timestamp is archived precisely to be re-read years later, and one boolean
709
709
  * cannot answer both questions then.
@@ -714,8 +714,8 @@ function _buildTsaChains(leaf, pool) {
714
714
  * un-revoked. It is `false` whenever no path ran at all. `anchorConstraints` carries whatever the anchor
715
715
  * itself constrained, from `pki.path.validate`.
716
716
  *
717
- * @opts trustAnchor Anchor `{ name, publicKey, algorithm }`. Runs `pki.path.validate` on the
718
- * TSA certificate chain ordered from the token's embedded certificates
717
+ * @opts trustAnchors Anchor `{ name, publicKey, algorithm }`, or a non-empty array of them. Runs
718
+ * `pki.path.validate` on the TSA certificate chain from the token's certificates
719
719
  * (validity at genTime, requiredEku timeStamping, and revocation when a
720
720
  * `revocationChecker` is supplied, with `revocationChecked` reporting which),
721
721
  * so a TSA under an intermediate CA validates, not only one directly under
@@ -739,11 +739,11 @@ function _buildTsaChains(leaf, pool) {
739
739
  * var imprint = { hashAlgorithm: "sha256", hashedMessage: sha256Digest };
740
740
  * var token = await pki.tsp.sign(imprint, { cert: signerCertDer, key: signerKeyPkcs8 }, { policy: "1.3.6.1.4.1.1", serialNumber: 1 });
741
741
  * var res = await pki.tsp.verify(token, Buffer.from("hello"), {});
742
- * res.valid; // boolean; pass opts.trustAnchor to also chain the TSA cert to a root
742
+ * res.valid; // boolean; pass opts.trustAnchors to also chain the TSA cert to a root
743
743
  * res.genTime; // Date, read from the verified eContent
744
744
  */
745
745
  // Every option pki.tsp.verify reads. Adding one here is the only way to make it accepted.
746
- var _VERIFY_OPTS = { certs: 1, trustAnchor: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
746
+ var _VERIFY_OPTS = { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
747
747
 
748
748
  // Does any extension carry the critical flag? The answer IS the unknown-critical-extension gate, so
749
749
  // it is decided by comparison instead of by a `some`. See guard-list.
@@ -754,13 +754,12 @@ function _anyCritical(extensions) {
754
754
  async function verify(token, data, opts) {
755
755
  opts = opts || {};
756
756
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
757
- // An unrecognized option is refused rather than ignored. This verb spells its anchor option
758
- // SINGULAR -- `trustAnchor`, an anchor tuple -- while pki.cms.verify and pki.cmp.verify spell it
759
- // `trustAnchors` and take certificate DER. A caller carrying the plural spelling here would
760
- // otherwise get no anchoring and no error: the TSA certificate unchained, `valid: true`, and
761
- // nothing to notice it by. Naming the difference at the boundary is the only place it is cheap.
757
+ // An unrecognized option is refused rather than ignored. The anchor option is `trustAnchors` (an
758
+ // anchor tuple, or a non-empty array of them), the same spelling pki.cms.verify / pki.cmp.verify use.
759
+ // A caller who mistyped it would otherwise get no anchoring and no error: the TSA certificate
760
+ // unchained, `valid: true`, and nothing to notice it by. Naming it at the boundary is cheap.
762
761
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "tsp/bad-input",
763
- "pki.tsp.verify has an unknown option (note the anchor option here is `trustAnchor`, singular, an anchor tuple -- not the `trustAnchors` certificate list pki.cms.verify takes) ");
762
+ "pki.tsp.verify has an unknown option (the anchor option is `trustAnchors`) ");
764
763
  if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return guard.bytes.isByteSource(c); }))) {
765
764
  throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers"); // a bad element is a caller error, never silently dropped
766
765
  }
@@ -827,7 +826,7 @@ async function verify(token, data, opts) {
827
826
  if (!_tsaMatchesCert(tst.tsa.bytes, sc)) return fail("tsp/tsa-mismatch");
828
827
  }
829
828
  // M18 -- full out-of-path TSA-cert validation (issuer-sig, validity at genTime, unknown-critical,
830
- // key-param inheritance, requiredEku, optional revocation), only when a trustAnchor is supplied.
829
+ // key-param inheritance, requiredEku, optional revocation), only when trustAnchors is supplied.
831
830
  // The path is ordered from the token's embedded certificates (leaf + any intermediates), so a TSA
832
831
  // issued under an intermediate CA -- not just directly under the anchor -- validates.
833
832
  // `trusted` is the SECOND claim, kept apart from `valid`. `valid` says the token's signature and
@@ -838,7 +837,7 @@ async function verify(token, data, opts) {
838
837
  // nothing to chain to and `trusted` is false: a definite answer rather than a missing one, the
839
838
  // same shape pki.cms.verify and pki.cmp.verify return.
840
839
  var trusted = false;
841
- if (opts.trustAnchor) {
840
+ if (opts.trustAnchors) {
842
841
  var pathRes = null;
843
842
  // tst.genTime floors to millisecond precision. When genTime carries sub-millisecond digits the true
844
843
  // instant lies in (floor, floor+1ms], so fail closed on BOTH validity boundaries: require each
@@ -877,7 +876,7 @@ async function verify(token, data, opts) {
877
876
  // revocation.
878
877
  async function validateAt(chain, when) {
879
878
  var res = await pathValidate.validate(chain, {
880
- time: when, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
879
+ time: when, trustAnchors: opts.trustAnchors, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
881
880
  });
882
881
  // The most any attempt ESTABLISHED, kept across every attempt: backtracking tries several
883
882
  // chains for one TSA certificate and a fractional genTime validates each at two endpoints,
@@ -506,7 +506,7 @@ function _chainToAnchor(chain, anchors, at, what, code) {
506
506
  // carries these forward as the working public key, and a certificate below the anchor may
507
507
  // inherit its key parameters from them (the DSA-style parameter-inheritance case), so
508
508
  // supplying the signature OID leaves such a key unreconstructable and refuses a valid chain.
509
- trustAnchor: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
509
+ trustAnchors: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
510
510
  algorithm: anchor.subjectPublicKeyInfo.algorithm.oid,
511
511
  parameters: anchor.subjectPublicKeyInfo.algorithm.parameters },
512
512
  }).then(function (r) { return !!(r && r.valid); }, function (e) { lastFault = e; return false; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:2f668b08-8394-4a54-b202-eac9892a6e2f",
5
+ "serialNumber": "urn:uuid:bfbe087f-30ed-484a-ba11-142da30522a8",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-31T15:15:24.682Z",
8
+ "timestamp": "2026-08-31T23:06:26.187Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.6.9",
22
+ "bom-ref": "@blamejs/pki@0.6.11",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.9",
25
+ "version": "0.6.11",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.6.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.11",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.6.9",
57
+ "ref": "@blamejs/pki@0.6.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]