mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(http): use correct client certificate for mTLS fetch() requests (#26129)
## Summary - Fixes bug where `fetch()` with mTLS would use the first client certificate for all subsequent requests to the same host, ignoring per-request `tls` options - Corrects `SSLConfig.isSame()` to properly compare all fields (was incorrectly returning early when both optional fields were null) - Sets `disable_keepalive=true` when reusing cached SSL contexts to prevent socket pooling issues Fixes #26125 ## Test plan - [x] Added regression test `test/regression/issue/26125.test.ts` - [x] Verified test fails with system Bun 1.3.6 (demonstrates the bug) - [x] Verified test passes with patched build 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,16 +117,24 @@ pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool {
|
||||
const second = @field(other, field.name);
|
||||
switch (field.type) {
|
||||
?[*:0]const u8 => {
|
||||
const a = first orelse return second == null;
|
||||
const b = second orelse return false;
|
||||
if (!stringsEqual(a, b)) return false;
|
||||
// Compare optional single strings
|
||||
if (first) |a| {
|
||||
const b = second orelse return false;
|
||||
if (!stringsEqual(a, b)) return false;
|
||||
} else {
|
||||
if (second != null) return false;
|
||||
}
|
||||
},
|
||||
?[][*:0]const u8 => {
|
||||
const slice1 = first orelse return second == null;
|
||||
const slice2 = second orelse return false;
|
||||
if (slice1.len != slice2.len) return false;
|
||||
for (slice1, slice2) |a, b| {
|
||||
if (!stringsEqual(a, b)) return false;
|
||||
// Compare optional arrays of strings (e.g., key, cert, ca)
|
||||
if (first) |slice1| {
|
||||
const slice2 = second orelse return false;
|
||||
if (slice1.len != slice2.len) return false;
|
||||
for (slice1, slice2) |a, b| {
|
||||
if (!stringsEqual(a, b)) return false;
|
||||
}
|
||||
} else {
|
||||
if (second != null) return false;
|
||||
}
|
||||
},
|
||||
else => if (first != second) return false,
|
||||
|
||||
128
test/regression/issue/26125.test.ts
Normal file
128
test/regression/issue/26125.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "fs";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { join } from "path";
|
||||
import tls from "tls";
|
||||
|
||||
// Load certificates from existing fixtures
|
||||
const fixturesDir = join(import.meta.dir, "..", "..", "js", "node", "tls", "fixtures");
|
||||
|
||||
// CA certs
|
||||
const ca1 = readFileSync(join(fixturesDir, "ca1-cert.pem"), "utf8");
|
||||
const ca2 = readFileSync(join(fixturesDir, "ca2-cert.pem"), "utf8");
|
||||
|
||||
// Server cert (agent1, signed by ca1)
|
||||
const serverKey = readFileSync(join(fixturesDir, "agent1-key.pem"), "utf8");
|
||||
const serverCert = readFileSync(join(fixturesDir, "agent1-cert.pem"), "utf8");
|
||||
|
||||
// Client 1: agent1 (CN=agent1, signed by ca1)
|
||||
const client1 = {
|
||||
name: "agent1",
|
||||
key: readFileSync(join(fixturesDir, "agent1-key.pem"), "utf8"),
|
||||
cert: readFileSync(join(fixturesDir, "agent1-cert.pem"), "utf8"),
|
||||
};
|
||||
|
||||
// Client 2: agent3 (CN=agent3, signed by ca2)
|
||||
const client2 = {
|
||||
name: "agent3",
|
||||
key: readFileSync(join(fixturesDir, "agent3-key.pem"), "utf8"),
|
||||
cert: readFileSync(join(fixturesDir, "agent3-cert.pem"), "utf8"),
|
||||
};
|
||||
|
||||
// Combined CA to accept both client certs
|
||||
const combinedCA = ca1 + "\n" + ca2;
|
||||
|
||||
describe("GitHub issue #26125: mTLS client certificate switching", () => {
|
||||
test("fetch() uses correct client certificate for each request when switching between certificates", async () => {
|
||||
const clientCNs: string[] = [];
|
||||
|
||||
// Create an mTLS server that records the client certificate CN
|
||||
const server = tls.createServer(
|
||||
{
|
||||
key: serverKey,
|
||||
cert: serverCert,
|
||||
ca: combinedCA,
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
socket => {
|
||||
const peerCert = socket.getPeerCertificate();
|
||||
const cn = peerCert?.subject?.CN || "unknown";
|
||||
clientCNs.push(cn);
|
||||
|
||||
// Send HTTP response
|
||||
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK");
|
||||
socket.end();
|
||||
},
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.on("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
const url = `https://127.0.0.1:${port}/`;
|
||||
|
||||
// Custom checkServerIdentity since cert is for agent1, not 127.0.0.1
|
||||
const checkServerIdentity = () => undefined;
|
||||
|
||||
try {
|
||||
// Test sequence: alternate between client certificates
|
||||
// If the bug exists, connection pooling will reuse the first certificate's connection
|
||||
|
||||
// Request 1: client1 (agent1)
|
||||
const res1 = await fetch(url, {
|
||||
tls: {
|
||||
ca: ca1,
|
||||
key: client1.key,
|
||||
cert: client1.cert,
|
||||
checkServerIdentity,
|
||||
},
|
||||
});
|
||||
expect(res1.status).toBe(200);
|
||||
await res1.text();
|
||||
|
||||
// Request 2: client2 (agent3) - should use agent3's certificate
|
||||
const res2 = await fetch(url, {
|
||||
tls: {
|
||||
ca: ca1,
|
||||
key: client2.key,
|
||||
cert: client2.cert,
|
||||
checkServerIdentity,
|
||||
},
|
||||
});
|
||||
expect(res2.status).toBe(200);
|
||||
await res2.text();
|
||||
|
||||
// Request 3: client1 (agent1) again
|
||||
const res3 = await fetch(url, {
|
||||
tls: {
|
||||
ca: ca1,
|
||||
key: client1.key,
|
||||
cert: client1.cert,
|
||||
checkServerIdentity,
|
||||
},
|
||||
});
|
||||
expect(res3.status).toBe(200);
|
||||
await res3.text();
|
||||
|
||||
// Request 4: client2 (agent3) again
|
||||
const res4 = await fetch(url, {
|
||||
tls: {
|
||||
ca: ca1,
|
||||
key: client2.key,
|
||||
cert: client2.cert,
|
||||
checkServerIdentity,
|
||||
},
|
||||
});
|
||||
expect(res4.status).toBe(200);
|
||||
await res4.text();
|
||||
|
||||
// Verify the correct certificates were used for each request
|
||||
expect(clientCNs).toEqual(["agent1", "agent3", "agent1", "agent3"]);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user