Files
bun.sh/src/bun.js/api/server/SSLConfig.zig
Ciro Spaciari ed6f099e5e fix(tls) fix ciphers (#21545)
### What does this PR do?
Uses same ciphers than node.js for compatibility and do the same error
checking on empty ciphers
Fixes https://github.com/oven-sh/bun/issues/9425
Fixes https://github.com/oven-sh/bun/issues/21518
Fixes https://github.com/oven-sh/bun/issues/19859
Fixes https://github.com/oven-sh/bun/issues/18980

You can see more about redis ciphers here
https://redis.io/docs/latest/operate/rs/security/encryption/tls/ciphers/
this should fix redis related ciphers issues
### How did you verify your code works?
Tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-08-04 19:42:40 -07:00

636 lines
24 KiB
Zig

const SSLConfig = @This();
server_name: [*c]const u8 = null,
key_file_name: [*c]const u8 = null,
cert_file_name: [*c]const u8 = null,
ca_file_name: [*c]const u8 = null,
dh_params_file_name: [*c]const u8 = null,
passphrase: [*c]const u8 = null,
key: ?[][*c]const u8 = null,
key_count: u32 = 0,
cert: ?[][*c]const u8 = null,
cert_count: u32 = 0,
ca: ?[][*c]const u8 = null,
ca_count: u32 = 0,
secure_options: u32 = 0,
request_cert: i32 = 0,
reject_unauthorized: i32 = 0,
ssl_ciphers: ?[*:0]const u8 = null,
protos: ?[*:0]const u8 = null,
protos_len: usize = 0,
client_renegotiation_limit: u32 = 0,
client_renegotiation_window: u32 = 0,
requires_custom_request_ctx: bool = false,
is_using_default_ciphers: bool = true,
low_memory_mode: bool = false,
const BlobFileContentResult = struct {
data: [:0]const u8,
fn init(comptime fieldname: []const u8, js_obj: jsc.JSValue, global: *jsc.JSGlobalObject) bun.JSError!?BlobFileContentResult {
{
const body = try jsc.WebCore.Body.Value.fromJS(global, js_obj);
if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) {
var fs: jsc.Node.fs.NodeFS = .{};
const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated);
switch (read) {
.err => {
return global.throwValue(read.err.toJS(global));
},
else => {
const str = read.result.null_terminated;
if (str.len > 0) {
return .{ .data = str };
}
return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{});
},
}
}
}
return null;
}
};
pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions {
var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{};
if (this.key_file_name != null)
ctx_opts.key_file_name = this.key_file_name;
if (this.cert_file_name != null)
ctx_opts.cert_file_name = this.cert_file_name;
if (this.ca_file_name != null)
ctx_opts.ca_file_name = this.ca_file_name;
if (this.dh_params_file_name != null)
ctx_opts.dh_params_file_name = this.dh_params_file_name;
if (this.passphrase != null)
ctx_opts.passphrase = this.passphrase;
ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(this.low_memory_mode);
if (this.key) |key| {
ctx_opts.key = key.ptr;
ctx_opts.key_count = this.key_count;
}
if (this.cert) |cert| {
ctx_opts.cert = cert.ptr;
ctx_opts.cert_count = this.cert_count;
}
if (this.ca) |ca| {
ctx_opts.ca = ca.ptr;
ctx_opts.ca_count = this.ca_count;
}
if (this.ssl_ciphers != null) {
ctx_opts.ssl_ciphers = this.ssl_ciphers;
}
ctx_opts.request_cert = this.request_cert;
ctx_opts.reject_unauthorized = this.reject_unauthorized;
return ctx_opts;
}
pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool {
{ //strings
const fields = .{
"server_name",
"key_file_name",
"cert_file_name",
"ca_file_name",
"dh_params_file_name",
"passphrase",
"ssl_ciphers",
"protos",
};
inline for (fields) |field| {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
if (lhs != null and rhs != null) {
if (!stringsEqual(lhs, rhs))
return false;
} else if (lhs != null or rhs != null) {
return false;
}
}
}
{
//numbers
const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" };
inline for (fields) |field| {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
if (lhs != rhs)
return false;
}
}
{
// complex fields
const fields = .{ "key", "ca", "cert" };
inline for (fields) |field| {
const lhs_count = @field(thisConfig, field ++ "_count");
const rhs_count = @field(otherConfig, field ++ "_count");
if (lhs_count != rhs_count)
return false;
if (lhs_count > 0) {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
for (0..lhs_count) |i| {
if (!stringsEqual(lhs.?[i], rhs.?[i]))
return false;
}
}
}
}
return true;
}
fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool {
const lhs = bun.asByteSlice(a);
const rhs = bun.asByteSlice(b);
return strings.eqlLong(lhs, rhs, true);
}
pub fn deinit(this: *SSLConfig) void {
const fields = .{
"server_name",
"key_file_name",
"cert_file_name",
"ca_file_name",
"dh_params_file_name",
"passphrase",
"protos",
};
if (!this.is_using_default_ciphers) {
if (this.ssl_ciphers) |slice_ptr| {
const slice = std.mem.span(slice_ptr);
if (slice.len > 0) {
bun.freeSensitive(bun.default_allocator, slice);
}
}
}
inline for (fields) |field| {
if (@field(this, field)) |slice_ptr| {
const slice = std.mem.span(slice_ptr);
if (slice.len > 0) {
bun.freeSensitive(bun.default_allocator, slice);
}
@field(this, field) = "";
}
}
if (this.cert) |cert| {
for (0..this.cert_count) |i| {
const slice = std.mem.span(cert[i]);
if (slice.len > 0) {
bun.freeSensitive(bun.default_allocator, slice);
}
}
bun.default_allocator.free(cert);
this.cert = null;
}
if (this.key) |key| {
for (0..this.key_count) |i| {
const slice = std.mem.span(key[i]);
if (slice.len > 0) {
bun.freeSensitive(bun.default_allocator, slice);
}
}
bun.default_allocator.free(key);
this.key = null;
}
if (this.ca) |ca| {
for (0..this.ca_count) |i| {
const slice = std.mem.span(ca[i]);
if (slice.len > 0) {
bun.freeSensitive(bun.default_allocator, slice);
}
}
bun.default_allocator.free(ca);
this.ca = null;
}
}
pub const zero = SSLConfig{};
pub fn fromJS(vm: *jsc.VirtualMachine, global: *jsc.JSGlobalObject, obj: jsc.JSValue) bun.JSError!?SSLConfig {
var result = zero;
errdefer result.deinit();
var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator);
defer arena.deinit();
if (!obj.isObject()) {
return global.throwInvalidArguments("tls option expects an object", .{});
}
var any = false;
result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized());
// Required
if (try obj.getTruthy(global, "keyFile")) |key_file_name| {
var sliced = try key_file_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice());
if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) {
return global.throwInvalidArguments("Unable to access keyFile path", .{});
}
any = true;
result.requires_custom_request_ctx = true;
}
}
if (try obj.getTruthy(global, "key")) |js_obj| {
if (js_obj.jsType().isArray()) {
const count = try js_obj.getLength(global);
if (count > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, count);
var valid_count: u32 = 0;
for (0..count) |i| {
const item = try js_obj.getIndex(global, @intCast(i));
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced);
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (try BlobFileContentResult.init("key", item, global)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
result.requires_custom_request_ctx = true;
any = true;
} else {
// mark and free all CA's
result.cert = native_array;
result.deinit();
return null;
}
} else {
// mark and free all keys
result.key = native_array;
return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
if (valid_count == 0) {
bun.default_allocator.free(native_array);
} else {
result.key = native_array;
}
result.key_count = valid_count;
}
} else if (try BlobFileContentResult.init("key", js_obj, global)) |content| {
if (content.data.len > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
native_array[0] = content.data.ptr;
result.key = native_array;
result.key_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
}
} else {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[0] = try bun.default_allocator.dupeZ(u8, sliced);
any = true;
result.requires_custom_request_ctx = true;
result.key = native_array;
result.key_count = 1;
} else {
bun.default_allocator.free(native_array);
}
} else {
// mark and free all certs
result.key = native_array;
return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
}
if (try obj.getTruthy(global, "certFile")) |cert_file_name| {
var sliced = try cert_file_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice());
if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) {
return global.throwInvalidArguments("Unable to access certFile path", .{});
}
any = true;
result.requires_custom_request_ctx = true;
}
}
if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| {
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
result.protos = try bun.default_allocator.dupeZ(u8, sliced);
result.protos_len = sliced.len;
}
any = true;
result.requires_custom_request_ctx = true;
} else {
return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{});
}
}
if (try obj.getTruthy(global, "cert")) |js_obj| {
if (js_obj.jsType().isArray()) {
const count = try js_obj.getLength(global);
if (count > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, count);
var valid_count: u32 = 0;
for (0..count) |i| {
const item = try js_obj.getIndex(global, @intCast(i));
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced);
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (try BlobFileContentResult.init("cert", item, global)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
result.requires_custom_request_ctx = true;
any = true;
} else {
// mark and free all CA's
result.cert = native_array;
result.deinit();
return null;
}
} else {
// mark and free all certs
result.cert = native_array;
return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
if (valid_count == 0) {
bun.default_allocator.free(native_array);
} else {
result.cert = native_array;
}
result.cert_count = valid_count;
}
} else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| {
if (content.data.len > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
native_array[0] = content.data.ptr;
result.cert = native_array;
result.cert_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
}
} else {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[0] = try bun.default_allocator.dupeZ(u8, sliced);
any = true;
result.requires_custom_request_ctx = true;
result.cert = native_array;
result.cert_count = 1;
} else {
bun.default_allocator.free(native_array);
}
} else {
// mark and free all certs
result.cert = native_array;
return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
}
if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| {
result.request_cert = if (request_cert) 1 else 0;
any = true;
}
if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| {
result.reject_unauthorized = if (reject_unauthorized) 1 else 0;
any = true;
}
if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| {
var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice());
result.is_using_default_ciphers = false;
any = true;
result.requires_custom_request_ctx = true;
}
}
if (result.is_using_default_ciphers) {
result.ssl_ciphers = global.bunVM().rareData().tlsDefaultCiphers() orelse null;
}
if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| {
var sliced = try server_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice());
any = true;
result.requires_custom_request_ctx = true;
}
}
if (try obj.getTruthy(global, "ca")) |js_obj| {
if (js_obj.jsType().isArray()) {
const count = try js_obj.getLength(global);
if (count > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, count);
var valid_count: u32 = 0;
for (0..count) |i| {
const item = try js_obj.getIndex(global, @intCast(i));
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (try BlobFileContentResult.init("ca", item, global)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
// mark and free all CA's
result.cert = native_array;
result.deinit();
return null;
}
} else {
// mark and free all CA's
result.cert = native_array;
return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
if (valid_count == 0) {
bun.default_allocator.free(native_array);
} else {
result.ca = native_array;
}
result.ca_count = valid_count;
}
} else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| {
if (content.data.len > 0) {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
native_array[0] = content.data.ptr;
result.ca = native_array;
result.ca_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
}
} else {
const native_array = try bun.default_allocator.alloc([*c]const u8, 1);
if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| {
defer sb.deinit();
const sliced = sb.slice();
if (sliced.len > 0) {
native_array[0] = try bun.default_allocator.dupeZ(u8, sliced);
any = true;
result.requires_custom_request_ctx = true;
result.ca = native_array;
result.ca_count = 1;
} else {
bun.default_allocator.free(native_array);
}
} else {
// mark and free all certs
result.ca = native_array;
return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{});
}
}
}
if (try obj.getTruthy(global, "caFile")) |ca_file_name| {
var sliced = try ca_file_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice());
if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) {
return global.throwInvalidArguments("Invalid caFile path", .{});
}
}
}
// Optional
if (any) {
if (try obj.getTruthy(global, "secureOptions")) |secure_options| {
if (secure_options.isNumber()) {
result.secure_options = secure_options.toU32();
}
}
if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| {
if (client_renegotiation_limit.isNumber()) {
result.client_renegotiation_limit = client_renegotiation_limit.toU32();
}
}
if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| {
if (client_renegotiation_window.isNumber()) {
result.client_renegotiation_window = client_renegotiation_window.toU32();
}
}
if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| {
var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice());
if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) {
return global.throwInvalidArguments("Invalid dhParamsFile path", .{});
}
}
}
if (try obj.getTruthy(global, "passphrase")) |passphrase| {
var sliced = try passphrase.toSlice(global, bun.default_allocator);
defer sliced.deinit();
if (sliced.len > 0) {
result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice());
}
}
if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| {
if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) {
result.low_memory_mode = low_memory_mode.toBoolean();
any = true;
} else {
return global.throw("Expected lowMemoryMode to be a boolean", .{});
}
}
}
if (!any)
return null;
return result;
}
const std = @import("std");
const bun = @import("bun");
const strings = bun.strings;
const uws = bun.uws;
const jsc = bun.jsc;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;
const VirtualMachine = jsc.VirtualMachine;