Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3623,6 +3623,13 @@ impl VirtualMachine {
self.transpiler.env_mut().get_tls_reject_unauthorized()
}

/// The TLS 1.2 list assigned through `tls.DEFAULT_CIPHERS`, `None` when never assigned.
pub fn tls_default_ciphers(&self) -> Option<&[u8]> {
self.rare_data
.as_deref()
.and_then(RareData::tls_default_ciphers)
}

/// Registers a spawned subprocess with the auto-killer.
pub fn on_subprocess_spawn(&mut self, process: core::ptr::NonNull<bun_spawn::Process>) {
self.auto_killer.on_subprocess_spawn(process);
Expand Down
8 changes: 4 additions & 4 deletions src/runtime/api/bun/SecureContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

use crate::crypto::boringssl_jsc::err_to_js;
use crate::socket::uws_jsc::create_bun_socket_error_to_js;
use crate::socket::{SSLConfig, SSLConfigFromJs};
use crate::socket::{SSLConfig, SSLConfigFromJs, tls_true_defaults};
use bun_boringssl_sys as boringssl;
use bun_core::EncodedSlice;
use bun_jsc::EncodedSliceJsc as _;
Expand Down Expand Up @@ -73,7 +73,7 @@ impl SecureContext {

// SAFETY: `bun_vm()` returns the live per-global VM pointer; valid for the call.
let vm = global.bun_vm().as_mut();
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(SSLConfig::zero);
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(|| tls_true_defaults(vm));
// `defer config.deinit()` — handled by Drop.

SecureContext::create(global, &config)
Expand Down Expand Up @@ -231,7 +231,7 @@ impl SecureContext {

// SAFETY: `bun_vm()` returns the live per-global VM pointer; valid for the call.
let vm = global.bun_vm().as_mut();
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(SSLConfig::zero);
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(|| tls_true_defaults(vm));
// `defer config.deinit()` — handled by Drop.

let ctx_opts = config.as_usockets();
Expand Down Expand Up @@ -271,7 +271,7 @@ impl SecureContext {

// SAFETY: `bun_vm()` returns the live per-global VM pointer; valid for the call.
let vm = global.bun_vm().as_mut();
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(SSLConfig::zero);
let config = SSLConfig::from_js(vm, global, opts)?.unwrap_or_else(|| tls_true_defaults(vm));
// `defer config.deinit()` — handled by Drop.

let ctx_opts = config.as_usockets();
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/socket/SSLConfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ impl SSLConfigFromJs for SSLConfig {
result.ssl_ciphers = zbox_into_raw(&ciphers.to_owned_slice_z());
result.is_using_default_ciphers = false;
result.requires_custom_request_ctx = true;
} else {
apply_default_ciphers(vm, &mut result);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

result.client_renegotiation_limit = generated.client_renegotiation_limit;
Comment thread
robobun marked this conversation as resolved.
Expand All @@ -271,9 +273,23 @@ impl SSLConfigFromJs for SSLConfig {
pub fn tls_true_defaults(vm: &VirtualMachine) -> SSLConfig {
let mut cfg = SSLConfig::zero();
cfg.reject_unauthorized = vm.get_tls_reject_unauthorized() as i32;
apply_default_ciphers(vm, &mut cfg);
cfg
}

/// No `ciphers` option means `tls.DEFAULT_CIPHERS`, like Node's configSecureContext.
fn apply_default_ciphers(vm: &VirtualMachine, cfg: &mut SSLConfig) {
let Some(ciphers) = vm.tls_default_ciphers() else {
return;
};
cfg.ssl_ciphers = dupe_z(ciphers);
// An empty TLS 1.2 list would leave BoringSSL's built-in one in effect.
let tls1_3 = i32::from(bun_boringssl_sys::TLS1_3_VERSION);
if ciphers.is_empty() && cfg.ssl_min_version < tls1_3 {
cfg.ssl_min_version = tls1_3;
}
}

/// Whether a new TLS socket must enforce `rejectUnauthorized`: close the
/// connection when the peer certificate fails verification.
pub fn resolve_reject_unauthorized(
Expand Down
76 changes: 76 additions & 0 deletions test/js/node/tls/node-tls-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2685,3 +2685,79 @@ describe("pauseOnConnect", () => {
}
});
});

// tls.DEFAULT_CIPHERS is the process-wide cipher policy. Node applies it to
// every secure context built without an explicit `ciphers`, so a server
// created after the assignment refuses the suites the policy excludes.
it("tls.DEFAULT_CIPHERS applies to servers created without a ciphers option", async () => {
const script = `
import tls from "node:tls";
import https from "node:https";
import { once } from "node:events";
const cert = ${JSON.stringify({ key: cert1.key, cert: cert1.cert })};

function probe(port, ciphers) {
return new Promise(resolve => {
const c = tls.connect({ port, host: "127.0.0.1", ciphers, maxVersion: "TLSv1.2", rejectUnauthorized: false });
c.on("secureConnect", () => {
const name = c.getCipher().name;
c.end();
resolve("ok:" + name);
});
c.on("error", e => resolve("err:" + e.code));
});
}
async function listen(server) {
server.listen(0, "127.0.0.1");
await once(server, "listening");
return server;
}
// The policy in effect when a server starts listening is the one it keeps.
tls.DEFAULT_CIPHERS = "ECDHE-RSA-AES128-GCM-SHA256";
const tlsServer = await listen(tls.createServer(cert, s => s.end("x")));
const httpsServer = await listen(https.createServer(cert, (req, res) => res.end("x")));
const bunServer = Bun.serve({ port: 0, tls: cert, fetch: () => new Response("x") });
tls.DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256";
const tls13OnlyServer = await listen(tls.createServer(cert, s => s.end("x")));

const ports = {
tls: tlsServer.address().port,
https: httpsServer.address().port,
serve: bunServer.port,
tls13Only: tls13OnlyServer.address().port,
};
const results = {};
await Promise.all(
Object.entries(ports).map(async ([name, port]) => {
const [excluded, allowed] = await Promise.all([
probe(port, "ECDHE-RSA-AES256-GCM-SHA384"),
probe(port, "ECDHE-RSA-AES128-GCM-SHA256"),
]);
results[name] = { excluded, allowed };
}),
);
tlsServer.close();
httpsServer.close();
tls13OnlyServer.close();
await bunServer.stop(true);
console.log(JSON.stringify(results));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout.trim())).toEqual({
tls: { excluded: "err:ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE", allowed: "ok:ECDHE-RSA-AES128-GCM-SHA256" },
https: { excluded: "err:ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE", allowed: "ok:ECDHE-RSA-AES128-GCM-SHA256" },
serve: { excluded: "err:ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE", allowed: "ok:ECDHE-RSA-AES128-GCM-SHA256" },
tls13Only: {
excluded: "err:ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION",
allowed: "err:ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION",
},
});
expect(exitCode).toBe(0);
});
Loading