Skip to content

Commit

Permalink
ssl: separate SSLContext#min_version= and #max_version=
Browse files Browse the repository at this point in the history
Make these methods simple wrappers around
SSL_CTX_set_{min,max}_proto_version().

When we introduced these methods in commit 1860394 [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 00bec0d and commit 77c3db2 [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] #142
[2] #767
  • Loading branch information
rhenium committed Jan 31, 2025
1 parent 5f0e60e commit 5766386
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 96 deletions.
146 changes: 89 additions & 57 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 1 addition & 39 deletions lib/openssl/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,43 +153,6 @@ def set_params(params={})
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"
Expand All @@ -214,8 +177,7 @@ def ssl_version=(meth)
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 = {
Expand Down
44 changes: 44 additions & 0 deletions test/openssl/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,50 @@ def test_minmax_version
}
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
Expand Down

0 comments on commit 5766386

Please sign in to comment.