diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index 05833d7eea..dbc10d8234 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -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, diff --git a/test/regression/issue/26125.test.ts b/test/regression/issue/26125.test.ts new file mode 100644 index 0000000000..96187cf864 --- /dev/null +++ b/test/regression/issue/26125.test.ts @@ -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((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(); + } + }); +});