diff --git a/src/main/java/org/jruby/ext/openssl/SSLContext.java b/src/main/java/org/jruby/ext/openssl/SSLContext.java index 2405675d..cb02f440 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLContext.java +++ b/src/main/java/org/jruby/ext/openssl/SSLContext.java @@ -53,6 +53,7 @@ import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; +import org.jruby.RubyFixnum; import org.jruby.RubyHash; import org.jruby.RubyInteger; import org.jruby.RubyModule; @@ -98,6 +99,10 @@ public class SSLContext extends RubyObject { private static final HashMap SSL_VERSION_OSSL2JSSE; // Mapping table for JSEE's enabled protocols for the algorithm. private static final Map ENABLED_PROTOCOLS; + // Mapping table from CRuby parse_proto_version(VALUE str) + private static final Map PROTO_VERSION_MAP; + + private static final Map JSSE_TO_VERSION; static { SSL_VERSION_OSSL2JSSE = new LinkedHashMap(20, 1); @@ -142,6 +147,22 @@ public class SSLContext extends RubyObject { SSL_VERSION_OSSL2JSSE.put("TLSv1.2", "TLSv1.2"); // just for completeness SSL_VERSION_OSSL2JSSE.put("TLSv1_2_server", "TLSv1.2"); SSL_VERSION_OSSL2JSSE.put("TLSv1_2_client", "TLSv1.2"); + + PROTO_VERSION_MAP = new HashMap(); + PROTO_VERSION_MAP.put("SSL2", SSL.SSL2_VERSION); + PROTO_VERSION_MAP.put("SSL3", SSL.SSL3_VERSION); + PROTO_VERSION_MAP.put("TLS1", SSL.TLS1_VERSION); + PROTO_VERSION_MAP.put("TLS1_1", SSL.TLS1_1_VERSION); + PROTO_VERSION_MAP.put("TLS1_2", SSL.TLS1_2_VERSION); + PROTO_VERSION_MAP.put("TLS1_3", SSL.TLS1_3_VERSION); + + JSSE_TO_VERSION = new HashMap(); + JSSE_TO_VERSION.put("SSLv2", SSL.SSL2_VERSION); + JSSE_TO_VERSION.put("SSLv3", SSL.SSL3_VERSION); + JSSE_TO_VERSION.put("TLSv1", SSL.TLS1_VERSION); + JSSE_TO_VERSION.put("TLSv1.1", SSL.TLS1_1_VERSION); + JSSE_TO_VERSION.put("TLSv1.2", SSL.TLS1_2_VERSION); + JSSE_TO_VERSION.put("TLSv1.3", SSL.TLS1_3_VERSION); } private static ObjectAllocator SSLCONTEXT_ALLOCATOR = new ObjectAllocator() { @@ -270,6 +291,8 @@ public SSLContext(Ruby runtime, RubyClass type) { private String protocol = "SSL"; // SSLv23 in OpenSSL by default private boolean protocolForServer = true; private boolean protocolForClient = true; + private int minProtocolVersion = 0; + private int maxProtocolVersion = 0; private PKey t_key; private X509Cert t_cert; @@ -464,7 +487,7 @@ public RubyArray ciphers(final ThreadContext context) { private RubyArray matchedCiphers(final ThreadContext context) { final Ruby runtime = context.runtime; try { - final String[] supported = getSupportedCipherSuites(this.protocol); + final String[] supported = getSupportedCipherSuites(protocol); final Collection cipherDefs = CipherStrings.matchingCiphers(this.ciphers, supported, false); @@ -527,6 +550,31 @@ public IRubyObject set_ssl_version(IRubyObject version) { return version; } + @JRubyMethod(name = "set_minmax_proto_version") + public IRubyObject set_minmax_proto_version(ThreadContext context, IRubyObject minVersion, IRubyObject maxVersion) { + minProtocolVersion = parseProtoVersion(minVersion); + maxProtocolVersion = parseProtoVersion(maxVersion); + + return context.nil; + } + + private int parseProtoVersion(IRubyObject version) { + if (version.isNil()) + return 0; + if (version instanceof RubyFixnum) { + return RubyFixnum.fix2int(version); + } + + String string = version.asString().asJavaString(); + Integer sslVersion = PROTO_VERSION_MAP.get(string); + + if (sslVersion == null) { + throw getRuntime().newArgumentError("unrecognized version \"" + string + "\""); + } + + return sslVersion; + } + final String getProtocol() { return this.protocol; } @JRubyMethod(name = "session_cache_mode") @@ -651,6 +699,10 @@ private String[] getEnabledProtocols(final SSLEngine engine) { final String[] engineProtocols = engine.getEnabledProtocols(); final List protocols = new ArrayList(enabledProtocols.length); for ( final String enabled : enabledProtocols ) { + int protocolVersion = JSSE_TO_VERSION.get(enabled); + if (minProtocolVersion != 0 && protocolVersion < minProtocolVersion) continue; + if (maxProtocolVersion != 0 && protocolVersion > maxProtocolVersion) continue; + if (((options & OP_NO_SSLv2) != 0) && enabled.equals("SSLv2")) continue; if (((options & OP_NO_SSLv3) != 0) && enabled.equals("SSLv3")) continue; if (((options & OP_NO_TLSv1) != 0) && enabled.equals("TLSv1")) continue; diff --git a/src/test/integration/ssl_test.rb b/src/test/integration/ssl_test.rb index 7256ff9f..cdc90f13 100644 --- a/src/test/integration/ssl_test.rb +++ b/src/test/integration/ssl_test.rb @@ -38,4 +38,26 @@ def test_connect_net_http_1 puts http.get('/') end + def test_connect_ssl_minmax_version + require 'openssl' + require 'socket' + + puts "\n" + puts "------------------------------------------------------------" + puts "-- SSL min/max version ... 'https://google.co.uk'" + puts "------------------------------------------------------------" + + ctx = OpenSSL::SSL::SSLContext.new() + ctx.min_version = OpenSSL::SSL::TLS1_VERSION + ctx.max_version = OpenSSL::SSL::TLS1_1_VERSION + client = TCPSocket.new('google.co.uk', 443) + ssl = OpenSSL::SSL::SSLSocket.new(client, ctx) + ssl.sync_close = true + ssl.connect + begin + assert_equal 'TLSv1.1', ssl.ssl_version + ensure + ssl.sysclose + end + end end \ No newline at end of file diff --git a/src/test/ruby/ssl/test_context.rb b/src/test/ruby/ssl/test_context.rb index 9d6158d5..a441c622 100644 --- a/src/test/ruby/ssl/test_context.rb +++ b/src/test/ruby/ssl/test_context.rb @@ -105,6 +105,12 @@ def test_context_set_ssl_version assert_raises(TypeError) { context.ssl_version = 12 } end + def test_context_minmax_version + context = OpenSSL::SSL::SSLContext.new + context.min_version = OpenSSL::SSL::TLS1_VERSION + context.max_version = OpenSSL::SSL::TLS1_2_VERSION + end if RUBY_VERSION > '2.3' + def test_context_ciphers self.class.disable_security_restrictions diff --git a/src/test/ruby/ssl/test_helper.rb b/src/test/ruby/ssl/test_helper.rb index d82f012b..29297dbb 100644 --- a/src/test/ruby/ssl/test_helper.rb +++ b/src/test/ruby/ssl/test_helper.rb @@ -1,4 +1,5 @@ require File.expand_path('../test_helper', File.dirname(__FILE__)) +require 'openssl' module SSLTestHelper @@ -7,7 +8,7 @@ module SSLTestHelper PORT = 20443 ITERATIONS = ($0 == __FILE__) ? 100 : 10 - def setup; require 'openssl' + def setup; @ca_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048 @svr_key = OpenSSL::PKey::RSA.new TEST_KEY_RSA1024 diff --git a/src/test/ruby/ssl/test_ssl.rb b/src/test/ruby/ssl/test_ssl.rb index 9088b662..5bac79c0 100644 --- a/src/test/ruby/ssl/test_ssl.rb +++ b/src/test/ruby/ssl/test_ssl.rb @@ -137,6 +137,38 @@ def test_ssl_version_tlsv1_2 end end + # Ruby supports TLSv1.3 already. Java - TLSv1.2. + MAX_SSL_VERSION = if defined? JRUBY_VERSION + "TLSv1.2" + else + "TLSv1.3" + end + [ + [OpenSSL::SSL::TLS1_VERSION, nil, MAX_SSL_VERSION, "(TLSv1,)"], + [OpenSSL::SSL::TLS1_1_VERSION, nil, MAX_SSL_VERSION, "(TLSv1.1,)"], + [OpenSSL::SSL::TLS1_2_VERSION, nil, MAX_SSL_VERSION, "(TLSv1.2,)"], + [nil, OpenSSL::SSL::TLS1_VERSION, "TLSv1", "(,TLSv1)"], + [nil, OpenSSL::SSL::TLS1_1_VERSION, "TLSv1.1", "(,TLSv1.1)"], + [nil, OpenSSL::SSL::TLS1_2_VERSION, "TLSv1.2", "(,TLSv1.2)"], + [OpenSSL::SSL::TLS1_VERSION, OpenSSL::SSL::TLS1_VERSION, "TLSv1", "(TLSv1,TLSv1)"], + [OpenSSL::SSL::TLS1_VERSION, OpenSSL::SSL::TLS1_1_VERSION, "TLSv1.1", "(TLSv1,TLSv1.1)"], + [OpenSSL::SSL::TLS1_VERSION, OpenSSL::SSL::TLS1_2_VERSION, "TLSv1.2", "(TLSv1,TLSv1.2)"] + ].each do |min_version, max_version, expected_version, desc| + define_method("test_ssl_minmax_#{desc}") do + ctx_proc = Proc.new do |ctx| + ctx.min_version = min_version unless min_version.nil? + ctx.max_version = max_version unless max_version.nil? + end + start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true, :ctx_proc => ctx_proc) do |server, port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.connect + assert_equal(expected_version, ssl.ssl_version) + ssl.close + end + end + end if RUBY_VERSION > '2.3' + def test_read_nonblock_would_block start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true) do |server, port| sock = TCPSocket.new("127.0.0.1", port)