Skip to content
Open
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
1 change: 1 addition & 0 deletions ext/openssl/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def find_openssl_library

# added in OpenSSL 1.1.1, currently not in LibreSSL
have_func("OPENSSL_sk_new_reserve(NULL, 0)", stack_h)
have_func("SSL_CTX_set_client_hello_cb(NULL, NULL, NULL)", ssl_h)

# added in 3.0.0
have_func("SSL_CTX_set0_tmp_dh_pkey(NULL, NULL)", ssl_h)
Expand Down
155 changes: 151 additions & 4 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode,
id_i_session_id_context, id_i_session_get_cb, id_i_session_new_cb,
id_i_session_remove_cb, id_i_npn_select_cb, id_i_npn_protocols,
id_i_alpn_select_cb, id_i_alpn_protocols, id_i_servername_cb,
id_i_verify_hostname, id_i_keylog_cb, id_i_tmp_dh_callback;
id_i_client_hello_cb, id_i_verify_hostname, id_i_keylog_cb,
id_i_tmp_dh_callback;
static ID id_i_io, id_i_context, id_i_hostname, id_i_sync_close;

static int ossl_ssl_ex_ptr_idx;
Expand Down Expand Up @@ -441,16 +442,27 @@ ossl_sslctx_add_extra_chain_cert_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, arg))

static VALUE ossl_sslctx_setup(VALUE self);

struct ossl_call_servername_cb_args {
SSL *ssl;
SSL_CTX *ctx;
};

static VALUE
ossl_call_servername_cb(VALUE arg)
{
SSL *ssl = (void *)arg;
struct ossl_call_servername_cb_args *args = (void *)arg;
SSL *ssl = args->ssl;
const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
if (!servername)
return Qnil;

VALUE ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
VALUE sslctx_obj = rb_attr_get(ssl_obj, id_i_context);
/*
* OpenSSL may invoke the callback of the original SSL_CTX after
* SSL_set_SSL_CTX(), so use the SSLContext that registered it
*/
Comment on lines +460 to +463

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens when both of servername_cb and client_hello_cb are used.

VALUE sslctx_obj = (VALUE)SSL_CTX_get_ex_data(args->ctx,
ossl_sslctx_ex_ptr_idx);
VALUE cb = rb_attr_get(sslctx_obj, id_i_servername_cb);
VALUE ary = rb_assoc_new(ssl_obj, rb_str_new_cstr(servername));

Expand All @@ -473,9 +485,10 @@ ossl_call_servername_cb(VALUE arg)
static int
ssl_servername_cb(SSL *ssl, int *ad, void *arg)
{
struct ossl_call_servername_cb_args args = { ssl, arg };
int state;

rb_protect(ossl_call_servername_cb, (VALUE)ssl, &state);
rb_protect(ossl_call_servername_cb, (VALUE)&args, &state);
if (state) {
VALUE ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
rb_ivar_set(ssl_obj, ID_callback_state, INT2NUM(state));
Expand All @@ -485,6 +498,115 @@ ssl_servername_cb(SSL *ssl, int *ad, void *arg)
return SSL_TLSEXT_ERR_OK;
}

#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
static VALUE cClientHello;

/*
* The SSL object is borrowed from the SSLSocket and is only valid while
* the client_hello_cb is running.
*/
static const rb_data_type_t ossl_client_hello_type = {
.wrap_struct_name = "OpenSSL/SSL/ClientHello",
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED,

Recently we switched all RUBY_TYPED_FREE_IMMEDIATELY wherever possible.

};
Comment on lines +508 to +511

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add bindings of SSL_client_hello_*() to SSLSocket, we don't need to define this new class. I defined this class for easy to use (I think that client_hello.get_extension is better than ssl_socket.client_hello_get_extension) but we can remove this class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree a method that is only callable from the callback is not a good fit.

Another possible alternative would be to pass everything (for example in a Hash mapping the type to the raw bytes) to the callback, though I'm not sure which approach is better.


static inline SSL *
GetClientHello(VALUE obj)
{
SSL *ssl;

TypedData_Get_Struct(obj, SSL, &ossl_client_hello_type, ssl);
if (!ssl)
ossl_raise(rb_eRuntimeError, "ClientHello is out of scope!");
return ssl;
}

/*
* call-seq:
* client_hello.get_extension(type) -> String or nil
*
* Returns the raw data of the extension specified by the extension type
* number _type_, or +nil+ if the ClientHello doesn't contain it.
*
* See SSL_client_hello_get0_ext(3).
*/
static VALUE
ossl_client_hello_get_extension(VALUE self, VALUE type)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenSSL provides more SSL_client_hello_*() APIs but I just implement only this as the first step. We can add more methods as follow-up tasks.

{
SSL *ssl;
const unsigned char *out;
size_t outlen;

ssl = GetClientHello(self);
if (!SSL_client_hello_get0_ext(ssl, NUM2UINT(type), &out, &outlen))
return Qnil;
return rb_str_new((const char *)out, (long)outlen);
}

struct ossl_call_client_hello_cb_args {
SSL *ssl;
VALUE client_hello;
};

static VALUE
ossl_call_client_hello_cb(VALUE arg)
{
struct ossl_call_client_hello_cb_args *args = (void *)arg;
SSL *ssl = args->ssl;
VALUE ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
VALUE sslctx_obj = rb_attr_get(ssl_obj, id_i_context);
VALUE cb = rb_attr_get(sslctx_obj, id_i_client_hello_cb);

args->client_hello =
TypedData_Wrap_Struct(cClientHello, &ossl_client_hello_type, ssl);
VALUE ret_obj = rb_funcall(cb, id_call, 2, ssl_obj, args->client_hello);
if (rb_obj_is_kind_of(ret_obj, cSSLContext)) {
SSL_CTX *ctx2;
ossl_sslctx_setup(ret_obj);
GetSSLCTX(ret_obj, ctx2);
if (!SSL_set_SSL_CTX(ssl, ctx2))
ossl_raise(eSSLError, "SSL_set_SSL_CTX");
Comment on lines +567 to +568

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented on #1125, but I don't think we should increase exposure to SSL_set_SSL_CTX().

The following SSL_*() calls below are still not exhaustive, and some fields of SSL can't be changed after SSL_new(), for example session management.

/* SSL_set_SSL_CTX() doesn't update these settings */
SSL_set_verify(ssl,
SSL_CTX_get_verify_mode(ctx2),
SSL_CTX_get_verify_callback(ctx2));
SSL_set_verify_depth(ssl, SSL_CTX_get_verify_depth(ctx2));
SSL_clear_options(ssl,
SSL_get_options(ssl) & ~SSL_CTX_get_options(ctx2));
SSL_set_options(ssl, SSL_CTX_get_options(ctx2));
Comment on lines +569 to +576

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do them in ossl_call_servername_cb() too but it's out of scope of this PR. I can open a PR for it after this is merged.

if (!SSL_set_min_proto_version(ssl, SSL_CTX_get_min_proto_version(ctx2)))
ossl_raise(eSSLError, "SSL_set_min_proto_version");
if (!SSL_set_max_proto_version(ssl, SSL_CTX_get_max_proto_version(ctx2)))
ossl_raise(eSSLError, "SSL_set_max_proto_version");
Comment on lines +569 to +580

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rb_ivar_set(ssl_obj, id_i_context, ret_obj);
} else if (!NIL_P(ret_obj)) {
ossl_raise(rb_eArgError, "client_hello_cb must return an "
"OpenSSL::SSL::SSLContext object or nil");
}

return Qnil;
}

static int
ssl_client_hello_cb(SSL *ssl, int *al, void *arg)
{
struct ossl_call_client_hello_cb_args args = { ssl, Qnil };
int state;

rb_protect(ossl_call_client_hello_cb, (VALUE)&args, &state);
if (!NIL_P(args.client_hello))
RTYPEDDATA_DATA(args.client_hello) = NULL;
if (state) {
VALUE ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
rb_ivar_set(ssl_obj, ID_callback_state, INT2NUM(state));
*al = SSL_AD_INTERNAL_ERROR;
return SSL_CLIENT_HELLO_ERROR;
}

return SSL_CLIENT_HELLO_SUCCESS;
}
#endif

static void
ssl_renegotiation_cb(const SSL *ssl)
{
Expand Down Expand Up @@ -841,6 +963,16 @@ ossl_sslctx_setup(VALUE self)
OSSL_Debug("SSL ALPN select callback added");
}

if (RTEST(rb_attr_get(self, id_i_client_hello_cb))) {
#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
SSL_CTX_set_client_hello_cb(ctx, ssl_client_hello_cb, NULL);
OSSL_Debug("SSL client hello callback added");
#else
rb_raise(rb_eNotImpError, "client_hello_cb is not supported "
"by the linked SSL library");
#endif
}

rb_obj_freeze(self);

val = rb_attr_get(self, id_i_session_id_context);
Expand Down Expand Up @@ -868,6 +1000,7 @@ ossl_sslctx_setup(VALUE self)
val = rb_attr_get(self, id_i_servername_cb);
if (!NIL_P(val)) {
SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb);
SSL_CTX_set_tlsext_servername_arg(ctx, ctx);
OSSL_Debug("SSL TLSEXT servername callback added");
}

Expand Down Expand Up @@ -2944,6 +3077,19 @@ Init_ossl_ssl(void)
rb_define_method(cSSLContext, "options", ossl_sslctx_get_options, 0);
rb_define_method(cSSLContext, "options=", ossl_sslctx_set_options, 1);

#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
/*
* Document-class: OpenSSL::SSL::ClientHello
*
* A ClientHello message received by the server. It is passed to
* SSLContext#client_hello_cb and is only valid while the callback is
* running.
*/
cClientHello = rb_define_class_under(mSSL, "ClientHello", rb_cObject);
rb_undef_alloc_func(cClientHello);
rb_define_method(cClientHello, "get_extension", ossl_client_hello_get_extension, 1);
#endif

/*
* Document-class: OpenSSL::SSL::SSLSocket
*/
Expand Down Expand Up @@ -3165,6 +3311,7 @@ Init_ossl_ssl(void)
DefIVarID(alpn_protocols);
DefIVarID(alpn_select_cb);
DefIVarID(servername_cb);
DefIVarID(client_hello_cb);
DefIVarID(verify_hostname);
DefIVarID(keylog_cb);
DefIVarID(tmp_dh_callback);
Expand Down
16 changes: 16 additions & 0 deletions lib/openssl/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ class SSLContext
# callback must return an SSLContext for the server name or nil.
attr_accessor :servername_cb

# A callback invoked when the server receives a ClientHello message,
# before the protocol version and session resumption are decided.
#
# The callback is invoked with an SSLSocket and an
# OpenSSL::SSL::ClientHello. The ClientHello is only valid while the
# callback is running. The callback must return an SSLContext to use
# for the connection or nil to keep using the current one.
#
# #verify_mode, #verify_depth, #options, #min_version= and
# #max_version= of the returned SSLContext take effect. To prevent a
# session established with one SSLContext from being resumed with
# another, set a different #session_id_context to each SSLContext.
#
# This is not supported by LibreSSL.
attr_accessor :client_hello_cb

# call-seq:
# SSLContext.new -> ctx
# SSLContext.new(:TLSv1) -> ctx
Expand Down
99 changes: 98 additions & 1 deletion test/openssl/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,102 @@ def test_servername_cb_raises_an_exception_on_unknown_objects
end
end

def test_client_hello_cb
omit "LibreSSL doesn't support client_hello_cb" if libressl?

tls12_ctx = make_server_context
tls12_ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
# server_name extension (RFC 6066) containing a single host_name
name = "example.org"
ext = [name.bytesize + 3, 0, name.bytesize, name].pack("nCna*")
exts = []
saved_client_hello = nil
sctx = make_server_context
sctx.min_version = OpenSSL::SSL::TLS1_3_VERSION
sctx.client_hello_cb = lambda { |ssl, client_hello|
saved_client_hello = client_hello
exts << client_hello.get_extension(0) # server_name
exts.last ? tls12_ctx : nil
}
start_server(sctx) do |port|
server_connect(port, hostname: name) { |ssl|
assert_equal "TLSv1.2", ssl.ssl_version
ssl.puts "abc"
assert_equal "abc\n", ssl.gets
}
server_connect(port) { |ssl|
assert_equal "TLSv1.3", ssl.ssl_version
ssl.puts "abc"
assert_equal "abc\n", ssl.gets
}
end
assert_equal [ext, nil], exts
assert_raise_with_message(RuntimeError, /out of scope/) {
saved_client_hello.get_extension(0)
}
end

def test_client_hello_cb_verify_mode
omit "LibreSSL doesn't support client_hello_cb" if libressl?

client_auth_ctx = make_server_context
client_auth_ctx.verify_mode =
OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
sctx = make_server_context
sctx.client_hello_cb = lambda { |ssl, client_hello| client_auth_ctx }
start_server(sctx, ignore_listener_error: true) { |port|
assert_raise(OpenSSL::SSL::SSLError) {
server_connect(port) { |ssl|
ssl.puts("abc")
ssl.gets
}
}
}
end

def test_client_hello_cb_with_servername_cb
omit "LibreSSL doesn't support client_hello_cb" if libressl?

called = false
ctx_without_servername_cb = make_server_context
sctx = make_server_context
sctx.client_hello_cb = lambda { |ssl, client_hello|
ctx_without_servername_cb
}
# OpenSSL invokes the servername_cb of sctx instead
sctx.servername_cb = lambda { |args|
called = true
nil
}
start_server(sctx) do |port|
server_connect(port, hostname: "example.org") { |ssl|
ssl.puts "abc"
assert_equal "abc\n", ssl.gets
}
end
assert_equal true, called
end

def test_client_hello_cb_exception
omit "LibreSSL doesn't support client_hello_cb" if libressl?

server_proc = proc do |sock|
sctx = make_server_context
sctx.client_hello_cb = lambda { |ssl, client_hello|
raise RuntimeError, "foo"
}
ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx)
assert_raise_with_message(RuntimeError, "foo") { ssl.accept }
end
start_server_proc(server_proc) do |port|
sock = TCPSocket.new("127.0.0.1", port)
ssl = OpenSSL::SSL::SSLSocket.new(sock)
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
ensure
sock&.close
end
end

def test_verify_hostname_on_connect
exts = [
["keyUsage", "keyEncipherment,digitalSignature", true],
Expand Down Expand Up @@ -2446,10 +2542,11 @@ def test_ractor_set_params

private

def server_connect(port, ctx = nil)
def server_connect(port, ctx = nil, hostname: nil)
sock = TCPSocket.new("127.0.0.1", port)
ssl = ctx ? OpenSSL::SSL::SSLSocket.new(sock, ctx) : OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.hostname = hostname if hostname
ssl.connect
yield ssl if block_given?
ensure
Expand Down
Loading