diff --git a/History.md b/History.md index c4449a81a8..c1e9a8274b 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,7 @@ * Features * Your feature goes here (#Github Number) + * Add no_tlsv1_3 to Puma::DSL#ssl_bindings and Puma::MiniSSL::Context (#2426) * Bugfixes * Cleanup daemonization in rc.d script (#2409) diff --git a/ext/puma_http11/extconf.rb b/ext/puma_http11/extconf.rb index 0f2de80e2d..684afcb60c 100644 --- a/ext/puma_http11/extconf.rb +++ b/ext/puma_http11/extconf.rb @@ -17,12 +17,11 @@ have_header "openssl/bio.h" # below is yes for 1.0.2 & later - have_func "DTLS_method" , "openssl/ssl.h" + have_func "DTLS_method" , "openssl/ssl.h" - # below are yes for 1.1.0 & later, may need to check func rather than macro - # with versions after 1.1.1 - have_func "TLS_server_method" , "openssl/ssl.h" - have_macro "SSL_CTX_set_min_proto_version", "openssl/ssl.h" + # below are yes for 1.1.0 & later + have_func "TLS_server_method" , "openssl/ssl.h" + have_func "SSL_CTX_set_min_proto_version(NULL, 0)", "openssl/ssl.h" end end diff --git a/ext/puma_http11/mini_ssl.c b/ext/puma_http11/mini_ssl.c index fd964cf3f4..af1b1cd92f 100644 --- a/ext/puma_http11/mini_ssl.c +++ b/ext/puma_http11/mini_ssl.c @@ -171,6 +171,9 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) { ID sym_no_tlsv1_1 = rb_intern("no_tlsv1_1"); VALUE no_tlsv1_1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1_1, 0); + ID sym_no_tlsv1_3 = rb_intern("no_tlsv1_3"); + VALUE no_tlsv1_3 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1_3, 0); + #ifdef HAVE_TLS_SERVER_METHOD ctx = SSL_CTX_new(TLS_server_method()); #else @@ -198,11 +201,14 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) { else { min = TLS1_VERSION; } - + SSL_CTX_set_min_proto_version(ctx, min); - SSL_CTX_set_options(ctx, ssl_options); + if (RTEST(no_tlsv1_3)) { + SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION); + } + SSL_CTX_set_options(ctx, ssl_options); #else /* As of 1.0.2f, SSL_OP_SINGLE_DH_USE key use is always on */ ssl_options |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE; @@ -504,27 +510,27 @@ void Init_mini_ssl(VALUE puma) { #else rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(SSLeay_version(SSLEAY_VERSION))); #endif - -#if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD) - /* True if SSL3 is not available */ - rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue); -#else - rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse); -#endif - -#if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD) - /* True if TLS1 is not available */ - rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue); -#else - rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse); -#endif - -#if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD) - /* True if TLS1_1 is not available */ - rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue); -#else - rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse); -#endif + +#if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD) + /* True if SSL3 is not available */ + rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue); +#else + rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse); +#endif + +#if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD) + /* True if TLS1 is not available */ + rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue); +#else + rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse); +#endif + +#if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD) + /* True if TLS1_1 is not available */ + rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue); +#else + rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse); +#endif rb_define_singleton_method(mod, "check", noop, 0); diff --git a/lib/puma/dsl.rb b/lib/puma/dsl.rb index 7334d888db..cb8cf5ae00 100644 --- a/lib/puma/dsl.rb +++ b/lib/puma/dsl.rb @@ -369,6 +369,12 @@ def threads(min, max) # Instead of `bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'` you # can also use the this method. # + # Protocols can be disabled by the use of `no_tlsv1`, `no_tlsv1_1`, and + # `no_tlsv1_3`. `no_tlsv1`, and `no_tlsv1_1` set the minimum protocol to + # TLSv1.1 and TLSv1.2 respectively. `no_tlsv1_3` should only be used when a + # front-end does not support TLSv1.3. + # The default value for all three is `false`, set them to `true` to enable. + # # @example # ssl_bind '127.0.0.1', '9292', { # cert: path_to_cert, @@ -387,16 +393,26 @@ def threads(min, max) # } def ssl_bind(host, port, opts) verify = opts.fetch(:verify_mode, 'none').to_s - no_tlsv1 = opts.fetch(:no_tlsv1, 'false') - no_tlsv1_1 = opts.fetch(:no_tlsv1_1, 'false') + + tls_str = nil + tls_str = if opts[:no_tlsv1_1] + '&no_tlsv1_1=true' + elsif opts[:no_tlsv1] + '&no_tlsv1=true' + end + + if opts[:no_tlsv1_3] + tls_str = "#{tls_str || ''}&no_tlsv1_3=true" + end + ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify) if defined?(JRUBY_VERSION) keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}" - bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}&no_tlsv1_1=#{no_tlsv1_1}#{ca_additions}" + bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}#{tls_str}#{ca_additions}" else ssl_cipher_filter = "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" if opts[:ssl_cipher_filter] - bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}&no_tlsv1_1=#{no_tlsv1_1}#{ca_additions}" + bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}#{tls_str}#{ca_additions}" end end diff --git a/lib/puma/minissl.rb b/lib/puma/minissl.rb index 86468d5eb4..cbd10e967c 100644 --- a/lib/puma/minissl.rb +++ b/lib/puma/minissl.rb @@ -217,11 +217,12 @@ class SSLError < StandardError class Context attr_accessor :verify_mode - attr_reader :no_tlsv1, :no_tlsv1_1 + attr_reader :no_tlsv1, :no_tlsv1_1, :no_tlsv1_3 def initialize @no_tlsv1 = false @no_tlsv1_1 = false + @no_tlsv1_3 = false end if IS_JRUBY @@ -267,20 +268,26 @@ def check end end - # disables TLSv1 + # Disables TLSv1 # @!attribute [w] no_tlsv1= def no_tlsv1=(tlsv1) raise ArgumentError, "Invalid value of no_tlsv1=" unless ['true', 'false', true, false].include?(tlsv1) @no_tlsv1 = tlsv1 end - # disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=` + # Disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=` # @!attribute [w] no_tlsv1_1= def no_tlsv1_1=(tlsv1_1) raise ArgumentError, "Invalid value of no_tlsv1_1=" unless ['true', 'false', true, false].include?(tlsv1_1) @no_tlsv1_1 = tlsv1_1 end + # Disables TLSv1.3. + # @!attribute [w] no_tlsv1_3= + def no_tlsv1_3=(tlsv1_3) + raise ArgumentError, "Invalid value of no_tlsv1_3=" unless ['true', 'false', true, false].include?(tlsv1_3) + @no_tlsv1_3 = tlsv1_3 if HAS_TLS1_3 + end end VERIFY_NONE = 0 diff --git a/lib/puma/minissl/context_builder.rb b/lib/puma/minissl/context_builder.rb index f499b5b4ad..46212e8d6f 100644 --- a/lib/puma/minissl/context_builder.rb +++ b/lib/puma/minissl/context_builder.rb @@ -45,8 +45,9 @@ def context ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter'] end - ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true' + ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true' ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true' + ctx.no_tlsv1_3 = true if params['no_tlsv1_3'] == 'true' if params['verify_mode'] ctx.verify_mode = case params['verify_mode'] diff --git a/test/test_config.rb b/test/test_config.rb index 773447b2c7..6a698baa3c 100644 --- a/test/test_config.rb +++ b/test/test_config.rb @@ -16,7 +16,6 @@ def test_default_max_threads assert_equal max_threads, Puma::Configuration.new.default_max_threads end - def test_app_from_rackup conf = Puma::Configuration.new do |c| c.rackup "test/rackup/hello-bind.ru" @@ -74,7 +73,82 @@ def test_ssl_bind conf.load - ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1=false&no_tlsv1_1=false" + ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode" + assert_equal [ssl_binding], conf.options[:binds] + end + + def test_ssl_bind + skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL + + conf = Puma::Configuration.new do |c| + c.ssl_bind "0.0.0.0", "9292", { + cert: "/path/to/cert", + key: "/path/to/key", + verify_mode: "the_verify_mode", + } + end + + conf.load + + ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode" + assert_equal [ssl_binding], conf.options[:binds] + end + + def test_ssl_bind_no_tlsv1 + skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL + + conf = Puma::Configuration.new do |c| + c.ssl_bind "0.0.0.0", "9292", { + cert: "/path/to/cert", + key: "/path/to/key", + verify_mode: "the_verify_mode", + no_tlsv1: true, + } + end + + conf.load + + ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1=true" + assert_equal [ssl_binding], conf.options[:binds] + end + + def test_ssl_bind_no_tlsv1_1 + skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL + + conf = Puma::Configuration.new do |c| + c.ssl_bind "0.0.0.0", "9292", { + cert: "/path/to/cert", + key: "/path/to/key", + verify_mode: "the_verify_mode", + no_tlsv1_1: true, + } + end + + conf.load + + ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1_1=true" + assert_equal [ssl_binding], conf.options[:binds] + end + + def test_ssl_bind_no_tlsv1_3 + skip_on :jruby + skip 'No ssl support' unless ::Puma::HAS_SSL + + conf = Puma::Configuration.new do |c| + c.ssl_bind "0.0.0.0", "9292", { + cert: "/path/to/cert", + key: "/path/to/key", + verify_mode: "the_verify_mode", + no_tlsv1_3: true, + } + end + + conf.load + + ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1_3=true" assert_equal [ssl_binding], conf.options[:binds] end diff --git a/test/test_puma_server_ssl.rb b/test/test_puma_server_ssl.rb index 1a5cf2e608..ffff7322e6 100644 --- a/test/test_puma_server_ssl.rb +++ b/test/test_puma_server_ssl.rb @@ -204,6 +204,23 @@ def test_tls_v1_1_rejection end end + def test_tls_v1_3_rejection + skip("No TLSv1.3 support") unless ::Puma::MiniSSL::HAS_TLS1_3 + ssl_version = '' + start_server { |ctx| ctx.no_tlsv1_3 = true } + + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE + port = @server.connected_ports[0] + socket = OpenSSL::SSL::SSLSocket.new TCPSocket.new(@host, port), ctx + socket.connect + socket.syswrite 'help Me' + ssl_version = socket.ssl_version + socket.close + + assert_equal 'TLSv1.2', ssl_version + end + def test_http_rejection body_http = nil body_https = nil