aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/api
diff options
context:
space:
mode:
authorGravatar Ciro Spaciari <ciro.spaciari@gmail.com> 2023-07-17 23:39:09 -0300
committerGravatar GitHub <noreply@github.com> 2023-07-17 19:39:09 -0700
commit13b54fbdb8cc36bbe027238654360f159ecaefbb (patch)
tree9f905b3520a01ba64cf10df0eefe2fe35c196648 /src/bun.js/api
parent9273e29f0eba9c9d185a602d30def5a6b981ad55 (diff)
downloadbun-13b54fbdb8cc36bbe027238654360f159ecaefbb.tar.gz
bun-13b54fbdb8cc36bbe027238654360f159ecaefbb.tar.zst
bun-13b54fbdb8cc36bbe027238654360f159ecaefbb.zip
[tls] General compatibility improvements (#3596)
* wip * subjectaltname * more progress * bindings * fmt * getCert/getPeerCertificate * fix checkServerIdentity * fix checkServerIdentity * add a lot of TLSSocket functions * getEphemeralKeyInfo fix and comment * add alternative for getEphemeralKeyInfo * add get session and set session * fix isSessionReused * get back the raw data for MSSQL * fixeup * fixup getSession + tests * fix doc + fmt * getFinished/getPeerFinished * codegen * fixup * revert webkit * more fixes * ssl helper + revert test oops * asserts
Diffstat (limited to 'src/bun.js/api')
-rw-r--r--src/bun.js/api/bun/socket.zig679
-rw-r--r--src/bun.js/api/bun/x509.zig560
-rw-r--r--src/bun.js/api/sockets.classes.ts53
3 files changed, 1288 insertions, 4 deletions
diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig
index 1d85c705c..0a18dd015 100644
--- a/src/bun.js/api/bun/socket.zig
+++ b/src/bun.js/api/bun/socket.zig
@@ -16,6 +16,8 @@ const Which = @import("../../../which.zig");
const uws = @import("root").bun.uws;
const ZigString = JSC.ZigString;
const BoringSSL = bun.BoringSSL;
+const X509 = @import("./x509.zig");
+
// const Corker = struct {
// ptr: ?*[16384]u8 = null,
// holder: ?*anyopaque = null,
@@ -56,6 +58,79 @@ const BoringSSL = bun.BoringSSL;
// }
// };
+noinline fn getSSLException(globalThis: *JSC.JSGlobalObject, defaultMessage: []const u8) JSValue {
+ var zig_str: ZigString = ZigString.init("");
+ var output_buf: [4096]u8 = undefined;
+
+ output_buf[0] = 0;
+ var written: usize = 0;
+ var ssl_error = BoringSSL.ERR_get_error();
+ while (ssl_error != 0 and written < output_buf.len) : (ssl_error = BoringSSL.ERR_get_error()) {
+ if (written > 0) {
+ output_buf[written] = '\n';
+ written += 1;
+ }
+
+ if (BoringSSL.ERR_reason_error_string(
+ ssl_error,
+ )) |reason_ptr| {
+ const reason = std.mem.span(reason_ptr);
+ if (reason.len == 0) {
+ break;
+ }
+ @memcpy(output_buf[written..][0..reason.len], reason);
+ written += reason.len;
+ }
+
+ if (BoringSSL.ERR_func_error_string(
+ ssl_error,
+ )) |reason_ptr| {
+ const reason = std.mem.span(reason_ptr);
+ if (reason.len > 0) {
+ output_buf[written..][0.." via ".len].* = " via ".*;
+ written += " via ".len;
+ @memcpy(output_buf[written..][0..reason.len], reason);
+ written += reason.len;
+ }
+ }
+
+ if (BoringSSL.ERR_lib_error_string(
+ ssl_error,
+ )) |reason_ptr| {
+ const reason = std.mem.span(reason_ptr);
+ if (reason.len > 0) {
+ output_buf[written..][0] = ' ';
+ written += 1;
+ @memcpy(output_buf[written..][0..reason.len], reason);
+ written += reason.len;
+ }
+ }
+ }
+
+ if (written > 0) {
+ var message = output_buf[0..written];
+ zig_str = ZigString.init(std.fmt.allocPrint(bun.default_allocator, "OpenSSL {s}", .{message}) catch unreachable);
+ var encoded_str = zig_str.withEncoding();
+ encoded_str.mark();
+
+ // We shouldn't *need* to do this but it's not entirely clear.
+ BoringSSL.ERR_clear_error();
+ }
+
+ if (zig_str.len == 0) {
+ zig_str = ZigString.init(defaultMessage);
+ }
+
+ // store the exception in here
+ // toErrorInstance clones the string
+ const exception = zig_str.toErrorInstance(globalThis);
+
+ // reference it in stack memory
+ exception.ensureStillAlive();
+
+ return exception;
+}
+
fn normalizeHost(input: anytype) @TypeOf(input) {
if (input.len == 0) {
return "localhost";
@@ -66,7 +141,6 @@ fn normalizeHost(input: anytype) @TypeOf(input) {
return input;
}
-
const BinaryType = JSC.BinaryType;
const WrappedType = enum {
@@ -1175,7 +1249,7 @@ fn NewSocket(comptime ssl: bool) type {
// Add SNI support for TLS (mongodb and others requires this)
if (comptime ssl) {
- var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, socket.getNativeHandle());
+ var ssl_ptr = this.socket.ssl();
if (!ssl_ptr.isInitFinished()) {
if (this.server_name) |server_name| {
const host = normalizeHost(server_name);
@@ -1880,6 +1954,107 @@ fn NewSocket(comptime ssl: bool) type {
return JSValue.jsUndefined();
}
+ pub fn getTLSTicket(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ var ssl_ptr = this.socket.ssl();
+ const session = BoringSSL.SSL_get_session(ssl_ptr) orelse return JSValue.jsUndefined();
+ var ticket: [*c]const u8 = undefined;
+ var length: usize = 0;
+ //The pointer is only valid while the connection is in use so we need to copy it
+ BoringSSL.SSL_SESSION_get0_ticket(session, @ptrCast([*c][*c]const u8, &ticket), &length);
+
+ if (ticket == null or length == 0) {
+ return JSValue.jsUndefined();
+ }
+
+ return JSC.ArrayBuffer.createBuffer(globalObject, ticket[0..length]);
+ }
+
+ pub fn setSession(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const args = callframe.arguments(1);
+
+ if (args.len < 1) {
+ globalObject.throw("Expected session to be a string, Buffer or TypedArray", .{});
+ return .zero;
+ }
+
+ const session_arg = args.ptr[0];
+ var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator);
+ defer arena.deinit();
+
+ var exception_ref = [_]JSC.C.JSValueRef{null};
+ var exception: JSC.C.ExceptionRef = &exception_ref;
+ if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), session_arg, exception)) |sb| {
+ var session_slice = sb.slice();
+ var ssl_ptr = this.socket.ssl();
+ var tmp = @ptrCast([*c]const u8, session_slice.ptr);
+ const session = BoringSSL.d2i_SSL_SESSION(null, &tmp, @intCast(c_long, session_slice.len)) orelse return JSValue.jsUndefined();
+ if (BoringSSL.SSL_set_session(ssl_ptr, session) != 1) {
+ globalObject.throwValue(getSSLException(globalObject, "SSL_set_session error"));
+ return .zero;
+ }
+ return JSValue.jsUndefined();
+ } else if (exception.* != null) {
+ globalObject.throwValue(JSC.JSValue.c(exception.*));
+ return .zero;
+ } else {
+ globalObject.throw("Expected session to be a string, Buffer or TypedArray", .{});
+ return .zero;
+ }
+ }
+
+ pub fn getSession(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ var ssl_ptr = this.socket.ssl();
+ const session = BoringSSL.SSL_get_session(ssl_ptr) orelse return JSValue.jsUndefined();
+ const size = BoringSSL.i2d_SSL_SESSION(session, null);
+ if (size <= 0) {
+ return JSValue.jsUndefined();
+ }
+
+ const buffer_size = @intCast(usize, size);
+ var buffer = JSValue.createBufferFromLength(globalObject, buffer_size);
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result_size = BoringSSL.i2d_SSL_SESSION(session, &buffer_ptr);
+ std.debug.assert(result_size == size);
+ return buffer;
+ }
+
pub fn getALPNProtocol(
this: *This,
globalObject: *JSC.JSGlobalObject,
@@ -1895,7 +2070,7 @@ fn NewSocket(comptime ssl: bool) type {
var alpn_proto: [*c]const u8 = null;
var alpn_proto_len: u32 = 0;
- var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ var ssl_ptr = this.socket.ssl();
BoringSSL.SSL_get0_alpn_selected(ssl_ptr, &alpn_proto, &alpn_proto_len);
if (alpn_proto == null or alpn_proto_len == 0) {
return JSValue.jsBoolean(false);
@@ -1910,7 +2085,502 @@ fn NewSocket(comptime ssl: bool) type {
}
return ZigString.fromUTF8(slice).toValueGC(globalObject);
}
+ pub fn exportKeyingMaterial(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const args = callframe.arguments(3);
+ if (args.len < 2) {
+ globalObject.throw("Expected length and label to be provided", .{});
+ return .zero;
+ }
+ const length_arg = args.ptr[0];
+ if (!length_arg.isNumber()) {
+ globalObject.throw("Expected length to be a number", .{});
+ return .zero;
+ }
+
+ const length = length_arg.coerceToInt64(globalObject);
+ if (length < 0) {
+ globalObject.throw("Expected length to be a positive number", .{});
+ return .zero;
+ }
+
+ const label_arg = args.ptr[1];
+ if (!label_arg.isString()) {
+ globalObject.throw("Expected label to be a string", .{});
+ return .zero;
+ }
+
+ var label = label_arg.toSliceOrNull(globalObject) orelse {
+ globalObject.throw("Expected label to be a string", .{});
+ return .zero;
+ };
+
+ defer label.deinit();
+ const label_slice = label.slice();
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+
+ if (args.len > 2) {
+ const context_arg = args.ptr[2];
+
+ var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator);
+ defer arena.deinit();
+
+ var exception_ref = [_]JSC.C.JSValueRef{null};
+ var exception: JSC.C.ExceptionRef = &exception_ref;
+ if (JSC.Node.StringOrBuffer.fromJS(globalObject, arena.allocator(), context_arg, exception)) |sb| {
+ const context_slice = sb.slice();
+
+ const buffer_size = @intCast(usize, length);
+ var buffer = JSValue.createBufferFromLength(globalObject, buffer_size);
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result = BoringSSL.SSL_export_keying_material(ssl_ptr, buffer_ptr, buffer_size, @ptrCast([*c]const u8, label_slice.ptr), label_slice.len, @ptrCast([*c]const u8, context_slice.ptr), context_slice.len, 1);
+ if (result != 1) {
+ globalObject.throwValue(getSSLException(globalObject, "Failed to export keying material"));
+ return .zero;
+ }
+ return buffer;
+ } else if (exception.* != null) {
+ globalObject.throwValue(JSC.JSValue.c(exception.*));
+ return .zero;
+ } else {
+ globalObject.throw("Expected context to be a string, Buffer or TypedArray", .{});
+ return .zero;
+ }
+ } else {
+ const buffer_size = @intCast(usize, length);
+ var buffer = JSValue.createBufferFromLength(globalObject, buffer_size);
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result = BoringSSL.SSL_export_keying_material(ssl_ptr, buffer_ptr, buffer_size, @ptrCast([*c]const u8, label_slice.ptr), label_slice.len, null, 0, 0);
+ if (result != 1) {
+ globalObject.throwValue(getSSLException(globalObject, "Failed to export keying material"));
+ return .zero;
+ }
+ return buffer;
+ }
+ }
+
+ pub fn getEphemeralKeyInfo(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsNull();
+ }
+
+ if (this.detached) {
+ return JSValue.jsNull();
+ }
+
+ // only available for clients
+ if (this.handlers.is_server) {
+ return JSValue.jsNull();
+ }
+ var result = JSValue.createEmptyObject(globalObject, 3);
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ // TODO: investigate better option or compatible way to get the key
+ // this implementation follows nodejs but for BoringSSL SSL_get_server_tmp_key will always return 0
+ // wich will result in a empty object
+ // var raw_key: [*c]BoringSSL.EVP_PKEY = undefined;
+ // if (BoringSSL.SSL_get_server_tmp_key(ssl_ptr, @ptrCast([*c][*c]BoringSSL.EVP_PKEY, &raw_key)) == 0) {
+ // return result;
+ // }
+ var raw_key: [*c]BoringSSL.EVP_PKEY = BoringSSL.SSL_get_privatekey(ssl_ptr);
+ if (raw_key == null) {
+ return result;
+ }
+
+ const kid = BoringSSL.EVP_PKEY_id(raw_key);
+ const bits = BoringSSL.EVP_PKEY_bits(raw_key);
+
+ switch (kid) {
+ BoringSSL.EVP_PKEY_DH => {
+ result.put(globalObject, ZigString.static("type"), ZigString.static("DH").toValue(globalObject));
+ result.put(globalObject, ZigString.static("size"), JSValue.jsNumber(bits));
+ },
+
+ BoringSSL.EVP_PKEY_EC, BoringSSL.EVP_PKEY_X25519, BoringSSL.EVP_PKEY_X448 => {
+ var curve_name: []const u8 = undefined;
+ if (kid == BoringSSL.EVP_PKEY_EC) {
+ const ec = BoringSSL.EVP_PKEY_get1_EC_KEY(raw_key);
+ const nid = BoringSSL.EC_GROUP_get_curve_name(BoringSSL.EC_KEY_get0_group(ec));
+ const nid_str = BoringSSL.OBJ_nid2sn(nid);
+ if (nid_str != null) {
+ curve_name = nid_str[0..bun.len(nid_str)];
+ } else {
+ curve_name = "";
+ }
+ } else {
+ const kid_str = BoringSSL.OBJ_nid2sn(kid);
+ if (kid_str != null) {
+ curve_name = kid_str[0..bun.len(kid_str)];
+ } else {
+ curve_name = "";
+ }
+ }
+ result.put(globalObject, ZigString.static("type"), ZigString.static("ECDH").toValue(globalObject));
+ result.put(globalObject, ZigString.static("name"), ZigString.fromUTF8(curve_name).toValueGC(globalObject));
+ result.put(globalObject, ZigString.static("size"), JSValue.jsNumber(bits));
+ },
+ else => {},
+ }
+ return result;
+ }
+
+ pub fn getCipher(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+ var result = JSValue.createEmptyObject(globalObject, 3);
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ const cipher = BoringSSL.SSL_get_current_cipher(ssl_ptr);
+ if (cipher == null) {
+ result.put(globalObject, ZigString.static("name"), JSValue.jsNull());
+ result.put(globalObject, ZigString.static("standardName"), JSValue.jsNull());
+ result.put(globalObject, ZigString.static("version"), JSValue.jsNull());
+ return result;
+ }
+
+ const name = BoringSSL.SSL_CIPHER_get_name(cipher);
+ if (name == null) {
+ result.put(globalObject, ZigString.static("name"), JSValue.jsNull());
+ } else {
+ result.put(globalObject, ZigString.static("name"), ZigString.fromUTF8(name[0..bun.len(name)]).toValueGC(globalObject));
+ }
+
+ const standard_name = BoringSSL.SSL_CIPHER_standard_name(cipher);
+ if (standard_name == null) {
+ result.put(globalObject, ZigString.static("standardName"), JSValue.jsNull());
+ } else {
+ result.put(globalObject, ZigString.static("standardName"), ZigString.fromUTF8(standard_name[0..bun.len(standard_name)]).toValueGC(globalObject));
+ }
+
+ const version = BoringSSL.SSL_CIPHER_get_version(cipher);
+ if (version == null) {
+ result.put(globalObject, ZigString.static("version"), JSValue.jsNull());
+ } else {
+ result.put(globalObject, ZigString.static("version"), ZigString.fromUTF8(version[0..bun.len(version)]).toValueGC(globalObject));
+ }
+
+ return result;
+ }
+
+ pub fn getTLSPeerFinishedMessage(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ // We cannot just pass nullptr to SSL_get_peer_finished()
+ // because it would further be propagated to memcpy(),
+ // where the standard requirements as described in ISO/IEC 9899:2011
+ // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated.
+ // Thus, we use a dummy byte.
+ var dummy: [1]u8 = undefined;
+ const size = BoringSSL.SSL_get_peer_finished(ssl_ptr, @ptrCast(*anyopaque, &dummy), @sizeOf(@TypeOf(dummy)));
+ if (size == 0) return JSValue.jsUndefined();
+
+ const buffer_size = @intCast(usize, size);
+ var buffer = JSValue.createBufferFromLength(globalObject, buffer_size);
+ var buffer_ptr = @ptrCast(*anyopaque, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result_size = BoringSSL.SSL_get_peer_finished(ssl_ptr, buffer_ptr, buffer_size);
+ std.debug.assert(result_size == size);
+ return buffer;
+ }
+
+ pub fn getTLSFinishedMessage(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ // We cannot just pass nullptr to SSL_get_finished()
+ // because it would further be propagated to memcpy(),
+ // where the standard requirements as described in ISO/IEC 9899:2011
+ // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated.
+ // Thus, we use a dummy byte.
+ var dummy: [1]u8 = undefined;
+ const size = BoringSSL.SSL_get_finished(ssl_ptr, @ptrCast(*anyopaque, &dummy), @sizeOf(@TypeOf(dummy)));
+ if (size == 0) return JSValue.jsUndefined();
+
+ const buffer_size = @intCast(usize, size);
+ var buffer = JSValue.createBufferFromLength(globalObject, buffer_size);
+ var buffer_ptr = @ptrCast(*anyopaque, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result_size = BoringSSL.SSL_get_finished(ssl_ptr, buffer_ptr, buffer_size);
+ std.debug.assert(result_size == size);
+ return buffer;
+ }
+
+ pub fn getSharedSigalgs(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ JSC.markBinding(@src());
+ if (comptime ssl == false) {
+ return JSValue.jsNull();
+ }
+
+ if (this.detached) {
+ return JSValue.jsNull();
+ }
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+
+ const nsig = BoringSSL.SSL_get_shared_sigalgs(ssl_ptr, 0, null, null, null, null, null);
+
+ const array = JSC.JSValue.createEmptyArray(globalObject, @intCast(usize, nsig));
+
+ for (0..@intCast(usize, nsig)) |i| {
+ var hash_nid: c_int = 0;
+ var sign_nid: c_int = 0;
+ var sig_with_md: []const u8 = "";
+
+ _ = BoringSSL.SSL_get_shared_sigalgs(ssl_ptr, @intCast(c_int, i), &sign_nid, &hash_nid, null, null, null);
+ switch (sign_nid) {
+ BoringSSL.EVP_PKEY_RSA => {
+ sig_with_md = "RSA";
+ },
+ BoringSSL.EVP_PKEY_RSA_PSS => {
+ sig_with_md = "RSA-PSS";
+ },
+
+ BoringSSL.EVP_PKEY_DSA => {
+ sig_with_md = "DSA";
+ },
+
+ BoringSSL.EVP_PKEY_EC => {
+ sig_with_md = "ECDSA";
+ },
+
+ BoringSSL.NID_ED25519 => {
+ sig_with_md = "Ed25519";
+ },
+
+ BoringSSL.NID_ED448 => {
+ sig_with_md = "Ed448";
+ },
+ BoringSSL.NID_id_GostR3410_2001 => {
+ sig_with_md = "gost2001";
+ },
+
+ BoringSSL.NID_id_GostR3410_2012_256 => {
+ sig_with_md = "gost2012_256";
+ },
+ BoringSSL.NID_id_GostR3410_2012_512 => {
+ sig_with_md = "gost2012_512";
+ },
+ else => {
+ const sn_str = BoringSSL.OBJ_nid2sn(sign_nid);
+ if (sn_str != null) {
+ sig_with_md = sn_str[0..bun.len(sn_str)];
+ } else {
+ sig_with_md = "UNDEF";
+ }
+ },
+ }
+
+ const hash_str = BoringSSL.OBJ_nid2sn(hash_nid);
+ if (hash_str != null) {
+ const hash_str_len = bun.len(hash_str);
+ const hash_slice = hash_str[0..hash_str_len];
+ const buffer = bun.default_allocator.alloc(u8, sig_with_md.len + hash_str_len + 1) catch unreachable;
+ defer bun.default_allocator.free(buffer);
+
+ bun.copy(u8, buffer, sig_with_md);
+ buffer[sig_with_md.len] = '+';
+ bun.copy(u8, buffer[sig_with_md.len + 1 ..], hash_slice);
+ array.putIndex(globalObject, @intCast(u32, i), JSC.ZigString.fromUTF8(buffer).toValueGC(globalObject));
+ } else {
+ const buffer = bun.default_allocator.alloc(u8, sig_with_md.len + 6) catch unreachable;
+ defer bun.default_allocator.free(buffer);
+
+ bun.copy(u8, buffer, sig_with_md);
+ bun.copy(u8, buffer[sig_with_md.len..], "+UNDEF");
+ array.putIndex(globalObject, @intCast(u32, i), JSC.ZigString.fromUTF8(buffer).toValueGC(globalObject));
+ }
+ }
+ return array;
+ }
+
+ pub fn getTLSVersion(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ JSC.markBinding(@src());
+ if (comptime ssl == false) {
+ return JSValue.jsNull();
+ }
+
+ if (this.detached) {
+ return JSValue.jsNull();
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ const version = BoringSSL.SSL_get_version(ssl_ptr);
+ if (version == null) return JSValue.jsNull();
+ const version_len = bun.len(version);
+ if (version_len == 0) return JSValue.jsNull();
+ const slice = version[0..version_len];
+ return ZigString.fromUTF8(slice).toValueGC(globalObject);
+ }
+
+ pub fn setMaxSendFragment(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ JSC.markBinding(@src());
+ if (comptime ssl == false) {
+ return JSValue.jsBoolean(false);
+ }
+
+ if (this.detached) {
+ return JSValue.jsBoolean(false);
+ }
+
+ const args = callframe.arguments(1);
+
+ if (args.len < 1) {
+ globalObject.throw("Expected size to be a number", .{});
+ return .zero;
+ }
+
+ const arg = args.ptr[0];
+ if (!arg.isNumber()) {
+ globalObject.throw("Expected size to be a number", .{});
+ return .zero;
+ }
+ const size = args.ptr[0].coerceToInt64(globalObject);
+ if (size < 1) {
+ globalObject.throw("Expected size to be greater than 1", .{});
+ return .zero;
+ }
+ if (size > 16384) {
+ globalObject.throw("Expected size to be less than 16385", .{});
+ return .zero;
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ return JSValue.jsBoolean(BoringSSL.SSL_set_max_send_fragment(ssl_ptr, @intCast(usize, size)) == 1);
+ }
+ pub fn getPeerCertificate(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ JSC.markBinding(@src());
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const args = callframe.arguments(1);
+ var abbreviated: bool = true;
+ if (args.len > 0) {
+ const arg = args.ptr[0];
+ if (!arg.isBoolean()) {
+ globalObject.throw("Expected abbreviated to be a boolean", .{});
+ return .zero;
+ }
+ abbreviated = arg.toBoolean();
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+
+ if (abbreviated) {
+ if (this.handlers.is_server) {
+ const cert = BoringSSL.SSL_get_peer_certificate(ssl_ptr);
+ if (cert) |x509| {
+ return X509.toJS(x509, globalObject);
+ }
+ }
+
+ const cert_chain = BoringSSL.SSL_get_peer_cert_chain(ssl_ptr) orelse return JSValue.jsUndefined();
+ const cert = BoringSSL.sk_X509_value(cert_chain, 0) orelse return JSValue.jsUndefined();
+ return X509.toJS(cert, globalObject);
+ }
+ var cert: ?*BoringSSL.X509 = null;
+ if (this.handlers.is_server) {
+ cert = BoringSSL.SSL_get_peer_certificate(ssl_ptr);
+ }
+
+ const cert_chain = BoringSSL.SSL_get_peer_cert_chain(ssl_ptr);
+ const first_cert = if (cert) |c| c else if (cert_chain) |cc| BoringSSL.sk_X509_value(cc, 0) else null;
+
+ if (first_cert == null) {
+ return JSValue.jsUndefined();
+ }
+
+ // TODO: we need to support the non abbreviated version of this
+ return JSValue.jsUndefined();
+ }
+
+ pub fn getCertificate(
+ this: *This,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ if (comptime ssl == false) {
+ return JSValue.jsUndefined();
+ }
+
+ if (this.detached) {
+ return JSValue.jsUndefined();
+ }
+
+ const ssl_ptr = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ const cert = BoringSSL.SSL_get_certificate(ssl_ptr);
+
+ if (cert) |x509| {
+ return X509.toJS(x509, globalObject);
+ }
+ return JSValue.jsUndefined();
+ }
pub fn setServername(
this: *This,
globalObject: *JSC.JSGlobalObject,
@@ -1952,7 +2622,7 @@ fn NewSocket(comptime ssl: bool) type {
const host = normalizeHost(@as([]const u8, slice));
if (host.len > 0) {
- var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle());
+ var ssl_ptr = this.socket.ssl();
if (ssl_ptr.isInitFinished()) {
// match node.js exceptions
globalObject.throw("Already started.", .{});
@@ -2222,6 +2892,7 @@ pub fn NewWrappedHandler(comptime tls: bool) type {
if (comptime tls) {
TLSSocket.onData(this.tls, socket, data);
} else {
+ // tedius use this
TLSSocket.onData(this.tcp, socket, data);
}
}
diff --git a/src/bun.js/api/bun/x509.zig b/src/bun.js/api/bun/x509.zig
new file mode 100644
index 000000000..707009936
--- /dev/null
+++ b/src/bun.js/api/bun/x509.zig
@@ -0,0 +1,560 @@
+const BoringSSL = bun.BoringSSL;
+const bun = @import("root").bun;
+const ZigString = JSC.ZigString;
+const std = @import("std");
+const JSC = @import("root").bun.JSC;
+const JSValue = JSC.JSValue;
+const JSGlobalObject = JSC.JSGlobalObject;
+
+fn x509GetNameObject(globalObject: *JSGlobalObject, name: ?*BoringSSL.X509_NAME) JSValue {
+ const cnt = BoringSSL.X509_NAME_entry_count(name);
+ if (cnt <= 0) {
+ return JSValue.jsUndefined();
+ }
+ var result = JSValue.createEmptyObject(globalObject, 1);
+
+ for (0..@intCast(usize, cnt)) |i| {
+ const entry = BoringSSL.X509_NAME_get_entry(name, @intCast(c_int, i)) orelse continue;
+ // We intentionally ignore the value of X509_NAME_ENTRY_set because the
+ // representation as an object does not allow grouping entries into sets
+ // anyway, and multi-value RDNs are rare, i.e., the vast majority of
+ // Relative Distinguished Names contains a single type-value pair only.
+ const type_ = BoringSSL.X509_NAME_ENTRY_get_object(entry);
+
+ // If BoringSSL knows the type, use the short name of the type as the key, and
+ // the numeric representation of the type's OID otherwise.
+ const type_nid = BoringSSL.OBJ_obj2nid(type_);
+ var type_buf: [80]u8 = undefined;
+ var name_slice: []const u8 = undefined;
+ if (type_nid != BoringSSL.NID_undef) {
+ const type_str = BoringSSL.OBJ_nid2sn(type_nid);
+ if (type_str == null) {
+ continue;
+ }
+ name_slice = type_str[0..bun.len(type_str)];
+ } else {
+ const length = BoringSSL.OBJ_obj2txt(&type_buf, @sizeOf(@TypeOf(type_buf)), type_, 1);
+ if (length <= 0) {
+ continue;
+ }
+ name_slice = type_buf[0..@intCast(usize, length)];
+ }
+
+ const value_data = BoringSSL.X509_NAME_ENTRY_get_data(entry);
+
+ var value_str: [*c]u8 = undefined;
+ const value_str_len = BoringSSL.ASN1_STRING_to_UTF8(&value_str, value_data);
+ if (value_str_len < 0) {
+ continue;
+ }
+ const value_slice = value_str[0..@intCast(usize, value_str_len)];
+ defer BoringSSL.OPENSSL_free(value_str);
+ // For backward compatibility, we only create arrays if multiple values
+ // exist for the same key. That is not great but there is not much we can
+ // change here without breaking things. Note that this creates nested data
+ // structures, yet still does not allow representing Distinguished Names
+ // accurately.
+ if (result.getTruthy(globalObject, name_slice)) |value| {
+ if (value.jsType().isArray()) {
+ value.push(globalObject, JSC.ZigString.fromUTF8(value_slice).toValueGC(globalObject));
+ } else {
+ const prop_name = JSC.ZigString.fromUTF8(name_slice);
+ const array = JSValue.createEmptyArray(globalObject, 2);
+ array.putIndex(globalObject, 0, value);
+ array.putIndex(globalObject, 1, JSC.ZigString.fromUTF8(value_slice).toValueGC(globalObject));
+ result.put(globalObject, &prop_name, array);
+ }
+ } else {
+ const prop_name = JSC.ZigString.fromUTF8(name_slice);
+ result.put(globalObject, &prop_name, JSC.ZigString.fromUTF8(value_slice).toValueGC(globalObject));
+ }
+ }
+ return result;
+}
+
+inline fn isSafeAltName(name: []const u8, utf8: bool) bool {
+ for (name) |c| {
+ switch (c) {
+ '"',
+ '\\',
+ // These mess with encoding rules.
+ // Fall through.
+ ',',
+ // Commas make it impossible to split the list of subject alternative
+ // names unambiguously, which is why we have to escape.
+ // Fall through.
+ '\'',
+ => {
+ // Single quotes are unlikely to appear in any legitimate values, but they
+ // could be used to make a value look like it was escaped (i.e., enclosed
+ // in single/double quotes).
+ return false;
+ },
+ else => {
+ if (utf8) {
+ // In UTF8 strings, we require escaping for any ASCII control character,
+ // but NOT for non-ASCII characters. Note that all bytes of any code
+ // point that consists of more than a single byte have their MSB set.
+ if (c < ' ' or c == '\x7f') {
+ return false;
+ }
+ } else {
+ // Check if the char is a control character or non-ASCII character. Note
+ // that char may or may not be a signed type. Regardless, non-ASCII
+ // values will always be outside of this range.
+ if (c < ' ' or c > '~') {
+ return false;
+ }
+ }
+ },
+ }
+ }
+ return true;
+}
+
+inline fn printAltName(out: *BoringSSL.BIO, name: []const u8, utf8: bool, safe_prefix: ?[*]const u8) void {
+ if (isSafeAltName(name, utf8)) {
+ // For backward-compatibility, append "safe" names without any
+ // modifications.
+ if (safe_prefix) |prefix| {
+ _ = BoringSSL.BIO_printf(out, "%s:", prefix);
+ }
+ _ = BoringSSL.BIO_write(out, @ptrCast([*]const u8, name.ptr), @intCast(c_int, name.len));
+ } else {
+ // If a name is not "safe", we cannot embed it without special
+ // encoding. This does not usually happen, but we don't want to hide
+ // it from the user either. We use JSON compatible escaping here.
+ _ = BoringSSL.BIO_write(out, "\"", 1);
+ if (safe_prefix) |prefix| {
+ _ = BoringSSL.BIO_printf(out, "%s:", prefix);
+ }
+ for (name) |c| {
+ if (c == '\\') {
+ _ = BoringSSL.BIO_write(out, "\\\\", 2);
+ } else if (c == '"') {
+ _ = BoringSSL.BIO_write(out, "\\\"", 2);
+ } else if ((c >= ' ' and c != ',' and c <= '~') or (utf8 and (c & 0x80) != 0)) {
+ // Note that the above condition explicitly excludes commas, which means
+ // that those are encoded as Unicode escape sequences in the "else"
+ // block. That is not strictly necessary, and Node.js itself would parse
+ // it correctly either way. We only do this to account for third-party
+ // code that might be splitting the string at commas (as Node.js itself
+ // used to do).
+ _ = BoringSSL.BIO_write(out, bun.cast([*]const u8, &c), 1);
+ } else {
+ // Control character or non-ASCII character. We treat everything as
+ // Latin-1, which corresponds to the first 255 Unicode code points.
+ const hex = "0123456789abcdef";
+ const u = [_]u8{ '\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f] };
+ _ = BoringSSL.BIO_write(out, &u, @sizeOf(@TypeOf(u)));
+ }
+ }
+ _ = BoringSSL.BIO_write(out, "\"", 1);
+ }
+}
+
+inline fn printLatin1AltName(out: *BoringSSL.BIO, name: *BoringSSL.ASN1_IA5STRING, safe_prefix: ?[*]const u8) void {
+ printAltName(out, name.data[0..@intCast(usize, name.length)], false, safe_prefix);
+}
+
+inline fn printUTF8AltName(out: *BoringSSL.BIO, name: *BoringSSL.ASN1_UTF8STRING, safe_prefix: ?[*]const u8) void {
+ printAltName(out, name.data[0..@intCast(usize, name.length)], true, safe_prefix);
+}
+
+pub const kX509NameFlagsRFC2253WithinUtf8JSON = BoringSSL.XN_FLAG_RFC2253 & ~BoringSSL.ASN1_STRFLGS_ESC_MSB & ~BoringSSL.ASN1_STRFLGS_ESC_CTRL;
+
+// This function emulates the behavior of i2v_GENERAL_NAME in a safer and less
+// ambiguous way. "othername:" entries use the GENERAL_NAME_print format.
+fn x509PrintGeneralName(out: *BoringSSL.BIO, name: *BoringSSL.GENERAL_NAME) bool {
+ if (name.name_type == .GEN_DNS) {
+ _ = BoringSSL.BIO_write(out, "DNS:", 4);
+ // Note that the preferred name syntax (see RFCs 5280 and 1034) with
+ // wildcards is a subset of what we consider "safe", so spec-compliant DNS
+ // names will never need to be escaped.
+ printLatin1AltName(out, name.d.dNSName, null);
+ } else if (name.name_type == .GEN_EMAIL) {
+ _ = BoringSSL.BIO_write(out, "email:", 6);
+ printLatin1AltName(out, name.d.rfc822Name, null);
+ } else if (name.name_type == .GEN_URI) {
+ _ = BoringSSL.BIO_write(out, "URI:", 4);
+ // The set of "safe" names was designed to include just about any URI,
+ // with a few exceptions, most notably URIs that contains commas (see
+ // RFC 2396). In other words, most legitimate URIs will not require
+ // escaping.
+ printLatin1AltName(out, name.d.uniformResourceIdentifier, null);
+ } else if (name.name_type == .GEN_DIRNAME) {
+ // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME
+ // object. The format was non standard and should be avoided. The use of
+ // X509_NAME_oneline is discouraged by OpenSSL but was required for backward
+ // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the
+ // output was unlikely to contains commas or other characters that would
+ // require escaping. However, it SHOULD NOT produce ASCII output since an
+ // RFC5280 AttributeValue may be a UTF8String.
+ // Newer versions of Node.js have since switched to X509_NAME_print_ex to
+ // produce a better format at the cost of backward compatibility. The new
+ // format may contain Unicode characters and it is likely to contain commas,
+ // which require escaping. Fortunately, the recently safeguarded function
+ // printAltName handles all of that safely.
+ _ = BoringSSL.BIO_printf(out, "DirName:");
+
+ const tmp = BoringSSL.BIO_new(BoringSSL.BIO_s_mem()) orelse return false;
+
+ if (BoringSSL.X509_NAME_print_ex(tmp, name.d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) < 0) {
+ return false;
+ }
+ var oline: [*]const u8 = undefined;
+ const n_bytes = BoringSSL.BIO_get_mem_data(tmp, @ptrCast([*c][*c]u8, &oline));
+ if (n_bytes <= 0) return false;
+ printAltName(out, oline[0..@intCast(usize, n_bytes)], true, null);
+ } else if (name.name_type == .GEN_OTHERNAME) {
+ // The format that is used here is based on OpenSSL's implementation of
+ // GENERAL_NAME_print (as of OpenSSL 3.0.1). Earlier versions of Node.js
+ // instead produced the same format as i2v_GENERAL_NAME, which was somewhat
+ // awkward, especially when passed to translatePeerCertificate.
+ var unicode: bool = true;
+ var prefix: ?[*]const u8 = null;
+
+ const nid = BoringSSL.OBJ_obj2nid(name.d.otherName.type_id);
+ switch (nid) {
+ BoringSSL.NID_id_on_SmtpUTF8Mailbox => {
+ prefix = "SmtpUTF8Mailbox";
+ },
+ BoringSSL.NID_XmppAddr => {
+ prefix = "XmppAddr";
+ },
+ BoringSSL.NID_SRVName => {
+ prefix = "SRVName";
+ unicode = false;
+ },
+ BoringSSL.NID_ms_upn => {
+ prefix = "UPN";
+ },
+ BoringSSL.NID_NAIRealm => {
+ prefix = "NAIRealm";
+ },
+ else => {
+ prefix = null;
+ },
+ }
+ if (name.d.otherName.value) |v| {
+ const val_type = v.type;
+ if (prefix == null or
+ (unicode and val_type != BoringSSL.V_ASN1_UTF8STRING) or
+ (!unicode and val_type != BoringSSL.V_ASN1_IA5STRING))
+ {
+ _ = BoringSSL.BIO_printf(out, "othername:<unsupported>");
+ } else {
+ _ = BoringSSL.BIO_printf(out, "othername:");
+ if (unicode) {
+ printUTF8AltName(out, v.value.utf8string, prefix);
+ } else {
+ printLatin1AltName(out, v.value.ia5string, prefix);
+ }
+ }
+ } else {
+ _ = BoringSSL.BIO_printf(out, "othername:<unsupported>");
+ }
+ } else if (name.name_type == .GEN_IPADD) {
+ _ = BoringSSL.BIO_printf(out, "IP Address:");
+ const ip = name.d.ip;
+ const b = ip.data;
+ if (ip.length == 4) {
+ _ = BoringSSL.BIO_printf(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
+ } else if (ip.length == 16) {
+ for (0..8) |j| {
+ const pair: u16 = (@intCast(u16, b[2 * j]) << 8) | @intCast(u16, b[2 * j + 1]);
+ _ = BoringSSL.BIO_printf(out, if (j == 0) "%X" else ":%X", pair);
+ }
+ } else {
+ _ = BoringSSL.BIO_printf(out, "<invalid length=%d>", ip.length);
+ }
+ } else if (name.name_type == .GEN_RID) {
+ // Unlike OpenSSL's default implementation, never print the OID as text and
+ // instead always print its numeric representation.
+ var oline: [256]u8 = undefined;
+ _ = BoringSSL.OBJ_obj2txt(&oline, @sizeOf(@TypeOf(oline)), name.d.rid, 1);
+ _ = BoringSSL.BIO_printf(out, "Registered ID:%s", &oline);
+ } else if (name.name_type == .GEN_X400) {
+ _ = BoringSSL.BIO_printf(out, "X400Name:<unsupported>");
+ } else if (name.name_type == .GEN_EDIPARTY) {
+ _ = BoringSSL.BIO_printf(out, "EdiPartyName:<unsupported>");
+ } else {
+ return false;
+ }
+ return true;
+}
+
+fn x509InfoAccessPrint(out: *BoringSSL.BIO, ext: *BoringSSL.X509_EXTENSION) bool {
+ const method = BoringSSL.X509V3_EXT_get(ext);
+ if (method != BoringSSL.X509V3_EXT_get_nid(BoringSSL.NID_info_access)) {
+ return false;
+ }
+
+ if (BoringSSL.X509V3_EXT_d2i(ext)) |descs_| {
+ const descs: *BoringSSL.AUTHORITY_INFO_ACCESS = bun.cast(*BoringSSL.AUTHORITY_INFO_ACCESS, descs_);
+ defer BoringSSL.sk_ACCESS_DESCRIPTION_pop_free(descs, BoringSSL.sk_ACCESS_DESCRIPTION_free);
+ for (0..BoringSSL.sk_ACCESS_DESCRIPTION_num(descs)) |i| {
+ const gen = BoringSSL.sk_ACCESS_DESCRIPTION_value(descs, i);
+ if (gen) |desc| {
+ if (i != 0) {
+ _ = BoringSSL.BIO_write(out, "\n", 1);
+ }
+ var tmp: [80]u8 = undefined;
+ _ = BoringSSL.i2t_ASN1_OBJECT(&tmp, @sizeOf(@TypeOf(tmp)), desc.method);
+ _ = BoringSSL.BIO_printf(out, "%s - ", &tmp);
+
+ if (!x509PrintGeneralName(out, desc.location)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+}
+fn x509SubjectAltNamePrint(out: *BoringSSL.BIO, ext: *BoringSSL.X509_EXTENSION) bool {
+ const method = BoringSSL.X509V3_EXT_get(ext);
+ if (method != BoringSSL.X509V3_EXT_get_nid(BoringSSL.NID_subject_alt_name)) {
+ return false;
+ }
+
+ if (BoringSSL.X509V3_EXT_d2i(ext)) |names_| {
+ const names: *BoringSSL.struct_stack_st_GENERAL_NAME = bun.cast(*BoringSSL.struct_stack_st_GENERAL_NAME, names_);
+ defer BoringSSL.sk_GENERAL_NAME_pop_free(names, BoringSSL.sk_GENERAL_NAME_free);
+ for (0..BoringSSL.sk_GENERAL_NAME_num(names)) |i| {
+ const gen = BoringSSL.sk_GENERAL_NAME_value(names, i);
+ if (gen) |gen_name| {
+ if (i != 0) {
+ _ = BoringSSL.BIO_write(out, ", ", 2);
+ }
+
+ if (!x509PrintGeneralName(out, gen_name)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ return false;
+}
+
+fn x509GetSubjectAltNameString(globalObject: *JSGlobalObject, bio: *BoringSSL.BIO, cert: *BoringSSL.X509) JSValue {
+ const index = BoringSSL.X509_get_ext_by_NID(cert, BoringSSL.NID_subject_alt_name, -1);
+ if (index < 0)
+ return JSValue.jsUndefined();
+
+ defer _ = BoringSSL.BIO_reset(bio);
+
+ const ext = BoringSSL.X509_get_ext(cert, index) orelse return JSValue.jsUndefined();
+
+ if (!x509SubjectAltNamePrint(bio, ext)) {
+ return JSValue.jsNull();
+ }
+
+ return JSC.ZigString.fromUTF8(bio.slice()).toValueGC(globalObject);
+}
+
+fn x509GetInfoAccessString(globalObject: *JSGlobalObject, bio: *BoringSSL.BIO, cert: *BoringSSL.X509) JSValue {
+ const index = BoringSSL.X509_get_ext_by_NID(cert, BoringSSL.NID_info_access, -1);
+ if (index < 0)
+ return JSValue.jsUndefined();
+ defer _ = BoringSSL.BIO_reset(bio);
+ const ext = BoringSSL.X509_get_ext(cert, index) orelse return JSValue.jsUndefined();
+
+ if (!x509InfoAccessPrint(bio, ext)) {
+ return JSValue.jsNull();
+ }
+
+ return JSC.ZigString.fromUTF8(bio.slice()).toValueGC(globalObject);
+}
+
+fn addFingerprintDigest(md: []const u8, mdSize: c_uint, fingerprint: []u8) usize {
+ const hex: []const u8 = "0123456789ABCDEF";
+ var idx: usize = 0;
+
+ const slice = md[0..@intCast(usize, mdSize)];
+ for (slice) |byte| {
+ fingerprint[idx] = hex[(byte & 0xF0) >> 4];
+ fingerprint[idx + 1] = hex[byte & 0x0F];
+ fingerprint[idx + 2] = ':';
+ idx += 3;
+ }
+ const length = if (idx > 0) (idx - 1) else 0;
+ fingerprint[length] = 0;
+ return length;
+}
+
+fn getFingerprintDigest(cert: *BoringSSL.X509, method: *const BoringSSL.EVP_MD, globalObject: *JSGlobalObject) JSValue {
+ var md: [BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined;
+ var md_size: c_uint = 0;
+ var fingerprint: [BoringSSL.EVP_MAX_MD_SIZE * 3]u8 = undefined;
+
+ if (BoringSSL.X509_digest(cert, method, @ptrCast([*c]u8, &md), &md_size) != 0) {
+ const length = addFingerprintDigest(&md, md_size, &fingerprint);
+ return JSC.ZigString.fromUTF8(fingerprint[0..length]).toValueGC(globalObject);
+ }
+ return JSValue.jsUndefined();
+}
+
+fn getSerialNumber(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue {
+ const serial_number = BoringSSL.X509_get_serialNumber(cert);
+ if (serial_number != null) {
+ const bignum = BoringSSL.ASN1_INTEGER_to_BN(serial_number, null);
+ if (bignum != null) {
+ const data = BoringSSL.BN_bn2hex(bignum);
+ if (data != null) {
+ const slice = data[0..bun.len(data)];
+ // BoringSSL prints the hex value of the serialNumber in lower case, but we need upper case
+ toUpper(slice);
+ return JSC.ZigString.fromUTF8(slice).toValueGC(globalObject);
+ }
+ }
+ }
+ return JSValue.jsUndefined();
+}
+
+fn getRawDERCertificate(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue {
+ const size = BoringSSL.i2d_X509(cert, null);
+ var buffer = JSValue.createBufferFromLength(globalObject, @intCast(usize, size));
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+ const result_size = BoringSSL.i2d_X509(cert, &buffer_ptr);
+ std.debug.assert(result_size == size);
+ return buffer;
+}
+
+fn toUpper(slice: []u8) void {
+ for (0..slice.len) |i| {
+ const c = slice[i];
+ if (c >= 'a' and c <= 'z') {
+ slice[i] &= 223;
+ }
+ }
+}
+
+pub fn toJS(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue {
+ const bio = BoringSSL.BIO_new(BoringSSL.BIO_s_mem()) orelse {
+ globalObject.throw("Failed to create BIO", .{});
+ return .zero;
+ };
+ defer _ = BoringSSL.BIO_free(bio);
+ var result = JSValue.createEmptyObject(globalObject, 8);
+ // X509_check_ca() returns a range of values. Only 1 means "is a CA"
+ const is_ca = BoringSSL.X509_check_ca(cert) == 1;
+ const subject = BoringSSL.X509_get_subject_name(cert);
+ result.put(globalObject, ZigString.static("subject"), x509GetNameObject(globalObject, subject));
+ const issuer = BoringSSL.X509_get_issuer_name(cert);
+ result.put(globalObject, ZigString.static("issuer"), x509GetNameObject(globalObject, issuer));
+ result.put(globalObject, ZigString.static("subjectaltname"), x509GetSubjectAltNameString(globalObject, bio, cert));
+ result.put(globalObject, ZigString.static("infoAccess"), x509GetInfoAccessString(globalObject, bio, cert));
+ result.put(globalObject, ZigString.static("ca"), JSValue.jsBoolean(is_ca));
+
+ const pkey = BoringSSL.X509_get_pubkey(cert);
+
+ switch (BoringSSL.EVP_PKEY_id(pkey)) {
+ BoringSSL.EVP_PKEY_RSA => {
+ const rsa_key = BoringSSL.EVP_PKEY_get1_RSA(pkey);
+ if (rsa_key) |rsa| {
+ var n: [*c]const BoringSSL.BIGNUM = undefined;
+ var e: [*c]const BoringSSL.BIGNUM = undefined;
+ BoringSSL.RSA_get0_key(rsa, @ptrCast([*c][*c]const BoringSSL.BIGNUM, &n), @ptrCast([*c][*c]const BoringSSL.BIGNUM, &e), null);
+ _ = BoringSSL.BN_print(bio, n);
+
+ var bits = JSValue.jsUndefined();
+
+ const bits_value = BoringSSL.BN_num_bits(n);
+ if (bits_value > 0) {
+ bits = JSValue.jsNumber(bits_value);
+ }
+
+ result.put(globalObject, ZigString.static("bits"), bits);
+ const slice = bio.slice();
+ // BoringSSL prints the hex value of the modulus in lower case, but we need upper case
+ toUpper(slice);
+ const modulus = JSC.ZigString.fromUTF8(slice).toValueGC(globalObject);
+ _ = BoringSSL.BIO_reset(bio);
+ result.put(globalObject, ZigString.static("modulus"), modulus);
+
+ const exponent_word = BoringSSL.BN_get_word(e);
+ _ = BoringSSL.BIO_printf(bio, "0x" ++ BoringSSL.BN_HEX_FMT1, exponent_word);
+ const exponent = JSC.ZigString.fromUTF8(bio.slice()).toValueGC(globalObject);
+ _ = BoringSSL.BIO_reset(bio);
+ result.put(globalObject, ZigString.static("exponent"), exponent);
+
+ const size = BoringSSL.i2d_RSA_PUBKEY(rsa, null);
+ if (size <= 0) {
+ globalObject.throw("Failed to get public key length", .{});
+ return .zero;
+ }
+
+ var buffer = JSValue.createBufferFromLength(globalObject, @intCast(usize, size));
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ _ = BoringSSL.i2d_RSA_PUBKEY(rsa, &buffer_ptr);
+
+ result.put(globalObject, ZigString.static("pubkey"), buffer);
+ }
+ },
+ BoringSSL.EVP_PKEY_EC => {
+ const ec_key = BoringSSL.EVP_PKEY_get1_EC_KEY(pkey);
+ if (ec_key) |ec| {
+ const group = BoringSSL.EC_KEY_get0_group(ec);
+ var bits = JSValue.jsUndefined();
+ if (group) |g| {
+ const bits_value = BoringSSL.EC_GROUP_order_bits(g);
+ if (bits_value > 0) {
+ bits = JSValue.jsNumber(bits_value);
+ }
+ }
+ result.put(globalObject, ZigString.static("bits"), bits);
+
+ const ec_pubkey = BoringSSL.EC_KEY_get0_public_key(ec);
+ if (ec_pubkey) |point| {
+ const form = BoringSSL.EC_KEY_get_conv_form(ec);
+ const size = BoringSSL.EC_POINT_point2oct(group, point, form, null, 0, null);
+ if (size <= 0) {
+ globalObject.throw("Failed to get public key length", .{});
+ return .zero;
+ }
+
+ var buffer = JSValue.createBufferFromLength(globalObject, @intCast(usize, size));
+ var buffer_ptr = @ptrCast([*c]u8, buffer.asArrayBuffer(globalObject).?.ptr);
+
+ const result_size = BoringSSL.EC_POINT_point2oct(group, point, form, buffer_ptr, size, null);
+ std.debug.assert(result_size == size);
+ result.put(globalObject, ZigString.static("pubkey"), buffer);
+ } else {
+ result.put(globalObject, ZigString.static("pubkey"), JSValue.jsUndefined());
+ }
+ const nid = BoringSSL.EC_GROUP_get_curve_name(group);
+
+ if (nid != 0) {
+ // Curve is well-known, get its OID and NIST nick-name (if it has one).
+ const asn1Curve_str = BoringSSL.OBJ_nid2sn(nid);
+ if (asn1Curve_str != null) {
+ result.put(globalObject, ZigString.static("asn1Curve"), JSC.ZigString.fromUTF8(asn1Curve_str[0..bun.len(asn1Curve_str)]).toValueGC(globalObject));
+ }
+ const nistCurve_str = BoringSSL.EC_curve_nid2nist(nid);
+ if (nistCurve_str != null) {
+ result.put(globalObject, ZigString.static("nistCurve"), JSC.ZigString.fromUTF8(nistCurve_str[0..bun.len(nistCurve_str)]).toValueGC(globalObject));
+ }
+ }
+ }
+ },
+ else => {},
+ }
+ _ = BoringSSL.ASN1_TIME_print(bio, BoringSSL.X509_get0_notBefore(cert));
+ result.put(globalObject, ZigString.static("valid_from"), JSC.ZigString.fromUTF8(bio.slice()).toValueGC(globalObject));
+ _ = BoringSSL.BIO_reset(bio);
+
+ _ = BoringSSL.ASN1_TIME_print(bio, BoringSSL.X509_get0_notAfter(cert));
+ result.put(globalObject, ZigString.static("valid_to"), JSC.ZigString.fromUTF8(bio.slice()).toValueGC(globalObject));
+ _ = BoringSSL.BIO_reset(bio);
+
+ result.put(globalObject, ZigString.static("fingerprint"), getFingerprintDigest(cert, BoringSSL.EVP_sha1(), globalObject));
+ result.put(globalObject, ZigString.static("fingerprint256"), getFingerprintDigest(cert, BoringSSL.EVP_sha256(), globalObject));
+ result.put(globalObject, ZigString.static("fingerprint512"), getFingerprintDigest(cert, BoringSSL.EVP_sha512(), globalObject));
+ result.put(globalObject, ZigString.static("serialNumber"), getSerialNumber(cert, globalObject));
+ result.put(globalObject, ZigString.static("raw"), getRawDERCertificate(cert, globalObject));
+ return result;
+}
diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts
index 5bd073b9f..2a17ca39d 100644
--- a/src/bun.js/api/sockets.classes.ts
+++ b/src/bun.js/api/sockets.classes.ts
@@ -12,6 +12,59 @@ function generate(ssl) {
fn: "getAuthorizationError",
length: 0,
},
+
+ getTLSFinishedMessage: {
+ fn: "getTLSFinishedMessage",
+ length: 0,
+ },
+ getTLSPeerFinishedMessage: {
+ fn: "getTLSPeerFinishedMessage",
+ length: 0,
+ },
+ getEphemeralKeyInfo: {
+ fn: "getEphemeralKeyInfo",
+ length: 0,
+ },
+ getCipher: {
+ fn: "getCipher",
+ length: 0,
+ },
+ getSession: {
+ fn: "getSession",
+ length: 0,
+ },
+ setSession: {
+ fn: "setSession",
+ length: 0,
+ },
+ getTLSTicket: {
+ fn: "getTLSTicket",
+ length: 0,
+ },
+ exportKeyingMaterial: {
+ fn: "exportKeyingMaterial",
+ length: 3,
+ },
+ setMaxSendFragment: {
+ fn: "setMaxSendFragment",
+ length: 1,
+ },
+ getSharedSigalgs: {
+ fn: "getSharedSigalgs",
+ length: 0,
+ },
+ getTLSVersion: {
+ fn: "getTLSVersion",
+ length: 0,
+ },
+ getPeerCertificate: {
+ fn: "getPeerCertificate",
+ length: 1,
+ },
+ getCertificate: {
+ fn: "getCertificate",
+ length: 0,
+ },
authorized: {
getter: "getAuthorized",
},