Skip to content
Merged
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
46 changes: 34 additions & 12 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ void *us_socket_session_sink(struct us_socket_t *s) {
/* socket.c — raw TCP FIN that does NOT re-enter the SSL layer. */
extern void us_internal_socket_raw_shutdown(struct us_socket_t *s);

static void ssl_update_handshake(struct us_socket_t *s);
static void ssl_update_handshake(struct us_socket_t *s, int fin_ends_handshake);
static int ssl_flush_write_batch(struct loop_ssl_data *loop_ssl_data, struct us_socket_t *s);
static int us_ssl_inline_reject_tripped(struct us_socket_t *s);
static inline int ssl_gone(struct us_socket_t *s);
Expand Down Expand Up @@ -1101,9 +1101,11 @@ static int us_cert_verify_cb(X509_STORE_CTX *ctx, void *arg) {
(void)arg;
int ok = X509_verify_cert(ctx);
SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
if (!ssl || SSL_is_server(ssl) || X509_STORE_CTX_get_error(ctx) != X509_V_OK) return ok;
if (!ssl) return ok;
void *wrapper = us_ssl_wrapper(ssl);
struct us_socket_t *s = wrapper ? NULL : us_ssl_socket(ssl);
if (s) s->ssl_peer_chain_checked = 1;
if (SSL_is_server(ssl) || X509_STORE_CTX_get_error(ctx) != X509_V_OK) return ok;
int verdict = wrapper ? us_ssl_wrapper_server_identity(wrapper, ssl)
: s ? us_dispatch_server_identity(s, ssl)
: US_IDENTITY_UNCHECKED;
Expand Down Expand Up @@ -1700,6 +1702,7 @@ void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
s->ssl_inline_reject = 0;
s->ssl_verify_failed = 0;
s->ssl_identity_checked = 0;
s->ssl_peer_chain_checked = 0;
s->ssl_sni_pending = US_SNI_NONE;
s->ssl_sni_resolver = 0;
s->ssl_has_pending_events = 0;
Expand Down Expand Up @@ -1805,13 +1808,27 @@ struct us_bun_verify_error_t us_ssl_socket_verify_error_from_ssl(SSL *ssl) {
return (struct us_bun_verify_error_t){.error = x509_verify_error, .code = code, .reason = reason};
}

/* A sent FIN, a sent close_notify or a fatal error says nothing about the
* peer's certificate: on an open socket the SSL alone answers, like node's
* TLSWrap::VerifyError
* (https://github.com/nodejs/node/blob/v26.3.0/src/crypto/crypto_tls.cc#L1840-L1853). */
struct us_bun_verify_error_t us_internal_ssl_verify_error(struct us_socket_t *s) {
if (!s->ssl || !s_ssl(s) || us_socket_is_closed(s) || us_internal_ssl_is_shut_down(s)) {
if (!s->ssl || !s_ssl(s) || us_socket_is_closed(s)) {
return (struct us_bun_verify_error_t){.error = 0, .code = NULL, .reason = NULL};
}
return us_ssl_socket_verify_error_from_ssl(s_ssl(s));
}

/* After our own FIN a failed handshake reports the SSL's verdict only for a
* chain that it checked: node:tls reads a failure with an X509 code as an
* established session. */
static struct us_bun_verify_error_t ssl_failed_handshake_verify_error(struct us_socket_t *s) {
if (us_internal_ssl_is_shut_down(s) && !s->ssl_peer_chain_checked) {
return (struct us_bun_verify_error_t){.error = 0, .code = NULL, .reason = NULL};
}
return us_internal_ssl_verify_error(s);
}

/* ── Handshake state machine ─────────────────────────────────────────────── */

/* Park the fatal OpenSSL reason behind a failed SSL_* call where the
Expand Down Expand Up @@ -1903,7 +1920,7 @@ static void ssl_trigger_handshake(struct us_socket_t *s, int success) {
/* A finished handshake reports the SSL's X509 verdict in every socket state. */
struct us_bun_verify_error_t verify_error =
success && s->ssl ? us_ssl_socket_verify_error_from_ssl(s_ssl(s))
: us_internal_ssl_verify_error(s);
: ssl_failed_handshake_verify_error(s);
us_dispatch_handshake(s, success, verify_error);
}

Expand Down Expand Up @@ -2057,7 +2074,7 @@ struct us_socket_t *us_internal_ssl_close(struct us_socket_t *s, int code, void
return us_internal_socket_close_raw(s, code, reason);
}
ssl_set_loop_data(s);
ssl_update_handshake(s);
ssl_update_handshake(s, 1);
if (ssl_gone(s)) return s;

if (s->ssl_handshake_state != HANDSHAKE_COMPLETED) {
Expand Down Expand Up @@ -2094,7 +2111,11 @@ struct us_socket_t *us_internal_ssl_close(struct us_socket_t *s, int code, void
}
#define ssl_close us_internal_ssl_close

static void ssl_update_handshake(struct us_socket_t *s) {
/* `fin_ends_handshake` is 0 only from the writable event, which the read path
* re-enters while a handshake is in progress: the socket keeps reading after
* our FIN or close_notify, and the peer's next flight can still complete that
* handshake. For every other caller a half-closed socket's handshake is over. */
static void ssl_update_handshake(struct us_socket_t *s, int fin_ends_handshake) {
/* The OpenSSL error queue is per-thread and another socket's failure (a
* server and a client commonly share this thread) may have left entries on
* it; clear it before this socket's handshake step so any reason captured
Expand All @@ -2113,8 +2134,9 @@ static void ssl_update_handshake(struct us_socket_t *s) {
return;
}

if (us_socket_is_closed(s) || us_internal_ssl_is_shut_down(s) ||
(s_ssl(s) && SSL_get_shutdown(s_ssl(s)) & SSL_RECEIVED_SHUTDOWN)) {
if (us_socket_is_closed(s) || s->ssl_fatal_error ||
(SSL_get_shutdown(s_ssl(s)) & SSL_RECEIVED_SHUTDOWN) ||
(fin_ends_handshake && us_internal_ssl_is_shut_down(s))) {
Comment thread
robobun marked this conversation as resolved.
Resolved
ssl_trigger_handshake(s, 0);
return;
}
Expand Down Expand Up @@ -2188,7 +2210,7 @@ struct us_socket_t *us_internal_ssl_on_open(struct us_socket_t *s, int is_client
if (!result || ssl_gone(result)) return result;
/* Kick the handshake immediately — some peers stall waiting for ClientHello. */
ssl_set_loop_data(result);
ssl_update_handshake(result);
ssl_update_handshake(result, 1);
return result;
}

Expand Down Expand Up @@ -2331,7 +2353,7 @@ struct us_socket_t *us_internal_ssl_on_writable(struct us_socket_t *s) {
return us_internal_ssl_close(s, s->ssl_pending_close_code, NULL);
}
}
ssl_update_handshake(s);
ssl_update_handshake(s, 0);
if (ssl_gone(s)) return s;

if (s->ssl_read_wants_write) {
Expand Down Expand Up @@ -2859,7 +2881,7 @@ void us_socket_sni_resolve(struct us_socket_t *s, struct ssl_ctx_st *ctx, int er
loop_ssl_data->ssl_sni_resolved_ctx = ctx; /* may be NULL = default ctx */
}
/* Re-drive the handshake; select_cert_cb re-fires and consumes the state. */
ssl_update_handshake(s);
ssl_update_handshake(s, 1);
Comment thread
robobun marked this conversation as resolved.
Resolved
if (loop_ssl_data->ssl_sni_resolved_ctx) SSL_CTX_free(loop_ssl_data->ssl_sni_resolved_ctx);
loop_ssl_data->ssl_sni_resolved_ctx = outer_ctx;
}
Expand Down Expand Up @@ -2912,7 +2934,7 @@ struct us_socket_t *us_socket_adopt_tls(struct us_socket_t *s,
void us_socket_start_tls_handshake(struct us_socket_t *s) {
if (!s->ssl || us_socket_is_closed(s)) return;
ssl_set_loop_data(s);
ssl_update_handshake(s);
ssl_update_handshake(s, 1);
}

/* ── SNI on listen sockets ───────────────────────────────────────────────── */
Expand Down
2 changes: 2 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ struct us_socket_t {
unsigned char ssl_verify_failed : 1;
/* The owner checked the server's name inside this handshake. */
unsigned char ssl_identity_checked : 1;
/* The peer sent a certificate chain in this handshake, and it was checked. */
unsigned char ssl_peer_chain_checked : 1;
/* US_SNI_*: an async SNICallback has the handshake suspended. */
unsigned char ssl_sni_pending : 2;
/* Server-side socket adopted into TLS with its own SNICallback. */
Expand Down
16 changes: 14 additions & 2 deletions src/runtime/socket/socket_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,11 @@ impl<const SSL: bool> NewSocket<SSL> {
// node:tls sockets defer the hostname verdict: their JS layer applies
// `checkServerIdentity` (default or user override) itself.
let flags = this.flags.get();
// node:tls closes its own sockets, and nothing marks the ones its servers accept.
let failed_native_client = SSL
&& success == 0
&& !flags.contains(Flags::DEFERS_SERVER_IDENTITY)
&& !this.acts_as_tls_server();
// Deliberately independent of `success`: the inline-reject path
// dispatches with success=0 after suppressing the client Finished, and
// REJECTED must still be set there or the write-refusal guards are
Expand All @@ -1841,6 +1846,7 @@ impl<const SSL: bool> NewSocket<SSL> {
// failure flavor).
let reject_unauthorized = flags.contains(Flags::REJECT_UNAUTHORIZED)
&& (verify_failed
|| failed_native_client
|| (hostname_mismatch && !flags.contains(Flags::DEFERS_SERVER_IDENTITY)));
// A handshake that failed outright (success == 0 with no policy
// verdict: protocol error, peer alert, EOF mid-handshake) never has a
Expand Down Expand Up @@ -2403,7 +2409,8 @@ impl<const SSL: bool> NewSocket<SSL> {
) -> JsResult<JSValue> {
jsc::mark_binding!();

if this.socket.get().is_detached() {
let socket = this.socket.get();
if socket.is_detached() {
// The verdict must survive the forced close.
return Ok(this
.stored_verify_error_to_js(global)
Expand All @@ -2412,7 +2419,7 @@ impl<const SSL: bool> NewSocket<SSL> {

// this error can change if called in different stages of hanshake
// is very usefull to have this feature depending on the user workflow
let ssl_error = this.socket.get().get_verify_error();
let ssl_error = socket.get_verify_error();
// `on_handshake` stores the name verdict, with its full message, for the in-handshake check too.
if ssl_error.error_no == 0
|| ssl_error.error_no == uws::us_bun_verify_error_t::HOSTNAME_MISMATCH
Expand All @@ -2423,6 +2430,11 @@ impl<const SSL: bool> NewSocket<SSL> {
if ssl_error.error_no == 0 {
return Ok(JSValue::NULL);
}
} else if this.flags.get().contains(Flags::HANDSHAKE_COMPLETE) && socket.is_shutdown() {
// What `on_handshake` reported stands.
return Ok(this
.stored_verify_error_to_js(global)
.unwrap_or(JSValue::NULL));
}

let code: &[u8] = ssl_error.code_bytes();
Expand Down
Loading
Loading