Files
bun-src/test/js/node/tls/test-node-extra-ca-certs.test.ts
2026-08-27 21:09:14 +00:00

173 lines
5.9 KiB
TypeScript

import { spawn } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
describe("NODE_EXTRA_CA_CERTS", () => {
test("loads additional certificates from file", async () => {
// Create a test certificate file
const testCert = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKLdQVPy90WjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgwNDEwMDgwNzQ4WhcNMjgwNDA3MDgwNzQ4WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAyOB7tY2Uo2lTNjJgGEhJAVZDWnHbLjbmTMP4pSXLlNMr9KdyaKE+J3xn
xAz7TbGPHUBH5dqMzlWqEkZxcY9u9GL19SJPpC7dl8K8V5dKBwvgOubcLp4qLvZU
-----END CERTIFICATE-----`;
await using dir = tempDir("test-extra-ca", {
"extra-ca.pem": testCert,
"test.js": `console.log('OK');`,
});
const certPath = join(dir, "extra-ca.pem");
// Test that NODE_EXTRA_CA_CERTS loads the certificate
await using proc = spawn({
cmd: [bunExe(), "test.js"],
env: { ...bunEnv, NODE_EXTRA_CA_CERTS: certPath },
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("OK");
});
test("handles missing certificate file gracefully", async () => {
await using dir = tempDir("test-missing-ca", {
"test.js": `console.log('OK');`,
});
const nonExistentPath = join(dir, "non-existent.pem");
// Test that missing file doesn't crash the process
await using proc = spawn({
cmd: [bunExe(), "test.js"],
env: { ...bunEnv, NODE_EXTRA_CA_CERTS: nonExistentPath },
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Process should still run successfully even with missing cert file
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("OK");
// Bun may or may not warn about the missing file in stderr
// The important thing is that the process doesn't crash
});
test("works with both NODE_EXTRA_CA_CERTS and --use-system-ca", async () => {
const testCert = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKLdQVPy90WjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgwNDEwMDgwNzQ4WhcNMjgwNDA3MDgwNzQ4WjBF
-----END CERTIFICATE-----`;
await using dir = tempDir("test-extra-and-system", {
"extra-ca.pem": testCert,
"test.js": `console.log('OK');`,
});
const certPath = join(dir, "extra-ca.pem");
// Test that both work together
await using proc = spawn({
cmd: [bunExe(), "--use-system-ca", "test.js"],
env: { ...bunEnv, NODE_EXTRA_CA_CERTS: certPath },
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("OK");
});
});
test("explicit ca option replaces the default trust store instead of appending to it", async () => {
const fixtures = join(import.meta.dir, "fixtures");
await using dir = tempDir("ca-replaces-default", {
"main.js": `
const tls = require("node:tls");
const fs = require("node:fs");
const path = require("node:path");
const fixtures = process.env.TLS_FIXTURES_DIR;
const read = name => fs.readFileSync(path.join(fixtures, name), "utf8");
// ca1 issued the server certificate AND is part of the default trust
// store for this process (via NODE_EXTRA_CA_CERTS).
const issuerCa = read("ca1-cert.pem");
// ca2 did not issue the server certificate.
const unrelatedCa = read("ca2-cert.pem");
const server = tls.createServer(
{ key: read("agent1-key.pem"), cert: read("agent1-cert.pem") },
socket => socket.end(),
);
function attempt(ca) {
return new Promise(resolve => {
const socket = tls.connect(
{
host: "127.0.0.1",
port: server.address().port,
ca,
rejectUnauthorized: true,
// The fixture cert is not issued for 127.0.0.1; this test is
// about chain validation, not hostname verification.
checkServerIdentity: () => undefined,
},
() => {
socket.end();
resolve("connected");
},
);
socket.on("error", () => resolve("rejected"));
});
}
server.listen(0, "127.0.0.1", async () => {
console.log("pinned-to-unrelated-ca", await attempt(unrelatedCa));
console.log("pinned-to-issuer", await attempt(issuerCa));
server.close();
});
`,
});
await using proc = spawn({
cmd: [bunExe(), "main.js"],
env: {
...bunEnv,
// Put the issuing CA into the *default* trust store. A connection that
// supplies its own `ca` must validate exclusively against that `ca` and
// must not fall back to the default store.
NODE_EXTRA_CA_CERTS: join(fixtures, "ca1-cert.pem"),
TLS_FIXTURES_DIR: fixtures,
},
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Pinning a CA that did not issue the server certificate must reject the
// connection, even though the actual issuer is present in the default store.
expect(stdout).toContain("pinned-to-unrelated-ca rejected");
// Pinning the actual issuer still connects.
expect(stdout).toContain("pinned-to-issuer connected");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});