@blamejs/pki 0.8.11 → 0.8.14

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,53 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.8.14 — 2026-09-24
8
+
9
+ The 0.8.14 tarball is the one to install for everything 0.8.13 added.
10
+
11
+ ### Fixed
12
+
13
+ - The changelog entry for this release is dated by the UTC day its commit landed on, which is the day the release tooling and the entry now agree on.
14
+
15
+ ## v0.8.13 — 2026-09-24
16
+
17
+ A trust anchor list published as RFC 5914 drives path validation, constraints and all.
18
+
19
+ ### Added
20
+
21
+ - `pki.schema.trustanchor.parse(input, caps?)` reads an RFC 5914 `TrustAnchorList` into `{ anchors }`, each entry naming which of the three `TrustAnchorChoice` arms it came from in `kind`: `certificate`, `tbsCert` (a bare `TBSCertificate`, an anchor having no signature to check) or `taInfo`. `parseInfo` reads a bare `TrustAnchorInfo`. `pemDecode` and `pemEncode` carry the `TRUST ANCHOR LIST` label, and `pki.schema.parse` detects and routes the format.
22
+ - A `taInfo` entry is `{ version, pubKey, keyId, taTitle, certPath, exts, excludedExtensions, taTitleLangTag }`. `certPath` carries the anchor's name and its constraints: `policySet`, `policyFlags`, `nameConstr` and `pathLenConstraint`. Every rule RFC 5914 states is enforced: an empty `taName`, a `policySet` carrying policy qualifiers, a negative `pathLenConstraint` and a `requireExplicitPolicy` with no `policySet` are each refused, and where `certPath` carries a certificate its subject, its public key and its `subjectKeyIdentifier` must match `taName`, `pubKey` and `keyId`. The four extension types section 3 excludes from `exts` are listed in `excludedExtensions` and left out of `exts`, because the RFC ignores them rather than refusing the anchor.
23
+ - `pki.trust.parseTrustAnchorList(input, opts?)` turns a list into the trust-store entries `pki.trust.anchor` takes, the same shape `parseCertdata` and `parseCcadbCsv` produce, so a published list drives `pki.path.validate` without being mapped by hand.
24
+ - `pki.path.validate` applies an RFC 5914 anchor's constraints as RFC 5937 section 3.2 states. The anchor's name constraints intersect the caller's permitted subtrees and union its excluded ones, its `policySet` intersects `opts.userInitialPolicySet`, each of its `policyFlags` may turn the matching `initialExplicitPolicy` / `initialPolicyMappingInhibit` / `initialAnyPolicyInhibit` on and never off, and its `pathLenConstraint` bounds the path. An anchor can only narrow what the caller asked for, which is what makes the format's "always enforced" safe to apply.
25
+ - `pki.trustanchor.build(spec, opts?)` writes the list a root program publishes, with `opts.pem` for the PEM form. Every rule the parser enforces is enforced here, so the toolkit does not emit an anchor it would refuse to read, and the result round-trips through the parser before it is returned. The `version` is not a field a caller sets: v1 is the ASN.1 DEFAULT and DER omits it, so each v1 anchor has one encoding.
26
+
27
+ ### Changed
28
+
29
+ - `pki.trust.anchor` keeps an entry's `purposes` as `null` where the entry states it as `null`, rather than reading it as a set granting nothing. A store that lists a root and grants it no purpose still produces the all-false set, which is what `pki.path.validate` requires `opts.checkPurpose` to judge; a format carrying no purpose bits at all, such as an RFC 5914 anchor, now says nothing instead of saying "trusted for no purpose". An entry that omits `purposes` altogether is unchanged and still defaults to all-false.
30
+
31
+ ### Fixed
32
+
33
+ - An option supplied as a non-enumerable own property now reaches the code that acts on it. Every verb accepted such an option and some then skipped it, with nothing reported. Two of them let a check pass that should have failed: `pki.webauthn.verifyAssertion` ignored an `expectedRpId` written that way and reported the assertion valid against the wrong relying party, and an `opts.tpmPolicy.objectAttributes` entry went unchecked in `pki.webauthn.verify`. The rest changed what was produced or refused: `pki.attrcert.sign` left the named extension or attribute out of the certificate it signed, `pki.crmf.build` left the control out of the request, and `pki.cmp.session` answered that no enrollment arm or support operation had been named, or classified an `ir` request as central key generation.
34
+ - The same read now applies wherever the toolkit copies or enumerates an object it was handed: `pki.path.validate`, `pki.trust`, `pki.cms.verify`, `pki.cmc`, `pki.est`, `pki.ocsp`, `pki.acme`, `pki.ct`, `pki.scep`, `pki.smime`, `pki.webauthn` and the WebCrypto algorithm surface. It covers the response an `opts.transport` returns, so a `Location`, `WWW-Authenticate`, `Retry-After` or `Content-Type` field a custom transport defines non-enumerably is now found. A caller passing plain objects sees no change.
35
+ - In `pki.path.validate`, an anchor carrying any RFC 5914 constraint could accept a path that the same call without the anchor refused, because the caller's `initialExplicitPolicy`, `initialPolicyMappingInhibit`, `initialAnyPolicyInhibit` and `userInitialPolicySet` were re-read from a copy that kept only the enumerable ones. The caller's options are now read where they were passed.
36
+ - A trust anchor's own `basicConstraints` `pathLenConstraint` now bounds the path below it, as RFC 5937 section 3.2 requires of a basic constraints extension associated with the anchor. It applies wherever the anchor states one: a certificate passed to `pki.path.validate` or `pki.trust.anchor`, the `exts` of an RFC 5914 `TrustAnchorInfo`, and that anchor's `certPath.pathLenConstraint`. Where two of them state a limit, the lower applies. **A path that runs deeper than an anchor certificate's `pathLenConstraint` allows is now refused where it previously validated.** Check the certificates in your trust store for a `pathLenConstraint` if you validate chains with intermediates.
37
+ - `pki.trustanchor.build` refuses a malformed nested record with its own `trustanchor/bad-input` error. A `null` or non-object `taInfo`, `certPath` or `certPath.policyFlags` raised a native `TypeError` from the first field read below it.
38
+ - Parsing no longer writes a decoded field through an accessor planted on `Object.prototype`. Every format parser builds its record in the schema engine by the field names its structure declares, and those writes reached the prototype chain: a getter-only `signatureAlgorithm`, `issuer`, `extensions` or any other declared name made an ordinary `pki.schema.x509.parse` of a valid certificate throw a native `TypeError`, and a setter took the decoded value instead of the record.
39
+
40
+ ## v0.8.12 — 2026-09-23
41
+
42
+ A certification path whose policies map to each other validates instead of exhausting the policy-node budget.
43
+
44
+ ### Changed
45
+
46
+ - The `pki.path.validate` result carries `validPolicyGraph` where it carried `validPolicyTree`, and the structure it holds is a graph rather than a tree. It is `{ nodes }`, a flat list linked by index, each entry `{ id, depth, validPolicy, qualifierSet, expectedPolicySet, parents, children }`, where `parents` and `children` hold the ids of other entries and the root is id 0. A caller that walked `.children` from a root and read `.parent` reads the list and follows ids instead. The list is flat because a graph handed back as a root to walk re-expands into the exponential structure the moment anything recurses into every child, `JSON.stringify` included.
47
+ - `C.LIMITS.PATH_MAX_POLICY_NODES` is unchanged at 4096 and still fails closed with `path/policy-tree-cap`, and `opts.maxPolicyNodes` still tightens it. What it bounds has changed with the structure: the graph grows with the number of distinct policies and mappings the path actually carries, so the cap is reached by a path that names that many policies, not by a path that names a few and is several certificates long. A single certificate carrying 4096 distinct policies still reaches it.
48
+
49
+ ### Fixed
50
+
51
+ - `pki.path.validate` accepts a conforming certification path whose certificate policies map to each other across several levels. Such a path was refused with `path/policy-tree-cap` once its policy tree exceeded `maxPolicyNodes`, which the tree reached at six certificates carrying four mutually mapped policies. Certificate policies are now processed as the RFC 9618 `valid_policy_graph`, whose size is linear in the policies and mappings on the path rather than exponential in its length.
52
+ - `userConstrainedPolicySet` reports the policies of the nodes whose only parent is the `anyPolicy` node, which is what RFC 5280 section 6.1.5 (g)(iii)(1) and RFC 9618 section 5.5 step (g)(2) both define it as. It reported the policies of the deepest nodes instead. The two answers differ wherever a policy mapping is in play: for a chain where an issuer domain policy is mapped to a subject domain policy, the set now names the issuer domain policy, which is the domain the relying party asked about and the reason mapping exists.
53
+
7
54
  ## v0.8.11 — 2026-09-23
8
55
 
9
56
  What a certificate, CRL, request or attribute certificate asserts in its extensions is read in one call.
package/README.md CHANGED
@@ -247,6 +247,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
247
247
  | `pki.schema.c509` | C509 certificates (draft-ietf-cose-cbor-encoded-cert), the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR. `encode` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte, so the original signature still verifies. A `parse` result re-emits its native array in canonical deterministic CBOR, with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms: the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, inhibitAnyPolicy, OCSP No Check, TLS Feature), the general-name-bearing extensions (subjectAltName, issuer alternative name, name constraints, CRL distribution points, freshest CRL, authority and subject information access, and the full authority key identifier) over one shared GeneralNames codec, certificate policies with their CPS-URI and UserNotice qualifiers, policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers) with their RFC 8360 v2 twins, whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets. A value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact; a certificate outside the invertible set throws a typed `C509Error`. Called explicitly, since it is CBOR rather than DER and so is not auto-routed |
248
248
  | `pki.schema.crl` | X.509 CRLs (RFC 5280 §5): revoked serials with real-`Date` revocation times, named and partly decoded extensions, each record carrying the raw `extnValue` octets as `valueBytes`. `decodeExtensions` returns the CRL's own extensions and every revoked entry's in one array, each row naming the scope it was read at, and covers the whole §5.2 and §5.3 profile including the issuing distribution point and the delta CRL indicator. `decodeExtension` takes the scope explicitly, since RFC 6960 §4.4.5 carries a CRL entry extension into an OCSP response Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
249
249
  | `pki.schema.csr` | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature. `decodeExtensions` is what a CA reads to see what a request asked for, returning the rows of its one `extensionRequest` attribute; a request carrying more than one is refused, and `groups: true` reads each separately Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
250
+ | `pki.schema.trustanchor` | RFC 5914 trust anchor lists: a trust anchor as a structure rather than as a certificate, carrying the constraints a root program otherwise states out of band. `parse` reads a `TrustAnchorList` into `{ anchors }`, each naming which of the three `TrustAnchorChoice` arms it came from: a `certificate`, a bare `tbsCert` (an anchor has no signature to check), or a `taInfo`. A `taInfo` carries the public key, the key identifier, an optional title, and a `certPath` stating the name the anchor is known by plus the policies it may assert, the namespace it is trusted for, and how far below it a path may run. Every rule RFC 5914 states is enforced: an empty `taName`, a policy set carrying qualifiers, a negative path length, and a `requireExplicitPolicy` with no policy set are each refused, and where the anchor carries a certificate its subject, public key and `subjectKeyIdentifier` must match the anchor's own. The four extension types the RFC excludes from `exts` are reported and left out rather than refused, as it says to ignore them. `parseInfo` reads a bare `TrustAnchorInfo`. `pki.trust.parseTrustAnchorList` turns a list into anchors `pki.path.validate` consumes, and `pki.trustanchor.build` writes one Fail-closed — `parse`, `parseInfo`, `pemDecode`, `pemEncode` |
250
251
  | `pki.schema.pkcs8` | PKCS#8 private keys (RFC 5208 / 5958): algorithm, raw key bytes, attributes, optional public key. Encrypted keys are recognized but not decrypted. Fail-closed — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
251
252
  | `pki.schema.cms` | CMS (RFC 5652 / 5083 / 9629): SignedData (§5, signer infos plus raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds including RFC 5753 key agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface plus raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation). §11 attribute placement is enforced, countersignatures validate recursively, certificates and CRLs are validated against the closed CHOICE sets and kept raw, and every result is tagged `contentTypeName` Fail-closed — `parse`, `pemDecode`, `pemEncode` |
252
253
  | `pki.schema.ocsp` | OCSP requests and responses (RFC 6960): per-certificate status (good, revoked, unknown), responder identity, raw tbs bytes for external verification, certificates kept raw. A response's nonce, and a single response's archive cutoff (§4.4.4) and CRL reference (§4.4.2), are decoded onto their extension entries by name; a malformed one is refused. Non-basic response types are recognized but not decoded. Fail-closed — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
@@ -265,7 +266,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
265
266
  | `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` |
266
267
  | `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` |
267
268
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
268
- | `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, and the certificate-policy tree. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
269
+ | `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, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
269
270
  | `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` |
270
271
  | `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. 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, and the RFC 6960 OCSP no-check responder marker, 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`. 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` |
271
272
  | `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` |
@@ -282,7 +283,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
282
283
  | `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 (obsoleted by RFC 9919) or under SHA-2, plus an optional RFC 9654 nonce (32 to 128 octets, the requester floor that section sets) and an optional requestor signature, under which the signer certificate's subject is the requestorName unless one is stated (RFC 6960 §4.1.2). `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, and per-response `singleExtensions` naming the §4.4.4 archive cutoff and the §4.4.2 CRL reference as fields, each read back by name by the parser. The signer refuses what its own lint would grade an error: a pre-encoded singleExtension that RFC 6960 places in requests or in responseExtensions, a CRL entry extension carried at the wrong criticality, and CRL references or entry extensions on a non-issued response, which itself places the §4.4.8 extended revoked definition in responseExtensions without being asked. `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 the signature and whose subject is the `requestorName` the request states (`signerCerts`, so a key carried under an expired certificate beside its renewal is not hidden behind ordering, while a same-key certificate under another name is not the requestor) with the first one's decoded subject, the full embedded certificate bag (`certs`, including intermediates for path building), and the request's CertIDs; the name check is reported as `requestorNamed` (RFC 6960 §4.1.2) and `valid` requires it, a request no verifying certificate names falls back to listing the verifying ones with `valid: false`, and an unsigned request is reported with `signed: false` rather than refused. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify`, `verifyRequest` |
283
284
  | `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` |
284
285
  | `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` |
285
- | `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. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
286
+ | `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. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `anchor` |
286
287
  | `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` |
287
288
  | `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) in all four modes, proven against the RFC 9180 Appendix A vectors, and 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; HKDF-SHA256/SHA384/SHA512 by AES-GCM / ChaCha20Poly1305 / export-only. DHKEM(P-384) has no vector under an HKDF key schedule and fails closed. Pure composition over `node:crypto`; the PQ/T hybrid KEMs are a registry data-row extension once the CFRG combiner draft stabilizes — `suites`, `setupS`, `setupR`, `seal`, `open` |
288
289
  | `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/index.js CHANGED
@@ -63,6 +63,7 @@ var transport = require("./lib/http-transport");
63
63
  var jose = require("./lib/jose");
64
64
  var acme = require("./lib/acme");
65
65
  var trust = require("./lib/trust");
66
+ var trustanchorBuild = require("./lib/trustanchor-build");
66
67
  var inspect = require("./lib/inspect");
67
68
  var lint = require("./lib/lint");
68
69
  var webauthn = require("./lib/webauthn");
@@ -213,6 +214,11 @@ module.exports = {
213
214
  // root list omits); pki.trust.anchor hands one to pki.path.validate. Offline:
214
215
  // the operator supplies the text; no fetch.
215
216
  trust: trust,
217
+ // `trustanchor` writes the RFC 5914 TrustAnchorList a root program publishes:
218
+ // each anchor as a certificate, a bare TBSCertificate, or a TrustAnchorInfo
219
+ // carrying the name it is known by plus the policies, namespace and path
220
+ // length a relying party applies beneath it. pki.schema.trustanchor reads one.
221
+ trustanchor: trustanchorBuild,
216
222
  // `inspect` is human-readable inspection -- pki.inspect.certificate(pem|der|parsed)
217
223
  // renders an OpenSSL-familiar `x509 -text`-style report from the strict parser +
218
224
  // OID registry, naming extension/algorithm OIDs OpenSSL shows only as raw bytes.
package/lib/acme.js CHANGED
@@ -1631,7 +1631,10 @@ function _parseLinkValue(raw) {
1631
1631
  function _parseLinkRelation(headers, base, relName, opts) {
1632
1632
  opts = opts || {};
1633
1633
  var raw = null;
1634
- for (var k in headers) { if (_hasOwn(headers, k) && k.toLowerCase() === "link") { raw = headers[k]; break; } }
1634
+ var hNames = guard.identifier.optionNames(headers);
1635
+ for (var hi = 0; hi < hNames.length; hi++) {
1636
+ if (hNames[hi].toLowerCase() === "link") { raw = headers[hNames[hi]]; break; }
1637
+ }
1635
1638
  if (raw == null) return [];
1636
1639
  var baseUrl = new URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
1637
1640
  var baseKey = _dedupKey(baseHref);
@@ -1867,8 +1870,7 @@ function client(directoryUrl, opts) {
1867
1870
  return guard.async.invoked(transport, [{ method: method, url: url, headers: reqHeaders, body: body, tls: _tlsFor(url), proxy: budgets.proxy, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }], E, "acme/bad-input", "opts.transport");
1868
1871
  }).then(function (res) {
1869
1872
  res = res || {};
1870
- var h = intrinsic.create(null);
1871
- Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
1873
+ var h = guard.header.lowerCased(res.headers);
1872
1874
  var blen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8")
1873
1875
  : (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, E, "acme/bad-response", "the response body"))));
1874
1876
  if (blen > budgets.maxResponseBytes) throw E("acme/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
@@ -1940,7 +1942,11 @@ function client(directoryUrl, opts) {
1940
1942
  }
1941
1943
  function _post(url, builder, extra, mode, accept, okStatuses) {
1942
1944
  return _postJws(url, function (nonce, override) {
1943
- var base = Object.assign({}, extra || {}, { key: override ? override.key : accountKey, alg: override ? override.alg : alg, nonce: nonce, url: url });
1945
+ var base = guard.identifier.ownOptions(extra);
1946
+ base.key = override ? override.key : accountKey;
1947
+ base.alg = override ? override.alg : alg;
1948
+ base.nonce = nonce;
1949
+ base.url = url;
1944
1950
  if (mode === "jwk") base.jwk = override ? _jwkForAlg(accountJwk, override.alg) : accountJwk; else base.kid = _requireKid();
1945
1951
  return builder(base);
1946
1952
  }, accept, okStatuses, true);
@@ -2136,7 +2142,9 @@ function client(directoryUrl, opts) {
2136
2142
  return _resource("revokeCert").then(function (url) {
2137
2143
  if (certKeyMode) {
2138
2144
  return _postJws(url, function (nonce) {
2139
- return revokeCert(Object.assign({}, extra, { key: o.certKey, alg: o.certAlg, nonce: nonce, url: url, jwk: o.certJwk }));
2145
+ var rb = guard.identifier.ownOptions(extra);
2146
+ rb.key = o.certKey; rb.alg = o.certAlg; rb.nonce = nonce; rb.url = url; rb.jwk = o.certJwk;
2147
+ return revokeCert(rb);
2140
2148
  }).then(function () { return true; });
2141
2149
  }
2142
2150
  return _post(url, revokeCert, extra, "kid").then(function () { return true; });
@@ -202,7 +202,7 @@ function _objectDigestInfoContent(odi) {
202
202
  function _encodeHolder(holder) {
203
203
  if (!holder || typeof holder !== "object" || Buffer.isBuffer(holder)) throw _err("attrcert/bad-input", "holder must be an object with exactly one form");
204
204
  guard.identifier.assertKnownKeys(holder, KNOWN_HOLDER_KEYS, _err, "attrcert/bad-input", "unknown holder form ");
205
- var forms = Object.keys(holder).filter(function (k) { return holder[k] != null; });
205
+ var forms = guard.identifier.optionNames(holder, KNOWN_HOLDER_KEYS).filter(function (k) { return holder[k] != null; });
206
206
  if (forms.length !== 1) throw _err("attrcert/bad-input", "holder must carry exactly one form (entityName, baseCertificateID, fromCertificate, or objectDigestInfo), got " + forms.length);
207
207
  if (holder.entityName != null) {
208
208
  return b.sequence([_b.encodeGeneralNames(_gnList(holder.entityName), 1)]);
@@ -403,7 +403,7 @@ function _buildAttributes(attrSpec) {
403
403
  guard.identifier.assertKnownKeys(attrSpec, ATTR_VALUE_ENCODER, _err, "attrcert/bad-input", function (k) {
404
404
  return "unknown attribute " + JSON.stringify(k) + "; pass a pre-encoded Attribute DER via the array form for a custom attribute";
405
405
  });
406
- Object.keys(attrSpec).forEach(function (k) {
406
+ guard.identifier.optionNames(attrSpec, ATTR_VALUE_ENCODER).forEach(function (k) {
407
407
  add(O(ATTR_OID_NAME[k]), ATTR_VALUE_ENCODER[k](attrSpec[k]));
408
408
  });
409
409
  if (!attrs.length) throw _err("attrcert/bad-attributes", "attributes must carry at least one Attribute (RFC 5755 sec. 4.2.7)");
@@ -462,7 +462,7 @@ function _buildExtensions(extSpec, ctx) {
462
462
  });
463
463
  var out = [], seen = intrinsic.create(null);
464
464
  var akiDeclined = false;
465
- Object.keys(extSpec).forEach(function (k) {
465
+ guard.identifier.optionNames(extSpec, EXT_META).forEach(function (k) {
466
466
  var val = extSpec[k];
467
467
  if (val == null) return;
468
468
  if (k === "authorityKeyIdentifier" && val === false) { akiDeclined = true; return; }
package/lib/cmc-build.js CHANGED
@@ -983,10 +983,10 @@ async function _build(spec, signer, opts) {
983
983
  function _copySigners(signer, owned) {
984
984
  function one(s) {
985
985
  if (!s || typeof s !== "object") return s;
986
- var out = intrinsic.create(null), k;
987
- for (k in s) {
988
- if (!_hasOwn(s, k)) continue;
989
- out[k] = copyValue(s[k], k === "key");
986
+ var out = intrinsic.create(null);
987
+ var sNames = guard.identifier.optionNames(s);
988
+ for (var si = 0; si < sNames.length; si++) {
989
+ out[sNames[si]] = copyValue(s[sNames[si]], sNames[si] === "key");
990
990
  }
991
991
  return out;
992
992
  }
@@ -997,11 +997,11 @@ function _copySigners(signer, owned) {
997
997
  }
998
998
  if (Buffer.isBuffer(v) || v instanceof Uint8Array) return _own(Buffer.from(v), true); // allow:byte-source-narrow -- private key bytes (Buffer borrowed / Uint8Array copied+wiped)
999
999
  if (!v || typeof v !== "object" || typeof v.type === "string") return v;
1000
- var c = intrinsic.create(null), ck;
1001
- for (ck in v) {
1002
- if (!_hasOwn(v, ck)) continue;
1003
- var cv = v[ck];
1004
- c[ck] = (Buffer.isBuffer(cv) || cv instanceof Uint8Array) ? _own(Buffer.from(cv), true) : cv; // allow:byte-source-narrow -- composite key component
1000
+ var c = intrinsic.create(null);
1001
+ var cNames = guard.identifier.optionNames(v);
1002
+ for (var ci = 0; ci < cNames.length; ci++) {
1003
+ var cv = v[cNames[ci]];
1004
+ c[cNames[ci]] = (Buffer.isBuffer(cv) || cv instanceof Uint8Array) ? _own(Buffer.from(cv), true) : cv; // allow:byte-source-narrow -- composite key component
1005
1005
  }
1006
1006
  return c;
1007
1007
  }
package/lib/cmc-verify.js CHANGED
@@ -336,8 +336,7 @@ function _copyAnyBytes(v) {
336
336
  }
337
337
 
338
338
  function _snapshotSent(sent) {
339
- var out = intrinsic.create(null), k;
340
- for (k in sent) { if (_hasOwn(sent, k)) out[k] = sent[k]; }
339
+ var out = guard.identifier.ownOptions(sent);
341
340
  if (out.transactionId != null) {
342
341
  out.transactionIdValue = guard.range.authoredInteger(out.transactionId, E, "cmc/bad-input", "sent.transactionId");
343
342
  }
@@ -355,9 +354,10 @@ function _snapshotSent(sent) {
355
354
  var takenSecret = null;
356
355
  try {
357
356
  if (out.recipient && typeof out.recipient === "object") {
358
- var r = intrinsic.create(null), rk;
359
- for (rk in out.recipient) {
360
- if (!_hasOwn(out.recipient, rk)) continue;
357
+ var r = intrinsic.create(null);
358
+ var rNames = guard.identifier.optionNames(out.recipient);
359
+ for (var ri = 0; ri < rNames.length; ri++) {
360
+ var rk = rNames[ri];
361
361
  r[rk] = _copyAnyBytes(out.recipient[rk]);
362
362
  if (rk === "secret" && guard.bytes.isByteSource(r[rk])) takenSecret = r[rk];
363
363
  }
package/lib/cmp-build.js CHANGED
@@ -552,7 +552,7 @@ function _encodeNestedContent(list) {
552
552
 
553
553
  function _encodeBody(bodySpec, key, opts) {
554
554
  if (!bodySpec || typeof bodySpec !== "object" || Buffer.isBuffer(bodySpec)) throw _err("cmp/bad-input", "message.body must be a single-key object");
555
- var keys = Object.keys(bodySpec);
555
+ var keys = guard.identifier.optionNames(bodySpec);
556
556
  if (keys.length !== 1) throw _err("cmp/bad-input", "message.body must have exactly one arm, got " + keys.length);
557
557
  var arm = keys[0];
558
558
  if (!KNOWN_BODY_KEYS[guard.text.keyOf(arm)]) throw _err("cmp/bad-input", "unknown body arm " + guard.text.showValue(arm));
@@ -1055,7 +1055,7 @@ function _transfer(url, message, opts) {
1055
1055
  try { proxy = httpTransport.snapshotProxy(opts.proxy); }
1056
1056
  catch (e) { throw _err("cmp/bad-input", "the options object could not be read", e); }
1057
1057
  var headers = intrinsic.create(null);
1058
- Object.keys(opts.headers || {}).forEach(function (k) {
1058
+ guard.identifier.optionNames(opts.headers).forEach(function (k) {
1059
1059
  var lk = k.toLowerCase();
1060
1060
  if (lk !== "content-length" && lk !== "transfer-encoding" && lk !== "content-type") headers[k] = opts.headers[k];
1061
1061
  });
@@ -1063,8 +1063,7 @@ function _transfer(url, message, opts) {
1063
1063
  var tls = _tlsForTransfer(opts);
1064
1064
  return guard.async.invoked(transport, [{ method: "POST", url: parsedUrl.href, headers: headers, body: der, tls: tls, proxy: proxy, timeout: timeout, maxResponseBytes: maxResponseBytes }], _err, "cmp/bad-input", "opts.transport").then(function (res) {
1065
1065
  res = res || {};
1066
- var h = intrinsic.create(null);
1067
- Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
1066
+ var h = guard.header.lowerCased(res.headers);
1068
1067
  var body = guard.bytes.isByteSource(res.body)
1069
1068
  ? guard.bytes.source(res.body, CmpError, "cmp/bad-response", "the CMP response body")
1070
1069
  : Buffer.from(String(res.body == null ? "" : res.body), "utf8");
@@ -435,7 +435,7 @@ function session(opts) {
435
435
  * drop the one the caller supplied, and send the session to the default client instead. */
436
436
  var _transport = guard.identifier.assertCallableOption(opts, "transport", _err, "cmp/bad-input",
437
437
  "opts.transport", "(url, reqDer, opts) => Promise<{ responseBytes, status }>");
438
- opts = Object.assign({}, opts);
438
+ opts = guard.identifier.ownOptions(opts);
439
439
  if (typeof opts.url !== "string" || !opts.url) throw _err("cmp/bad-input", "opts.url (the CMP endpoint) is required");
440
440
 
441
441
  var isSig = opts.key != null || opts.cert != null;
@@ -930,12 +930,12 @@ function session(opts) {
930
930
  * than a record, and is left to the builder's own snapshot. */
931
931
  function _snapshotArm(armSpec) {
932
932
  if (!_isRecord(armSpec)) return armSpec;
933
- var copy = Object.assign({}, armSpec);
934
- if (_isRecord(copy.certTemplate)) copy.certTemplate = Object.assign({}, copy.certTemplate);
933
+ var copy = guard.identifier.ownOptions(armSpec);
934
+ if (_isRecord(copy.certTemplate)) copy.certTemplate = guard.identifier.ownOptions(copy.certTemplate);
935
935
  /** @internal The proof of possession is copied for the same reason the template is: the session
936
936
  * decides on `pop.type` which modes it will send, and a field answering one way to that decision
937
937
  * and another to the builder would put a request on the wire in a mode the session refused. */
938
- if (_isRecord(copy.pop)) copy.pop = Object.assign({}, copy.pop);
938
+ if (_isRecord(copy.pop)) copy.pop = guard.identifier.ownOptions(copy.pop);
939
939
  return copy;
940
940
  }
941
941
  /** @internal A record the session copies field by field, as opposed to a byte source, which every
@@ -1132,7 +1132,7 @@ function session(opts) {
1132
1132
  * sends the ONE arm it read out of this object, so a key the session does not recognize would
1133
1133
  * otherwise be dropped silently instead of naming the caller's mistake. */
1134
1134
  guard.identifier.assertKnownKeys(request, ENROLL_ARMS, _err, "cmp/bad-input", "unknown enroll request field ");
1135
- var arms = Object.keys(request).filter(function (k) { return request[k] != null; });
1135
+ var arms = guard.identifier.optionNames(request, ENROLL_ARMS).filter(function (k) { return request[k] != null; });
1136
1136
  if (arms.length !== 1 || !ENROLL_ARMS[arms[0]]) {
1137
1137
  throw _err("cmp/bad-input", "enroll(request) must carry EXACTLY ONE enrollment arm (ir / cr / kur / p10cr)");
1138
1138
  }
@@ -1569,7 +1569,7 @@ function session(opts) {
1569
1569
  async function _info(request) {
1570
1570
  guard.identifier.assertPlainRecord(request, _err, "cmp/bad-input", "info(request), an object naming one support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate),");
1571
1571
  guard.identifier.assertKnownKeys(request, INFO_OPS, _err, "cmp/bad-input", "unknown info request field ");
1572
- var keys = Object.keys(request), names = [], values = [];
1572
+ var keys = guard.identifier.optionNames(request, INFO_OPS), names = [], values = [];
1573
1573
  for (var ki = 0; ki < keys.length; ki++) {
1574
1574
  var kv = request[keys[ki]];
1575
1575
  if (kv == null) continue;
package/lib/cms-verify.js CHANGED
@@ -608,10 +608,10 @@ function _cloneAnchorValue(v, depth) {
608
608
  " levels, so it cannot be copied before the chain is walked; pass the anchor as certificate DER or a { name, publicKey, algorithm } tuple");
609
609
  }
610
610
  if (Array.isArray(v)) return guard.list.copyMap(v, function (e) { return _cloneAnchorValue(e, depth + 1); });
611
- var out = intrinsic.create(null), k;
612
- for (k in v) {
613
- if (!intrinsic.hasOwn(v, k)) continue;
614
- out[k] = _cloneAnchorValue(v[k], depth + 1);
611
+ var out = intrinsic.create(null);
612
+ var vNames = guard.identifier.optionNames(v);
613
+ for (var vi = 0; vi < vNames.length; vi++) {
614
+ out[vNames[vi]] = _cloneAnchorValue(v[vNames[vi]], depth + 1);
615
615
  }
616
616
  return out;
617
617
  }
package/lib/crl-sign.js CHANGED
@@ -196,7 +196,7 @@ function _validatePreEncodedIdp(inner) {
196
196
  function _freshestValue(spec) {
197
197
  if (!Array.isArray(spec) || !spec.length) throw _err("crl/bad-input", "freshestCRL must be a non-empty array of GeneralNames (or { fullName } distribution points)");
198
198
  var dense = _b.reqDenseArray(spec, "freshestCRL");
199
- var dps = dense.every(function (e) { return e && typeof e === "object" && !Buffer.isBuffer(e) && Object.keys(e).length === 1 && e.fullName != null; })
199
+ var dps = dense.every(function (e) { return e && typeof e === "object" && !Buffer.isBuffer(e) && guard.identifier.optionNames(e).length === 1 && e.fullName != null; })
200
200
  ? dense.map(function (dp) { return dp.fullName; })
201
201
  : [dense];
202
202
  return b.sequence(dps.map(function (fullName) {
package/lib/crmf-sign.js CHANGED
@@ -136,7 +136,7 @@ function _buildAttrTypeAndValues(spec, code, label, valueMap) {
136
136
  guard.identifier.assertKnownKeys(spec, valueMap, _err, "crmf/bad-input", function (k) {
137
137
  return "unknown " + label + " " + JSON.stringify(k) + "; pass a pre-encoded AttributeTypeAndValue DER via the array form for a " + label + " entry outside " + Object.keys(valueMap).join("/");
138
138
  });
139
- Object.keys(spec).forEach(function (k) {
139
+ guard.identifier.optionNames(spec, valueMap).forEach(function (k) {
140
140
  var enc = valueMap[k];
141
141
  var typeOid = O(k);
142
142
  if (seen[typeOid]) throw _err(code, "duplicate " + label + " type " + k);
package/lib/ct.js CHANGED
@@ -1076,7 +1076,7 @@ function _fetchBody(transport, url, req, label) {
1076
1076
  }).then(function (res) {
1077
1077
  res = res || {};
1078
1078
  var h = intrinsic.create(null);
1079
- Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
1079
+ guard.identifier.optionNames(res.headers).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
1080
1080
  var body = guard.bytes.isByteSource(res.body)
1081
1081
  ? guard.bytes.source(res.body, CtError, "ct/bad-input", label)
1082
1082
  : Buffer.from(String(res.body == null ? "" : res.body), "utf8");
@@ -1149,7 +1149,7 @@ async function fetchLogList(opts) {
1149
1149
  var timeout = guard.limits.cap(opts.timeout, "timeout", DEFAULT_FETCH_TIMEOUT, { E: _ctErr, code: "ct/bad-input", min: 1, max: MAX_FETCH_TIMEOUT });
1150
1150
  var maxResponseBytes = guard.limits.cap(opts.maxResponseBytes, "maxResponseBytes", C.LIMITS.CT_LOG_LIST_MAX_BYTES, { E: _ctErr, code: "ct/bad-input", min: 1, max: C.LIMITS.CT_LOG_LIST_MAX_BYTES });
1151
1151
  var headers = intrinsic.create(null);
1152
- Object.keys(opts.headers || {}).forEach(function (k) {
1152
+ guard.identifier.optionNames(opts.headers).forEach(function (k) {
1153
1153
  var lk = k.toLowerCase();
1154
1154
  if (lk !== "content-length" && lk !== "transfer-encoding") headers[k] = opts.headers[k];
1155
1155
  });
package/lib/est.js CHANGED
@@ -105,7 +105,8 @@ var CLIENT_OPTS = intrinsic.assign(intrinsic.create(null), {
105
105
  function _withClient(extra) {
106
106
  var out = intrinsic.create(null);
107
107
  Object.keys(CLIENT_OPTS).forEach(function (k) { out[k] = 1; });
108
- Object.keys(extra || {}).forEach(function (k) { out[k] = 1; });
108
+ var en = guard.identifier.optionNames(extra);
109
+ for (var ei = 0; ei < en.length; ei++) out[en[ei]] = 1;
109
110
  return out;
110
111
  }
111
112
  var CACERTS_OPTS = _withClient(null);
@@ -886,7 +887,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
886
887
  function _headersFor(u) {
887
888
  digestSent = false;
888
889
  if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
889
- var hh = Object.assign({}, headers);
890
+ var hh = guard.identifier.ownOptions(headers);
890
891
  if (digestChallenge) {
891
892
  if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
892
893
  var nkey = digestChallenge.nonce;
@@ -912,7 +913,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
912
913
  if (status === 303 && method !== "GET" && method !== "HEAD") {
913
914
  method = "GET";
914
915
  body = null;
915
- if (headers["content-type"]) { headers = Object.assign({}, headers); delete headers["content-type"]; }
916
+ if (headers["content-type"]) { headers = guard.identifier.ownOptions(headers); delete headers["content-type"]; }
916
917
  }
917
918
  url = _redirectTarget(url, h.location, method, opts);
918
919
  redirects += 1;
@@ -987,7 +988,7 @@ function _client(op, method, baseUrl, body, headers, opts, proxySnap, onAttempt,
987
988
  onAttempt: typeof onAttempt === "function" ? onAttempt : null,
988
989
  };
989
990
  _authScheme(opts);
990
- return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
991
+ return _drive(method, url, body, guard.identifier.ownOptions(headers), opts, transport, budgets);
991
992
  }
992
993
 
993
994
  function _certsResult(op, res, opts, csrSpki) {
@@ -1313,18 +1314,16 @@ function _assertAgrees(name, supplied, carried) {
1313
1314
 
1314
1315
  function _copyRecipient(r) {
1315
1316
  if (!r || typeof r !== "object") return r;
1316
- var out = intrinsic.create(null), k;
1317
- for (k in r) { if (_hasOwn(r, k)) out[k] = _copyBytes(r[k]); }
1317
+ var out = intrinsic.create(null);
1318
+ var rNames = guard.identifier.optionNames(r);
1319
+ for (var ri = 0; ri < rNames.length; ri++) out[rNames[ri]] = _copyBytes(r[rNames[ri]]);
1318
1320
  return out;
1319
1321
  }
1320
1322
 
1321
1323
  function _shallowCopy(o) {
1322
- var out = intrinsic.create(null), k;
1323
- for (k in o) { if (_hasOwn(o, k)) out[k] = o[k]; }
1324
+ var out = guard.identifier.ownOptions(o);
1324
1325
  if (out.auth && typeof out.auth === "object") {
1325
- var a = intrinsic.create(null), ak;
1326
- for (ak in out.auth) { if (_hasOwn(out.auth, ak)) a[ak] = out.auth[ak]; }
1327
- out.auth = a;
1326
+ out.auth = guard.identifier.ownOptions(out.auth);
1328
1327
  }
1329
1328
  return out;
1330
1329
  }
@@ -1696,7 +1695,7 @@ function _assertConfidentialCipher(res) {
1696
1695
 
1697
1696
  function _ciHeaderList(headers, name) {
1698
1697
  headers = headers || {};
1699
- var lname = name.toLowerCase(), keys = Object.keys(headers), parts = [];
1698
+ var lname = name.toLowerCase(), keys = guard.identifier.optionNames(headers), parts = [];
1700
1699
  for (var i = 0; i < keys.length; i++) {
1701
1700
  if (keys[i].toLowerCase() !== lname) continue;
1702
1701
  var v = headers[keys[i]];
@@ -1708,7 +1707,7 @@ function _ciHeaderList(headers, name) {
1708
1707
 
1709
1708
  function _ciHeader(headers, name) {
1710
1709
  headers = headers || {};
1711
- var lname = name.toLowerCase(), keys = Object.keys(headers), found = null, n = 0;
1710
+ var lname = name.toLowerCase(), keys = guard.identifier.optionNames(headers), found = null, n = 0;
1712
1711
  for (var i = 0; i < keys.length; i++) {
1713
1712
  if (keys[i].toLowerCase() !== lname) continue;
1714
1713
  n += 1;
@@ -176,6 +176,8 @@ var AttrCertError = defineClass("AttrCertError", { withCause: true });
176
176
 
177
177
  var CrmfError = defineClass("CrmfError", { withCause: true });
178
178
 
179
+ var TrustAnchorError = defineClass("TrustAnchorError", { withCause: true });
180
+
179
181
  var Pkcs12Error = defineClass("Pkcs12Error", { withCause: true });
180
182
 
181
183
  var CmpError = defineClass("CmpError", { withCause: true });
@@ -238,6 +240,7 @@ module.exports = {
238
240
  TspError: TspError,
239
241
  AttrCertError: AttrCertError,
240
242
  CrmfError: CrmfError,
243
+ TrustAnchorError: TrustAnchorError,
241
244
  Pkcs12Error: Pkcs12Error,
242
245
  CmpError: CmpError,
243
246
  PathError: PathError,
@@ -9,6 +9,9 @@ var _freeze = require("./guard-intrinsic").freeze;
9
9
  var _charCodeAt = require("./guard-intrinsic").uncurry(String.prototype.charCodeAt);
10
10
  var _stringify = require("./guard-intrinsic").stringify;
11
11
  var _String = require("./guard-intrinsic").String;
12
+ var _create = require("./guard-intrinsic").create;
13
+ var _toLowerCase = require("./guard-intrinsic").uncurry(String.prototype.toLowerCase);
14
+ var _optionNames = require("./guard-identifier").optionNames;
12
15
 
13
16
  // @enforced-by behavioral -- the CR/LF/NUL value reject + the ftext name check have
14
17
  function assertField(name, value, E, code) {
@@ -28,4 +31,15 @@ function assertField(name, value, E, code) {
28
31
  return v;
29
32
  }
30
33
 
31
- module.exports = _freeze({ assertField: assertField });
34
+ /** @internal A response's header fields on a prototype-less object, keyed by the lowercased name,
35
+ * which is how RFC 9110 sec. 5.1 says to compare them. Read by own key, so a field an injected
36
+ * transport defines non-enumerably is not lost between the response and the lookup. */
37
+ // @enforced-by option-bag-read-by-enumerable-key
38
+ function lowerCased(headers) {
39
+ var out = _create(null);
40
+ var names = _optionNames(headers);
41
+ for (var i = 0; i < names.length; i++) out[_toLowerCase(names[i])] = headers[names[i]];
42
+ return out;
43
+ }
44
+
45
+ module.exports = _freeze({ assertField: assertField, lowerCased: lowerCased });