@blamejs/pki 0.8.55 → 0.8.56
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 +66 -0
- package/lib/acme.js +2 -2
- package/lib/asn1-der.js +17 -5
- package/lib/attrcert-verify.js +1 -1
- package/lib/cmc-verify.js +1 -1
- package/lib/cmp-session.js +2 -2
- package/lib/cmp-verify.js +2 -2
- package/lib/cms-decrypt.js +4 -1
- package/lib/cms-encrypt.js +4 -1
- package/lib/cms-sign.js +26 -12
- package/lib/cms-verify.js +1 -1
- package/lib/composite-kem.js +30 -12
- package/lib/composite-sig.js +42 -23
- package/lib/constants.js +7 -2
- package/lib/crl-verify.js +1 -1
- package/lib/crmf-sign.js +72 -43
- package/lib/crmf-verify.js +1 -1
- package/lib/csr-verify.js +1 -1
- package/lib/ct.js +2 -2
- package/lib/edwards-point.js +1 -1
- package/lib/est.js +3 -3
- package/lib/guard-async.js +22 -1
- package/lib/guard-bytes.js +6 -3
- package/lib/guard-identifier.js +1 -1
- package/lib/guard-intrinsic.js +474 -0
- package/lib/http-digest.js +146 -62
- package/lib/http-transport.js +82 -17
- package/lib/identity-match.js +2 -2
- package/lib/merkle.js +2 -2
- package/lib/ocsp-request-verify.js +1 -1
- package/lib/ocsp.js +46 -22
- package/lib/path-validate.js +169 -76
- package/lib/pbes2.js +14 -7
- package/lib/schema-attrcert.js +2 -2
- package/lib/schema-cmc.js +2 -2
- package/lib/schema-cmp.js +2 -2
- package/lib/schema-cms.js +2 -2
- package/lib/schema-crmf.js +2 -2
- package/lib/schema-csr.js +2 -2
- package/lib/schema-pkix.js +3 -3
- package/lib/schema-x509.js +2 -2
- package/lib/sigstore.js +1 -1
- package/lib/tlog.js +7 -4
- package/lib/trust.js +187 -104
- package/lib/tsp-sign.js +1 -1
- package/lib/webauthn-mds.js +257 -121
- package/lib/webauthn.js +327 -164
- package/lib/webcrypto.js +10 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,72 @@ 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.56 — 2026-10-06
|
|
8
|
+
|
|
9
|
+
Decide a name, an address, a revocation and a trust bit through operations and values no caller can replace.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The case folds that remain read protocol details rather than deciding whether something is admitted: a response charset, a challenge flag, the relation name on a `Link` header, a contact's URI scheme. Each is read from a peer that has already been authenticated by the time it is consulted, so these are being converted for consistency rather than because a verdict turns on them.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `pki.path.validate` holds an `excludedSubtrees` name constraint whatever `String.prototype.toLowerCase` answers. The matcher folded both the constraint and the certificate's name before comparing them, so a replacement that mangled the constraint alone made the exclusion stop matching, and a leaf naming `www.bad.example` under an exclusion of `bad.example` was admitted with no failure code at all. The three name-constraint matchers now read the captured fold, along with the captured slice and index operations they mixed with it.
|
|
18
|
+
- `pki.webauthn.verify` holds the android-safetynet hostname requirement of WebAuthn 8.5. The check answers yes when the x5c leaf is issued to `attest.android.com`, and it folded the leaf's name first, so a replacement answering with that name admitted any leaf: an attestation whose leaf named `other.example` verified.
|
|
19
|
+
- `pki.webauthn.verify` reads that leaf's name from its subject alternative name, which is the authoritative one, and falls back to the common name only for a leaf that carries no such extension at all. Two faults are fixed. It had been looking for a shape the decoder does not produce, so it selected no name and every leaf fell through to the fallback: a leaf whose SAN named another host was accepted on a common name that said `attest.android.com`, and a leaf that correctly named the host in its SAN alone was refused when its common name differed. And the fallback was reached whenever no DNS name was selected rather than only when the extension is absent, so a leaf whose SAN holds just a URI or an address also fell through to its common name, which is the one place that does not bind a host. A leaf carrying no SAN at all is still read by its common name, which is how the service issued its certificates.
|
|
20
|
+
- `pki.transport.https` keeps refusing a private, loopback or link-local address literal under `blockPrivateAddresses`. Both families were steerable: the IPv6 arm folded the address before classifying it, so a private address answered as a global-unicast one cleared the refusal, and the IPv4 arm split the literal into octets, so a replacement answering with a public address's octets did the same. `127.0.0.1` and `fc00::1` both reached a connection attempt.
|
|
21
|
+
- `pki.trust.parseCcadbCsv` grants an anchor only the purposes its Trust Bits cell names. The cell was split, trimmed and folded through live operations before each token was looked up, and the lookup grants a purpose, so a replacement answering with a recognized token widened what the anchor was trusted for: a row stating only `Email` produced an anchor carrying `serverAuth`.
|
|
22
|
+
- `pki.path.validate` holds a `permittedSubtrees` constraint as well, which opens in the opposite direction from an exclusion: it refuses when no subtree matches, and an empty list of subtrees for a name form reads as "this issuer constrains no name of that form" and skips the check. A replacement that answered with an empty list therefore admitted a name no subtree permits. The subtree selection and the walk over it are both converted, and the test asserts the stronger property that neither consults a replaced operation at all rather than only that the verdict came out right.
|
|
23
|
+
- `pki.path.validate` holds an excluded `rfc822Name` constraint as well. A constraint naming a whole mailbox is told apart from one naming a domain by whether it contains an `@`, so a replacement answering that it does not routed a mailbox constraint down the domain branch and the exclusion stopped matching: a leaf naming `alice@bad.example` under an exclusion of that same mailbox was admitted. Every name-constraint matcher reads the captured operations, whichever name form it tests.
|
|
24
|
+
- `pki.trust.parseCcadbCsv` still refuses a distrust date it cannot parse. An empty cell means there is no distrust date, and the cell was trimmed through a live operation first, so a replacement answering with the empty string removed the date rather than failing: a row carrying `not-a-date` parsed as an anchor with no distrust date at all.
|
|
25
|
+
- `pki.transport.https` still refuses a colon in a Basic proxy user-id (RFC 7617 sec. 2). The check asked a live index operation, so a replacement answering that the colon is absent accepted a credential the scheme cannot encode unambiguously.
|
|
26
|
+
- `pki.trust.parseCcadbCsv` reads each field from the column its header names. The header names were trimmed through a live operation, so a replacement that renamed the real `Trust Bits` header and answered `Trust Bits` for another column made an anchor's purposes come from that other cell: a row stating `Email` produced an anchor carrying `serverAuth` taken from a decoy column.
|
|
27
|
+
- The Authority Information Access fetch decides whether a URL's host is an address literal through a captured reference. A host the classification calls a literal is fetched without requiring the transport to declare that it filters the addresses it resolves, so a replacement answering that a DNS name is a literal fetched it under a guarantee nothing had made. That classification runs before the private-address blocklist it feeds.
|
|
28
|
+
- `pki.trust` reads a CA bundle through captured file operations. The bytes they return become the anchors the host is trusted to use, which is a wider result than any single verdict: a replaced read substitutes the anchor set itself.
|
|
29
|
+
- Everything the proxy Digest exchange is decided by now reads a captured operation. That covers the quality-of-protection selection and the refusal of a challenge offering none this client supports, the protection-space test that decides whether a credential may be sent to a request's origin at all, the normalization of the credentials that are hashed, and the assembly of the header itself, where a dropped `cnonce` or nonce count is a credential the server reads under different rules than the one it was computed under.
|
|
30
|
+
- A Digest challenge is read character by character through captured operations, so a refusal names the fault it actually found. Rewriting the algorithm as the lexer copies it does not reach an accepted credential either way, which is worth saying plainly rather than overstating: the policy still refuses. What moved was the reason, from naming the weak algorithm to reporting an unsupported one, and a refusal that misreports why is a refusal an operator cannot act on.
|
|
31
|
+
- A proxy Digest credential is computed with a captured hash and a captured random source, and the random bytes are encoded through a captured operation as well. The hash is what the response value is, so a replacement chooses what is sent in place of the password-derived proof. For the client nonce, capturing the source alone was not enough: the bytes are encoded to text before they reach the header, and a replaced encoding answered with a fixed string, which makes the response replayable. Unpredictability is a property of what reaches the header, not of where the bytes came from. A caller's own random source still takes precedence.
|
|
32
|
+
- `pki.transport.https` answers the TLS identity question through a captured reference, and it now passes that reference to the connection rather than leaving the option unset. Left unset, the TLS stack looks the check up on its own module export when the handshake completes, and that export returns the verdict on whether a certificate belongs to the host that was asked for: a replacement answering with no error is server authentication reporting success for a certificate issued to someone else. A request asking for a name the server's certificate does not carry was admitted that way. The function passed is the same default the stack would have used, taken when this module loaded, so no verdict changes for a caller running ordinary code, and a caller's own `checkServerIdentity` hook still runs after it as before.
|
|
33
|
+
- `pki.transport.https` classifies an address family through a captured reference, which is the operation consulted before any of the others. The classification comes from a writable module export, so answering that `127.0.0.1` is not an address at all made the blocklist fall through to permitting it, leaving every captured operation after it deciding nothing.
|
|
34
|
+
- The android-safetynet hostname requirement is decided by captured membership walks as well as a captured fold. The fold alone was not the gate: a walk answering yes passes the requirement whatever the names compare as, and a selection that drops the certificate's DNS names sends the check down its common-name fallback instead. Both reach the same acceptance the check exists to withhold.
|
|
35
|
+
- `pki.transport.https` applies its Digest policy to the challenge the proxy actually sent. The challenge was read, split, trimmed and classified through live operations before the policy saw it, so a replacement that rewrote an `MD5` challenge as a strong one turned the weak-algorithm refusal into an issued credential. The parse is now captured end to end, which covers the algorithm, the quality-of-protection list, the charset and the stale and userhash flags.
|
|
36
|
+
- The global conversions these checks pass values through are captured too, not only the methods called on them. The private-address classification reads its address hextets with a numeric conversion, and the Trust Bits reader converts a cell to text before splitting it: with the methods captured and those two left alone, both gates were exactly as open as before, because an operation that decides which number or which token the safe code is handed decides the answer. A private address answered as global unicast and a cell reading `Email` answered as `Websites` each reproduced the bypass in full.
|
|
37
|
+
- Every operation `pki.trust` reads a CCADB CSV or an NSS `certdata.txt` through now comes from the toolkit's own captures. Securing a cell reader is not enough on its own while the lexer that feeds it is open, nor a token lookup while the header-to-column mapping that chose the cell is open, and the pairing check that refuses two disagreeing rows for one root could be answered `true` from outside. A row's purposes, its distrust dates, which column each came from, and the certificate bytes themselves are all decided on captured operations, so the module cannot be steered a level at a time.
|
|
38
|
+
- `pki.trust.parseCertdata` joins an NSS trust object to its certificate on an encoding of the pair's issuer and serial bytes, and that encoding is now captured. The join is only as byte-exact as the text it compares, so a replacement answering with another certificate's issuer or serial attached an otherwise orphaned trust object's delegator purposes to an unrelated root: a certificate with no trust object of its own was trusted for server authentication by a trust object naming a different serial.
|
|
39
|
+
- `pki.webauthn.verify` binds an android-safetynet response to the ceremony it was produced for through a nonce, and the digest behind that nonce is encoded to text before the comparison. The encoding is now captured along with the hash: capturing the hash alone left the encoding choosing the operand, so a replacement answering with the nonce the response carries verified a correctly signed response against authenticatorData and client data it was never made over. The same response then replays against any registration.
|
|
40
|
+
- `pki.transport.https` applies its Digest algorithm policy to a directive map the challenge parse builds, and that map is now constructed from a capture. RFC 7616 sec. 3.3 makes an absent `algorithm` mean MD5, which `allowMD5: false` refuses, and the default is read out of the map: a replaced construction handed back a map already naming SHA-256, so a challenge that names no algorithm at all produced a credential instead of the refusal.
|
|
41
|
+
- The integer readers a wire structure's lengths and counters are read with are captured too. A length read this way is what the bounds check beside it and the slice after it are computed from, so a replaced reader decides which bytes a check runs over: the credential-id length in authenticatorData chose the slice the COSE credential key was then decoded from, the signature counter answered the replay rule that compares it with the stored one, and the length prefix in a transparency-log bundle chose where one entry ended and the next began. The key-length and counter blocks that go into the CMS key-agreement and X9.63 key derivations are written through captures for the same reason, since both are derivation input: a replaced write derives a different key-encryption key from the same shared secret, and a repeated counter block makes two blocks of derived key material identical.
|
|
42
|
+
- `pki.webauthn.verify` accepts metadata only as a `verifyMetadataBlob` result, and the marker that distinguishes one from a raw object a caller assembled is now read through a capture. Membership in that marker is the whole check, so a replacement answering yes admitted unverified metadata, after which the status policy and the attestation roots both came from a BLOB whose signature was never checked. The marker that binds an individual entry to the result it was read out of is captured with it.
|
|
43
|
+
- `pki.trust.anchor` tells a store-produced entry from a rebuilt one through a capture. The lookup that answers that question both supplies the per-purpose metadata the purpose gate reads and waives the refusal that stops a rebuilt entry carrying such metadata at all, so a replacement answering with an object of its own produced an anchor trusted as a delegator for a purpose no trust store ever granted. `pki.identity.snapshotReferences` marks its frozen output the same way, and that marker is captured as well: a replacement answering yes returned a caller's live array as though it had been snapshotted.
|
|
44
|
+
- The key import that precedes an attestation or assertion signature check is captured alongside the verify itself. The import produces the key the check runs under, so capturing only the verify left a replacement able to answer with a key of its own: a signature made by whoever holds that key verified, while the SPKI bytes handed in were never the credential's.
|
|
45
|
+
- `pki.webauthn.verifyMetadataBlob` and the lookups that read its result reach their decisions through captures as well. The status-report check is what refuses a revoked or compromised authenticator, and it works by selecting the reports in force and then testing them for a disqualifying status, so each operation that narrows the set decided the refusal: one answering with an empty list left nothing disqualifying to find, and the membership test answering no permitted the entry whatever its reports said. A `REVOKED` entry was permitted either way, after which `metadataAnchors` handed back the roots it registers. Under `statusPolicy: "latest-by-date"` the fold that picks the newest report decided it too, since only the reports sharing that date are considered.
|
|
46
|
+
- `pki.transport.https` keeps refusing a private, loopback or link-local address literal under `blockPrivateAddresses` whatever a `Symbol.split` method installed on the string prototype answers. Dividing a string on a separator is specified to look that method up on the separator and call it, so the separator's prototype chain is part of the operation and taking the split operation from a capture does not close it. Measured on Node 24: a hook answering with a public address's octets made `127.0.0.1` classify as global unicast and a request to it proceeded. The parsers that divide a value on a single delimiter now scan for that character instead, which consults no separator at all. Those are the ones where the pieces decide something: the octets an address literal is classified by and the hextets of its IPv6 form, the Trust Bits tokens that grant an anchor its purposes, the quality-of-protection list a Digest credential is computed under, the segments an attestation JWS is divided into, which decide the signed input, the sub-identifiers of an object identifier, and the labels a host name is compared by.
|
|
47
|
+
- `pki.path.validate` builds its name-constraint collections, its policy-set intersections and the chains it validates through values this toolkit constructs itself. A collection built through a replaceable construction answers as empty while still holding its elements, and empty is the dangerous direction for a permitted subtree: an issuer's `permittedSubtrees` generation that reads as empty is discarded as though that issuer constrained no name of that form, so a certificate outside it is accepted. The same construction decides whether an excluded-subtree list survives being joined with another, where a dropped list loses the exclusion, and whether a chain keeps every element it is validated over.
|
|
48
|
+
- The selections and byte slices these checks depend on consult no construction protocol. Taking an array operation from a capture closes the method and leaves open what the method asks on the way: a selection builds its result through a constructor read off the receiver, and one answering with an ordinary object collects every match as an indexed property while the length stays zero, so the result reads as empty. Empty is rarely inert. An empty subject alternative name selection sends the android-safetynet hostname check to its common-name fallback, where a leaf whose authoritative name is another host passes on a matching common name; an empty permitted-subtree list reads as an issuer that constrains no name of that form and skips the check; and an empty quality-of-protection list reads as a Digest challenge offering none. A selection answering with elements of its own substitutes them instead, which is how a fabricated certificate chain or anchor set reaches a signature check. Slicing bytes asks the same question: a slice of authenticatorData answering with the expected RP ID hash made an assertion produced for another relying party satisfy `expectedRpId` while the signature was verified over the original bytes. Every selection and slice behind these checks now appends into, or builds, a value this toolkit constructs itself. The element count those operations iterate by is read once and required to be a plain non-negative integer, because a proxy over an array satisfies every array test while answering `length` with an object whose value changes each time it is read. A loop re-reading it then visits fewer elements than the first read promised, which drops the tail in silence: a two-certificate path presented that way reported valid with the second certificate never checked. Appending is the other half of the same question: appending with `push` writes through an assignment that walks the prototype chain for a numeric setter, so an accessor installed there takes the value, no property lands on the list, and the index reads back as whatever that accessor answers. That was enough to substitute a quality-of-protection token into a Digest credential, so these lists are built by defining each element as an own property, which consults no accessor.
|
|
49
|
+
- The refusal that stops a revoked or compromised authenticator being trusted selects its reports with plain index loops, so no part of it runs through an operation that consults a replaceable protocol. Taking the array operations from a capture is not sufficient here: a selection still builds its result through the constructor it reads off the receiver, so one answering with an ordinary object collects the matching reports as indexed properties while the length stays zero and the test at the end visits nothing, and the join that merges two selections reads a spreadability flag the same way, leaving both nested where no report has a status to recognize. Each report's status and effective date are read as its own properties too, so a caller-supplied record cannot inherit a date and have an undated revocation read as one not yet in force. Every route ended in a permit for an entry the catalogue has revoked.
|
|
50
|
+
- The lookup that finds a metadata entry builds its key out of an AAGUID or an attestation certificate key identifier, and every operation that key is built from is captured: the digest, the encoding that renders it, the slices that punctuate an AAGUID and the case fold applied to both sides. The entry a key finds supplies the status reports and the attestation roots, so an operation answering with another authenticator's identifier governed the attestation by another entry. The same digest tells two certificates apart where a status report names one, so a replacement also made a certificate-scoped compromise report apply to the wrong leaf.
|
|
51
|
+
- `pki.webauthn.verifyAssertion` snapshots the bytes its verdict is computed over, and the walk that takes those snapshots is captured. A walk visiting nothing left the caller's own buffers in place, so they could be rewritten between the call returning its promise and the verification running: authenticatorData substituted along with a signature matching it verified, and the verdict described bytes the call never saw. `pki.webauthn.verify` validates its format-scoped boolean options by walking them too, and a walk that visits nothing left a mistyped value in place for an arm that wants exactly `true`.
|
|
52
|
+
- `pki.webauthn.verify` holds the requirement that an Apple attestation certificate carry the anonymous-attestation extension. The extension was taken as index 0 of the selection that looks for it, and index 0 of an EMPTY selection is not absence: it reads through to the array prototype, so a value installed there answered for an extension the certificate does not carry. A leaf with no extensions at all verified, against a nonce that was never in the signed certificate. The selection is indexed only once it is known to hold something.
|
|
53
|
+
- The algorithm tables `pki.webauthn.verifyMetadataBlob` reads its parameters out of carry no prototype and nothing writable, row by row. Freezing a table is not enough on its own when its rows are object literals, because a row still inherits: a `fromLeaf` property installed on the base object prototype was read off every row and sent the strongest algorithm down the branch that takes its parameters from the leaf key instead, and a scheme name installed the same way made a key type read as supporting a scheme it does not. The table a leaf key's permitted schemes are looked up in, the status tables a report is judged against, and the header parameters a `crit` refusal is read from are all closed the same way. `pki.webauthn.verify` and `pki.trust`'s own option bags default to a record with no prototype as well, so a property installed on the base object prototype no longer reads as an option a caller never set. The Digest policy goes further, because every field of it is read off the record it is given: the bag is asked for `policy` by own key and the policy is copied by own key, so neither an inherited `policy` supplied for a caller who passed none nor an inherited `allowMD5` supplied for a caller who passed an empty one turns the default MD5 refusal into an issued credential. The whole parameter bag is taken as an own-key copy for the same reason, since every field of it is another value an inherited read can supply: an inherited `rng` answered for the client nonce, and a client nonce that is the same on every exchange makes the credential replayable, which is the one property that value exists to have.
|
|
54
|
+
- `pki.path.validate` is reached by name from the metadata reader's anchoring fold, and that module now freezes what it exports. The validator read that way IS the check at the point it is read, so a replacement reporting a valid path made a metadata BLOB whose signing certificate chains to none of the supplied roots pass anchoring, after which the catalogue was branded verified and its status entries and attestation roots governed WebAuthn verification. The reference cannot be substituted now.
|
|
55
|
+
- The modules behind these checks freeze what they export, so a cross-module call cannot be answered by a replacement. Capturing a module's own operations is not enough on its own when the consumer reads the operation off the producing module at the call: `pki.webauthn.verify` reaches the metadata lookup and the chain validation that way, and both are the whole check at the point they are read. A lookup answering with nothing turned a disqualified authenticator into one with no catalogue entry, which takes a different branch and skips the status refusal, and a chain validation that settles without walking a path reported the attestation anchored with nothing compared. Neither can be substituted now.
|
|
56
|
+
- A metadata BLOB's own chain is anchored through captures. The fold over the candidate roots is what walks each one until a path validates, and the slices inside it are the chain a root is tried against and what is left once a self-presented anchor is stripped: a replacement answering with a single-element list left nothing to validate, and the empty path reported trusted on the strength of the strip alone. The key comparison that decides a certificate IS the anchor is captured with them, so a certificate carrying the anchor's subject but a different key no longer counts as the anchor.
|
|
57
|
+
- `pki.transport.https` refuses a Digest challenge whose `qop` directive lists no usable value, and that refusal no longer depends on a global conversion. The empty-member test was the global `Boolean` passed as a predicate, so a replacement answering true for the empty string left `qop=""` with one member and returned a challenge whose quality of protection is not a value the scheme defines. The predicate is written out instead.
|
|
58
|
+
- The rest of `pki.webauthn`'s decisions read captured operations too, not only the folds and walks in the hostname check. That covers the slices authenticatorData is cut into, which are the RP ID hash, AAGUID and credential id each check runs against; the assembly of the bytes every attestation format and the assertion path verify a signature over; the comparisons that bind an Apple nonce, a TPM quote and a TPM attested name; the origin and top-origin allow-lists, where the membership test is what refuses a response completed on another site; and the COSE algorithm allow-list, including the explicit opt-in that is the only way SHA-1 is accepted.
|
|
59
|
+
- The ASN.1 reader and builder tables are frozen, and so is the module object that names them. Every module in this toolkit reaches a leaf reader off that table by name, hundreds of times across sixty-odd modules, and each one is the step that turns bytes into the value a check compares. A replaced time reader made a CCADB row stating a 2020 distrust date produce an anchor with a 2099 cutoff, so a root stayed trusted long past the date the store names. Freezing the two tables left the properties naming them assignable, which reaches the same reader one level out; both levels are closed now, for every consumer at once.
|
|
60
|
+
- Every module that answers a verdict for another module freezes what it exports: the signature, certificate, CRL, CSR, attribute-certificate, CMC, CMP, CRMF, timestamp and certificate-transparency verifiers, the Merkle proof checker, the Edwards point validator, and the schema modules whose `verify` is the structural half of those checks. Each is reached by name at the call rather than captured, so the reference is what the check amounts to at the point it is read. `pki.cms.verify` hands a signature timestamp to the timestamp module that way, and a replacement adding the archive opt-in on the way through reported a SHA-1 imprint as a valid timestamp while the caller had asked for no such thing.
|
|
61
|
+
- An allow-list a caller leaves empty admits nothing. `pki.webauthn.verify` reads `expectedOrigin`, `expectedTopOrigin` and `allowedAlgorithms` as the list's own entries: a sparse array answers from the array prototype at the index it has a gap at, and the test that requires every entry to be well-formed skips that gap, so a one-element list holding no origin of its own passed the door and then matched an origin installed on the prototype, with the origin reported as checked. Those lists are now held to their own entries at the door and at the comparison.
|
|
62
|
+
- The results of the two component verifications a composite ML-DSA signature is the conjunction of are awaited through an aggregate that consults nothing replaceable. The standard aggregate reads `Promise.resolve` off the constructor on every call, so a replacement there answers for each component and none is awaited: measured, a certificate whose classical half is corrupted reported as valid. The same aggregate now carries the composite KEM combiner's two shared-secret halves, the proof-of-possession verdict for a set of certificate requests, the signer infos a CMS signature is assembled from, and the certificate identifiers an OCSP request is built around.
|
|
63
|
+
- The composite ML-KEM combiner derives both halves of a shared secret through captured continuations, and so does every step that produces one. A continuation is read off the promise at the call, so a replacement settling the decapsulation step handed the combiner bytes the key never produced and the secret this side derived was not the secret the other side holds. The step that wipes the component halves once the combiner is done runs through a capture as well.
|
|
64
|
+
- `pki.ocsp` builds the certificate identifier a request names, and the key hash a responder names itself by, through captured continuations. A replacement settling the digest step named another certificate's identity in bytes that are then signed. The verbs that sign a request or a response hold the signature to verifying under the certificate they carry before emitting it, and that step is captured too, as are the one that wipes a key the verb owns and the one that turns a response's status into the verdict a caller reads.
|
|
65
|
+
- `pki.crmf.verifyPop` reads each message's proof-of-possession verdict through a captured continuation, and the verb that builds a proof uses the same ones. The continuation is where the signature answer becomes a message's verdict, so a replacement settling it handed back a record stating the proof verified and the set-wide answer reported every request as verified. On the building side those steps are what emit a proof only once its signature has been checked against the key being certified, and what wipe an enclosed private key whichever way the work settles. The verb still hands back an ordinary array of message verdicts.
|
|
66
|
+
- `pki.path.validate` reports a signature verdict it computed and builds its policy graph from the policies a certificate actually asserts. Two operations decided both and neither was the toolkit's: the continuation that delivers the signature result, where a replacement settling the key-import step reported a verified signature with nothing checked and a chain whose issuer name matches a supplied root was accepted on a signature that does not verify; and the walk over a certificate's certificate-policies extension, where a replacement supplying a fabricated policy identifier built a graph node for a policy the certificate never asserted, which under `initialExplicitPolicy` with a stated `userInitialPolicySet` is what makes an otherwise policy-invalid chain acceptable. The same module's revocation checkers, policy mappings, graph pruning and user-policy intersection are taken from captures alongside them, and the CRL and OCSP lists a caller supplies are mapped without consulting a construction protocol. The composite signature verifier the path validator calls for an ML-DSA plus classical algorithm is covered too: both component verdicts and the conjunction over them are read through captures, so a composite signature cannot report as verified with one half, or neither, actually checked.
|
|
67
|
+
- `pki.webauthn.verify`, `pki.webauthn.verifyAssertion` and `pki.webauthn.verifyMetadataBlob` continue every step of their work through captured operations, which covers the step that delivers each verdict. Three things stood between a check and its result, and each one was enough on its own. A promise's continuation is read off the promise at the call, and the callback holding `if (!ok) throw` is where a signature refusal lives, so a replacement that settles without running it carries the response past the refusal. The promise a continuation hands back is built through a constructor read off the promise and a species read off that. And resolving with a promise is specified to read that promise's own continuation and call it, so a callback that returns one is routed through the same replaceable step. Measured on each: a packed self-attestation whose signature does not verify returned as verified, an assertion with no input at all fulfilled with a fabricated verdict instead of being refused, and a chain reaching none of the supplied roots returned a verified catalogue. These routes now settle through operations taken at load, and they settle identically to the method they stand in for across the eighteen outcomes a caller can produce. Where they do deviate it is toward a refusal: a value presenting as a promise without being one is refused rather than passed along as a result. Two things remain outside what this can reach, and neither is on a route here: the caller's own `await`, by which point every check has run and any refusal has been raised, and a value that is a thenable without being a promise, which the language's own resolution procedure adopts by calling the `then` it finds.
|
|
68
|
+
- `pki.transport.https` requires `tls.servername` to be a string, whether it arrives with the request or as a transport default. The name is read twice more after it is accepted: by the TLS layer for the name sent in the handshake, and as the identity the server certificate is matched against. A value that converts to a different string per read sent one name and verified another. The proxy arm had always typed this option; the origin arm now does too.
|
|
69
|
+
- `pki.transport.isBlockedIp` treats a value it cannot classify as blocked, which is what `blockPrivateAddresses` turns on. The classifier converts its argument, so a value whose conversion answered a loopback literal to the address-family check and a public one to the octet scan was measured twice and the second answer decided: it came back not blocked while the family check had seen `127.0.0.1`. A resolver supplied through `opts.lookup` reaches this with whatever it yields. A hostname that is not an address literal still answers false.
|
|
70
|
+
- `intrinsic.byteSlice` and `intrinsic.copyList` coerce each window bound once. Read again by the clamping arithmetic, an object bound answers one value to the comparison and another to the slice, so the window returned covers bytes the bound excluded; a fractional bound is now refused rather than returning a window of absent values. `intrinsic.splitChar` requires a string receiver and a numeric delimiter, because a receiver converted afresh per character is scanned as one value and cut as another, and a delimiter that is not a number matches nothing and hands back the whole input as a single field.
|
|
71
|
+
- The tables that say which digests are weak and which a signature timestamp may take an imprint under are frozen together with the module object that names them. `pki.cms.attachTimestamp` and `pki.cms.sign` ask the weak-digest list by name at the call, so a copy of it with its one entry removed turned the default refusal of a SHA-1 timestamp imprint into an acceptance. That imprint carries the whole binding between a token and the signature it covers, and a collision in it substitutes one signature under a genuine token.
|
|
72
|
+
|
|
7
73
|
## v0.8.55 — 2026-10-06
|
|
8
74
|
|
|
9
75
|
Tell a caller when the path search stopped looking, rather than reporting the chain it had.
|
package/lib/acme.js
CHANGED
|
@@ -208,7 +208,7 @@ function _isObject(v) { return v && typeof v === "object" && !Array.isArray(v);
|
|
|
208
208
|
function _isString(v) { return typeof v === "string"; }
|
|
209
209
|
function _isRfc3339(v) { return rfc3339.isValid(v); }
|
|
210
210
|
var _RANDOM_DENOM = Math.pow(2, 48);
|
|
211
|
-
function _defaultRandom() { return Buffer.from(webcrypto.getRandomValues(new Uint8Array(6)))
|
|
211
|
+
function _defaultRandom() { return intrinsic.readUIntBE(Buffer.from(webcrypto.getRandomValues(new Uint8Array(6))), 0, 6) / _RANDOM_DENOM; }
|
|
212
212
|
var RENEWAL_RETRY_MIN_SECONDS = 60;
|
|
213
213
|
var RENEWAL_RETRY_MAX_SECONDS = constants.TIME.days(1) / constants.TIME.seconds(1);
|
|
214
214
|
var RENEWAL_RETRY_DEFAULT_SECONDS = constants.TIME.hours(6) / constants.TIME.seconds(1);
|
|
@@ -1690,7 +1690,7 @@ function _dedupKey(href) {
|
|
|
1690
1690
|
return out;
|
|
1691
1691
|
}
|
|
1692
1692
|
function _hasEncodedDotSegment(uri) {
|
|
1693
|
-
var segs =
|
|
1693
|
+
var segs = intrinsic.splitChar(_strSlice(uri, 0, _firstDelim(uri, 0, 0x3f, 0x23, -1)), 47);
|
|
1694
1694
|
for (var _si = 0; _si < segs.length; _si++) {
|
|
1695
1695
|
var r = _foldPct2e(segs[_si]);
|
|
1696
1696
|
if (!r.folded) continue;
|
package/lib/asn1-der.js
CHANGED
|
@@ -1164,7 +1164,12 @@ function _asBufferArray(arr, who) {
|
|
|
1164
1164
|
return out;
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
-
|
|
1167
|
+
/** @internal Frozen, not merely holding frozen tables. Freezing `read` and `build` leaves the
|
|
1168
|
+
* PROPERTIES that name them writable, so the whole table can be swapped for a copy carrying one
|
|
1169
|
+
* changed reader: assigning `asn1.read = assign({}, asn1.read, { time: fake })` moved a stated
|
|
1170
|
+
* distrust date into the future through `trust`'s time decoder. Every verb here is reached by name
|
|
1171
|
+
* from another module at the call, so the object itself has to be closed. */
|
|
1172
|
+
module.exports = intrinsic.freeze({
|
|
1168
1173
|
TAGS: TAGS,
|
|
1169
1174
|
reservedUniversalTag: reservedUniversalTag,
|
|
1170
1175
|
decode: decode,
|
|
@@ -1176,8 +1181,15 @@ module.exports = {
|
|
|
1176
1181
|
encodeIdentifier: encodeIdentifier,
|
|
1177
1182
|
decodeOidContent: decodeOidContent,
|
|
1178
1183
|
encodeOidContent: encodeOidContent,
|
|
1179
|
-
build: build,
|
|
1180
|
-
|
|
1184
|
+
build: intrinsic.freeze(build),
|
|
1185
|
+
/** @internal The reader table is frozen, because every module in this toolkit reads a leaf reader
|
|
1186
|
+
* off it BY NAME at the call: `asn1.read.time`, `asn1.read.oid`, `asn1.read.integer` and the rest
|
|
1187
|
+
* are reached through this object hundreds of times across sixty-odd modules, and each one is the
|
|
1188
|
+
* step that turns bytes into the value a check compares. A replaced `time` made a CCADB row
|
|
1189
|
+
* stating a 2020 distrust date produce an anchor with a 2099 cutoff, so a root stayed trusted long
|
|
1190
|
+
* past the date the row names. Freezing the table closes that for every consumer at once, which
|
|
1191
|
+
* capturing the readers one module at a time cannot. */
|
|
1192
|
+
read: intrinsic.freeze({
|
|
1181
1193
|
boolean: readBoolean,
|
|
1182
1194
|
integer: readInteger,
|
|
1183
1195
|
integerImplicit: readIntegerImplicit,
|
|
@@ -1193,5 +1205,5 @@ module.exports = {
|
|
|
1193
1205
|
string: readString,
|
|
1194
1206
|
numericString: readNumericString,
|
|
1195
1207
|
time: readTime,
|
|
1196
|
-
},
|
|
1197
|
-
};
|
|
1208
|
+
}),
|
|
1209
|
+
});
|
package/lib/attrcert-verify.js
CHANGED
|
@@ -12,4 +12,4 @@ function verifyAcSignature(ac, spkiBytes) {
|
|
|
12
12
|
return _seam.verify(ac.signatureAlgorithm, ac.signatureValue, spkiBytes, ac.tbsBytes);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
module.exports = { setEngine: _seam.setEngine, verifyAcSignature: verifyAcSignature };
|
|
15
|
+
module.exports = Object.freeze({ setEngine: _seam.setEngine, verifyAcSignature: verifyAcSignature });
|
package/lib/cmc-verify.js
CHANGED
package/lib/cmp-session.js
CHANGED
|
@@ -1586,8 +1586,8 @@ function session(opts) {
|
|
|
1586
1586
|
for (var ki = 0; ki < keys.length; ki++) {
|
|
1587
1587
|
var kv = request[keys[ki]];
|
|
1588
1588
|
if (kv == null) continue;
|
|
1589
|
-
names
|
|
1590
|
-
values
|
|
1589
|
+
guard.list.append(names, keys[ki]);
|
|
1590
|
+
guard.list.append(values, kv);
|
|
1591
1591
|
}
|
|
1592
1592
|
if (names.length !== 1) {
|
|
1593
1593
|
throw _err("cmp/bad-input", "info(request) must name EXACTLY ONE support operation (caCerts / rootCaCert / certReqTemplate / crlUpdate); a genm carries a sequence of one InfoTypeAndValue (RFC 9483 sec. 4.3)");
|
package/lib/cmp-verify.js
CHANGED
|
@@ -1281,7 +1281,7 @@ function openKeyPackage(container, opts) {
|
|
|
1281
1281
|
* if (v.valid && v.trusted) console.log("the response protection is authentic and the signer is trusted");
|
|
1282
1282
|
*/
|
|
1283
1283
|
|
|
1284
|
-
module.exports = {
|
|
1284
|
+
module.exports = intrinsic.freeze({
|
|
1285
1285
|
build: cmpBuild.build,
|
|
1286
1286
|
transfer: cmpBuild.transfer,
|
|
1287
1287
|
wellKnownUrl: cmpBuild.wellKnownUrl,
|
|
@@ -1289,4 +1289,4 @@ module.exports = {
|
|
|
1289
1289
|
openKeyPackage: openKeyPackage,
|
|
1290
1290
|
setEngine: setEngine,
|
|
1291
1291
|
senderBoundToCert: _senderBoundToCert,
|
|
1292
|
-
};
|
|
1292
|
+
});
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -566,7 +566,10 @@ async function _aesKwUnwrap(kek, wrapped) {
|
|
|
566
566
|
function _eccSharedInfo(wrapName, ukm, kekBytes) {
|
|
567
567
|
var kids = [b.sequence([b.oid(O(wrapName))])];
|
|
568
568
|
if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
|
|
569
|
-
|
|
569
|
+
/** @internal The SuppPubInfo key length is written with the capture, for the same reason the
|
|
570
|
+
* encrypt side does it: this is KDF input, and a replaced write derives a different KEK from the
|
|
571
|
+
* same shared secret. */
|
|
572
|
+
var supp = Buffer.alloc(4); intrinsic.writeUInt32BE(supp, kekBytes * 8, 0);
|
|
570
573
|
kids.push(b.explicit(2, b.octetString(supp)));
|
|
571
574
|
return b.sequence(kids);
|
|
572
575
|
}
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -125,7 +125,10 @@ MONT_KA[O("X448")] = { name: "X448", hkdf: "SHA-512", scheme: "dhSinglePass-stdD
|
|
|
125
125
|
function _eccSharedInfo(wrapName, ukm, kekBytes) {
|
|
126
126
|
var kids = [_algId(wrapName, "absent")];
|
|
127
127
|
if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
|
|
128
|
-
|
|
128
|
+
/** @internal The SuppPubInfo key length is written with the capture. It is KDF input, so it is part
|
|
129
|
+
* of what the key-encryption key derives from, and a replaced write derives a different KEK from
|
|
130
|
+
* the same shared secret. */
|
|
131
|
+
var supp = Buffer.alloc(4); intrinsic.writeUInt32BE(supp, kekBytes * 8, 0);
|
|
129
132
|
kids.push(b.explicit(2, b.octetString(supp)));
|
|
130
133
|
return b.sequence(kids);
|
|
131
134
|
}
|
package/lib/cms-sign.js
CHANGED
|
@@ -22,7 +22,6 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
22
22
|
* load. */
|
|
23
23
|
var _Promise = intrinsic.Promise;
|
|
24
24
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
25
|
-
var _promiseAll = intrinsic.uncurry(intrinsic.promiseAll);
|
|
26
25
|
var pkiBuild = require("./pki-build");
|
|
27
26
|
var cms = require("./schema-cms");
|
|
28
27
|
var constants = require("./constants");
|
|
@@ -66,8 +65,10 @@ var OID_SIGNED_DATA = O("signedData");
|
|
|
66
65
|
var OID_SKI = O("subjectKeyIdentifier");
|
|
67
66
|
|
|
68
67
|
|
|
68
|
+
/** @internal Continued through the capture. This digest becomes the messageDigest attribute a
|
|
69
|
+
* signature is computed over, so a replacement settling it signs over a digest of nothing. */
|
|
69
70
|
function _digest(digestName, content) {
|
|
70
|
-
return subtle.digest(DIGEST_HASH[digestName], content)
|
|
71
|
+
return intrinsic.chain(subtle.digest(DIGEST_HASH[digestName], content), function (d) { return Buffer.from(d); });
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
|
|
@@ -239,7 +240,7 @@ async function _finishSignerInfo(rc, md, content, eContentType, opts) {
|
|
|
239
240
|
function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
240
241
|
var rc = _resolveSignerContext(signer, opts);
|
|
241
242
|
var mdP = opts.signedAttributes === false ? _promiseResolve(_Promise, null) : _digest(rc.scheme.digest, content);
|
|
242
|
-
return
|
|
243
|
+
return intrinsic.chain(mdP, function (md) { return _finishSignerInfo(rc, md, content, eContentType, opts); });
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
|
|
@@ -406,8 +407,15 @@ function _sign(content, signers, opts) {
|
|
|
406
407
|
}
|
|
407
408
|
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertEncodable(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
/** @internal The aggregate consults nothing replaceable. Its static form reads `resolve` off the
|
|
411
|
+
* constructor at each call, so a replacement answers for every signer and the members assembled
|
|
412
|
+
* below are its values rather than the signer infos that were built. */
|
|
413
|
+
var builtAll = intrinsic.allOf(intrinsic.mapList(list, function (s) {
|
|
414
|
+
return _buildSignerInfo(s, contentBuf, eContentType, opts);
|
|
415
|
+
}));
|
|
416
|
+
return intrinsic.chain(builtAll, function (built) {
|
|
417
|
+
return _assembleSignedData(built, eContentType, opts, contentBuf);
|
|
418
|
+
});
|
|
411
419
|
}
|
|
412
420
|
|
|
413
421
|
function _assembleSignedData(built, eContentType, opts, contentBuf) {
|
|
@@ -518,9 +526,13 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
|
|
|
518
526
|
var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
|
|
519
527
|
var sidv = _buildSid(cert, opts.sid === "ski");
|
|
520
528
|
var soKey = so.key;
|
|
521
|
-
|
|
529
|
+
/** @internal Every step is continued through the capture. The attributes a countersignature is
|
|
530
|
+
* computed over, the signature itself and the proof that it verifies under the countersigner's own
|
|
531
|
+
* certificate each arrive this way, so a settled value in place of one emits a countersignature
|
|
532
|
+
* over bytes that were never assembled or a signature nothing checked. */
|
|
533
|
+
var attrsP = intrinsic.chain(_promiseResolve(_Promise, undefined), function () {
|
|
522
534
|
if (opts.signedAttributes === false) return null;
|
|
523
|
-
return _digest(scheme.digest, targetSigOctets)
|
|
535
|
+
return intrinsic.chain(_digest(scheme.digest, targetSigOctets), function (md) {
|
|
524
536
|
var pairs = [{ type: O("messageDigest"), values: [b.octetString(md)] }];
|
|
525
537
|
if (opts.signingTime !== false) pairs.push({ type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
526
538
|
var csApOid = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
|
|
@@ -533,14 +545,16 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
|
|
|
533
545
|
_assertAlgorithmProtectionPairs(all, scheme.digestAlgId, scheme.sigAlgId, 1);
|
|
534
546
|
return _buildSignedAttrs(all);
|
|
535
547
|
});
|
|
536
|
-
})
|
|
548
|
+
});
|
|
549
|
+
return intrinsic.chain(attrsP, function (attrs) {
|
|
537
550
|
var preimage = attrs ? attrs.setOf : targetSigOctets;
|
|
538
|
-
return signScheme.signOverTbs(scheme, soKey, preimage, _signE)
|
|
551
|
+
return intrinsic.chain(signScheme.signOverTbs(scheme, soKey, preimage, _signE), function (sig) {
|
|
539
552
|
/** @internal The countersignature is held to the same proof the signature is: it verifies
|
|
540
553
|
* against the public key the countersigner's certificate declares. */
|
|
541
|
-
|
|
554
|
+
var selfChecked = guard.async.deferred(function () {
|
|
542
555
|
return _b.assertSignatureVerifies(preimage, sig, cert.subjectPublicKeyInfo.bytes, scheme);
|
|
543
|
-
})
|
|
556
|
+
});
|
|
557
|
+
return intrinsic.chain(selfChecked, function () {
|
|
544
558
|
var fields = [b.integer(BigInt(sidv.version)), sidv.sid, scheme.digestAlgId];
|
|
545
559
|
if (attrs) fields.push(attrs.wire);
|
|
546
560
|
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
@@ -641,7 +655,7 @@ function _countersign(cmsInput, signers, opts) {
|
|
|
641
655
|
var preimage = _targetPreimage(siSet.children[t], opts);
|
|
642
656
|
list.forEach(function (cs) { jobs.push({ t: t, p: _buildCountersignature(preimage, cs, opts) }); });
|
|
643
657
|
});
|
|
644
|
-
return
|
|
658
|
+
return intrinsic.chain(intrinsic.allOf(intrinsic.mapList(jobs, function (j) { return j.p; })), function (built) {
|
|
645
659
|
var byTarget = intrinsic.create(null), certDers = [], csDigestAlgs = [];
|
|
646
660
|
intrinsic.forEach(built, function (res, i) {
|
|
647
661
|
var slot = byTarget[jobs[i].t] || (byTarget[jobs[i].t] = []);
|
package/lib/cms-verify.js
CHANGED
|
@@ -1698,4 +1698,4 @@ function isCertsOnly(input) {
|
|
|
1698
1698
|
}
|
|
1699
1699
|
|
|
1700
1700
|
// @internal
|
|
1701
|
-
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, digest: cmsDigest.digest, verifyDigest: cmsDigest.verifyDigest, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine, timestampImprint: timestampImprint, attachTimestamp: attachTimestamp };
|
|
1701
|
+
module.exports = intrinsic.freeze({ verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress, digest: cmsDigest.digest, verifyDigest: cmsDigest.verifyDigest, certsOnly: certsOnly, parseCertsOnly: parseCertsOnly, isCertsOnly: isCertsOnly, setEngine: setEngine, timestampImprint: timestampImprint, attachTimestamp: attachTimestamp });
|
package/lib/composite-kem.js
CHANGED
|
@@ -37,7 +37,6 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
37
37
|
var _Promise = intrinsic.Promise;
|
|
38
38
|
var _promiseResolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
39
39
|
var _promiseReject = intrinsic.uncurry(intrinsic.promiseReject);
|
|
40
|
-
var _promiseAllSettled = intrinsic.uncurry(intrinsic.promiseAllSettled);
|
|
41
40
|
var pkix = require("./schema-pkix");
|
|
42
41
|
var schemaPkcs8 = require("./schema-pkcs8");
|
|
43
42
|
var subtle = webcrypto.webcrypto.subtle;
|
|
@@ -92,17 +91,25 @@ function _mlkemSeedPkcs8(d, seed) {
|
|
|
92
91
|
function _mlkemSpki(d, ek) {
|
|
93
92
|
return _b.sequence([_b.sequence([_b.oid(oid.byName(d.mlkemOidName))]), _b.bitString(ek, 0)]);
|
|
94
93
|
}
|
|
94
|
+
/** @internal Every step of a component's work is continued through the capture. A continuation is
|
|
95
|
+
* read off the promise at the call, and these carry the ML-KEM half of the shared secret, so a
|
|
96
|
+
* replacement settling one hands back bytes the key never produced and the combiner mixes those
|
|
97
|
+
* into the secret both sides are meant to derive. */
|
|
95
98
|
function _mlkemEncaps(d, mlkemEK) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
var imported = subtle.importKey("spki", _mlkemSpki(d, mlkemEK), { name: d.mlkemWc }, false, ["encapsulateBits"]);
|
|
100
|
+
var encapsulated = intrinsic.chain(imported, function (k) { return subtle.encapsulateBits({ name: d.mlkemWc }, k); });
|
|
101
|
+
var shaped = intrinsic.chain(encapsulated,
|
|
102
|
+
function (r) { return { ss: Buffer.from(r.sharedKey), ct: Buffer.from(r.ciphertext) }; });
|
|
103
|
+
return intrinsic.chain(shaped, undefined,
|
|
104
|
+
function (e) { throw _err("kem/bad-key", "the ML-KEM component public key is invalid", e); });
|
|
100
105
|
}
|
|
101
106
|
function _mlkemDecaps(d, seed, mlkemCT) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
var imported = subtle.importKey("pkcs8", _mlkemSeedPkcs8(d, seed), { name: d.mlkemWc }, false, ["decapsulateBits"]);
|
|
108
|
+
var decapsulated = intrinsic.chain(imported,
|
|
109
|
+
function (k) { return subtle.decapsulateBits({ name: d.mlkemWc }, k, mlkemCT); });
|
|
110
|
+
var shaped = intrinsic.chain(decapsulated, function (ss) { return Buffer.from(ss); });
|
|
111
|
+
return intrinsic.chain(shaped, undefined,
|
|
112
|
+
function (e) { throw _err("kem/bad-key", "the ML-KEM component private key is invalid", e); });
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function _ecPubFromPoint(d, point) {
|
|
@@ -207,15 +214,26 @@ function _tradDecaps(d, tradSK, tradCT) {
|
|
|
207
214
|
|
|
208
215
|
function _combine(d, mlkemSS, tradSS, tradCT, tradPK) {
|
|
209
216
|
var preimage = Buffer.concat([mlkemSS, tradSS, tradCT, tradPK, d.label]);
|
|
210
|
-
|
|
217
|
+
/** @internal The combiner's own step is captured, and the zeroize runs on either outcome: this
|
|
218
|
+
* digest IS the shared secret, so a settled value in place of it is a secret neither side derived,
|
|
219
|
+
* and the wipe is what keeps the component halves from outliving the call. */
|
|
220
|
+
var combined = intrinsic.chain(subtle.digest("SHA3-256", preimage), function (h) {
|
|
211
221
|
return Buffer.from(h);
|
|
212
|
-
})
|
|
222
|
+
});
|
|
223
|
+
/** @internal The cleanup is attached on BOTH outcomes rather than through `finally`, which calls
|
|
224
|
+
* the receiver's own `then` and so is the live method again: a replacement there settles the chain
|
|
225
|
+
* with bytes of its own AND skips this callback, which exposes those bytes as the shared secret
|
|
226
|
+
* while the component halves stay in memory. */
|
|
227
|
+
return guard.async.cleanup(combined, function () {
|
|
213
228
|
guard.secret.zeroizeAll([preimage, mlkemSS, tradSS], KemError, "kem/bad-input", "a KEM combiner secret");
|
|
214
229
|
});
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
async function _settleOrWipe(pA, pB, secA, secB) {
|
|
218
|
-
|
|
233
|
+
/** @internal The outcomes come from the aggregate that consults nothing replaceable. The static
|
|
234
|
+
* form reads `resolve` off the constructor at each call, so a replacement there answers for both
|
|
235
|
+
* members and the values read out of these records become the two halves of the shared secret. */
|
|
236
|
+
var s = await intrinsic.settledOf([pA, pB]);
|
|
219
237
|
if (s[0].status === "rejected" || s[1].status === "rejected") {
|
|
220
238
|
if (s[0].status === "fulfilled") guard.secret.zeroize(secA(s[0].value), KemError, "kem/bad-input", "an ML-KEM shared secret");
|
|
221
239
|
if (s[1].status === "fulfilled") guard.secret.zeroize(secB(s[1].value), KemError, "kem/bad-input", "a traditional shared secret");
|
package/lib/composite-sig.js
CHANGED
|
@@ -24,7 +24,6 @@ var _promiseResolve = _intrinsic.uncurry(_intrinsic.promiseResolve);
|
|
|
24
24
|
/** @internal The aggregator is the same defect one step out: the composite verdict is `ok` only when
|
|
25
25
|
* BOTH component verifications resolve true, so a replaced `all` that resolves `[true, true]` without
|
|
26
26
|
* awaiting either half reports a forged signature as verified. */
|
|
27
|
-
var _promiseAll = _intrinsic.uncurry(_intrinsic.promiseAll);
|
|
28
27
|
var _b = asn1.build;
|
|
29
28
|
|
|
30
29
|
function _codeOf(e, fallback) {
|
|
@@ -85,7 +84,11 @@ function compositeMprime(d, phBuf, ctx) {
|
|
|
85
84
|
function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
86
85
|
if (trad.ec) {
|
|
87
86
|
if (tradPK.length < 1 || tradPK[0] !== 0x04) return _promiseResolve(_Promise, false);
|
|
88
|
-
|
|
87
|
+
/** @internal Every continuation on this path comes from the capture. A continuation is read off
|
|
88
|
+
* the promise at the call, and these deliver the two component verdicts a composite signature is
|
|
89
|
+
* the conjunction of: a replacement settling either one reports a component as verified without
|
|
90
|
+
* it having been checked, and the path validator that calls this reads the conjunction. */
|
|
91
|
+
return _intrinsic.chain(subtle.importKey("spki", _ecSpki(trad.ec, tradPK), { name: "ECDSA" }, false, ["verify"]), function (k) {
|
|
89
92
|
var p1363 = validator.sig.ecdsaDerToP1363(tradSig, trad.ec, E, badSig);
|
|
90
93
|
return _subtleVerify(subtle, { name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
|
|
91
94
|
});
|
|
@@ -94,8 +97,8 @@ function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
|
94
97
|
if (!edwardsPoint.validate(tradPK, trad.eddsa === "Ed25519" ? 6 : 7)) {
|
|
95
98
|
throw new E(badSig, "the composite EdDSA component public key is not a valid, full-order Edwards point");
|
|
96
99
|
}
|
|
97
|
-
return subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"])
|
|
98
|
-
|
|
100
|
+
return _intrinsic.chain(subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"]),
|
|
101
|
+
function (k) { return _subtleVerify(subtle, { name: trad.eddsa }, k, tradSig, mprime); });
|
|
99
102
|
}
|
|
100
103
|
if (trad.rsaPss || trad.rsaPkcs1) {
|
|
101
104
|
var bits;
|
|
@@ -103,11 +106,11 @@ function _verifyTradComponent(trad, tradPK, tradSig, mprime, E, badSig) {
|
|
|
103
106
|
catch (_e) { return _promiseResolve(_Promise, false); }
|
|
104
107
|
if (bits !== trad.rsaBits) return _promiseResolve(_Promise, false);
|
|
105
108
|
if (trad.rsaPss) {
|
|
106
|
-
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"])
|
|
107
|
-
|
|
109
|
+
return _intrinsic.chain(subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"]),
|
|
110
|
+
function (k) { return _subtleVerify(subtle, { name: "RSA-PSS", saltLength: trad.salt }, k, tradSig, mprime); });
|
|
108
111
|
}
|
|
109
|
-
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
|
|
110
|
-
|
|
112
|
+
return _intrinsic.chain(subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"]),
|
|
113
|
+
function (k) { return _subtleVerify(subtle, { name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
|
|
111
114
|
}
|
|
112
115
|
return _promiseResolve(_Promise, false);
|
|
113
116
|
}
|
|
@@ -132,13 +135,21 @@ function compositeVerify(spkiBytes, sigBytes, message, d, E, unsupported, badSig
|
|
|
132
135
|
}
|
|
133
136
|
var mldsaPK = rawKey.subarray(0, d.mldsaPk), tradPK = rawKey.subarray(d.mldsaPk);
|
|
134
137
|
var mldsaSig = sigBytes.subarray(0, d.mldsaSig), tradSig = sigBytes.subarray(d.mldsaSig);
|
|
135
|
-
|
|
138
|
+
var combined = _intrinsic.chain(subtle.digest({ name: d.ph }, message), function (phBuf) {
|
|
136
139
|
var mprime = compositeMprime(d, phBuf);
|
|
137
|
-
var mldsaP = subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"])
|
|
138
|
-
|
|
140
|
+
var mldsaP = _intrinsic.chain(subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"]),
|
|
141
|
+
function (mk) { return _subtleVerify(subtle, { name: d.mldsa, context: d.label }, mk, mldsaSig, mprime); });
|
|
139
142
|
var tradP = _verifyTradComponent(d.trad, tradPK, tradSig, mprime, E, badSig);
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
/** @internal The conjunction is read from an aggregate that consults nothing replaceable: both
|
|
144
|
+
* components must answer `true`, so a settled value in place of either is a composite signature
|
|
145
|
+
* reported as verified on one half or on neither. A captured `Promise.all` is not enough, since
|
|
146
|
+
* it reads `resolve` off the constructor at each call and a replacement there answers for every
|
|
147
|
+
* member without any component being awaited. */
|
|
148
|
+
return _intrinsic.chain(_intrinsic.allOf([mldsaP, tradP]),
|
|
149
|
+
function (r) { return _verdictOf({ ok: r[0] === true && r[1] === true }); });
|
|
150
|
+
});
|
|
151
|
+
return _intrinsic.chain(combined, undefined,
|
|
152
|
+
function (e) { return _verdictOf({ ok: false, code: _codeOf(e, badSig), error: e }); });
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
function resolveCompositeDescriptor(sigAlg, E, unsupported) {
|
|
@@ -151,27 +162,35 @@ function resolveCompositeDescriptor(sigAlg, E, unsupported) {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
function compositeSign(d, keys, message, ctx) {
|
|
154
|
-
|
|
165
|
+
/** @internal The signing half is continued through the captures too. Its output is the signature a
|
|
166
|
+
* verifier is then held to, so a replacement settling a component hands back bytes neither key
|
|
167
|
+
* produced and the signature does not verify anywhere. */
|
|
168
|
+
return _intrinsic.chain(subtle.digest({ name: d.ph }, message), function (phBuf) {
|
|
155
169
|
var mprime = compositeMprime(d, phBuf, ctx);
|
|
156
|
-
var mldsaP = subtle.importKey("pkcs8", keys.mldsa, { name: d.mldsa }, false, ["sign"])
|
|
157
|
-
|
|
170
|
+
var mldsaP = _intrinsic.chain(subtle.importKey("pkcs8", keys.mldsa, { name: d.mldsa }, false, ["sign"]),
|
|
171
|
+
function (k) { return subtle.sign({ name: d.mldsa, context: d.label }, k, mprime); });
|
|
158
172
|
var tradP = _signTradComponent(d.trad, keys.trad, mprime);
|
|
159
|
-
return
|
|
173
|
+
return _intrinsic.chain(_intrinsic.allOf([mldsaP, tradP]),
|
|
174
|
+
function (s) { return Buffer.concat([Buffer.from(s[0]), Buffer.from(s[1])]); });
|
|
160
175
|
});
|
|
161
176
|
}
|
|
162
177
|
function _signTradComponent(trad, pkcs8, mprime) {
|
|
163
178
|
if (trad.ec) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
var ecSigned = _intrinsic.chain(subtle.importKey("pkcs8", pkcs8, { name: "ECDSA", namedCurve: trad.ec }, false, ["sign"]),
|
|
180
|
+
function (k) { return subtle.sign({ name: "ECDSA", hash: trad.hash }, k, mprime); });
|
|
181
|
+
return _intrinsic.chain(ecSigned,
|
|
182
|
+
function (raw) { var r = Buffer.from(raw); return validator.sig.rawToEcdsaDer(r, r.length / 2); });
|
|
167
183
|
}
|
|
168
184
|
if (trad.eddsa) {
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
var edSigned = _intrinsic.chain(subtle.importKey("pkcs8", pkcs8, { name: trad.eddsa }, false, ["sign"]),
|
|
186
|
+
function (k) { return subtle.sign({ name: trad.eddsa }, k, mprime); });
|
|
187
|
+
return _intrinsic.chain(edSigned, function (s) { return Buffer.from(s); });
|
|
171
188
|
}
|
|
172
189
|
var imp = trad.rsaPss ? { name: "RSA-PSS", hash: trad.hash } : { name: "RSASSA-PKCS1-v1_5", hash: trad.hash };
|
|
173
190
|
var alg = trad.rsaPss ? { name: "RSA-PSS", saltLength: trad.salt } : { name: "RSASSA-PKCS1-v1_5" };
|
|
174
|
-
|
|
191
|
+
var rsaSigned = _intrinsic.chain(subtle.importKey("pkcs8", pkcs8, imp, false, ["sign"]),
|
|
192
|
+
function (k) { return subtle.sign(alg, k, mprime); });
|
|
193
|
+
return _intrinsic.chain(rsaSigned, function (s) { return Buffer.from(s); });
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
module.exports = {
|
package/lib/constants.js
CHANGED
|
@@ -319,10 +319,15 @@ Object.freeze(NAMES);
|
|
|
319
319
|
|
|
320
320
|
var VERSION = require("../package.json").version;
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
/** @internal Frozen, because these tables are read BY NAME at call time to decide an admission.
|
|
323
|
+
* `cms.sign` asks `NAMES.WEAK_DIGESTS` whether a digest is weak and `NAMES.TIMESTAMP_IMPRINT_DIGESTS`
|
|
324
|
+
* whether one is permitted at all, so a caller able to assign `constants.NAMES` a copy with an empty
|
|
325
|
+
* weak list turns the refusal off and a SHA-1 timestamp imprint is accepted. Freezing the tables
|
|
326
|
+
* alone left the PROPERTIES naming them writable, which is the same hole one level out. */
|
|
327
|
+
module.exports = Object.freeze({
|
|
323
328
|
TIME: TIME,
|
|
324
329
|
BYTES: BYTES,
|
|
325
330
|
LIMITS: LIMITS,
|
|
326
331
|
NAMES: NAMES,
|
|
327
332
|
version: VERSION,
|
|
328
|
-
};
|
|
333
|
+
});
|
package/lib/crl-verify.js
CHANGED
|
@@ -12,4 +12,4 @@ function verifyCrlSignature(crl, spkiBytes) {
|
|
|
12
12
|
return _seam.verify(crl.signatureAlgorithm, crl.signatureValue, spkiBytes, crl.tbsBytes);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
module.exports = { setEngine: _seam.setEngine, verifyCrlSignature: verifyCrlSignature };
|
|
15
|
+
module.exports = Object.freeze({ setEngine: _seam.setEngine, verifyCrlSignature: verifyCrlSignature });
|