From 89a15cffd02b0d87ebad3ebbe4e57e6e964af33c Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Tue, 27 Oct 2026 17:09:46 +0900 Subject: [PATCH] ssl: add SSLSocket#min_version= and #max_version= Allow setting the supported protocol version range per connection. They are the SSLSocket counterparts of SSLContext#min_version= and #max_version=. This is a step toward configuring the SSL object directly from callbacks instead of switching the SSLContext. See also: https://github.com/ruby/openssl/issues/1125 Co-Authored-By: Claude Opus 5.5 --- ext/openssl/ossl_ssl.c | 50 ++++++++++++++++++++++++++++++++++++++++ test/openssl/test_ssl.rb | 33 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index e58764a26..141c9a1e4 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -954,6 +954,30 @@ ossl_sslctx_set_min_version(VALUE self, VALUE v) return v; } +/* + * call-seq: + * ssl.min_version = OpenSSL::SSL::TLS1_2_VERSION + * ssl.min_version = :TLS1_2 + * ssl.min_version = 0 + * ssl.min_version = nil + * + * Sets the lower bound of the supported SSL/TLS protocol version range for + * this connection only. See SSLContext#min_version= for the possible values. + */ +static VALUE +ossl_ssl_set_min_version(VALUE self, VALUE v) +{ + SSL *ssl; + int version; + + GetSSL(self, ssl); + version = parse_proto_version(v); + + if (!SSL_set_min_proto_version(ssl, version)) + ossl_raise(eSSLError, "SSL_set_min_proto_version"); + return v; +} + /* * call-seq: * ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION @@ -979,6 +1003,30 @@ ossl_sslctx_set_max_version(VALUE self, VALUE v) return v; } +/* + * call-seq: + * ssl.max_version = OpenSSL::SSL::TLS1_2_VERSION + * ssl.max_version = :TLS1_2 + * ssl.max_version = 0 + * ssl.max_version = nil + * + * Sets the upper bound of the supported SSL/TLS protocol version range for + * this connection only. See #min_version= for details. + */ +static VALUE +ossl_ssl_set_max_version(VALUE self, VALUE v) +{ + SSL *ssl; + int version; + + GetSSL(self, ssl); + version = parse_proto_version(v); + + if (!SSL_set_max_proto_version(ssl, version)) + ossl_raise(eSSLError, "SSL_set_max_proto_version"); + return v; +} + static VALUE ossl_ssl_cipher_to_ary(const SSL_CIPHER *cipher) { @@ -2973,6 +3021,8 @@ Init_ossl_ssl(void) rb_define_method(cSSLSocket, "client_ca", ossl_ssl_get_client_ca_list, 0); /* #hostname is defined in lib/openssl/ssl.rb */ rb_define_method(cSSLSocket, "hostname=", ossl_ssl_set_hostname, 1); + rb_define_method(cSSLSocket, "min_version=", ossl_ssl_set_min_version, 1); + rb_define_method(cSSLSocket, "max_version=", ossl_ssl_set_max_version, 1); rb_define_method(cSSLSocket, "finished_message", ossl_ssl_get_finished, 0); rb_define_method(cSSLSocket, "peer_finished_message", ossl_ssl_get_peer_finished, 0); rb_define_method(cSSLSocket, "tmp_key", ossl_ssl_tmp_key, 0); diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 78b45934a..4db0300be 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1500,6 +1500,39 @@ def test_minmax_version_symbols } end + def test_socket_minmax_version + start_server { |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.sync_close = true + ssl.min_version = ssl.max_version = :TLS1_2 + ssl.connect + assert_equal("TLSv1.2", ssl.ssl_version) + ssl.close + } + + # Invalid + IO.pipe { |r, _w| + ssl = OpenSSL::SSL::SSLSocket.new(r) + assert_raise(ArgumentError) { ssl.min_version = :bogus } + assert_raise(ArgumentError) { ssl.max_version = :bogus } + } + end + + def test_socket_minmax_version_server + sctx = make_server_context + server_proc = -> sock { + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + ssl.max_version = OpenSSL::SSL::TLS1_2_VERSION + ssl.accept + } + start_server_proc(server_proc) { |port| + server_connect(port) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } + } + end + def test_minmax_version_system_default omit "LibreSSL and AWS-LC do not support OPENSSL_CONF" if libressl? || aws_lc?