@blamejs/pki 0.5.15 → 0.5.17
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 +42 -0
- package/lib/acme.js +1 -0
- package/lib/asn1-der.js +1 -0
- package/lib/attrcert-sign.js +1 -0
- package/lib/cbor-det.js +1 -0
- package/lib/cmc-verify.js +1 -0
- package/lib/cmp-build.js +1 -0
- package/lib/cmp-verify.js +425 -82
- package/lib/cms-decrypt.js +58 -29
- package/lib/cms-encrypt.js +45 -24
- package/lib/cms-sign.js +3 -3
- package/lib/cms-verify.js +5 -1
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +1 -0
- package/lib/crmf-sign.js +1 -0
- package/lib/csr-sign.js +1 -0
- package/lib/ct.js +1 -0
- package/lib/est.js +1 -0
- package/lib/framework-error.js +1 -0
- package/lib/guard-all.js +2 -0
- package/lib/guard-async.js +14 -2
- package/lib/guard-bytes.js +183 -93
- package/lib/guard-compress.js +32 -12
- package/lib/guard-der.js +30 -7
- package/lib/guard-encoding.js +28 -12
- package/lib/guard-header.js +18 -7
- package/lib/guard-identifier.js +146 -79
- package/lib/guard-intrinsic.js +318 -0
- package/lib/guard-json.js +39 -12
- package/lib/guard-limits.js +15 -6
- package/lib/guard-name.js +72 -23
- package/lib/guard-parsed.js +71 -34
- package/lib/guard-range.js +17 -4
- package/lib/guard-secret.js +60 -2
- package/lib/guard-text.js +35 -6
- package/lib/guard-time.js +13 -4
- package/lib/hpke.js +6 -2
- package/lib/http-transport.js +1 -0
- package/lib/inspect.js +1 -0
- package/lib/jose.js +1 -0
- package/lib/key.js +2 -1
- package/lib/lint.js +1 -0
- package/lib/merkle.js +1 -0
- package/lib/ocsp.js +1 -0
- package/lib/oid.js +1 -0
- package/lib/path-validate.js +1 -0
- package/lib/pbes2.js +16 -8
- package/lib/pkcs12-build.js +2 -1
- package/lib/schema-all.js +1 -0
- package/lib/schema-c509.js +1 -0
- package/lib/schema-csrattrs.js +1 -0
- package/lib/schema-engine.js +1 -0
- package/lib/schema-pkcs8.js +1 -0
- package/lib/schema-smime.js +1 -0
- package/lib/schema-tsp.js +1 -0
- package/lib/schema-x509.js +1 -0
- package/lib/shbs.js +1 -0
- package/lib/sigstore.js +1 -0
- package/lib/smime.js +1 -0
- package/lib/tls-cert-compress.js +1 -0
- package/lib/trust.js +1 -0
- package/lib/tsp-sign.js +1 -0
- package/lib/webauthn.js +1 -0
- package/lib/webcrypto.js +11 -8
- package/lib/x509-sign.js +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.5.17 — 2026-08-20
|
|
8
|
+
|
|
9
|
+
A failed integrity check destroys the plaintext it recovered, AuthEnvelopedData validates the attributes it is asked to authenticate, and every refusal names the rule it is actually applying.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The refusals for a content cipher in the wrong container now cite the rule they apply. RFC 5083 sec. 2 defines `AuthEnvelopedData` in terms of a content-authenticated-encryption algorithm; it never names GCM, and RFC 5084 sec. 3.1 defines AES-CCM for that content type. Saying the standard required GCM sent an operator to argue with a specification that says the opposite. The check now draws the line the RFC draws -- authenticated versus not -- and an AES-CCM message is declined separately, by name, as an algorithm this toolkit does not implement rather than as an unrecognized OID. Error codes are unchanged.
|
|
14
|
+
- Documentation pages at pkijs.com carry the expanded name of the format they describe in the title and heading, so a page about CMS says Cryptographic Message Syntax rather than three letters that mean a content management system elsewhere. The sitemap now reports each page's real last-changed date, taken from the commit that last touched the source it is generated from, and omits the date where it is unknown instead of reporting the build time. The home page gained runnable examples for the six things people most often arrive wanting to do.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- A failed AEAD integrity check now destroys the plaintext it recovered. `update` returns the complete recovered plaintext and `final` is what decides whether the message was authentic at all, so on a forged `AuthEnvelopedData` the plaintext existed in full before anything had judged it, and the buffer was then dropped unreferenced and unzeroed. RFC 5083 sec. 1 requires it to be destroyed. The success path leaked too, less visibly: joining the two halves copies them, leaving the first buffer as a second complete copy of the plaintext that the caller never receives and nothing else would ever clear. Both exits are now cleared, at every cipher the toolkit runs -- CMS content decryption and the RFC 3211 password-recipient unwrap, PBES2, HPKE `open`, PKCS#12 safe decryption, and the AES-GCM, AES-CBC, AES-CTR and AES-KW paths of the WebCrypto engine.
|
|
19
|
+
- `pki.cms.encrypt` validates the authenticated attributes a caller supplies for an `AuthEnvelopedData` before it MACs them. Each must be a well-formed `Attribute SEQUENCE { type, non-empty SET OF value }` with no repeated type (RFC 5652 sec. 5.3). `AuthenticatedData` had enforced this all along; `AuthEnvelopedData` accepted whatever it was handed and emitted it, so a malformed attribute reached the wire with the MAC already computed over it and the operator learned of it from a peer's parser rather than from the builder.
|
|
20
|
+
- `pki.cms.encrypt` refuses a `message-digest` attribute in an `AuthEnvelopedData`'s `authAttrs`. Its value is the unencrypted one-way hash of the plaintext, so disclosing it alongside the ciphertext enables content tracking and confirms a guessed plaintext against a message that was encrypted to prevent exactly that (RFC 5083 sec. 2.1 and sec. 5). Decryption still accepts one, since another implementation may legitimately emit it; the toolkit will not produce one. `AuthenticatedData` is unaffected -- it builds and MACs its own `message-digest` by design (RFC 5652 sec. 9.2).
|
|
21
|
+
|
|
22
|
+
## v0.5.16 — 2026-08-19
|
|
23
|
+
|
|
24
|
+
`pki.cmp.verify` reads every option at the call, and every guard takes what it needs from the runtime when it loads, so code a caller runs afterwards cannot change what a verification decides.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `pki.cmp.verify` reduces every option to a value the caller no longer reaches, before verification begins. `transactionID` and `expectRecipNonce` are copied, so overwriting the buffer that was passed in changes nothing; `trustAnchors` and `intermediates` have their array copied, so appending an anchor mid-call cannot widen the set the chain is built against; and a `time` that is a real `Date` is re-made at the same instant, so calling `setTime` on it cannot move the point the signer's certificate is validated at. A `time` that is not a `Date` passes through untouched, so the input error it already earns is the one it still gets.
|
|
29
|
+
- `pki.cmp.verify` now takes its options through the same door every other verb uses, which it had been skipping in favor of two checks of its own. The door refuses an options object whose fields are accessors, and one that changes which fields it carries while they are read. An accessor is caller code running inside the call before the verb has done anything, and from there it can rewrite the very predicates and constructors the verb is about to use, so refusing it closes that whole class rather than one member at a time. What is asked of a caller is only that the options themselves be values.
|
|
30
|
+
- A `sharedSecret` given as a string is converted to bytes with a `Buffer.from` captured when the module loads. `Buffer.from` is a writable property of the `Buffer` global and the secret is the first option read, so the accessor supplying it could install a replacement, receive the plaintext as that replacement's argument, and have whatever it returned become the PBMAC1 key -- a message authenticated under a secret the caller never held would then verify. The captured reference is fixed before any caller code has run.
|
|
31
|
+
- A `sharedSecret` given as a string is converted to bytes once, at the door, and that copy is cleared with the rest. The conversion is what produces a wipeable plaintext copy, and it used to happen deeper inside the MAC path where nothing owned it, so the toolkit left a copy of the secret behind on every call. The string itself is immutable and cannot be cleared, which is still the reason to hand over bytes when the secret must not outlive the call.
|
|
32
|
+
- Whether a byte option is copied, and whether that copy is later wiped, is decided by asking the value's internal slot rather than its prototype. `Buffer.isBuffer` is a writable property and `instanceof Uint8Array` consults `Uint8Array[Symbol.hasInstance]`, and both are reachable from an option accessor that has already run by the time the test is evaluated. A caller who made those answer `false` for their own buffer got the copy skipped, which left their live buffer where the copy belongs -- and the wipe then cleared memory the toolkit does not own.
|
|
33
|
+
- The copies go through `guard.bytes.snapshot`, this toolkit's door for caller-supplied bytes, so a source the door refuses stays refused. A view backed by a `SharedArrayBuffer` is the case that matters: another thread can rewrite it at any moment, and copying it with `Buffer.from` would have turned it into an ordinary `Buffer` that every later check accepts without the door's rules ever having run.
|
|
34
|
+
- Each option is read from the caller's object exactly once and copied in the same step, before the next option is read. A property can be an accessor: asked twice it may answer differently, so the value that passed the check would not be the value that was used; and an accessor invoked for a later option can reach back and rewrite the buffer an earlier one handed over, so reading the whole bag before copying any of it would copy whatever the last accessor left behind.
|
|
35
|
+
- `opts.trustAnchors` and `opts.intermediates` must be a plain dense array (or a single certificate). A list with holes, or one whose elements are reachable only through its prototype, is refused with `cmp/bad-input` naming the problem. Reproducing what an ordinary array operation would consume means handling holes, inherited and non-enumerable elements, and the uint32 index boundary, and those pull against each other: reading every position covers inheritance but scans a sparse list's whole length, while enumerating present keys is bounded but silently drops an inherited anchor -- and dropping an anchor turns a trusted verification into an untrusted one. Refusing is the answer this verb gives instead of choosing quietly. No ceiling is imposed on how many anchors a dense list may hold.
|
|
36
|
+
- Two things stay caller-owned by design, and the release is explicit about them. A parsed certificate passed as an anchor or an intermediate is held by reference, because `pki.schema.x509.parse` results carry their provenance against the object's identity and a copy would stop being recognized as parser output; editing one mid-call changes nothing, since path validation re-derives a parsed certificate from the bytes it recorded. And a `revocationChecker` is the caller's own callback, so what it answers is theirs to decide whenever it is called.
|
|
37
|
+
- `pki.cmp.verify` refuses a Proxy-wrapped certificate list before it reads anything from it. A Proxy over an array reports itself as an array while answering from traps that need not agree with one another, so a count taken through them describes nothing that is still true by the time the anchors are used. Refusing a Proxy was already this toolkit's answer for a value it cannot ask honestly; that refusal now runs ahead of the length read and the descriptor walk, which would otherwise be the first thing to hand control back to the caller.
|
|
38
|
+
- Every guard that performs its work with a method captured at load now invokes that method without reading any property of it. Capturing freezes which function runs, so replacing the prototype method afterwards changes nothing; invoking it as `captured.call(receiver, ...)` then read `call` off the captured function, and that function is the one still sitting on the prototype. Assigning an own `call` to it shadows `Function.prototype.call` and hands the guard back to the caller with every check around it still passing: the secret wipe clears nothing, two unrelated distinguished names compare equal, a decode returns a string the caller chose, the instant a certificate is validated at becomes whatever the caller says, and a byte view reports a length and a backing store it does not have.
|
|
39
|
+
- Every operation the guard family performs while it is deciding something is taken from the runtime when the module loads, rather than at the moment it runs. That covers the reflective operations a guard inspects a value with, the predicates that answer what a value IS, the `Buffer` and `Promise` constructors, the numeric and array tests bounds are built on, and the string operations names and encodings are compared with. It also covers the array operations a guard walks its own lists with, where a `forEach` replaced by a no-op made a scan over real keys report nothing and every rule keyed on that scan then passed without examining anything. The whole family is driven with twenty-three of those globals replaced at once and each guard still reaches the answer it would have reached untouched. A check over the source refuses any guard that reads one of these at call time, so the rule holds for a guard written later without anyone remembering it.
|
|
40
|
+
- Canonicality takes both halves of its round trip at load, and the size cap measures a Buffer through the byte guard's captured length getter. Canonical form is decided by decoding the text and re-encoding the result, so either half settles the answer: a `toString` returning the input made `AB` compare equal to its own re-encoding and pass as canonical base64url, where its canonical form is `AA`. On a typed array `length` is a configurable accessor on the prototype, and the cap comparison read it, so one returning 0 admitted a buffer of any size and then decoded it in full.
|
|
41
|
+
- The `Buffer` constructor that turns a backing store into something the toolkit can read or clear is taken at load. It sits behind the re-view every byte boundary performs, so one returning a buffer over different memory handed each later step a decoy: a wipe zeroed the decoy and returned normally while the plaintext it was aimed at stayed readable, with every check in between satisfied. The same reference now backs the canonical-encoding round trip, where a replacement decides what that comparison sees, and the provenance copy, where it decides whether recorded bytes can still change afterwards.
|
|
42
|
+
- The index scan behind the accessor refusal asks the array question through a reference taken at load. A replacement answering `false` made the scan report no indices, so the refusal had nothing to refuse and an accessor under index 0 was reached by the copy that follows it -- the caller's own `Error` escaping a boundary whose contract is a typed refusal, with the accessor free to run before anything it returned was snapshotted. The same reference now backs the array questions in the byte-source, parsed-structure and identifier guards.
|
|
43
|
+
- The two predicates that decide whether a `pki.cmp.verify` option gets copied are taken at load rather than at the call. Both are ordinary writable properties, so reading either at call time handed that decision to the caller: an `isUint8Array` answering `false` left a live caller buffer where the copy belongs and left that buffer out of the list the call wipes, and an `Array.isArray` answering `false` routed a trust list past the list door and back out by reference, ready to be emptied or appended to while verification was suspended.
|
|
44
|
+
- The slot predicates a guard asks about a value are snapshotted at load, and so is the check that a `Date` holds a real instant. `util.types` is an ordinary object on an ordinary module export, so `util.types.isDate = () => true` reached every guard that asked it at call time; these predicates are how the toolkit decides what a value IS, in preference to a prototype test a caller can satisfy, so a caller answering them instead had replaced the one question a lookalike cannot lie about. The global `isNaN` was read the same way, and one returning `false` made `new Date(NaN)` pass the validity check that stands in front of every window comparison.
|
|
45
|
+
- The wipe that clears a secret the toolkit allocated writes its zeros through a fill captured when the module loads. `Buffer.prototype.fill` is a writable property, and it was the entire operation the wipe performed: replaced with a function that writes nothing, every wipe in the toolkit became a silent no-op. Nothing observable changed when that happened, since the wipe was still called and still returned, while a shared secret or a content-encryption key stayed readable in the heap after the call whose only purpose was to clear it.
|
|
46
|
+
- Distinguished-name comparison takes its string operations from the prototypes at load. Every value it compares is a plain string, so `value.toLowerCase()` reached whatever `String.prototype` carried at the moment it ran, and that is an ordinary writable property. One returning a constant made every DN compare equal to every other, which is the whole of what this comparison decides: chain building would accept an unrelated issuer, a revocation entry would match a certificate it was never written for, and a name constraint would stop excluding anything. The DN escaping used in reports and the control-byte reject that defends CVE-2009-2408 read characters and character codes the same way, and the same change covers them. `Array.isArray`, which decides whether a comparison can be performed at all, is captured alongside them; replacing that one points the other way, turning every name comparison the toolkit makes into a refusal.
|
|
47
|
+
- Decoding untrusted bytes to a string does its work through references captured at load, on both of its input arms and in both of its contracts. `Buffer.prototype.toString` produced the guard's entire output for a Buffer and `TextDecoder.prototype.decode` did the same under strict UTF-8, so replacing either handed the boundaries that compose it -- PEM headers, JOSE segments, DN attribute values -- a string the caller chose, with the size cap, the detached-view refusal and the strict-UTF-8 rule having all run and all passed. `Buffer.byteLength` measures a string against that cap, and one returning a small number admitted a string of any size, which is the allocation this guard exists to bound. `Number.isInteger` decides whether the cap is usable and `Buffer.isBuffer` decides which arm an input takes; both are captured for the same reason.
|
|
48
|
+
|
|
7
49
|
## v0.5.15 — 2026-08-19
|
|
8
50
|
|
|
9
51
|
`pki.attrcert.verify` checks an attribute certificate against the RFC 5755 validation rules, so a consumer reading its privilege attributes is reading ones an issuer actually granted.
|
package/lib/acme.js
CHANGED
package/lib/asn1-der.js
CHANGED
package/lib/attrcert-sign.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.attrcert
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Attribute certificates
|
|
9
|
+
* @fullname X.509 attribute certificates (RFC 5755)
|
|
9
10
|
* @intro The RFC 5755 attribute-certificate producing side. `pki.attrcert.sign` builds an
|
|
10
11
|
* `AttributeCertificateInfo` binding a Holder to a set of privilege attributes (role, clearance,
|
|
11
12
|
* group, ...) over a validity window, signs it with an Attribute Authority's private key, and emits an
|
package/lib/cbor-det.js
CHANGED
package/lib/cmc-verify.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module pki.cmc
|
|
6
6
|
* @nav Enrollment
|
|
7
7
|
* @title CMC
|
|
8
|
+
* @fullname CMC (Certificate Management over CMS, RFC 5272)
|
|
8
9
|
* @intro Interpret an RFC 5272 Full PKI Response. `verify(response, sent)` takes the response
|
|
9
10
|
* a CA returned and the state the client retained from its request, binds the two together
|
|
10
11
|
* (transaction identifier, the Sender/Recipient Nonce echo, the Data Return echo), reads the
|
package/lib/cmp-build.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module pki.cmp
|
|
7
7
|
* @nav Signing
|
|
8
8
|
* @title Certificate management protocol messages
|
|
9
|
+
* @fullname CMP (Certificate Management Protocol, RFC 4210)
|
|
9
10
|
* @intro The RFC 9810 Certificate Management Protocol producing side. `pki.cmp.build` assembles a
|
|
10
11
|
* `PKIMessage` -- a `PKIHeader` (sender / recipient / transaction metadata), a `PKIBody` carrying one
|
|
11
12
|
* request or transaction (an `ir` / `cr` / `kur` certificate request via `pki.crmf.build`, a `p10cr`
|