-
Notifications
You must be signed in to change notification settings - Fork 4k
Open
Labels
Description
What version of Bun is running?
1.1.43+76800b049
What platform is your computer?
Darwin 23.6.0 arm64 arm
What steps can reproduce the bug?
mTLS-demo.ts:
/*************************************
* bun run mTLS-demo.ts
* Given Output: Server response: Client certificate required
*
* OR NODE
*
* bun build mTLS-demo.ts --outfile mtls-node.js --target=node
* node mtls-node.js
* Expected output: Server response: Hello, mutual TLS world!
*
* Demonstrates how to:
* - Generate a CA (self-signed)
* - Generate Server + Client certs
* - Include SANs (Subject Alt Names) for localhost and 0.0.0.0
* - Spin up an mTLS Node server
* - Make a request using the client cert
*************************************/
import * as forge from "node-forge";
import * as https from "https";
import { IncomingMessage, ServerResponse } from "http";
/**
* The shape of our returned certs object.
*/
interface MTLSCerts {
ca: {
cert: string; // PEM-encoded CA cert
key: string; // PEM-encoded CA private key
};
server: {
cert: string; // PEM-encoded Server cert
key: string; // PEM-encoded Server private key
};
client: {
cert: string; // PEM-encoded Client cert
key: string; // PEM-encoded Client private key
};
}
/**
* Generates an RSA key pair (2048 bits).
*/
function generateKeyPair(): forge.pki.KeyPair {
return forge.pki.rsa.generateKeyPair({
bits: 2048,
e: 0x10001,
});
}
/**
* Certificate options interface for clarity.
*/
interface CertificateOptions {
commonName: string;
serialNumber: string;
issuerCert?: forge.pki.Certificate;
issuerKey?: forge.pki.PrivateKey;
daysValid?: number;
isCA?: boolean;
/**
* Optional Subject Alternative Names to include in the cert.
* Defaults to DNS:commonName if none provided.
*/
altNames?: forge.pki.CertificateExtensionSubjectAltName[];
}
/**
* Creates an X.509 certificate.
* If no issuer cert/key provided, it becomes self-signed (for CA).
*/
function createCertificate(
keyPair: forge.pki.KeyPair,
options: CertificateOptions
): forge.pki.Certificate {
const {
commonName,
serialNumber,
issuerCert,
issuerKey,
daysValid = 365,
isCA = false,
altNames,
} = options;
const cert = forge.pki.createCertificate();
cert.publicKey = keyPair.publicKey;
cert.serialNumber = serialNumber;
// Validity
const now = new Date();
cert.validity.notBefore = new Date(now.getTime() - 5 * 60 * 1000); // 5 min in the past
cert.validity.notAfter = new Date(
now.getTime() + daysValid * 24 * 60 * 60 * 1000
);
// If no issuer, self-signed for CA. Otherwise, use issuer's subject.
const issuerAttrs = issuerCert
? issuerCert.subject.attributes
: [
{ name: "commonName", value: commonName },
{ name: "countryName", value: "US" },
{ shortName: "ST", value: "CA" },
{ name: "organizationName", value: "MyOrg Inc." },
];
cert.setSubject([
{ name: "commonName", value: commonName },
{ name: "countryName", value: "US" },
{ shortName: "ST", value: "CA" },
{ name: "organizationName", value: "MyOrg Inc." },
]);
cert.setIssuer(issuerAttrs);
// Default altNames to DNS:commonName if not specified
const altNamesToUse = altNames?.length
? altNames
: [{ type: 2, value: commonName }];
// Basic constraints, key usage, alt names
cert.setExtensions([
{
name: "basicConstraints",
cA: isCA,
critical: isCA,
},
{
name: "keyUsage",
digitalSignature: true,
keyEncipherment: true,
dataEncipherment: true,
keyCertSign: isCA,
cRLSign: isCA,
},
{
name: "subjectAltName",
altNames: altNamesToUse,
},
]);
// Sign with issuer key (or self if CA)
const signingKey = issuerKey || keyPair.privateKey;
cert.sign(signingKey, forge.md.sha256.create());
return cert;
}
/**
* Generates a self-signed CA, plus server/client certificates
* in PEM format. Returns them in a single object.
*/
function generateMTLSCerts(): MTLSCerts {
// --- 1) CA (self-signed) ---
const caKeyPair = generateKeyPair();
const caCert = createCertificate(caKeyPair, {
commonName: "MyRootCA",
serialNumber: "01",
isCA: true,
daysValid: 3650, // 10-year CA
});
// --- 2) Server certificate (signed by CA) ---
// Subject Alt Names include server.example.com, localhost, 127.0.0.1, and 0.0.0.0
const serverKeyPair = generateKeyPair();
const serverCert = createCertificate(serverKeyPair, {
commonName: "server.example.com",
serialNumber: "02",
issuerCert: caCert,
issuerKey: caKeyPair.privateKey,
daysValid: 825, // ~2 years
altNames: [
{ type: 2, value: "server.example.com" }, // DNS
{ type: 2, value: "localhost" }, // DNS
{ type: 7, ip: "127.0.0.1" }, // IP
{ type: 7, ip: "0.0.0.0" }, // IP
],
});
// --- 3) Client certificate (signed by CA) ---
const clientKeyPair = generateKeyPair();
const clientCert = createCertificate(clientKeyPair, {
commonName: "client.example.com",
serialNumber: "03",
issuerCert: caCert,
issuerKey: caKeyPair.privateKey,
daysValid: 825,
});
return {
ca: {
cert: forge.pki.certificateToPem(caCert),
key: forge.pki.privateKeyToPem(caKeyPair.privateKey),
},
server: {
cert: forge.pki.certificateToPem(serverCert),
key: forge.pki.privateKeyToPem(serverKeyPair.privateKey),
},
client: {
cert: forge.pki.certificateToPem(clientCert),
key: forge.pki.privateKeyToPem(clientKeyPair.privateKey),
},
};
}
// -------------------------------------------------------------------
// MAIN: Start an HTTPS server requiring client cert (mTLS).
// Then make a request with our "client cert" to show it works.
// -------------------------------------------------------------------
async function main() {
// Generate PEM-encoded CA/server/client certs
const { ca, server, client } = generateMTLSCerts();
const port = 8443;
// Create the server with mTLS
const serverOptions: https.ServerOptions = {
key: server.key,
cert: server.cert,
// The server trusts our CA (so it’ll trust the client cert).
ca: ca.cert,
// Require client cert, otherwise reject.
requestCert: true,
rejectUnauthorized: true,
};
// Create the HTTPS server
const httpsServer = https.createServer(
serverOptions,
(req: IncomingMessage, res: ServerResponse) => {
if (req.socket.authorized) {
res.writeHead(200);
res.end("Hello, mutual TLS world!\n");
} else {
// If client cert wasn't validated
res.writeHead(401);
res.end("Client certificate required.\n");
}
}
);
httpsServer.listen(port, () => {
console.log(`mTLS server is listening on https://localhost:${port}`);
// Once the server is up, make a request using the client cert/key.
const clientOptions: https.RequestOptions = {
hostname: "localhost",
port,
method: "GET",
path: "/",
// Provide the client's key & cert
key: client.key,
cert: client.cert,
// The client trusts the same CA
ca: ca.cert,
rejectUnauthorized: true,
};
const req = https.request(clientOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
console.log("Server response:", data);
// Cleanly close the server for this demo
httpsServer.close();
});
});
req.on("error", (err) => {
console.error("Client request error:", err);
httpsServer.close();
});
req.end();
});
}
// Run the main function
main();package.json:
{
"type": "module",
"dependencies": {
"node-forge": "^1.3.1"
}
}What is the expected behavior?
On Bun, should be able to start the server and request to it , getting the message "Server response: Hello, mutual TLS world!"
What do you see instead?
Getting "Server response: Client certificate required". It seems like req.socket.authorized is always undefined.
Additional information
Was thinking of doing a server management utility, and mTLS would be great for internal cross-server communication, combined with the single binary option Bun has.
Shiv-hcr, JeffersonCarvalh0, andrenth, Foorack and psi-4ward