@blamejs/pki 0.9.7 → 0.9.9
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 +32 -0
- package/README.md +1 -1
- package/lib/acme.js +7 -0
- package/lib/guard-compress.js +36 -1
- package/lib/guard-identifier.js +3 -3
- package/lib/guard-intrinsic.js +1 -1
- package/lib/guard-name.js +10 -9
- package/lib/http-transport.js +23 -1
- package/lib/identity-match.js +27 -0
- package/lib/ip-utils.js +24 -1
- package/lib/tls-cert-compress.js +1 -1
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/vendor/README.md +1 -1
- package/package.json +2 -2
- package/sbom.cdx.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.9 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
Refuse an IPv4 address spelled as a host name wherever a certificate identity is matched, whatever the Node version.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- A reference identity such as `999.999.999.999`, which is neither an address nor a domain name, is refused with `identity/bad-reference` instead of being compared against dNSName entries.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- `pki.identity.match` refuses a reference identity whose last label is a number, or `0x` followed by hex digits, with `identity/bad-reference`, as a string and as the value of a `dns`, `srv` or `uri` record. Before, `0127.0.0.1`, `0177.0.0.1` and `0x7f.0.0.1` were matched by the dNSName `*.0.0.1`, `127.0.0.1.` by `127.0.0.1`, and `127.1` by `*.1`, and a `dns` record carrying the canonical `127.0.0.1` was matched by the dNSName `127.0.0.1`. An address is passed as the canonical dotted quad or as octets, which is compared only against `iPAddress` entries. A domain reference must also be made of letters, digits, hyphens, dots and underscores, so `127.0.0.%31` (which WHATWG URL decodes to 127.0.0.1), `::1` and `[::1]` are refused rather than compared against dNSName entries.
|
|
18
|
+
- `pki.path.validate` refuses such an `opts.identity` with `path/bad-input`. Before, a leaf whose only subject alternative name was the dNSName `*.0.0.1` validated for identity `0x7f.0.0.1` under an excluded `iPAddress` subtree of 127.0.0.0/8, because the subtree applies only to `iPAddress` entries.
|
|
19
|
+
- `pki.transport.https` and `pki.transport.peerChain`, and every client built on them, refuse a `tls.servername` that is such a value with `transport/bad-input`, and a `proxy.tls.servername` with `transport/bad-proxy`. A servername holding any character other than a letter, digit, hyphen, dot or underscore, including a fullwidth digit, a NUL, a percent escape or a bracketed IPv6 address, is refused the same way; a canonical address literal is still accepted. Node below 24.22.1 matched `0x7f.0.0.1` or `127` written in fullwidth digits against the dNSName `127.0.0.1`.
|
|
20
|
+
- The ACME verbs refuse such a value as a `dns` identifier with `acme/bad-identifier`, in an authorization, an order, the subject `commonName` of a finalize CSR and `pki.acme.verifyTlsAlpn01`, so a finalize CSR naming one in a dNSName matches no order identifier. An address takes the `ip` identifier type (RFC 8738 sec. 3); as a `dns` identifier, a validation certificate's dNSName would prove it.
|
|
21
|
+
- A URI name constraint, and the presented host of a uniformResourceIdentifier name, no longer read a host such as `0x7f.0.0.1` as a domain name. A constraint base of that form is refused as an address.
|
|
22
|
+
|
|
23
|
+
## v0.9.8 — 2026-10-27
|
|
24
|
+
|
|
25
|
+
Require Node 24.22.1, which fixes the Brotli decoder memory corruption in CVE-2026-111497.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `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.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
|
|
7
39
|
## v0.9.7 — 2026-10-21
|
|
8
40
|
|
|
9
41
|
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.
|
|
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
|
package/lib/acme.js
CHANGED
|
@@ -456,6 +456,13 @@ function _assertDnsName(name) {
|
|
|
456
456
|
if (!_isLdhLabel(l)) throw E("acme/bad-identifier", "a dns label must be lowercase LDH ASCII (A-label): " + JSON.stringify(l));
|
|
457
457
|
if (l.indexOf("xn--") === 0 && !_isXnLabel(l)) throw E("acme/bad-identifier", "a malformed xn-- A-label: " + JSON.stringify(l));
|
|
458
458
|
}
|
|
459
|
+
/** @internal A resolver and WHATWG URL read a name whose last label is a number as IPv4, so
|
|
460
|
+
* `127.0.0.1`, `0x7f.0.0.1` and `127.1` are addresses rather than domain names. An address takes the
|
|
461
|
+
* `ip` identifier type (RFC 8738 sec. 3); as a `dns` identifier, a dNSName SAN would prove it. */
|
|
462
|
+
if (ipUtils.endsInANumber(name)) {
|
|
463
|
+
throw E("acme/bad-identifier", "a dns identifier value is read as an IPv4 address by a resolver and by WHATWG URL; " +
|
|
464
|
+
"an address uses the ip identifier type (RFC 8738 sec. 3): " + _stringify(name));
|
|
465
|
+
}
|
|
459
466
|
}
|
|
460
467
|
|
|
461
468
|
function _assertIpAddress(value) {
|
package/lib/guard-compress.js
CHANGED
|
@@ -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
|
|
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 });
|
package/lib/guard-identifier.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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. */
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -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.
|
|
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
|
package/lib/guard-name.js
CHANGED
|
@@ -446,12 +446,6 @@ function _charTable(chars) {
|
|
|
446
446
|
for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true;
|
|
447
447
|
return t;
|
|
448
448
|
}
|
|
449
|
-
var _DIGDOT_TABLE = _charTable("0123456789.");
|
|
450
|
-
function _allCharsIn(s, table) {
|
|
451
|
-
if (s.length === 0) return false;
|
|
452
|
-
for (var i = 0; i < s.length; i++) if (!table[_charCodeAt(s, i)]) return false;
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
449
|
|
|
456
450
|
/** @internal The one trailing dot a host name may be written with, removed before it is compared.
|
|
457
451
|
*
|
|
@@ -485,7 +479,7 @@ function isFqdnHost(host) {
|
|
|
485
479
|
if (typeof host !== "string") return false;
|
|
486
480
|
var h = stripTrailingDot(host);
|
|
487
481
|
if (!_isHostName(h)) return false;
|
|
488
|
-
if (
|
|
482
|
+
if (_ipUtils.endsInANumber(h)) return false;
|
|
489
483
|
return true;
|
|
490
484
|
}
|
|
491
485
|
|
|
@@ -527,6 +521,13 @@ function _isHostName(s) {
|
|
|
527
521
|
// @enforced-by behavioral -- an LDH label walk has no rename-proof shape that separates it from any other per-character scan; the vectors refusing an empty label, a hyphen at a label edge and a name past 253 octets are the guard
|
|
528
522
|
function isHostName(s) { return typeof s === "string" && _isHostName(s); }
|
|
529
523
|
|
|
524
|
+
/** @internal Whether a character code can appear in a host name a caller hands the toolkit: a letter,
|
|
525
|
+
* a digit, a hyphen, a dot or an underscore. A percent escape is decoded by WHATWG URL, so
|
|
526
|
+
* `127.0.0.%31` reaches 127.0.0.1, and a colon or a bracket spells an IPv6 address; neither is a
|
|
527
|
+
* host name. */
|
|
528
|
+
// @enforced-by behavioral -- a character-class test has no rename-proof shape; the vectors refusing a percent escape, a colon and a bracket in a reference identity and in a TLS servername are the guard
|
|
529
|
+
function isHostNameChar(c) { return _LDH_ALNUM[c] === true || c === 0x2d || c === 0x2e || c === 0x5f; }
|
|
530
|
+
|
|
530
531
|
/** @internal The labels of `s`, in order, with no trailing-dot handling of its own: the caller
|
|
531
532
|
* normalizes the dot through `stripTrailingDot` so both sides of a comparison are normalized by
|
|
532
533
|
* one symbol. A name carrying an empty label yields an empty string at that position, which a
|
|
@@ -1019,7 +1020,7 @@ function constraintBaseRefusal(tag, base) {
|
|
|
1019
1020
|
var uriBody = _toLowerCase(base);
|
|
1020
1021
|
if (_charAt(uriBody, 0) === ".") uriBody = _strSlice(uriBody, 1);
|
|
1021
1022
|
if (isFqdnHost(uriBody)) return null;
|
|
1022
|
-
return
|
|
1023
|
+
return _ipUtils.endsInANumber(uriBody)
|
|
1023
1024
|
? "must be a domain name rather than an address, RFC 5280 sec. 4.2.1.10 naming an address in a URI's authority as what a URI constraint cannot be compared against"
|
|
1024
1025
|
: "must be a domain name, which RFC 5280 sec. 4.2.1.10 requires of a URI constraint, optionally written as a subtree with a leading dot";
|
|
1025
1026
|
}
|
|
@@ -1148,7 +1149,7 @@ module.exports = intrinsic.freeze({
|
|
|
1148
1149
|
isAttributeDescriptor: isAttributeDescriptor,
|
|
1149
1150
|
lowerAscii: lowerAscii, uriSchemeIs: uriSchemeIs,
|
|
1150
1151
|
stripTrailingDot: stripTrailingDot, isFqdnHost: isFqdnHost, splitMailbox: splitMailbox,
|
|
1151
|
-
isHostName: isHostName, hostLabels: hostLabels, isXnLabel: isXnLabel, uriParts: uriParts,
|
|
1152
|
+
isHostName: isHostName, isHostNameChar: isHostNameChar, hostLabels: hostLabels, isXnLabel: isXnLabel, uriParts: uriParts,
|
|
1152
1153
|
uriEqual: uriEqual, dnsNameEqual: dnsNameEqual, generalNameEqual: generalNameEqual,
|
|
1153
1154
|
constraintBaseRefusal: constraintBaseRefusal,
|
|
1154
1155
|
constraintBaseUnreadable: constraintBaseUnreadable,
|
package/lib/http-transport.js
CHANGED
|
@@ -88,6 +88,7 @@ var nodeUtil = require("node:util");
|
|
|
88
88
|
var constants = require("./constants");
|
|
89
89
|
var guard = require("./guard-all");
|
|
90
90
|
var intrinsic = require("./guard-intrinsic");
|
|
91
|
+
var ipUtils = require("./ip-utils");
|
|
91
92
|
/** @internal The certificate reader, and the two operations the distrust walk proves a link with,
|
|
92
93
|
* taken at load. A replaced `verify` would answer whether each link of a server chain is signed by
|
|
93
94
|
* the certificate above it. */
|
|
@@ -581,7 +582,7 @@ function _isBlockedIp(ip) {
|
|
|
581
582
|
/** @internal The literal is scanned on the delimiter CHARACTER rather than split on a separator
|
|
582
583
|
* string. `String.prototype.split` is specified to look the separator's `Symbol.split` method up
|
|
583
584
|
* 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.
|
|
585
|
+
* does not close it; hard rule 11 bans `split` in lib/ for that reason. MEASURED on Node 24.22.1,
|
|
585
586
|
* the supported runtime: a hook answering with a public address's octets made this refusal clear
|
|
586
587
|
* for `127.0.0.1` and a request to loopback proceeded. */
|
|
587
588
|
var o = intrinsic.splitChar(ip, 46), a = +o[0], b = +o[1], c = +o[2];
|
|
@@ -697,6 +698,25 @@ var PROXY_KEYS = _keySet(["url", "auth", "tls"]);
|
|
|
697
698
|
var PROXY_AUTH_KEYS = _keySet(["scheme", "username", "password", "allowMD5", "allowLegacyQop"]);
|
|
698
699
|
var PROXY_DIGEST_KNOBS = ["allowMD5", "allowLegacyQop"];
|
|
699
700
|
var PROXY_TLS_KEYS = _keySet(["anchors", "useSystemStore", "servername", "minVersion", "distrustedKeys"]);
|
|
701
|
+
/** @internal A servername is a DNS name (RFC 6066 sec. 3) of letters, digits, hyphens, dots and
|
|
702
|
+
* underscores, or a canonical address literal. A percent escape or a bracketed IPv6 address is
|
|
703
|
+
* neither. A value whose last label is a number but which is not a canonical address (`0x7f.0.0.1`,
|
|
704
|
+
* `0177.0.0.1`, `127.1`, `127.0.0.1.`, or `127.0.0.1` in fullwidth digits) is an address in another
|
|
705
|
+
* spelling. Node below 24.22.1 matched such a name against the certificate's dNSName entries
|
|
706
|
+
* (CVE-2026-106072, CVE-2026-105483), so `DNS:127.0.0.1` authenticated it. It is refused here
|
|
707
|
+
* whatever the runtime does. */
|
|
708
|
+
function _assertServername(s, E, code, what) {
|
|
709
|
+
if (_isIP(s)) return;
|
|
710
|
+
for (var i = 0; i < s.length; i++) {
|
|
711
|
+
if (!guard.name.isHostNameChar(_charCodeAt(s, i))) {
|
|
712
|
+
throw E(code, what + " carries a character at offset " + i + " that no host name holds; a servername is a DNS " +
|
|
713
|
+
"name of letters, digits, hyphens, dots and underscores (RFC 6066 sec. 3) or a canonical address literal");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (ipUtils.endsInANumber(s)) {
|
|
717
|
+
throw E(code, what + " " + guard.text.showValue(s) + " is an IPv4 address in a non-canonical spelling; pass the canonical dotted quad or a DNS name");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
700
720
|
/** @internal The names a `tls` policy bag may carry. A name outside them is refused rather than
|
|
701
721
|
* ignored: a misspelled `distrustedKeys` or `anchors` would otherwise leave the connection under a
|
|
702
722
|
* policy the caller did not state. */
|
|
@@ -820,6 +840,7 @@ function _validateProxy(request, defaults, E, C, originHost, originStatedPort, o
|
|
|
820
840
|
if (!_copyableRecord(ptls)) throw E(C("bad-proxy"), "opts.proxy.tls must be a plain object with only own enumerable data properties");
|
|
821
841
|
guard.identifier.assertKnownKeys(ptls, PROXY_TLS_KEYS, E, C("bad-proxy"), "opts.proxy.tls has an unknown option ");
|
|
822
842
|
if (ptls.servername !== undefined && ptls.servername !== null && typeof ptls.servername !== "string") throw E(C("bad-proxy"), "opts.proxy.tls.servername must be a string");
|
|
843
|
+
if (typeof ptls.servername === "string") _assertServername(ptls.servername, E, C("bad-proxy"), "opts.proxy.tls.servername");
|
|
823
844
|
}
|
|
824
845
|
/** @internal Taken here, so the value the tunnel is opened under is the one typed above rather
|
|
825
846
|
* than a later read of the caller's record. The door refuses an accessor and refuses a Proxy, so
|
|
@@ -1107,6 +1128,7 @@ function httpsTransport(defaults) {
|
|
|
1107
1128
|
* as `idHost` for the name the server certificate is matched against. A value answering a
|
|
1108
1129
|
* different string per read sends one name and verifies another. */
|
|
1109
1130
|
if (typeof sni !== "string") throw E(C("bad-input"), "tls.servername must be a string, got " + guard.text.showValue(sni));
|
|
1131
|
+
_assertServername(sni, E, C("bad-input"), "tls.servername");
|
|
1110
1132
|
if (sni && !_isIP(sni)) options.servername = sni;
|
|
1111
1133
|
var caList = [];
|
|
1112
1134
|
if (hasAnchors) {
|
package/lib/identity-match.js
CHANGED
|
@@ -89,6 +89,27 @@ function _assertNoWildcard(s, what) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** @internal A domain is a host name and never an address. Its characters are letters, digits,
|
|
93
|
+
* hyphens, dots and underscores, so a percent escape (`127.0.0.%31`, which WHATWG URL decodes to
|
|
94
|
+
* 127.0.0.1) and an IPv6 spelling (`::1`, `[::1]`) are refused. RFC 9525 sec. 3 classifies an IP
|
|
95
|
+
* address as an IP-ID and sec. 6.4 never compares one as a DNS-ID. A resolver and WHATWG URL read any host whose last label
|
|
96
|
+
* is a number as IPv4 (`0x7f.0.0.1`, `0177.0.0.1`, `127.1`, `2130706433`, `127.0.0.1.`), and
|
|
97
|
+
* `0127.0.0.1` reaches 87.0.0.1 under URL but 127.0.0.1 under some resolvers. Compared as a domain,
|
|
98
|
+
* such a value was authenticated by a dNSName like `127.0.0.1` or `*.0.0.1`. */
|
|
99
|
+
function _assertNotAnAddress(v, what) {
|
|
100
|
+
for (var i = 0; i < v.length; i++) {
|
|
101
|
+
if (!guard.name.isHostNameChar(_charCodeAt(v, i))) {
|
|
102
|
+
throw E("identity/bad-reference", what + " carries " + guard.text.showValue(v[i]) + " at offset " + i +
|
|
103
|
+
", which no host name holds. A host name is letters, digits, hyphens, dots and underscores; a percent " +
|
|
104
|
+
"escape is decoded by WHATWG URL and a colon or bracket spells an IPv6 address");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (ip.endsInANumber(v)) {
|
|
108
|
+
throw E("identity/bad-reference", what + " is read as an IPv4 address by a resolver and by WHATWG URL, so it is " +
|
|
109
|
+
"not a domain name. Pass an address as the canonical dotted quad or as octets (RFC 9525 sec. 3)");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
/** @internal The domain of a reference identity, normalized the way both sides are normalized: one
|
|
93
114
|
* trailing dot stripped, then held to the label rules so an empty label or a label past 63
|
|
94
115
|
* characters is a caller mistake, not a name that can never match. */
|
|
@@ -96,6 +117,7 @@ function _referenceDomain(v, what) {
|
|
|
96
117
|
if (typeof v !== "string" || v === "") throw E("identity/bad-reference", what + " must be a non-empty string");
|
|
97
118
|
_assertAsciiReference(v, what);
|
|
98
119
|
_assertNoWildcard(v, what);
|
|
120
|
+
_assertNotAnAddress(v, what);
|
|
99
121
|
var d = _stripTrailingDot(v);
|
|
100
122
|
if (d === "" || d.length > MAX_HOST) {
|
|
101
123
|
throw E("identity/bad-reference", what + " must be a domain name of 1 to " + MAX_HOST + " characters");
|
|
@@ -487,6 +509,11 @@ function _search(refs, entries, wildcardsAllowed) {
|
|
|
487
509
|
* its service type and its domain together, so no comparison can pair the service of one
|
|
488
510
|
* reference with the domain of another (sec. 6.5).
|
|
489
511
|
*
|
|
512
|
+
* A domain whose last label is a number, such as `0x7f.0.0.1`, `0177.0.0.1`, `127.1`,
|
|
513
|
+
* `2130706433` or `127.0.0.1.`, is an IPv4 address to a resolver and to WHATWG URL, so it is
|
|
514
|
+
* refused with `identity/bad-reference` in every form rather than compared against dNSName
|
|
515
|
+
* entries. An address is passed as the canonical dotted quad or as octets.
|
|
516
|
+
*
|
|
490
517
|
* The subject distinguished name is never a source of identity. The `commonName` is not read
|
|
491
518
|
* under any option or fallback, including when the certificate carries no subjectAltName at
|
|
492
519
|
* all, and neither is any other RDN (sec. 2, RFC 9110 sec. 4.3.4). A certificate presenting
|
package/lib/ip-utils.js
CHANGED
|
@@ -42,6 +42,29 @@ function isIPv4(s) {
|
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** @internal Whether a host is one WHATWG URL parses as IPv4: the "ends in a number" check of the
|
|
46
|
+
* URL Standard's host parser. With one trailing dot dropped, the last label is all ASCII digits, or
|
|
47
|
+
* `0x` / `0X` followed by hex digits or nothing. A resolver (inet_aton) reads the same strings as
|
|
48
|
+
* an address, so `0x7f.0.0.1`, `0177.0.0.1`, `127.1`, `2130706433` and `127.0.0.1.` all name
|
|
49
|
+
* 127.0.0.1. A host this answers true for and `isIPv4` answers false for is an address in a
|
|
50
|
+
* non-canonical spelling, never a domain name. */
|
|
51
|
+
function endsInANumber(s) {
|
|
52
|
+
if (typeof s !== "string") return false;
|
|
53
|
+
var parts = _splitOn(s, ".");
|
|
54
|
+
if (parts[parts.length - 1] === "") {
|
|
55
|
+
if (parts.length === 1) return false;
|
|
56
|
+
parts.length -= 1;
|
|
57
|
+
}
|
|
58
|
+
var last = parts[parts.length - 1], n = last.length, i;
|
|
59
|
+
if (n === 0) return false;
|
|
60
|
+
var digits = true;
|
|
61
|
+
for (i = 0; i < n; i += 1) { if (!_isDigitCode(_charCodeAt(last, i))) { digits = false; break; } }
|
|
62
|
+
if (digits) return true;
|
|
63
|
+
if (n < 2 || _charCodeAt(last, 0) !== 48 || (_charCodeAt(last, 1) | 0x20) !== 0x78) return false;
|
|
64
|
+
for (i = 2; i < n; i += 1) { if (!_isHexCode(_charCodeAt(last, i))) return false; }
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
function _isHexGroup(g) {
|
|
46
69
|
var n = g.length, i;
|
|
47
70
|
if (n < 1 || n > 4) return false;
|
|
@@ -151,6 +174,6 @@ function canonicalizeIpv6(value) {
|
|
|
151
174
|
}
|
|
152
175
|
|
|
153
176
|
module.exports = {
|
|
154
|
-
isIPv4: isIPv4, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, packIpLiteral: packIpLiteral,
|
|
177
|
+
isIPv4: isIPv4, endsInANumber: endsInANumber, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, packIpLiteral: packIpLiteral,
|
|
155
178
|
textFromOctets: textFromOctets, canonicalizeIpv6: canonicalizeIpv6,
|
|
156
179
|
};
|
package/lib/tls-cert-compress.js
CHANGED
|
@@ -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.
|
|
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
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/lib/vendor/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
3
|
+
"version": "0.9.9",
|
|
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.
|
|
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:
|
|
5
|
+
"serialNumber": "urn:uuid:8b9305d6-9fdd-4cc8-9197-8b59f0469f8f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-27T18:43:36.397Z",
|
|
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.
|
|
18
|
+
"version": "11.19.1"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.9.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.9.9",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.9.
|
|
25
|
+
"version": "0.9.9",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.9.9",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.9.9",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|