@launchdarkly/mcp-server 0.7.0 → 0.7.1
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/README.md +9 -9
- package/bin/mcp-server.js +145 -5
- package/bin/mcp-server.js.map +8 -7
- package/dist/commonjs/lib/config.d.ts +2 -2
- package/dist/commonjs/lib/config.js +2 -2
- package/dist/commonjs/mcp-server/cli/start/impl.d.ts.map +1 -1
- package/dist/commonjs/mcp-server/cli/start/impl.js +7 -0
- package/dist/commonjs/mcp-server/cli/start/impl.js.map +1 -1
- package/dist/commonjs/mcp-server/cli/start/tls-blocklist.d.ts +48 -0
- package/dist/commonjs/mcp-server/cli/start/tls-blocklist.d.ts.map +1 -0
- package/dist/commonjs/mcp-server/cli/start/tls-blocklist.js +206 -0
- package/dist/commonjs/mcp-server/cli/start/tls-blocklist.js.map +1 -0
- package/dist/commonjs/mcp-server/mcp-server.js +1 -1
- package/dist/commonjs/mcp-server/server.js +1 -1
- package/dist/esm/lib/config.d.ts +2 -2
- package/dist/esm/lib/config.js +2 -2
- package/dist/esm/mcp-server/cli/start/impl.d.ts.map +1 -1
- package/dist/esm/mcp-server/cli/start/impl.js +7 -0
- package/dist/esm/mcp-server/cli/start/impl.js.map +1 -1
- package/dist/esm/mcp-server/cli/start/tls-blocklist.d.ts +48 -0
- package/dist/esm/mcp-server/cli/start/tls-blocklist.d.ts.map +1 -0
- package/dist/esm/mcp-server/cli/start/tls-blocklist.js +192 -0
- package/dist/esm/mcp-server/cli/start/tls-blocklist.js.map +1 -0
- package/dist/esm/mcp-server/mcp-server.js +1 -1
- package/dist/esm/mcp-server/server.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/lib/config.ts +2 -2
- package/src/mcp-server/cli/start/impl.ts +11 -0
- package/src/mcp-server/cli/start/tls-blocklist.ts +252 -0
- package/src/mcp-server/mcp-server.ts +1 -1
- package/src/mcp-server/server.ts +1 -1
- package/test/tls-blocklist.test.mjs +314 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
/*
|
|
4
|
+
* INC-289: rejects TLS chains that go through the compromised eMudhra
|
|
5
|
+
* intermediates (https://emudhra.we-have-a-problem.com/). Go 1.26.10, Node.js
|
|
6
|
+
* 22.23.4 / 24.22.1 / 26.12.1 and Bun (unreleased, oven-sh/bun#46300) block
|
|
7
|
+
* the same keys natively. This check covers runtimes that predate those fixes.
|
|
8
|
+
* It is installed only by the `mcp start` CLI, never for library consumers.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* SHA-256 of the DER SubjectPublicKeyInfo of each compromised intermediate.
|
|
12
|
+
* Identical to Node.js's kBlockedSPKIs (nodejs/node#66871) and Go's fix
|
|
13
|
+
* (CVE-2026-111943). Each key was issued twice, under emSign Root TLS/CS CA -
|
|
14
|
+
* G3 and under Sectigo's AAA Certificate Services, so hashing the key rather
|
|
15
|
+
* than the certificate covers both.
|
|
16
|
+
*/
|
|
17
|
+
export const BLOCKED_SPKI_SHA256 = new Set([
|
|
18
|
+
"43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b",
|
|
19
|
+
"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f",
|
|
20
|
+
"5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0",
|
|
21
|
+
"63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8",
|
|
22
|
+
"92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30",
|
|
23
|
+
"e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41",
|
|
24
|
+
"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105",
|
|
25
|
+
"ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6",
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Common names of the compromised intermediates. Every one of them has
|
|
29
|
+
* pathlen:0, so any leaf signed with one of their keys must name the
|
|
30
|
+
* intermediate as its issuer. Bun's fetch only exposes the leaf to
|
|
31
|
+
* checkServerIdentity, so the issuer name is what blocks these chains there.
|
|
32
|
+
*/
|
|
33
|
+
const BLOCKED_ISSUER_CN = /^em (dv|ov|ev) (tls|cs) ca - g3[bc]$/;
|
|
34
|
+
/**
|
|
35
|
+
* Node.js releases that block these keys natively, by major version. Lines
|
|
36
|
+
* newer than the highest entry are treated as patched; any other line is
|
|
37
|
+
* treated as unpatched.
|
|
38
|
+
*/
|
|
39
|
+
const FIXED_NODE_VERSIONS = {
|
|
40
|
+
22: [22, 23, 4],
|
|
41
|
+
24: [24, 22, 1],
|
|
42
|
+
26: [26, 12, 1],
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* First Bun release that blocks these keys natively. Set this once one ships
|
|
46
|
+
* (the fix is oven-sh/bun#46300). While it is undefined, every Bun version
|
|
47
|
+
* gets the warning.
|
|
48
|
+
*/
|
|
49
|
+
const FIXED_BUN_VERSION = undefined;
|
|
50
|
+
export const BLOCKED_CERT_ERROR_CODE = "ERR_TLS_CERT_BLOCKED";
|
|
51
|
+
// Captured before installTlsBlocklist() replaces tls.checkServerIdentity.
|
|
52
|
+
const defaultCheckServerIdentity = tls.checkServerIdentity;
|
|
53
|
+
function normalizeName(value) {
|
|
54
|
+
return value.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
55
|
+
}
|
|
56
|
+
export function isBlockedIssuerName(cn) {
|
|
57
|
+
const names = Array.isArray(cn) ? cn : cn === undefined ? [] : [cn];
|
|
58
|
+
return names.some((name) => BLOCKED_ISSUER_CN.test(normalizeName(name)));
|
|
59
|
+
}
|
|
60
|
+
export function spkiSha256(raw) {
|
|
61
|
+
const spki = new X509Certificate(raw).publicKey.export({
|
|
62
|
+
type: "spki",
|
|
63
|
+
format: "der",
|
|
64
|
+
});
|
|
65
|
+
return createHash("sha256").update(spki).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Walks the peer chain (the full chain on Node.js, the leaf only on Bun) and
|
|
69
|
+
* returns a description of the first blocked certificate, if any.
|
|
70
|
+
*/
|
|
71
|
+
export function findBlockedCertificate(cert) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (let current = cert; current && !seen.has(current); current = current.issuerCertificate) {
|
|
74
|
+
seen.add(current);
|
|
75
|
+
if (isBlockedIssuerName(current.issuer?.CN)) {
|
|
76
|
+
return `certificate issued by "${String(current.issuer?.CN)}"`;
|
|
77
|
+
}
|
|
78
|
+
if (current.raw && current.raw.length > 0) {
|
|
79
|
+
const hash = spkiSha256(current.raw);
|
|
80
|
+
if (BLOCKED_SPKI_SHA256.has(hash)) {
|
|
81
|
+
return `certificate with blocked public key sha256/${hash}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A tls checkServerIdentity replacement: rejects blocked chains, then runs
|
|
89
|
+
* the runtime's normal hostname verification.
|
|
90
|
+
*/
|
|
91
|
+
export function checkServerIdentity(hostname, cert) {
|
|
92
|
+
let blocked;
|
|
93
|
+
try {
|
|
94
|
+
blocked = findBlockedCertificate(cert);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
blocked = `certificate that could not be parsed (${String(err)})`;
|
|
98
|
+
}
|
|
99
|
+
if (blocked) {
|
|
100
|
+
return Object.assign(new Error(`Refusing TLS connection to ${hostname}: the chain contains a ${blocked}, ` +
|
|
101
|
+
"which belongs to a compromised eMudhra intermediate CA."), { code: BLOCKED_CERT_ERROR_CODE });
|
|
102
|
+
}
|
|
103
|
+
return defaultCheckServerIdentity(hostname, cert);
|
|
104
|
+
}
|
|
105
|
+
let installed = false;
|
|
106
|
+
/**
|
|
107
|
+
* Installs the blocklist for this process. On Node.js, global fetch (undici)
|
|
108
|
+
* and node:https read tls.checkServerIdentity when they connect. Bun's fetch
|
|
109
|
+
* ignores it, so on Bun global fetch is wrapped to pass the check through
|
|
110
|
+
* Bun's `tls` fetch option.
|
|
111
|
+
*/
|
|
112
|
+
export function installTlsBlocklist() {
|
|
113
|
+
if (installed)
|
|
114
|
+
return;
|
|
115
|
+
installed = true;
|
|
116
|
+
if (isBun()) {
|
|
117
|
+
const originalFetch = globalThis.fetch;
|
|
118
|
+
const wrapped = (input, init) => {
|
|
119
|
+
const tlsOptions = init?.tls;
|
|
120
|
+
const callerCheck = tlsOptions?.checkServerIdentity;
|
|
121
|
+
const combined = callerCheck
|
|
122
|
+
? (hostname, cert) => checkServerIdentity(hostname, cert) ?? callerCheck(hostname, cert)
|
|
123
|
+
: checkServerIdentity;
|
|
124
|
+
return originalFetch(input, {
|
|
125
|
+
...init,
|
|
126
|
+
tls: { ...tlsOptions, checkServerIdentity: combined },
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
globalThis.fetch = Object.assign(wrapped, originalFetch);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
tls.checkServerIdentity =
|
|
133
|
+
checkServerIdentity;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isBun() {
|
|
137
|
+
return typeof globalThis.Bun !== "undefined";
|
|
138
|
+
}
|
|
139
|
+
function parseVersion(version) {
|
|
140
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
141
|
+
if (!match)
|
|
142
|
+
return undefined;
|
|
143
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
144
|
+
}
|
|
145
|
+
function compareVersions(a, b) {
|
|
146
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
147
|
+
}
|
|
148
|
+
export function isNodeVersionPatched(version) {
|
|
149
|
+
const parsed = parseVersion(version);
|
|
150
|
+
if (!parsed)
|
|
151
|
+
return false;
|
|
152
|
+
const fixed = FIXED_NODE_VERSIONS[parsed[0]];
|
|
153
|
+
if (fixed)
|
|
154
|
+
return compareVersions(parsed, fixed) >= 0;
|
|
155
|
+
const newestLine = Math.max(...Object.keys(FIXED_NODE_VERSIONS).map(Number));
|
|
156
|
+
return parsed[0] > newestLine;
|
|
157
|
+
}
|
|
158
|
+
export function isBunVersionPatched(version) {
|
|
159
|
+
if (FIXED_BUN_VERSION === undefined)
|
|
160
|
+
return false;
|
|
161
|
+
const parsed = parseVersion(version);
|
|
162
|
+
const fixed = parseVersion(FIXED_BUN_VERSION);
|
|
163
|
+
return !!parsed && !!fixed && compareVersions(parsed, fixed) >= 0;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Returns a warning when the current runtime does not block the compromised
|
|
167
|
+
* eMudhra intermediates natively, or undefined when it does.
|
|
168
|
+
*/
|
|
169
|
+
export function unpatchedRuntimeWarning(versions = process.versions) {
|
|
170
|
+
let runtime;
|
|
171
|
+
let upgrade;
|
|
172
|
+
if (versions.bun !== undefined) {
|
|
173
|
+
if (isBunVersionPatched(versions.bun))
|
|
174
|
+
return undefined;
|
|
175
|
+
runtime = `Bun ${versions.bun}`;
|
|
176
|
+
upgrade =
|
|
177
|
+
"run the server with Node.js 22.23.4, 24.22.1, 26.12.1 or later, or use the Docker image";
|
|
178
|
+
}
|
|
179
|
+
else if (versions.node !== undefined) {
|
|
180
|
+
if (isNodeVersionPatched(versions.node))
|
|
181
|
+
return undefined;
|
|
182
|
+
runtime = `Node.js ${versions.node}`;
|
|
183
|
+
upgrade = "upgrade to Node.js 22.23.4, 24.22.1, 26.12.1 or later";
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return (`WARNING: ${runtime} does not block the compromised eMudhra intermediate CAs. ` +
|
|
189
|
+
"The LaunchDarkly MCP server rejects them for its own requests, but the runtime does not; " +
|
|
190
|
+
`${upgrade}. See https://github.com/launchdarkly/mcp-server#runtime-requirements\n`);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=tls-blocklist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls-blocklist.js","sourceRoot":"","sources":["../../../../../src/mcp-server/cli/start/tls-blocklist.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,7 +29,7 @@ import { tool$featureFlagsPatch } from "./tools/featureFlagsPatch.js";
|
|
|
29
29
|
export function createMCPServer(deps) {
|
|
30
30
|
const server = new McpServer({
|
|
31
31
|
name: "LaunchDarkly",
|
|
32
|
-
version: "0.7.
|
|
32
|
+
version: "0.7.1",
|
|
33
33
|
});
|
|
34
34
|
const client = new LaunchDarklyCore({
|
|
35
35
|
apiKey: deps.apiKey,
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchdarkly/mcp-server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"author": "LaunchDarkly",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"feature-flags",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"build:mcp": "bun src/mcp-server/build.mts",
|
|
40
40
|
"build": "npm run build:mcp && tshy",
|
|
41
41
|
"test:sse-security": "node --test --test-force-exit test/sse-security.test.mjs",
|
|
42
|
+
"test:tls-blocklist": "node --test --test-force-exit test/tls-blocklist.test.mjs",
|
|
42
43
|
"prepublishOnly": "npm run build"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {},
|
package/src/lib/config.ts
CHANGED
|
@@ -69,8 +69,8 @@ export function serverURLFromOptions(options: SDKOptions): URL | null {
|
|
|
69
69
|
export const SDK_METADATA = {
|
|
70
70
|
language: "typescript",
|
|
71
71
|
openapiDocVersion: "2.0",
|
|
72
|
-
sdkVersion: "0.7.
|
|
72
|
+
sdkVersion: "0.7.1",
|
|
73
73
|
genVersion: "2.845.15",
|
|
74
74
|
userAgent:
|
|
75
|
-
"speakeasy-sdk/typescript 0.7.
|
|
75
|
+
"speakeasy-sdk/typescript 0.7.1 2.845.15 2.0 @launchdarkly/mcp-server",
|
|
76
76
|
} as const;
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
isLoopbackHost,
|
|
20
20
|
requireBearerToken,
|
|
21
21
|
} from "./sse-security.js";
|
|
22
|
+
import {
|
|
23
|
+
installTlsBlocklist,
|
|
24
|
+
unpatchedRuntimeWarning,
|
|
25
|
+
} from "./tls-blocklist.js";
|
|
22
26
|
|
|
23
27
|
interface StartCommandFlags {
|
|
24
28
|
readonly transport: "stdio" | "sse";
|
|
@@ -35,6 +39,13 @@ interface StartCommandFlags {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export async function main(this: LocalContext, flags: StartCommandFlags) {
|
|
42
|
+
// INC-289: block the compromised eMudhra intermediates before any request.
|
|
43
|
+
installTlsBlocklist();
|
|
44
|
+
const runtimeWarning = unpatchedRuntimeWarning();
|
|
45
|
+
if (runtimeWarning) {
|
|
46
|
+
process.stderr.write(runtimeWarning);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
flags.env?.forEach(([key, value]) => {
|
|
39
50
|
process.env[key] = value;
|
|
40
51
|
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* INC-289: rejects TLS chains that go through the compromised eMudhra
|
|
6
|
+
* intermediates (https://emudhra.we-have-a-problem.com/). Go 1.26.10, Node.js
|
|
7
|
+
* 22.23.4 / 24.22.1 / 26.12.1 and Bun (unreleased, oven-sh/bun#46300) block
|
|
8
|
+
* the same keys natively. This check covers runtimes that predate those fixes.
|
|
9
|
+
* It is installed only by the `mcp start` CLI, never for library consumers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SHA-256 of the DER SubjectPublicKeyInfo of each compromised intermediate.
|
|
14
|
+
* Identical to Node.js's kBlockedSPKIs (nodejs/node#66871) and Go's fix
|
|
15
|
+
* (CVE-2026-111943). Each key was issued twice, under emSign Root TLS/CS CA -
|
|
16
|
+
* G3 and under Sectigo's AAA Certificate Services, so hashing the key rather
|
|
17
|
+
* than the certificate covers both.
|
|
18
|
+
*/
|
|
19
|
+
export const BLOCKED_SPKI_SHA256: ReadonlySet<string> = new Set([
|
|
20
|
+
"43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b",
|
|
21
|
+
"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f",
|
|
22
|
+
"5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0",
|
|
23
|
+
"63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8",
|
|
24
|
+
"92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30",
|
|
25
|
+
"e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41",
|
|
26
|
+
"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105",
|
|
27
|
+
"ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Common names of the compromised intermediates. Every one of them has
|
|
32
|
+
* pathlen:0, so any leaf signed with one of their keys must name the
|
|
33
|
+
* intermediate as its issuer. Bun's fetch only exposes the leaf to
|
|
34
|
+
* checkServerIdentity, so the issuer name is what blocks these chains there.
|
|
35
|
+
*/
|
|
36
|
+
const BLOCKED_ISSUER_CN = /^em (dv|ov|ev) (tls|cs) ca - g3[bc]$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Node.js releases that block these keys natively, by major version. Lines
|
|
40
|
+
* newer than the highest entry are treated as patched; any other line is
|
|
41
|
+
* treated as unpatched.
|
|
42
|
+
*/
|
|
43
|
+
const FIXED_NODE_VERSIONS: Readonly<
|
|
44
|
+
Record<number, readonly [number, number, number]>
|
|
45
|
+
> = {
|
|
46
|
+
22: [22, 23, 4],
|
|
47
|
+
24: [24, 22, 1],
|
|
48
|
+
26: [26, 12, 1],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* First Bun release that blocks these keys natively. Set this once one ships
|
|
53
|
+
* (the fix is oven-sh/bun#46300). While it is undefined, every Bun version
|
|
54
|
+
* gets the warning.
|
|
55
|
+
*/
|
|
56
|
+
const FIXED_BUN_VERSION: string | undefined = undefined;
|
|
57
|
+
|
|
58
|
+
export const BLOCKED_CERT_ERROR_CODE = "ERR_TLS_CERT_BLOCKED";
|
|
59
|
+
|
|
60
|
+
/** The subset of tls.PeerCertificate this check reads. */
|
|
61
|
+
export interface PeerCertificateLike {
|
|
62
|
+
raw?: Uint8Array | undefined;
|
|
63
|
+
fingerprint256?: string | undefined;
|
|
64
|
+
issuer?: { CN?: string | string[] | undefined } | undefined;
|
|
65
|
+
issuerCertificate?: PeerCertificateLike | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type CheckServerIdentity = (
|
|
69
|
+
hostname: string,
|
|
70
|
+
cert: tls.PeerCertificate,
|
|
71
|
+
) => Error | undefined;
|
|
72
|
+
|
|
73
|
+
// Captured before installTlsBlocklist() replaces tls.checkServerIdentity.
|
|
74
|
+
const defaultCheckServerIdentity: CheckServerIdentity = tls.checkServerIdentity;
|
|
75
|
+
|
|
76
|
+
function normalizeName(value: string): string {
|
|
77
|
+
return value.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isBlockedIssuerName(
|
|
81
|
+
cn: string | string[] | undefined,
|
|
82
|
+
): boolean {
|
|
83
|
+
const names = Array.isArray(cn) ? cn : cn === undefined ? [] : [cn];
|
|
84
|
+
return names.some((name) => BLOCKED_ISSUER_CN.test(normalizeName(name)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function spkiSha256(raw: Uint8Array): string {
|
|
88
|
+
const spki = new X509Certificate(raw).publicKey.export({
|
|
89
|
+
type: "spki",
|
|
90
|
+
format: "der",
|
|
91
|
+
});
|
|
92
|
+
return createHash("sha256").update(spki).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Walks the peer chain (the full chain on Node.js, the leaf only on Bun) and
|
|
97
|
+
* returns a description of the first blocked certificate, if any.
|
|
98
|
+
*/
|
|
99
|
+
export function findBlockedCertificate(
|
|
100
|
+
cert: PeerCertificateLike,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
const seen = new Set<PeerCertificateLike>();
|
|
103
|
+
for (
|
|
104
|
+
let current: PeerCertificateLike | undefined = cert;
|
|
105
|
+
current && !seen.has(current);
|
|
106
|
+
current = current.issuerCertificate
|
|
107
|
+
) {
|
|
108
|
+
seen.add(current);
|
|
109
|
+
if (isBlockedIssuerName(current.issuer?.CN)) {
|
|
110
|
+
return `certificate issued by "${String(current.issuer?.CN)}"`;
|
|
111
|
+
}
|
|
112
|
+
if (current.raw && current.raw.length > 0) {
|
|
113
|
+
const hash = spkiSha256(current.raw);
|
|
114
|
+
if (BLOCKED_SPKI_SHA256.has(hash)) {
|
|
115
|
+
return `certificate with blocked public key sha256/${hash}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A tls checkServerIdentity replacement: rejects blocked chains, then runs
|
|
124
|
+
* the runtime's normal hostname verification.
|
|
125
|
+
*/
|
|
126
|
+
export function checkServerIdentity(
|
|
127
|
+
hostname: string,
|
|
128
|
+
cert: PeerCertificateLike,
|
|
129
|
+
): Error | undefined {
|
|
130
|
+
let blocked: string | undefined;
|
|
131
|
+
try {
|
|
132
|
+
blocked = findBlockedCertificate(cert);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
blocked = `certificate that could not be parsed (${String(err)})`;
|
|
135
|
+
}
|
|
136
|
+
if (blocked) {
|
|
137
|
+
return Object.assign(
|
|
138
|
+
new Error(
|
|
139
|
+
`Refusing TLS connection to ${hostname}: the chain contains a ${blocked}, ` +
|
|
140
|
+
"which belongs to a compromised eMudhra intermediate CA.",
|
|
141
|
+
),
|
|
142
|
+
{ code: BLOCKED_CERT_ERROR_CODE },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return defaultCheckServerIdentity(
|
|
146
|
+
hostname,
|
|
147
|
+
cert as unknown as tls.PeerCertificate,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let installed = false;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Installs the blocklist for this process. On Node.js, global fetch (undici)
|
|
155
|
+
* and node:https read tls.checkServerIdentity when they connect. Bun's fetch
|
|
156
|
+
* ignores it, so on Bun global fetch is wrapped to pass the check through
|
|
157
|
+
* Bun's `tls` fetch option.
|
|
158
|
+
*/
|
|
159
|
+
export function installTlsBlocklist(): void {
|
|
160
|
+
if (installed) return;
|
|
161
|
+
installed = true;
|
|
162
|
+
|
|
163
|
+
if (isBun()) {
|
|
164
|
+
const originalFetch = globalThis.fetch;
|
|
165
|
+
const wrapped = (
|
|
166
|
+
input: Parameters<typeof fetch>[0],
|
|
167
|
+
init?: Parameters<typeof fetch>[1],
|
|
168
|
+
): ReturnType<typeof fetch> => {
|
|
169
|
+
const tlsOptions = (
|
|
170
|
+
init as
|
|
171
|
+
{ tls?: { checkServerIdentity?: CheckServerIdentity } } | undefined
|
|
172
|
+
)?.tls;
|
|
173
|
+
const callerCheck = tlsOptions?.checkServerIdentity;
|
|
174
|
+
const combined: CheckServerIdentity = callerCheck
|
|
175
|
+
? (hostname, cert) =>
|
|
176
|
+
checkServerIdentity(hostname, cert) ?? callerCheck(hostname, cert)
|
|
177
|
+
: (checkServerIdentity as unknown as CheckServerIdentity);
|
|
178
|
+
return originalFetch(input, {
|
|
179
|
+
...init,
|
|
180
|
+
tls: { ...tlsOptions, checkServerIdentity: combined },
|
|
181
|
+
} as RequestInit);
|
|
182
|
+
};
|
|
183
|
+
globalThis.fetch = Object.assign(wrapped, originalFetch);
|
|
184
|
+
} else {
|
|
185
|
+
tls.checkServerIdentity =
|
|
186
|
+
checkServerIdentity as unknown as CheckServerIdentity;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isBun(): boolean {
|
|
191
|
+
return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseVersion(version: string): [number, number, number] | undefined {
|
|
195
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
196
|
+
if (!match) return undefined;
|
|
197
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function compareVersions(
|
|
201
|
+
a: readonly [number, number, number],
|
|
202
|
+
b: readonly [number, number, number],
|
|
203
|
+
): number {
|
|
204
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isNodeVersionPatched(version: string): boolean {
|
|
208
|
+
const parsed = parseVersion(version);
|
|
209
|
+
if (!parsed) return false;
|
|
210
|
+
const fixed = FIXED_NODE_VERSIONS[parsed[0]];
|
|
211
|
+
if (fixed) return compareVersions(parsed, fixed) >= 0;
|
|
212
|
+
const newestLine = Math.max(...Object.keys(FIXED_NODE_VERSIONS).map(Number));
|
|
213
|
+
return parsed[0] > newestLine;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isBunVersionPatched(version: string): boolean {
|
|
217
|
+
if (FIXED_BUN_VERSION === undefined) return false;
|
|
218
|
+
const parsed = parseVersion(version);
|
|
219
|
+
const fixed = parseVersion(FIXED_BUN_VERSION);
|
|
220
|
+
return !!parsed && !!fixed && compareVersions(parsed, fixed) >= 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns a warning when the current runtime does not block the compromised
|
|
225
|
+
* eMudhra intermediates natively, or undefined when it does.
|
|
226
|
+
*/
|
|
227
|
+
export function unpatchedRuntimeWarning(
|
|
228
|
+
versions: {
|
|
229
|
+
node?: string | undefined;
|
|
230
|
+
bun?: string | undefined;
|
|
231
|
+
} = process.versions,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
let runtime: string;
|
|
234
|
+
let upgrade: string;
|
|
235
|
+
if (versions.bun !== undefined) {
|
|
236
|
+
if (isBunVersionPatched(versions.bun)) return undefined;
|
|
237
|
+
runtime = `Bun ${versions.bun}`;
|
|
238
|
+
upgrade =
|
|
239
|
+
"run the server with Node.js 22.23.4, 24.22.1, 26.12.1 or later, or use the Docker image";
|
|
240
|
+
} else if (versions.node !== undefined) {
|
|
241
|
+
if (isNodeVersionPatched(versions.node)) return undefined;
|
|
242
|
+
runtime = `Node.js ${versions.node}`;
|
|
243
|
+
upgrade = "upgrade to Node.js 22.23.4, 24.22.1, 26.12.1 or later";
|
|
244
|
+
} else {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
return (
|
|
248
|
+
`WARNING: ${runtime} does not block the compromised eMudhra intermediate CAs. ` +
|
|
249
|
+
"The LaunchDarkly MCP server rejects them for its own requests, but the runtime does not; " +
|
|
250
|
+
`${upgrade}. See https://github.com/launchdarkly/mcp-server#runtime-requirements\n`
|
|
251
|
+
);
|
|
252
|
+
}
|