@blamejs/pki 0.9.8 → 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 +16 -0
- package/lib/acme.js +7 -0
- package/lib/guard-name.js +10 -9
- package/lib/http-transport.js +22 -0
- package/lib/identity-match.js +27 -0
- package/lib/ip-utils.js +24 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.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
|
+
|
|
7
23
|
## v0.9.8 — 2026-10-27
|
|
8
24
|
|
|
9
25
|
Require Node 24.22.1, which fixes the Brotli decoder memory corruption in CVE-2026-111497.
|
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-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. */
|
|
@@ -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/package.json
CHANGED
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"
|
|
@@ -19,14 +19,14 @@
|
|
|
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
|
]
|