Skip to content

Commit

Permalink
feat(SSL_renegociate) (#10256)
Browse files Browse the repository at this point in the history
* allow client renegotiation and allow server renegotiation with limits matching nodejs behavior

* wip before the refactoring and context separation

* investigate if BoringSSL can send a SSL_renegotiate request or only accept

* format-off

* option to disable server renegotiation

* allow tls options on https

* dead_socket when connectError

* propagate cert error

* test

* move the logic to the right place

* cleanup

* Update test/js/node/tls/renegotiation.test.ts

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Georgijs <48869301+gvilums@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 15, 2024
1 parent 9f81a62 commit 74d91f6
Show file tree
Hide file tree
Showing 13 changed files with 510 additions and 115 deletions.
249 changes: 152 additions & 97 deletions packages/bun-usockets/src/crypto/openssl.c

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ void us_internal_ssl_socket_context_on_data(
struct us_internal_ssl_socket_t *(*on_data)(
struct us_internal_ssl_socket_t *s, char *data, int length));

void us_internal_ssl_handshake(struct us_internal_ssl_socket_t *s);
void us_internal_update_handshake(struct us_internal_ssl_socket_t *s);
int us_internal_renegotiate(struct us_internal_ssl_socket_t *s);
void us_internal_trigger_handshake_callback(struct us_internal_ssl_socket_t *s,
int success);
void us_internal_on_ssl_handshake(
struct us_internal_ssl_socket_context_t *context,
us_internal_on_handshake_t onhandshake, void *custom_data);
Expand Down Expand Up @@ -289,7 +292,7 @@ struct us_listen_socket_t *us_internal_ssl_socket_context_listen(
int port, int options, int socket_ext_size);

struct us_listen_socket_t *us_internal_ssl_socket_context_listen_unix(
struct us_internal_ssl_socket_context_t *context, const char *path,
struct us_internal_ssl_socket_context_t *context, const char *path,
size_t pathlen, int options, int socket_ext_size);

struct us_internal_ssl_socket_t *us_internal_ssl_socket_context_connect(
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#define LIBUS_RECV_BUFFER_PADDING 32
/* Guaranteed alignment of extension memory */
#define LIBUS_EXT_ALIGNMENT 16
#define ALLOW_SERVER_RENEGOTIATION 0

/* Define what a socket descriptor is based on platform */
#ifdef _WIN32
Expand Down Expand Up @@ -195,6 +196,8 @@ struct us_bun_socket_context_options_t {
unsigned int secure_options;
int reject_unauthorized;
int request_cert;
unsigned int client_renegotiation_limit;
unsigned int client_renegotiation_window;
};

/* Return 15-bit timestamp for this context */
Expand Down
6 changes: 4 additions & 2 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// clang-format off
#ifndef UWS_APP_H
#define UWS_APP_H

Expand Down Expand Up @@ -77,6 +77,8 @@ namespace uWS {
unsigned int secure_options = 0;
int reject_unauthorized = 0;
int request_cert = 0;
unsigned int client_renegotiation_limit = 3;
unsigned int client_renegotiation_window = 600;

/* Conversion operator used internally */
operator struct us_bun_socket_context_options_t() const {
Expand Down Expand Up @@ -597,4 +599,4 @@ typedef TemplatedApp<true> SSLApp;

}

#endif // UWS_APP_H
#endif // UWS_APP_H
83 changes: 83 additions & 0 deletions src/bun.js/api/bun/socket.zig
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ noinline fn getSSLException(globalThis: *JSC.JSGlobalObject, defaultMessage: []c
return exception;
}

/// we always allow and check the SSL certificate after the handshake or renegotiation
fn alwaysAllowSSLVerifyCallback(_: c_int, _: ?*BoringSSL.X509_STORE_CTX) callconv(.C) c_int {
return 1;
}

fn normalizeHost(input: anytype) @TypeOf(input) {
return input;
}
Expand Down Expand Up @@ -2049,6 +2054,84 @@ fn NewSocket(comptime ssl: bool) type {
return JSValue.jsUndefined();
}

pub fn disableRenegotiation(
this: *This,
_: *JSC.JSGlobalObject,
_: *JSC.CallFrame,
) callconv(.C) JSValue {
if (comptime ssl == false) {
return JSValue.jsUndefined();
}
if (this.detached) {
return JSValue.jsUndefined();
}

const ssl_ptr = this.socket.ssl();
BoringSSL.SSL_set_renegotiate_mode(ssl_ptr, BoringSSL.ssl_renegotiate_never);
return JSValue.jsUndefined();
}

pub fn setVerifyMode(
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(2);

if (args.len < 2) {
globalObject.throw("Expected requestCert and rejectUnauthorized arguments", .{});
return .zero;
}
const request_cert_js = args.ptr[0];
const reject_unauthorized_js = args.ptr[1];
if (!request_cert_js.isBoolean() or !reject_unauthorized_js.isBoolean()) {
globalObject.throw("Expected requestCert and rejectUnauthorized arguments to be boolean", .{});
return .zero;
}

const request_cert = request_cert_js.toBoolean();
const reject_unauthorized = request_cert_js.toBoolean();
var verify_mode: c_int = BoringSSL.SSL_VERIFY_NONE;
if (this.handlers.is_server) {
if (request_cert) {
verify_mode = BoringSSL.SSL_VERIFY_PEER;
if (reject_unauthorized)
verify_mode |= BoringSSL.SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
}
}
const ssl_ptr = this.socket.ssl();
// we always allow and check the SSL certificate after the handshake or renegotiation
BoringSSL.SSL_set_verify(ssl_ptr, verify_mode, alwaysAllowSSLVerifyCallback);
return JSValue.jsUndefined();
}

pub fn renegotiate(
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 = this.socket.ssl();
BoringSSL.ERR_clear_error();
if (BoringSSL.SSL_renegotiate(ssl_ptr) != 1) {
globalObject.throwValue(getSSLException(globalObject, "SSL_renegotiate error"));
return .zero;
}
return JSValue.jsUndefined();
}
pub fn getTLSTicket(
this: *This,
globalObject: *JSC.JSGlobalObject,
Expand Down
14 changes: 14 additions & 0 deletions src/bun.js/api/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ pub const ServerConfig = struct {
ssl_ciphers: [*c]const u8 = null,
protos: [*c]const u8 = null,
protos_len: usize = 0,
client_renegotiation_limit: u32 = 0,
client_renegotiation_window: u32 = 0,

const log = Output.scoped(.SSLConfig, false);

Expand Down Expand Up @@ -660,6 +662,18 @@ pub const ServerConfig = struct {
}
}

if (obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| {
if (client_renegotiation_limit.isNumber()) {
result.client_renegotiation_limit = client_renegotiation_limit.toU32();
}
}

if (obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| {
if (client_renegotiation_window.isNumber()) {
result.client_renegotiation_window = client_renegotiation_window.toU32();
}
}

if (obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| {
var sliced = dh_params_file_name.toSlice(global, bun.default_allocator);
defer sliced.deinit();
Expand Down
12 changes: 12 additions & 0 deletions src/bun.js/api/sockets.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ function generate(ssl) {
fn: "getCipher",
length: 0,
},
renegotiate: {
fn: "renegotiate",
length: 0,
},
disableRenegotiation: {
fn: "disableRenegotiation",
length: 0,
},
setVerifyMode: {
fn: "setVerifyMode",
length: 2,
},
getSession: {
fn: "getSession",
length: 0,
Expand Down
2 changes: 2 additions & 0 deletions src/deps/uws.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,8 @@ pub const us_bun_socket_context_options_t = extern struct {
secure_options: u32 = 0,
reject_unauthorized: i32 = 0,
request_cert: i32 = 0,
client_renegotiation_limit: u32 = 3,
client_renegotiation_window: u32 = 600,
};

pub const us_bun_verify_error_t = extern struct {
Expand Down
18 changes: 11 additions & 7 deletions src/http.zig
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ const ProxyTunnel = struct {
ssl.configureHTTPClient(hostname);
BoringSSL.SSL_CTX_set_verify(ssl_ctx, BoringSSL.SSL_VERIFY_NONE, null);
BoringSSL.SSL_set_verify(ssl, BoringSSL.SSL_VERIFY_NONE, null);
// TODO: change this to ssl_renegotiate_explicit for optimization
// if we allow renegotiation, we need to set the mode here
// https://github.com/oven-sh/bun/issues/6197
// https://github.com/oven-sh/bun/issues/5363
// renegotiation is only valid for <= TLS1_2_VERSION
BoringSSL.SSL_set_renegotiate_mode(ssl, BoringSSL.ssl_renegotiate_freely);
return ProxyTunnel{ .ssl = ssl, .ssl_ctx = ssl_ctx, .in_bio = in_bio, .out_bio = out_bio, .read_buffer = bun.default_allocator.alloc(u8, 16 * 1024) catch unreachable, .partial_data = null };
}
unreachable;
Expand Down Expand Up @@ -466,10 +472,10 @@ fn NewHTTPContext(comptime ssl: bool) type {
return client.firstCall(comptime ssl, socket);
} else {
// if authorized it self is false, this means that the connection was rejected
return client.onConnectError(
comptime ssl,
socket,
);
socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, ActiveSocket.init(&dead_socket).ptr());
if (client.state.stage != .done and client.state.stage != .fail)
client.fail(error.ConnectionRefused);
return;
}
}

Expand Down Expand Up @@ -1062,11 +1068,9 @@ pub fn onTimeout(
pub fn onConnectError(
client: *HTTPClient,
comptime is_ssl: bool,
socket: NewHTTPContext(is_ssl).HTTPSocket,
_: NewHTTPContext(is_ssl).HTTPSocket,
) void {
_ = socket;
log("onConnectError {s}\n", .{client.url.href});

if (client.state.stage != .done and client.state.stage != .fail)
client.fail(error.ConnectionRefused);
}
Expand Down
3 changes: 3 additions & 0 deletions src/js/node/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,7 @@ class ClientRequest extends OutgoingMessage {
#timeoutTimer?: Timer = undefined;
#options;
#finished;
#tls;

get path() {
return this.#path;
Expand Down Expand Up @@ -1462,6 +1463,7 @@ class ClientRequest extends OutgoingMessage {
timeout: false,
// Disable auto gzip/deflate
decompress: false,
tls: this.#tls,
};

if (!!$debug) {
Expand Down Expand Up @@ -1681,6 +1683,7 @@ class ClientRequest extends OutgoingMessage {
this.#reusedSocket = false;
this.#host = host;
this.#protocol = protocol;
this.#tls = options.tls;

const timeout = options.timeout;
if (timeout !== undefined && timeout !== 0) {
Expand Down
47 changes: 40 additions & 7 deletions src/js/node/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,18 +399,50 @@ const TLSSocket = (function (InternalTLSSocket) {
return !!this.#session;
}

renegotiate() {
renegotiate(options, callback) {
if (this.#renegotiationDisabled) {
// if renegotiation is disabled should emit error event in nextTick for nodejs compatibility
const error = new Error("ERR_TLS_RENEGOTIATION_DISABLED: TLS session renegotiation disabled for this socket");
error.name = "ERR_TLS_RENEGOTIATION_DISABLED";
throw error;
typeof callback === "function" && process.nextTick(callback, error);
return false;
}

throw Error("Not implented in Bun yet");
const socket = this[bunSocketInternal];
// if the socket is detached we can't renegotiate, nodejs do a noop too (we should not return false or true here)
if (!socket) return;

if (options) {
let requestCert = !!this._requestCert;
let rejectUnauthorized = !!this._rejectUnauthorized;

if (options.requestCert !== undefined) requestCert = !!options.requestCert;
if (options.rejectUnauthorized !== undefined) rejectUnauthorized = !!options.rejectUnauthorized;

if (requestCert !== this._requestCert || rejectUnauthorized !== this._rejectUnauthorized) {
socket.setVerifyMode(requestCert, rejectUnauthorized);
this._requestCert = requestCert;
this._rejectUnauthorized = rejectUnauthorized;
}
}
try {
socket.renegotiate();
// if renegotiate is successful should emit secure event when done
typeof callback === "function" && this.once("secure", () => callback(null));
return true;
} catch (err) {
// if renegotiate fails should emit error event in nextTick for nodejs compatibility
typeof callback === "function" && process.nextTick(callback, err);
return false;
}
}

disableRenegotiation() {
this.#renegotiationDisabled = true;
// disable renegotiation on the socket
return this[bunSocketInternal]?.disableRenegotiation();
}

getTLSTicket() {
return this[bunSocketInternal]?.getTLSTicket();
}
Expand Down Expand Up @@ -485,7 +517,8 @@ const TLSSocket = (function (InternalTLSSocket) {
}
},
);

let CLIENT_RENEG_LIMIT = 3,
CLIENT_RENEG_WINDOW = 600;
class Server extends NetServer {
key;
cert;
Expand Down Expand Up @@ -592,6 +625,8 @@ class Server extends NetServer {
rejectUnauthorized: this._rejectUnauthorized,
requestCert: isClient ? true : this._requestCert,
ALPNProtocols: this.ALPNProtocols,
clientRenegotiationLimit: CLIENT_RENEG_LIMIT,
clientRenegotiationWindow: CLIENT_RENEG_WINDOW,
},
SocketClass,
];
Expand All @@ -601,9 +636,7 @@ class Server extends NetServer {
function createServer(options, connectionListener) {
return new Server(options, connectionListener);
}
const CLIENT_RENEG_LIMIT = 3,
CLIENT_RENEG_WINDOW = 600,
DEFAULT_ECDH_CURVE = "auto",
const DEFAULT_ECDH_CURVE = "auto",
// https://github.com/Jarred-Sumner/uSockets/blob/fafc241e8664243fc0c51d69684d5d02b9805134/src/crypto/openssl.c#L519-L523
DEFAULT_CIPHERS =
"DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
Expand Down
29 changes: 29 additions & 0 deletions test/js/node/tls/renegotiation-feature.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 74d91f6

Please sign in to comment.