From d005e6d16b93170e7fc11b5f727719f3aeef7ea4 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Mon, 31 Aug 2026 23:53:26 +0900 Subject: [PATCH 1/2] ssl: update docs for SSLContext#{min,max}_version= Fix some typos, document that assigning 0 has the same effect as assigning nil, and mention that SSLContext#security_level= may also affect protocol version selection. --- ext/openssl/ossl_ssl.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 7cedec7bf..91aadf0b6 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -919,11 +919,16 @@ parse_proto_version(VALUE str) * call-seq: * ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION * ctx.min_version = :TLS1_2 + * ctx.min_version = 0 * ctx.min_version = nil * - * Sets the lower bound on the supported SSL/TLS protocol version. The + * Sets the lower bound of the supported SSL/TLS protocol version range. The * version may be specified by an integer constant named - * OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version". + * OpenSSL::SSL::*_VERSION or a Symbol. A value of +0+ or +nil+ leaves the + * lower bound unspecified. + * + * See also #security_level=, which may further restrict the available protocol + * versions. * * === Example * ctx = OpenSSL::SSL::SSLContext.new @@ -952,9 +957,10 @@ ossl_sslctx_set_min_version(VALUE self, VALUE v) * call-seq: * ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION * ctx.max_version = :TLS1_2 + * ctx.max_version = 0 * ctx.max_version = nil * - * Sets the upper bound of the supported SSL/TLS protocol version. See + * Sets the upper bound of the supported SSL/TLS protocol version range. See * #min_version= for the possible values. */ static VALUE From 5d1ce0d4a08e464a30d12d48defafa7f861aa86b Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 1 Sep 2026 00:49:46 +0900 Subject: [PATCH 2/2] ssl: refactor tests for protocol version negotiation Since we have dropped support for OpenSSL 1.0.2, all versions of OpenSSL and compatible libraries we currently support can speak TLS 1.2 and TLS 1.3. We can use this to simplify tests around the protocol version negotiation. --- test/openssl/test_ssl.rb | 312 +++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 157 deletions(-) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index a252a0e64..ae3c4502e 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1296,16 +1296,7 @@ def test_connect_exception_message_include_peeraddr end end - def check_supported_protocol_versions - possible_versions = [ - OpenSSL::SSL::SSL3_VERSION, - OpenSSL::SSL::TLS1_VERSION, - OpenSSL::SSL::TLS1_1_VERSION, - OpenSSL::SSL::TLS1_2_VERSION, - OpenSSL::SSL::TLS1_3_VERSION, - ] - supported = [] - + private def protocol_version_available?(ver) sctx = make_server_context # The default security level is 1 in OpenSSL <= 3.1, 2 in OpenSSL >= 3.2 # In OpenSSL >= 3.0, TLS 1.1 or older is disabled at level 1 @@ -1313,32 +1304,27 @@ def check_supported_protocol_versions # Explicitly reset them to avoid influenced by OPENSSL_CONF sctx.min_version = sctx.max_version = nil + ok = false start_server(sctx, ignore_listener_error: true) do |port| - possible_versions.each do |ver| - ctx = OpenSSL::SSL::SSLContext.new - ctx.security_level = 0 - ctx.min_version = ctx.max_version = ver - server_connect(port, ctx) { |ssl| - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } - supported << ver - rescue OpenSSL::SSL::SSLError - end + ctx = OpenSSL::SSL::SSLContext.new + ctx.security_level = 0 + ctx.min_version = ctx.max_version = ver + server_connect(port, ctx) { |ssl| + ssl.puts "abc"; assert_equal "abc\n", ssl.gets + } + ok = true + rescue OpenSSL::SSL::SSLError end - # Sanity check: in our test suite we assume these are always supported - assert_include(supported, OpenSSL::SSL::TLS1_2_VERSION) - assert_include(supported, OpenSSL::SSL::TLS1_3_VERSION) - - supported + ok end def test_set_params_min_version - supported = check_supported_protocol_versions - return unless supported.include?(OpenSSL::SSL::SSL3_VERSION) + return unless protocol_version_available?(OpenSSL::SSL::SSL3_VERSION) # SSLContext#set_params properly disables SSL 3.0 by default sctx = make_server_context + sctx.security_level = 0 sctx.min_version = sctx.max_version = OpenSSL::SSL::SSL3_VERSION start_server(sctx, ignore_listener_error: true) { |port| @@ -1351,141 +1337,123 @@ def test_set_params_min_version end def test_minmax_version - supported = check_supported_protocol_versions - - # name: The string that would be returned by SSL_get_version() - # method: The version-specific method name (if any) - vmap = { - OpenSSL::SSL::SSL3_VERSION => { name: "SSLv3", method: "SSLv3" }, - OpenSSL::SSL::SSL3_VERSION => { name: "SSLv3", method: "SSLv3" }, - OpenSSL::SSL::TLS1_VERSION => { name: "TLSv1", method: "TLSv1" }, - OpenSSL::SSL::TLS1_1_VERSION => { name: "TLSv1.1", method: "TLSv1_1" }, - OpenSSL::SSL::TLS1_2_VERSION => { name: "TLSv1.2", method: "TLSv1_2" }, - OpenSSL::SSL::TLS1_3_VERSION => { name: "TLSv1.3", method: nil }, - } + # Assumption: OpenSSL supports TLS 1.2 and TLS 1.3, and they are both + # enabled by default - # Server enables a single version - supported.each do |ver| - sctx = make_server_context - sctx.security_level = 0 - sctx.min_version = sctx.max_version = ver - - start_server(sctx, ignore_listener_error: true) { |port| - supported.each do |cver| - # Client enables a single version - ctx1 = OpenSSL::SSL::SSLContext.new - ctx1.security_level = 0 - ctx1.min_version = ctx1.max_version = cver - if ver == cver - server_connect(port, ctx1) { |ssl| - assert_equal vmap[cver][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } - else - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx1) } - end + # Server supports TLS 1.2 only + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + start_server(sctx, ignore_listener_error: true) { |port| + # Client enables all supported protocol versions + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = ctx.max_version = 0 + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } - # There is no version-specific SSL methods for TLS 1.3 - if cver <= OpenSSL::SSL::TLS1_2_VERSION - # Client enables a single version using #ssl_version= - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.security_level = 0 - ctx2.ssl_version = vmap[cver][:method] - if ver == cver - server_connect(port, ctx2) { |ssl| - assert_equal vmap[cver][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } - else - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx2) } - end - end - end + # Client supports TLS 1.1 and up + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } - # Client enables all supported versions - ctx3 = OpenSSL::SSL::SSLContext.new - ctx3.security_level = 0 - ctx3.min_version = ctx3.max_version = nil - server_connect(port, ctx3) { |ssl| - assert_equal vmap[ver][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } + # Client supports TLS 1.2 and up + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) } - end - if supported.size == 1 - pend "More than one protocol version must be supported" - end + # Client supports TLS 1.3 and up + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = OpenSSL::SSL::TLS1_3_VERSION + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } + } - # Server sets min_version (earliest is disabled) - sver = supported[1] + # Server supports TLS 1.3 only sctx = make_server_context - sctx.security_level = 0 - sctx.min_version = sver - + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_3_VERSION start_server(sctx, ignore_listener_error: true) { |port| - supported.each do |cver| - # Client sets min_version - ctx1 = OpenSSL::SSL::SSLContext.new - ctx1.security_level = 0 - ctx1.min_version = cver - ctx1.max_version = 0 - server_connect(port, ctx1) { |ssl| - assert_equal vmap[supported.last][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } + # Client enables all supported protocol versions + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = ctx.max_version = 0 + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.3", ssl.ssl_version) + ssl.puts("abc"); assert_equal("abc\n", ssl.gets) + } - # Client sets max_version - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.security_level = 0 - ctx2.min_version = 0 - ctx2.max_version = cver - if cver >= sver - server_connect(port, ctx2) { |ssl| - assert_equal vmap[cver][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } - else - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx2) } - end + # Client supports up to TLS 1.1 + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = 0 + begin + ctx.max_version = OpenSSL::SSL::TLS1_1_VERSION + rescue OpenSSL::SSL::SSLError + # TLS 1.1 is disabled by a compile-time option; ignoring + else + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } end + + # Client supports up to TLS 1.2 + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = 0 + ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } + + # Client supports up to TLS 1.3 + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = 0 + ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.3", ssl.ssl_version) + ssl.puts("abc"); assert_equal("abc\n", ssl.gets) + } } + end - # Server sets max_version (latest is disabled) - sver = supported[-2] - sctx = make_server_context - sctx.security_level = 0 - sctx.min_version = 0 - sctx.max_version = sver + def test_minmax_version_symbols + start_server { |port| + # nil is equivalent to 0 + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = ctx.max_version = nil + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.3", ssl.ssl_version) + ssl.puts("abc"); assert_equal("abc\n", ssl.gets) + } - start_server(sctx, ignore_listener_error: true) { |port| - supported.each do |cver| - # Client sets min_version - ctx1 = OpenSSL::SSL::SSLContext.new - ctx1.min_version = cver - if cver <= sver - server_connect(port, ctx1) { |ssl| - assert_equal vmap[sver][:name], ssl.ssl_version - ssl.puts "abc"; assert_equal "abc\n", ssl.gets + # Symbol + ctx = OpenSSL::SSL::SSLContext.new + ctx.min_version = ctx.max_version = :TLS1_2 + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } + + # Known Symbols + known = [ + [:SSL3, OpenSSL::SSL::SSL3_VERSION], + [:TLS1, OpenSSL::SSL::TLS1_VERSION], + [:TLS1_1, OpenSSL::SSL::TLS1_1_VERSION], + [:TLS1_2, OpenSSL::SSL::TLS1_2_VERSION], + [:TLS1_3, OpenSSL::SSL::TLS1_3_VERSION], + ] + known.each do |sym, i| + ctx = OpenSSL::SSL::SSLContext.new + begin + ctx.min_version = sym + rescue OpenSSL::SSL::SSLError + # If the Symbol is rejected by OpenSSL due to its compile-time + # options, the numeric equivalent should also be rejected + assert_raise(OpenSSL::SSL::SSLError) { + ctx.min_version = i } - else - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx1) } end - - # Client sets max_version - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.security_level = 0 - ctx2.min_version = 0 - ctx2.max_version = cver - server_connect(port, ctx2) { |ssl| - if cver >= sver - assert_equal vmap[sver][:name], ssl.ssl_version - else - assert_equal vmap[cver][:name], ssl.ssl_version - end - ssl.puts "abc"; assert_equal "abc\n", ssl.gets - } end + + # Invalid + ctx = OpenSSL::SSL::SSLContext.new + assert_raise(ArgumentError) { + ctx.min_version = :bogus + } } end @@ -1584,12 +1552,6 @@ def test_options_disable_versions # It's recommended to use SSLContext#{min,max}_version= instead in real # applications. The purpose of this test case is to check that SSL options # are properly propagated to OpenSSL library. - supported = check_supported_protocol_versions - if !supported.include?(OpenSSL::SSL::TLS1_2_VERSION) || - !supported.include?(OpenSSL::SSL::TLS1_3_VERSION) - pend "this test case requires both TLS 1.2 and TLS 1.3 to be supported " \ - "and enabled by default" - end # Server disables TLS 1.2 and earlier sctx = make_server_context @@ -1605,7 +1567,10 @@ def test_options_disable_versions # Client only supports TLS 1.3 ctx2 = OpenSSL::SSL::SSLContext.new ctx2.min_version = ctx2.max_version = OpenSSL::SSL::TLS1_3_VERSION - assert_nothing_raised { server_connect(port, ctx2) { } } + server_connect(port, ctx2) { |ssl| + assert_equal("TLSv1.3", ssl.ssl_version) + ssl.puts("abc"); assert_equal("abc\n", ssl.gets) + } } # Server only supports TLS 1.2 @@ -1620,7 +1585,42 @@ def test_options_disable_versions # Client supports TLS 1.2 by default ctx2 = OpenSSL::SSL::SSLContext.new ctx2.options |= OpenSSL::SSL::OP_NO_TLSv1_3 - assert_nothing_raised { server_connect(port, ctx2) { } } + server_connect(port, ctx2) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } + } + end + + def test_ssl_set_version + start_server { |port| + # Sanity check: without any constraints, TLS 1.3 will be used + ctx = OpenSSL::SSL::SSLContext.new + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.3", ssl.ssl_version) + ssl.puts("abc"); assert_equal("abc\n", ssl.gets) + } + + # Client only supports TLS 1.2 + ctx = OpenSSL::SSL::SSLContext.new + ctx.ssl_version = "TLSv1_2" + server_connect(port, ctx) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } + } + + # Server only supports TLS 1.2 + sctx = make_server_context + sctx.ssl_version = :TLSv1_2 + start_server(sctx) { |port| + server_connect(port) { |ssl| + assert_equal("TLSv1.2", ssl.ssl_version) + } + } + + # Invalid version + ctx = OpenSSL::SSL::SSLContext.new + assert_raise_with_message(ArgumentError, /SSL method/) { + ctx.ssl_version = :TLSv1_3 } end @@ -1843,11 +1843,9 @@ def test_get_ephemeral_key end def test_fallback_scsv - supported = check_supported_protocol_versions - unless supported.include?(OpenSSL::SSL::TLS1_1_VERSION) + unless protocol_version_available?(OpenSSL::SSL::TLS1_1_VERSION) omit "TLS 1.1 support is required to run this test case" end - omit "Fallback SCSV is not supported" if libressl? start_server do |port|