[ruby/openssl] ssl: separate SSLContext#min_version= and #max_version=

Make these methods simple wrappers around
SSL_CTX_set_{min,max}_proto_version().

When we introduced these methods in commit https://github.com/ruby/openssl/commit/18603949d316 [1], which went
to v2.1.0, we added a private method to SSLContext that set both the
minimum and maximum protocol versions at the same time. This was to
allow emulating the behavior using SSL options on older OpenSSL versions
that lack SSL_CTX_set_{min,max}_proto_version(). Since we no longer
support OpenSSL 1.0.2, the related code has already been removed.

In OpenSSL 1.1.1 or later, setting the minimum or maximum version to 0
is not equivalent to leaving it unset. Similar to SSL options, which we
avoid overwriting as of commit https://github.com/ruby/openssl/commit/00bec0d905d5 and commit https://github.com/ruby/openssl/commit/77c3db2d6587 [2],
a system-wide configuration file may define a default protocol version
bounds. Setting the minimum version should not unset the maximum
version, and vice versa.

[1] https://github.com/ruby/openssl/pull/142
[2] https://github.com/ruby/openssl/pull/767

https://github.com/ruby/openssl/commit/5766386321
This commit is contained in:
Kazuki Yamaguchi 2025-01-30 23:39:51 +09:00 committed by git
parent 8cbff4fe45
commit 5a14f53695
3 changed files with 134 additions and 96 deletions

View File

@ -153,43 +153,6 @@ ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
return params
end
# call-seq:
# ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
# ctx.min_version = :TLS1_2
# ctx.min_version = nil
#
# Sets the lower bound on the supported SSL/TLS protocol version. The
# version may be specified by an integer constant named
# OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version".
#
# Be careful that you don't overwrite OpenSSL::SSL::OP_NO_{SSL,TLS}v*
# options by #options= once you have called #min_version= or
# #max_version=.
#
# === Example
# ctx = OpenSSL::SSL::SSLContext.new
# ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
#
# sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx)
# sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2
def min_version=(version)
set_minmax_proto_version(version, @max_proto_version ||= nil)
@min_proto_version = version
end
# call-seq:
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
# ctx.max_version = :TLS1_2
# ctx.max_version = nil
#
# Sets the upper bound of the supported SSL/TLS protocol version. See
# #min_version= for the possible values.
def max_version=(version)
set_minmax_proto_version(@min_proto_version ||= nil, version)
@max_proto_version = version
end
# call-seq:
# ctx.ssl_version = :TLSv1
# ctx.ssl_version = "SSLv23"
@ -214,8 +177,7 @@ ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
end
version = METHODS_MAP[meth.intern] or
raise ArgumentError, "unknown SSL method `%s'" % meth
set_minmax_proto_version(version, version)
@min_proto_version = @max_proto_version = version
self.min_version = self.max_version = version
end
METHODS_MAP = {

View File

@ -96,61 +96,6 @@ ossl_sslctx_s_alloc(VALUE klass)
return obj;
}
static int
parse_proto_version(VALUE str)
{
int i;
static const struct {
const char *name;
int version;
} map[] = {
{ "SSL2", SSL2_VERSION },
{ "SSL3", SSL3_VERSION },
{ "TLS1", TLS1_VERSION },
{ "TLS1_1", TLS1_1_VERSION },
{ "TLS1_2", TLS1_2_VERSION },
{ "TLS1_3", TLS1_3_VERSION },
};
if (NIL_P(str))
return 0;
if (RB_INTEGER_TYPE_P(str))
return NUM2INT(str);
if (SYMBOL_P(str))
str = rb_sym2str(str);
StringValue(str);
for (i = 0; i < numberof(map); i++)
if (!strncmp(map[i].name, RSTRING_PTR(str), RSTRING_LEN(str)))
return map[i].version;
rb_raise(rb_eArgError, "unrecognized version %+"PRIsVALUE, str);
}
/*
* call-seq:
* ctx.set_minmax_proto_version(min, max) -> nil
*
* Sets the minimum and maximum supported protocol versions. See #min_version=
* and #max_version=.
*/
static VALUE
ossl_sslctx_set_minmax_proto_version(VALUE self, VALUE min_v, VALUE max_v)
{
SSL_CTX *ctx;
int min, max;
GetSSLCTX(self, ctx);
min = parse_proto_version(min_v);
max = parse_proto_version(max_v);
if (!SSL_CTX_set_min_proto_version(ctx, min))
ossl_raise(eSSLError, "SSL_CTX_set_min_proto_version");
if (!SSL_CTX_set_max_proto_version(ctx, max))
ossl_raise(eSSLError, "SSL_CTX_set_max_proto_version");
return Qnil;
}
static VALUE
ossl_call_client_cert_cb(VALUE obj)
{
@ -915,6 +860,93 @@ ossl_sslctx_setup(VALUE self)
return Qtrue;
}
static int
parse_proto_version(VALUE str)
{
int i;
static const struct {
const char *name;
int version;
} map[] = {
{ "SSL2", SSL2_VERSION },
{ "SSL3", SSL3_VERSION },
{ "TLS1", TLS1_VERSION },
{ "TLS1_1", TLS1_1_VERSION },
{ "TLS1_2", TLS1_2_VERSION },
{ "TLS1_3", TLS1_3_VERSION },
};
if (NIL_P(str))
return 0;
if (RB_INTEGER_TYPE_P(str))
return NUM2INT(str);
if (SYMBOL_P(str))
str = rb_sym2str(str);
StringValue(str);
for (i = 0; i < numberof(map); i++)
if (!strncmp(map[i].name, RSTRING_PTR(str), RSTRING_LEN(str)))
return map[i].version;
rb_raise(rb_eArgError, "unrecognized version %+"PRIsVALUE, str);
}
/*
* call-seq:
* ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
* ctx.min_version = :TLS1_2
* ctx.min_version = nil
*
* Sets the lower bound on the supported SSL/TLS protocol version. The
* version may be specified by an integer constant named
* OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version".
*
* === Example
* ctx = OpenSSL::SSL::SSLContext.new
* ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION
* ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
*
* sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx)
* sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2
*/
static VALUE
ossl_sslctx_set_min_version(VALUE self, VALUE v)
{
SSL_CTX *ctx;
int version;
rb_check_frozen(self);
GetSSLCTX(self, ctx);
version = parse_proto_version(v);
if (!SSL_CTX_set_min_proto_version(ctx, version))
ossl_raise(eSSLError, "SSL_CTX_set_min_proto_version");
return v;
}
/*
* call-seq:
* ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
* ctx.max_version = :TLS1_2
* ctx.max_version = nil
*
* Sets the upper bound of the supported SSL/TLS protocol version. See
* #min_version= for the possible values.
*/
static VALUE
ossl_sslctx_set_max_version(VALUE self, VALUE v)
{
SSL_CTX *ctx;
int version;
rb_check_frozen(self);
GetSSLCTX(self, ctx);
version = parse_proto_version(v);
if (!SSL_CTX_set_max_proto_version(ctx, version))
ossl_raise(eSSLError, "SSL_CTX_set_max_proto_version");
return v;
}
static VALUE
ossl_ssl_cipher_to_ary(const SSL_CIPHER *cipher)
{
@ -2846,8 +2878,8 @@ Init_ossl_ssl(void)
rb_define_alias(cSSLContext, "ssl_timeout", "timeout");
rb_define_alias(cSSLContext, "ssl_timeout=", "timeout=");
rb_define_private_method(cSSLContext, "set_minmax_proto_version",
ossl_sslctx_set_minmax_proto_version, 2);
rb_define_method(cSSLContext, "min_version=", ossl_sslctx_set_min_version, 1);
rb_define_method(cSSLContext, "max_version=", ossl_sslctx_set_max_version, 1);
rb_define_method(cSSLContext, "ciphers", ossl_sslctx_get_ciphers, 0);
rb_define_method(cSSLContext, "ciphers=", ossl_sslctx_set_ciphers, 1);
rb_define_method(cSSLContext, "ciphersuites=", ossl_sslctx_set_ciphersuites, 1);

View File

@ -1375,6 +1375,50 @@ class OpenSSL::TestSSL < OpenSSL::SSLTestCase
}
end
def test_minmax_version_system_default
omit "LibreSSL does not support OPENSSL_CONF" if libressl?
Tempfile.create("openssl.cnf") { |f|
f.puts(<<~EOF)
openssl_conf = default_conf
[default_conf]
ssl_conf = ssl_sect
[ssl_sect]
system_default = ssl_default_sect
[ssl_default_sect]
MaxProtocol = TLSv1.2
EOF
f.close
start_server(ignore_listener_error: true) do |port|
assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;")
sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i)
ctx = OpenSSL::SSL::SSLContext.new
ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
ssl.sync_close = true
ssl.connect
assert_equal("TLSv1.2", ssl.ssl_version)
ssl.puts("abc"); assert_equal("abc\n", ssl.gets)
ssl.close
end;
assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;")
sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i)
ctx = OpenSSL::SSL::SSLContext.new
ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
ctx.max_version = nil
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
ssl.sync_close = true
ssl.connect
assert_equal("TLSv1.3", ssl.ssl_version)
ssl.puts("abc"); assert_equal("abc\n", ssl.gets)
ssl.close
end;
end
}
end
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