@blamejs/pki 0.9.7 → 0.9.8

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,22 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.9.8 — 2026-10-27
8
+
9
+ Require Node 24.22.1, which fixes the Brotli decoder memory corruption in CVE-2026-111497.
10
+
11
+ ### Changed
12
+
13
+ - `engines.node` is `>=24.22.1`. On a lower version npm reports the mismatch as EBADENGINE, and refuses the install where engine-strict is set. Node 24.22.1 ships OpenSSL 3.5.9.
14
+
15
+ ### Fixed
16
+
17
+ - A zstd-compressed input to `pki.tls.decompressCertificate`, or to any toolkit decompression, must be exactly one Zstandard frame, and anything else is refused with `tls/decompress-failed`. The zstd decoder in Node 24.22.1 and later decodes every concatenated frame and skips skippable frames (RFC 8878 sec. 3.1.2). An empty frame or a skippable frame of arbitrary bytes before or after the real one added no output and passed the declared-length check, so one certificate chain had unboundedly many compressed encodings. A skippable frame on its own decoded to empty output.
18
+
19
+ ### Security
20
+
21
+ - The supported Node floor moves to 24.22.1. Node 24.22.0 and earlier bundle a Brotli decoder in which malformed input can corrupt process memory, which can crash the process and may allow code execution (CVE-2026-111497). `pki.tls` passes a peer's Brotli-compressed certificate message (RFC 8879, algorithm 2) to that decoder, and the size cap the toolkit applies to the output does not protect against a fault inside the decoder. The fix is in the Node runtime, and updating an npm package does not replace it. Node 26 releases before 26.12.1 carry the same decoder and still satisfy `>=24.22.1`, so on Node 26 run 26.12.1 or later.
22
+
7
23
  ## v0.9.7 — 2026-10-21
8
24
 
9
25
  Refuse a key whose private key is public wherever it appears, with no option to turn the refusal off.
package/README.md CHANGED
@@ -64,7 +64,7 @@ Crypto API with its limits on streaming, opaque keys, and algorithm reach.
64
64
  npm i @blamejs/pki
65
65
  ```
66
66
 
67
- Requires Node.js 24.21+ and runs on the shipped runtime, with no build step and
67
+ Requires Node.js 24.22.1+ and runs on the shipped runtime, with no build step and
68
68
  no transpilation.
69
69
 
70
70
  ```js
@@ -27,6 +27,37 @@ var _PROBE_COMPRESS = _intrinsic.assign(_intrinsic.create(null), {
27
27
  zstd: zlib.zstdCompressSync,
28
28
  });
29
29
 
30
+ /** @internal The byte length of the one Zstandard frame at the start of `s` (RFC 8878 sec. 3.1.1),
31
+ * or -1 when `s` does not open with a complete one. A decoder that follows RFC 8878 decodes every
32
+ * concatenated frame and skips skippable frames, so the consumed-length check cannot see an empty
33
+ * frame or a skippable frame next to the real one; this walk is what holds zstd to one frame. Only
34
+ * the frame magic 0xFD2FB528 starts a frame here, so a skippable frame anywhere is refused. */
35
+ var _ZSTD_DID_BYTES = [0, 1, 2, 4];
36
+ var _ZSTD_FCS_BYTES = [0, 2, 4, 8];
37
+ function _zstdFrameLength(s) {
38
+ var n = _sizeOf(s);
39
+ if (n < 6 || s[0] !== 0x28 || s[1] !== 0xb5 || s[2] !== 0x2f || s[3] !== 0xfd) return -1;
40
+ var fhd = s[4];
41
+ if ((fhd & 0x08) !== 0) return -1;
42
+ var single = (fhd & 0x20) !== 0;
43
+ var fcs = _ZSTD_FCS_BYTES[fhd >>> 6];
44
+ if (fcs === 0 && single) fcs = 1;
45
+ var p = 5 + (single ? 0 : 1) + _ZSTD_DID_BYTES[fhd & 0x03] + fcs;
46
+ for (;;) {
47
+ if (p + 3 > n) return -1;
48
+ var h = s[p] | (s[p + 1] << 8) | (s[p + 2] << 16);
49
+ var type = (h >>> 1) & 0x03;
50
+ if (type === 3) return -1;
51
+ p += 3 + (type === 1 ? 1 : (h >>> 3));
52
+ if (p > n) return -1;
53
+ if ((h & 0x01) !== 0) break;
54
+ }
55
+ if ((fhd & 0x04) !== 0) p += 4;
56
+ return p > n ? -1 : p;
57
+ }
58
+
59
+ var FRAME_LENGTH = _intrinsic.assign(_intrinsic.create(null), { zstd: _zstdFrameLength });
60
+
30
61
  function _reportsTruncation(name) {
31
62
  var compress = _PROBE_COMPRESS[name];
32
63
  var decompress = DECOMPRESS[name];
@@ -51,7 +82,7 @@ var SAFE = _intrinsic.create(null);
51
82
  _intrinsic.forEach(_objectKeys(DECOMPRESS), function (n) { if (_reportsTruncation(n)) SAFE[n] = true; });
52
83
 
53
84
  // @enforced-by guard-shape-reinlined
54
- // @guard-shape \bzlib\.(inflate|brotliDecompress|zstdDecompress)Sync\b
85
+ // @guard-shape \bzlib\.(?:inflate|inflateRaw|gunzip|unzip|brotliDecompress|zstdDecompress|createInflate|createInflateRaw|createGunzip|createUnzip|createBrotliDecompress|createZstdDecompress)(?:Sync)?\b
55
86
  // @guard-via \bguard\.compress\.bounded\s*\(
56
87
  function bounded(algorithm, stream, cap, E, codes, label) {
57
88
  var decompress = DECOMPRESS[algorithm];
@@ -65,6 +96,10 @@ function bounded(algorithm, stream, cap, E, codes, label) {
65
96
  throw new TypeError("guard.compress.bounded: cap must be a positive integer");
66
97
  }
67
98
  var what = label || "the compressed input";
99
+ var frameLength = FRAME_LENGTH[algorithm];
100
+ if (frameLength && frameLength(stream) !== _sizeOf(stream)) {
101
+ throw E(codes.failed, what + " is not exactly one " + algorithm + " frame");
102
+ }
68
103
  var res;
69
104
  try {
70
105
  res = decompress(stream, { maxOutputLength: cap, info: true });
@@ -101,7 +101,7 @@ function assertKnownKeys(obj, known, E, code, message) {
101
101
  * Which of the two a given name is depends on the function and the engine. A STRICT function, which
102
102
  * is every function in an ES module or a `"use strict"` file, owns neither `arguments` nor `caller`,
103
103
  * and `Function.prototype` carries both as the restricted accessors, so a chain walk finds them
104
- * there. On the Node 24.21.0 floor a non-strict function owns both outright. Measured before this
104
+ * there. On the Node 24.22.1 floor a non-strict function owns both outright. Measured before this
105
105
  * covered the inherited side: a plain strict callable descriptor was refused for `arguments` by
106
106
  * `pki.cms.sign` and `pki.cms.countersign`, while the minted floor shape passed, which is the shape
107
107
  * the vectors were written on.
@@ -372,7 +372,7 @@ function _kindMembers(v) {
372
372
  /** @internal The names a callable carries before a caller assigns a field, asked of the engine
373
373
  * rather than listed here: a function's own names, plus `Function.prototype`'s, which is where the
374
374
  * restricted `arguments` and `caller` accessors live. Which of them a given function makes OWN
375
- * depends on the engine. Measured: on Node 24.21.0, the version `engines.node` floors at, every
375
+ * depends on the engine. Measured: on Node 24.22.1, the version `engines.node` floors at, every
376
376
  * non-strict function carries own `arguments` and `caller`; on 26.9.0 none does, for a declaration
377
377
  * and for `new Function` alike. A list written against one of those refuses a legitimate callable
378
378
  * descriptor on the other, and a caller's non-strict CommonJS module is where the shape comes
@@ -418,7 +418,7 @@ function isOwnMachineryName(v, k) {
418
418
  if (v === null || v === undefined) return false;
419
419
  var d = _getOwnPropertyDescriptor(v, k);
420
420
  /** @internal A DATA property, because that is what the machinery is: measured on both Node
421
- * 24.21.0 and 26.9.0, every one of these names a function owns is a non-enumerable data
421
+ * 24.22.1 and 26.9.0, every one of these names a function owns is a non-enumerable data
422
422
  * property. An accessor a caller installed under one of those names is therefore theirs, and it
423
423
  * reaches the unknown-field refusal by its name rather than being exempted and ignored. Asking
424
424
  * the descriptor rather than reading the property leaves the getter uninvoked either way. */
@@ -267,7 +267,7 @@ var _strSliceOwn = uncurry(String.prototype.slice);
267
267
  * `String.prototype.split` is specified to look the separator's `Symbol.split` method up and call
268
268
  * it (ES2015 21.1.3.19 step 2), so the separator's prototype chain is part of the operation and a
269
269
  * capture of `split` does not close it. Hard rule 11 bans `split` in `lib/` for exactly that
270
- * reason. MEASURED on Node 24.21, the supported runtime: a captured split with a string separator
270
+ * reason. MEASURED on Node 24.22.1, the supported runtime: a captured split with a string separator
271
271
  * DOES dispatch to an installed `String.prototype[Symbol.split]`, and that made
272
272
  * `isBlockedIp("127.0.0.1")` answer false, so a request to loopback proceeded under
273
273
  * `blockPrivateAddresses`. Node 26.9 fast-paths a primitive-string separator past the lookup, which
@@ -581,7 +581,7 @@ function _isBlockedIp(ip) {
581
581
  /** @internal The literal is scanned on the delimiter CHARACTER rather than split on a separator
582
582
  * string. `String.prototype.split` is specified to look the separator's `Symbol.split` method up
583
583
  * and call it, so the separator's prototype chain is part of the operation and capturing `split`
584
- * does not close it; hard rule 11 bans `split` in lib/ for that reason. MEASURED on Node 24.21,
584
+ * does not close it; hard rule 11 bans `split` in lib/ for that reason. MEASURED on Node 24.22.1,
585
585
  * the supported runtime: a hook answering with a public address's octets made this refusal clear
586
586
  * for `127.0.0.1` and a request to loopback proceeded. */
587
587
  var o = intrinsic.splitChar(ip, 46), a = +o[0], b = +o[1], c = +o[2];
@@ -40,7 +40,7 @@
40
40
  * all rather than faulting is dropped, because a runtime that accepts a cut
41
41
  * frame lets a peer strip a frame's tail and have the receiver read a prefix as
42
42
  * the whole message. A dropped algorithm is then neither compressed to nor
43
- * accepted. At the Node 24.21 engine floor all three qualify; on a runtime
43
+ * accepted. At the Node 24.22.1 engine floor all three qualify; on a runtime
44
44
  * where one does not, it is dropped.
45
45
  *
46
46
  * @card
@@ -1,4 +1,4 @@
1
1
  {
2
- "_comment": "Vendored dependencies: none currently. @blamejs/pki's cryptography runs entirely on Node's built-in node:crypto: the classical algorithm set (RSA, ECDSA, EdDSA, ECDH, AES, HMAC, HKDF, PBKDF2, the SHA family) AND the FIPS 203/204/205 post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA) via the platform OpenSSL 3.5 that the Node engine floor (>=24.21) ships. A built-in ships zero bytes and is OpenSSL-interoperable by construction, so no crypto bundle is vendored. A package is added here ONLY when a specific operation is confirmed missing from the engine floor; see lib/vendor/README.md for the policy.",
2
+ "_comment": "Vendored dependencies: none currently. @blamejs/pki's cryptography runs entirely on Node's built-in node:crypto: the classical algorithm set (RSA, ECDSA, EdDSA, ECDH, AES, HMAC, HKDF, PBKDF2, the SHA family) AND the FIPS 203/204/205 post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA) via the platform OpenSSL 3.5 that the Node engine floor (>=24.22.1) ships. A built-in ships zero bytes and is OpenSSL-interoperable by construction, so no crypto bundle is vendored. A package is added here ONLY when a specific operation is confirmed missing from the engine floor; see lib/vendor/README.md for the policy.",
3
3
  "packages": {}
4
4
  }
@@ -6,7 +6,7 @@ nothing. This directory holds only the manifest.
6
6
  ## Native-first crypto
7
7
 
8
8
  The toolkit's cryptography runs entirely on Node's built-in `node:crypto`. The
9
- engine floor (Node `>=24.21`) links OpenSSL 3.5, which provides:
9
+ engine floor (Node `>=24.22.1`) links OpenSSL 3.5, which provides:
10
10
 
11
11
  - the full classical set: RSA (PKCS#1 v1.5, PSS, OAEP), ECDSA, EdDSA
12
12
  (Ed25519/Ed448), ECDH including X25519 and X448, AES (GCM/CBC/CTR/KW), HMAC,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -51,7 +51,7 @@
51
51
  "owns-its-stack"
52
52
  ],
53
53
  "engines": {
54
- "node": ">=24.21.0"
54
+ "node": ">=24.22.1"
55
55
  },
56
56
  "files": [
57
57
  "index.js",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:1f2b6ec8-3dcd-4f4f-abc6-92aeb5ba8795",
5
+ "serialNumber": "urn:uuid:28a2dc7f-a34d-48fb-911a-b3c269f6e1bc",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-21T20:18:37.023Z",
8
+ "timestamp": "2026-10-27T17:35:33.081Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -15,18 +15,18 @@
15
15
  {
16
16
  "vendor": "npm",
17
17
  "name": "cli",
18
- "version": "11.19.0"
18
+ "version": "11.19.1"
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.9.7",
22
+ "bom-ref": "@blamejs/pki@0.9.8",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.9.7",
25
+ "version": "0.9.8",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.9.7",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.9.8",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.9.7",
57
+ "ref": "@blamejs/pki@0.9.8",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]