Skip to content

ssl: add SSLContext#client_hello_cb= - #1126

Open
kou wants to merge 1 commit into
ruby:masterfrom
kou:client-hello-cb
Open

kou wants to merge 1 commit into
ruby:masterfrom
kou:client-hello-cb

Conversation

@kou

@kou kou commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Add a binding for SSL_CTX_set_client_hello_cb(). The callback is called with an SSLSocket and an OpenSSL::SSL::ClientHello, and can return an SSLContext to switch to. OpenSSL::SSL::ClientHello is only valid while the callback is running and currently provides only #get_extension.

Unlike the servername callback used by SSLContext#servername_cb=, the ClientHello callback is called before the protocol version is negotiated. So the following settings of the returned SSLContext can be applied to the connection:

  • verify_mode
  • verify_depth
  • options
  • min_version
  • max_version

Apache httpd and nginx use the ClientHello callback for SNI-based virtual host selection for the same reason.

OpenSSL may call the servername callback of the original SSL_CTX after the SSL_CTX is switched by the ClientHello callback. Look up the SSLContext from the SSL_CTX that registered the servername callback instead of SSLSocket#context.

This is not supported by LibreSSL.

Fixes #1125.

Add a binding for `SSL_CTX_set_client_hello_cb()`. The callback is
called with an `SSLSocket` and an `OpenSSL::SSL::ClientHello`, and can
return an `SSLContext` to switch to. `OpenSSL::SSL::ClientHello` is
only valid while the callback is running and currently provides only
`#get_extension`.

Unlike the servername callback used by `SSLContext#servername_cb=`, the
ClientHello callback is called before the protocol version is
negotiated. So the following settings of the returned `SSLContext` can
be applied to the connection:

* `verify_mode`
* `verify_depth`
* `options`
* `min_version`
* `max_version`

Apache httpd and nginx use the ClientHello callback for SNI-based
virtual host selection for the same reason.

OpenSSL may call the servername callback of the original `SSL_CTX`
after the `SSL_CTX` is switched by the ClientHello callback. Look up
the `SSLContext` from the `SSL_CTX` that registered the servername
callback instead of `SSLSocket#context`.

This is not supported by LibreSSL.

Fixes ruby#1125.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kou
kou force-pushed the client-hello-cb branch from fe6ee8a to 00e2617 Compare October 13, 2026 07:28
Comment thread ext/openssl/ossl_ssl.c
Comment on lines +460 to +463
/*
* OpenSSL may invoke the callback of the original SSL_CTX after
* SSL_set_SSL_CTX(), so use the SSLContext that registered it
*/

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.

Comment thread ext/openssl/ossl_ssl.c
Comment on lines +508 to +511
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 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.

Comment thread ext/openssl/ossl_ssl.c
* 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.

Comment thread ext/openssl/ossl_ssl.c
Comment on lines +569 to +576
/* 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));

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.

Comment thread ext/openssl/ossl_ssl.c
Comment on lines +569 to +580
/* 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));
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");

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.

Comment thread ext/openssl/ossl_ssl.c
Comment on lines +567 to +568
if (!SSL_set_SSL_CTX(ssl, ctx2))
ossl_raise(eSSLError, "SSL_set_SSL_CTX");

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.

Comment thread ext/openssl/ossl_ssl.c
*/
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 thread ext/openssl/ossl_ssl.c
Comment on lines +508 to +511
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.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OpenSSL::SSL::SSLContext#client_hello_cb= (binding for SSL_CTX_set_client_hello_cb())

2 participants