mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
502 lines
13 KiB
TypeScript
502 lines
13 KiB
TypeScript
// This test ensures that when a TLS connection is established, the server
|
|
// selects the most recently added SecureContext that matches the servername.
|
|
|
|
import { describe, expect, it } from "bun:test";
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { AddressInfo } from "node:net";
|
|
import { join } from "node:path";
|
|
import tls from "node:tls";
|
|
|
|
function loadPEM(filename: string) {
|
|
return readFileSync(join(import.meta.dir, "fixtures", filename)).toString();
|
|
}
|
|
|
|
const agent1Cert = loadPEM("agent1-cert.pem");
|
|
const agent1Key = loadPEM("agent1-key.pem");
|
|
|
|
const agent2Cert = loadPEM("agent2-cert.pem");
|
|
const agent2Key = loadPEM("agent2-key.pem");
|
|
|
|
const agent3Cert = loadPEM("agent3-cert.pem");
|
|
const agent3Key = loadPEM("agent3-key.pem");
|
|
|
|
const agent6Cert = loadPEM("agent6-cert.pem");
|
|
const agent6Key = loadPEM("agent6-key.pem");
|
|
|
|
const ca1 = loadPEM("ca1-cert.pem");
|
|
const ca2 = loadPEM("ca2-cert.pem");
|
|
|
|
const SNIContexts = {
|
|
"a.example.com": {
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
},
|
|
"asterisk.test.com": {
|
|
key: agent3Key,
|
|
cert: agent3Cert,
|
|
},
|
|
"chain.example.com": {
|
|
key: agent6Key,
|
|
// NOTE: Contains ca3 chain cert
|
|
cert: agent6Cert,
|
|
},
|
|
};
|
|
|
|
const serverOptions = {
|
|
key: agent2Key,
|
|
cert: agent2Cert,
|
|
requestCert: true,
|
|
rejectUnauthorized: false,
|
|
};
|
|
|
|
const badSecureContext = {
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
ca: [ca2],
|
|
};
|
|
|
|
const goodSecureContext = {
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
ca: [ca1],
|
|
};
|
|
|
|
describe("tls.Server", () => {
|
|
it("addContext", async () => {
|
|
const serverOptions = {
|
|
key: agent2Key,
|
|
cert: agent2Cert,
|
|
ca: [ca2],
|
|
requestCert: true,
|
|
rejectUnauthorized: false,
|
|
};
|
|
|
|
let connections = 0;
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
let listening_server: tls.Server | null = null;
|
|
try {
|
|
listening_server = tls.createServer(serverOptions, async c => {
|
|
try {
|
|
if (++connections === 3) {
|
|
resolve();
|
|
}
|
|
//@ts-ignore
|
|
if (c.servername === "unknowncontext") {
|
|
expect(c.authorized).toBe(false);
|
|
return;
|
|
}
|
|
expect(c.authorized).toBe(true);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
const server = listening_server as tls.Server;
|
|
|
|
const secureContext = {
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
ca: [ca1],
|
|
};
|
|
server.addContext("context1", secureContext);
|
|
//@ts-ignore
|
|
server.addContext("context2", tls.createSecureContext(secureContext));
|
|
|
|
const clientOptionsBase = {
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
ca: [ca1],
|
|
rejectUnauthorized: false,
|
|
};
|
|
|
|
function connect(servername: string) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const client = tls.connect(
|
|
{
|
|
...clientOptionsBase,
|
|
port: (server.address() as AddressInfo).port,
|
|
host: "127.0.0.1",
|
|
servername,
|
|
},
|
|
() => {
|
|
client.end();
|
|
resolve();
|
|
},
|
|
);
|
|
client.on("error", reject);
|
|
});
|
|
}
|
|
|
|
server.listen(0, async () => {
|
|
await connect("context1");
|
|
await connect("context2");
|
|
await connect("unknowncontext");
|
|
});
|
|
await promise;
|
|
} finally {
|
|
listening_server?.close();
|
|
}
|
|
});
|
|
|
|
it("should select the most recently added SecureContext", async () => {
|
|
let listening_server: tls.Server | null = null;
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
try {
|
|
const timeout = setTimeout(() => {
|
|
reject("timeout");
|
|
}, 3000);
|
|
listening_server = tls.createServer(serverOptions, c => {
|
|
try {
|
|
// The 'a' and 'b' subdomains are used to distinguish between client
|
|
// connections.
|
|
// Connection to subdomain 'a' is made when the 'bad' secure context is
|
|
// the only one in use.
|
|
//@ts-ignore
|
|
if ("a.example.com" === c.servername) {
|
|
expect(c.authorized).toBe(false);
|
|
}
|
|
// Connection to subdomain 'b' is made after the 'good' context has been
|
|
// added.
|
|
//@ts-ignore
|
|
if ("b.example.com" === c.servername) {
|
|
expect(c.authorized).toBe(true);
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
}
|
|
} catch (e) {
|
|
clearTimeout(timeout);
|
|
reject(e);
|
|
}
|
|
});
|
|
const server = listening_server as tls.Server;
|
|
// 1. Add the 'bad' secure context. A connection using this context will not be
|
|
// authorized.
|
|
server.addContext("*.example.com", badSecureContext);
|
|
|
|
server.listen(0, () => {
|
|
const options = {
|
|
port: (server?.address() as AddressInfo).port,
|
|
host: "127.0.0.1",
|
|
key: agent1Key,
|
|
cert: agent1Cert,
|
|
ca: [ca1],
|
|
servername: "a.example.com",
|
|
rejectUnauthorized: false,
|
|
};
|
|
|
|
// 2. Make a connection using servername 'a.example.com'. Since a 'bad'
|
|
// secure context is used, this connection should not be authorized.
|
|
const client = tls.connect(options, () => {
|
|
client.end();
|
|
});
|
|
|
|
client.on("close", () => {
|
|
// 3. Add a 'good' secure context.
|
|
server.addContext("*.example.com", goodSecureContext);
|
|
|
|
options.servername = "b.example.com";
|
|
// 4. Make a connection using servername 'b.example.com'. This connection
|
|
// should be authorized because the 'good' secure context is the most
|
|
// recently added matching context.
|
|
|
|
const other = tls.connect(options, () => {
|
|
other.end();
|
|
});
|
|
|
|
other.on("close", () => {
|
|
// 5. Make another connection using servername 'b.example.com' to ensure
|
|
// that the array of secure contexts is not reversed in place with each
|
|
// SNICallback call, as someone might be tempted to refactor this piece of
|
|
// code by using Array.prototype.reverse() method.
|
|
const onemore = tls.connect(options, () => {
|
|
onemore.end();
|
|
});
|
|
|
|
onemore.on("close", () => {
|
|
server.close();
|
|
});
|
|
onemore.on("error", reject);
|
|
});
|
|
|
|
other.on("error", reject);
|
|
});
|
|
client.on("error", reject);
|
|
});
|
|
server.on("error", reject);
|
|
server.on("clientError", reject);
|
|
|
|
await promise;
|
|
} finally {
|
|
listening_server?.close();
|
|
}
|
|
});
|
|
|
|
function testCA(ca: Array<string>) {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const server = tls.createServer({ ca, cert: agent3Cert, key: agent3Key });
|
|
|
|
server.addContext("agent3", { ca, cert: agent3Cert, key: agent3Key });
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const options = {
|
|
servername: "agent3",
|
|
host: "127.0.0.1",
|
|
port: (server.address() as AddressInfo).port,
|
|
ca,
|
|
};
|
|
var authorized = false;
|
|
const socket = tls.connect(options, () => {
|
|
authorized = socket.authorized;
|
|
socket.end();
|
|
});
|
|
|
|
socket.on("error", reject);
|
|
socket.on("close", () => {
|
|
server.close(() => {
|
|
resolve(authorized);
|
|
});
|
|
});
|
|
});
|
|
return promise;
|
|
}
|
|
it("should allow multiple CA", async () => {
|
|
// Verify that multiple CA certificates can be provided, and that for
|
|
// convenience that can also be in newline-separated strings.
|
|
expect(await testCA([ca1, ca2])).toBeTrue();
|
|
});
|
|
|
|
it("should allow multiple CA in newline-separated strings", async () => {
|
|
expect(await testCA([ca2 + "\n" + ca1])).toBeTrue();
|
|
});
|
|
|
|
function testClient(options: any, clientResult: boolean, serverResult: string) {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const server = tls.createServer(serverOptions, c => {
|
|
try {
|
|
//@ts-ignore
|
|
expect(c.servername).toBe(serverResult);
|
|
expect(c.authorized).toBe(false);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
|
|
server.addContext("a.example.com", SNIContexts["a.example.com"]);
|
|
server.addContext("*.test.com", SNIContexts["asterisk.test.com"]);
|
|
server.addContext("chain.example.com", SNIContexts["chain.example.com"]);
|
|
|
|
server.on("tlsClientError", reject);
|
|
|
|
server.listen(0, () => {
|
|
const client = tls.connect(
|
|
{
|
|
...options,
|
|
port: (server.address() as AddressInfo).port,
|
|
host: "127.0.0.1",
|
|
rejectUnauthorized: false,
|
|
},
|
|
() => {
|
|
const result =
|
|
//@ts-ignore
|
|
client.authorizationError && client.authorizationError.indexOf("ERR_TLS_CERT_ALTNAME_INVALID") !== -1;
|
|
if (result !== clientResult) {
|
|
reject(new Error(`Expected ${clientResult}, got ${result} in ${options.servername}`));
|
|
} else {
|
|
resolve();
|
|
}
|
|
client.end();
|
|
},
|
|
);
|
|
client.on("error", reject);
|
|
client.on("close", () => {
|
|
server.close();
|
|
});
|
|
});
|
|
return promise;
|
|
}
|
|
it("SNI tls.Server + tls.connect", async () => {
|
|
await testClient(
|
|
{
|
|
ca: [ca1],
|
|
servername: "a.example.com",
|
|
},
|
|
true,
|
|
"a.example.com",
|
|
);
|
|
await testClient(
|
|
{
|
|
ca: [ca2],
|
|
servername: "b.test.com",
|
|
},
|
|
true,
|
|
"b.test.com",
|
|
);
|
|
await testClient(
|
|
{
|
|
ca: [ca2],
|
|
servername: "a.b.test.com",
|
|
},
|
|
false,
|
|
"a.b.test.com",
|
|
);
|
|
await testClient(
|
|
{
|
|
ca: [ca1],
|
|
servername: "c.wrong.com",
|
|
},
|
|
false,
|
|
"c.wrong.com",
|
|
);
|
|
await testClient(
|
|
{
|
|
ca: [ca1],
|
|
servername: "chain.example.com",
|
|
},
|
|
true,
|
|
"chain.example.com",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Bun.serve SNI", () => {
|
|
function doClientRequest(options: any) {
|
|
return new Promise((resolve, reject) => {
|
|
const client = tls.connect(
|
|
{
|
|
...options,
|
|
rejectUnauthorized: false,
|
|
},
|
|
() => {
|
|
resolve(
|
|
//@ts-ignore
|
|
client.authorizationError && client.authorizationError.indexOf("ERR_TLS_CERT_ALTNAME_INVALID") !== -1,
|
|
);
|
|
},
|
|
);
|
|
client.on("close", resolve);
|
|
client.on("error", reject);
|
|
});
|
|
}
|
|
it("single SNI", async () => {
|
|
{
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: {
|
|
...SNIContexts["asterisk.test.com"],
|
|
serverName: "*.test.com",
|
|
},
|
|
fetch(req, res) {
|
|
return new Response(new URL(req.url).hostname);
|
|
},
|
|
});
|
|
for (const servername of ["a.test.com", "b.test.com", "c.test.com"]) {
|
|
const client = await doClientRequest({
|
|
...SNIContexts["asterisk.test.com"],
|
|
port: server.port,
|
|
ca: [ca2],
|
|
servername,
|
|
});
|
|
expect(client).toBe(true);
|
|
}
|
|
{
|
|
const client = await doClientRequest({
|
|
...goodSecureContext,
|
|
port: server.port,
|
|
servername: "a.example.com",
|
|
});
|
|
expect(client).toBe(false);
|
|
}
|
|
}
|
|
{
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: {
|
|
...goodSecureContext,
|
|
serverName: "*.example.com",
|
|
},
|
|
fetch(req, res) {
|
|
return new Response(new URL(req.url).hostname);
|
|
},
|
|
});
|
|
{
|
|
const client = await doClientRequest({
|
|
...goodSecureContext,
|
|
port: server.port,
|
|
servername: "a.example.com",
|
|
});
|
|
expect(client).toBe(true);
|
|
}
|
|
|
|
{
|
|
const client = await doClientRequest({
|
|
...goodSecureContext,
|
|
port: server.port,
|
|
servername: "b.example.com",
|
|
});
|
|
expect(client).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
it("multiple SNI", async () => {
|
|
{
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: [
|
|
serverOptions,
|
|
{
|
|
serverName: "a.example.com",
|
|
...SNIContexts["a.example.com"],
|
|
},
|
|
{
|
|
serverName: "*.test.com",
|
|
...SNIContexts["asterisk.test.com"],
|
|
},
|
|
{
|
|
serverName: "chain.example.com",
|
|
...SNIContexts["chain.example.com"],
|
|
},
|
|
],
|
|
fetch(req, res) {
|
|
return new Response("OK");
|
|
},
|
|
});
|
|
expect(
|
|
await doClientRequest({
|
|
ca: [ca1],
|
|
servername: "a.example.com",
|
|
port: server.port,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
await doClientRequest({
|
|
ca: [ca2],
|
|
servername: "b.test.com",
|
|
port: server.port,
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
await doClientRequest({
|
|
ca: [ca2],
|
|
servername: "a.b.test.com",
|
|
port: server.port,
|
|
}),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
await doClientRequest({
|
|
ca: [ca1],
|
|
servername: "c.wrong.com",
|
|
port: server.port,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
await doClientRequest({
|
|
ca: [ca1],
|
|
servername: "chain.example.com",
|
|
port: server.port,
|
|
}),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
});
|