diff --git a/ext/puma_http11/extconf.rb b/ext/puma_http11/extconf.rb index 031c33ed..8ac3ca66 100644 --- a/ext/puma_http11/extconf.rb +++ b/ext/puma_http11/extconf.rb @@ -30,13 +30,14 @@ unless ENV["PUMA_DISABLE_SSL"] 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" + have_func "SSL_CTX_set_session_cache_mode(NULL, 0)", "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" + have_func "TLS_server_method" , "openssl/ssl.h" + have_func "SSL_CTX_set_min_proto_version(NULL, 0)" , "openssl/ssl.h" - have_func "X509_STORE_up_ref" + have_func "X509_STORE_up_ref" have_func "SSL_CTX_set_ecdh_auto(NULL, 0)" , "openssl/ssl.h" # below exists in 1.1.0 and later, but isn't documented until 3.0.0 diff --git a/ext/puma_http11/mini_ssl.c b/ext/puma_http11/mini_ssl.c index 4fe96e0f..d2264a8c 100644 --- a/ext/puma_http11/mini_ssl.c +++ b/ext/puma_http11/mini_ssl.c @@ -210,25 +210,28 @@ sslctx_alloc(VALUE klass) { VALUE sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { SSL_CTX* ctx; - -#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION - int min; -#endif int ssl_options; VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1, verification_flags, session_id_bytes, cert_pem, key_pem; -#ifndef HAVE_SSL_CTX_SET_DH_AUTO - DH *dh; -#endif BIO *bio; X509 *x509; EVP_PKEY *pkey; - +#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION + int min; +#endif +#ifndef HAVE_SSL_CTX_SET_DH_AUTO + DH *dh; +#endif #if OPENSSL_VERSION_NUMBER < 0x10002000L EC_KEY *ecdh; #endif +#ifdef HAVE_SSL_CTX_SET_SESSION_CACHE_MODE + VALUE reuse, reuse_cache_size, reuse_timeout; - TypedData_Get_Struct(self, SSL_CTX, &sslctx_type, ctx); + reuse = rb_funcall(mini_ssl_ctx, rb_intern_const("reuse"), 0); + reuse_cache_size = rb_funcall(mini_ssl_ctx, rb_intern_const("reuse_cache_size"), 0); + reuse_timeout = rb_funcall(mini_ssl_ctx, rb_intern_const("reuse_timeout"), 0); +#endif key = rb_funcall(mini_ssl_ctx, rb_intern_const("key"), 0); @@ -248,6 +251,8 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { no_tlsv1_1 = rb_funcall(mini_ssl_ctx, rb_intern_const("no_tlsv1_1"), 0); + TypedData_Get_Struct(self, SSL_CTX, &sslctx_type, ctx); + if (!NIL_P(cert)) { StringValue(cert); @@ -314,8 +319,6 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { SSL_CTX_set_min_proto_version(ctx, min); - 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; @@ -326,10 +329,23 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { if(RTEST(no_tlsv1_1)) { ssl_options |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1; } - SSL_CTX_set_options(ctx, ssl_options); #endif - SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); +#ifdef HAVE_SSL_CTX_SET_SESSION_CACHE_MODE + if (!NIL_P(reuse)) { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER); + if (!NIL_P(reuse_cache_size)) { + SSL_CTX_sess_set_cache_size(ctx, NUM2INT(reuse_cache_size)); + } + if (!NIL_P(reuse_timeout)) { + SSL_CTX_set_timeout(ctx, NUM2INT(reuse_timeout)); + } + } else { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + } +#endif + + SSL_CTX_set_options(ctx, ssl_options); if (!NIL_P(ssl_cipher_filter)) { StringValue(ssl_cipher_filter); @@ -340,8 +356,7 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { } #if OPENSSL_VERSION_NUMBER < 0x10002000L - // Remove this case if OpenSSL 1.0.1 (now EOL) support is no - // longer needed. + // Remove this case if OpenSSL 1.0.1 (now EOL) support is no longer needed. ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); if (ecdh) { SSL_CTX_set_tmp_ecdh(ctx, ecdh); diff --git a/lib/puma/dsl.rb b/lib/puma/dsl.rb index 92de2e63..90a98af2 100644 --- a/lib/puma/dsl.rb +++ b/lib/puma/dsl.rb @@ -71,8 +71,32 @@ module Puma cert_flags = (cert = opts[:cert]) ? "cert=#{Puma::Util.escape(cert)}" : nil key_flags = (key = opts[:key]) ? "&key=#{Puma::Util.escape(key)}" : nil + reuse_flag = + if (reuse = opts[:reuse]) + if reuse == true + '&reuse=dflt' + elsif reuse.is_a?(Hash) && (reuse.key?(:size) || reuse.key?(:timeout)) + val = ''.dup + if (size = reuse[:size]) && Integer === size + val << size.to_s + end + if (timeout = reuse[:timeout]) && Integer === timeout + val << ",#{timeout}" + end + if val.empty? + nil + else + "&reuse=#{val}" + end + else + nil + end + else + nil + end + "ssl://#{host}:#{port}?#{cert_flags}#{key_flags}#{ssl_cipher_filter}" \ - "&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}" + "#{reuse_flag}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}" end end @@ -454,6 +478,10 @@ module Puma # Puma will assume you are using the +localhost+ gem and try to load the # appropriate files. # + # When using the options hash parameter, the `reuse:` value is either + # `true`, which sets reuse 'on' with default values, or a hash, with `:size` + # and/or `:timeout` keys, each with integer values. + # # @example # ssl_bind '127.0.0.1', '9292', { # cert: path_to_cert, @@ -461,6 +489,7 @@ module Puma # ssl_cipher_filter: cipher_filter, # optional # verify_mode: verify_mode, # default 'none' # verification_flags: flags, # optional, not supported by JRuby + # reuse: true # optional # } # # @example Using self-signed certificate with the +localhost+ gem: @@ -470,6 +499,7 @@ module Puma # ssl_bind '127.0.0.1', '9292', { # cert_pem: File.read(path_to_cert), # key_pem: File.read(path_to_key), + # reuse: {size: 2_000, timeout: 20} # } # # @example For JRuby, two keys are required: +keystore+ & +keystore_pass+ diff --git a/lib/puma/minissl.rb b/lib/puma/minissl.rb index fa854f65..bcff3d4b 100644 --- a/lib/puma/minissl.rb +++ b/lib/puma/minissl.rb @@ -22,6 +22,7 @@ module Puma @socket = socket @engine = engine @peercert = nil + @reuse = nil end # @!attribute [r] to_io @@ -208,6 +209,9 @@ module Puma @cert = nil @key_pem = nil @cert_pem = nil + @reuse = nil + @reuse_cache_size = nil + @reuse_timeout = nil end def check_file(file, desc) @@ -279,6 +283,8 @@ module Puma attr_accessor :ssl_cipher_filter attr_accessor :verification_flags + attr_reader :reuse, :reuse_cache_size, :reuse_timeout + def key=(key) check_file key, 'Key' @key = key @@ -308,6 +314,35 @@ module Puma raise "Key not configured" if @key.nil? && @key_pem.nil? raise "Cert not configured" if @cert.nil? && @cert_pem.nil? end + + # Controls session reuse. Allowed values are as follows: + # * 'off' - matches the behavior of Puma 5.6 and earlier. This is included + # in case reuse 'on' is made the default in future Puma versions. + # * 'dflt' - sets session reuse on, with OpenSSL default cache size of + # 20k and default timeout of 300 seconds. + # * 's,t' - where s and t are integer strings, for size and timeout. + # * 's' - where s is an integer strings for size. + # * ',t' - where t is an integer strings for timeout. + # + def reuse=(reuse_str) + case reuse_str + when 'off' + @reuse = nil + when 'dflt' + @reuse = true + when /\A\d+\z/ + @reuse = true + @reuse_cache_size = reuse_str.to_i + when /\A\d+,\d+\z/ + @reuse = true + size, time = reuse_str.split ',' + @reuse_cache_size = size.to_i + @reuse_timeout = time.to_i + when /\A,\d+\z/ + @reuse = true + @reuse_timeout = reuse_str.delete(',').to_i + end + end end # disables TLSv1 diff --git a/lib/puma/minissl/context_builder.rb b/lib/puma/minissl/context_builder.rb index b03f266e..f8b7807c 100644 --- a/lib/puma/minissl/context_builder.rb +++ b/lib/puma/minissl/context_builder.rb @@ -54,10 +54,12 @@ module Puma ctx.ca = params['ca'] if params['ca'] ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter'] + + ctx.reuse = params['reuse'] if params['reuse'] end - ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true' - ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true' + ctx.no_tlsv1 = params['no_tlsv1'] == 'true' + ctx.no_tlsv1_1 = params['no_tlsv1_1'] == 'true' if params['verify_mode'] ctx.verify_mode = case params['verify_mode'] diff --git a/test/test_integration_ssl_session.rb b/test/test_integration_ssl_session.rb new file mode 100644 index 00000000..ffc72d66 --- /dev/null +++ b/test/test_integration_ssl_session.rb @@ -0,0 +1,138 @@ +require_relative 'helper' +require_relative 'helpers/integration' + +# These tests are used to verify that Puma works with SSL sockets. Only +# integration tests isolate the server from the test environment, so there +# should be a few SSL tests. +# +# For instance, since other tests make use of 'client' SSLSockets created by +# net/http, OpenSSL is loaded in the CI process. By shelling out with IO.popen, +# the server process isn't affected by whatever is loaded in the CI process. + +class TestIntegrationSSLSession < TestIntegration + parallelize_me! if Puma::IS_MRI + + require "net/http" + require "openssl" + + GET = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n" + + RESP = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 5\r\n\r\nhttps" + + CERT_PATH = File.expand_path "../examples/puma/client-certs", __dir__ + + def teardown + @server.close unless @server && @server.closed? + @server = nil + super + end + + def bind_port + @bind_port ||= UniquePort.call + end + + def control_tcp_port + @control_tcp_port ||= UniquePort.call + end + + def set_reuse(reuse) +<[:child, :out]) { |io| curl = io.read } + + curl.include? '* SSL re-using session ID' + end + end + + def test_dflt + reused = run_session true + assert reused, 'session was not reused' + end + + def test_dflt_tls1_2 + reused = run_session true, '--tls-max 1.2' + assert reused, 'session was not reused' + end + + def test_1000_tls1_2 + reused = run_session '{size: 1_000}', '--tls-max 1.2' + assert reused, 'session was not reused' + end + + def test_1000_10_tls1_2 + reused = run_session '{size: 1000, timeout: 10}', '--tls-max 1.2' + assert reused, 'session was not reused' + end + + def test__10_tls1_2 + reused = run_session '{timeout: 10}', '--tls-max 1.2' + assert reused, 'session was not reused' + end + + # session reuse has always worked with TLSv1.3 + def test_off_tls1_2 + ssl_vers = Puma::MiniSSL::OPENSSL_LIBRARY_VERSION + old_ssl = ssl_vers.include?(' 1.0.') || ssl_vers.match?(/ 1\.1\.1[ a-e]/) + skip 'Requires 1.1.1f or later' if old_ssl + reused = run_session 'nil', '--tls-max 1.2' + refute reused, 'session was reused' + end + + def test_off_tls1_3 + skip 'TLSv1.3 unavailable' unless Puma::MiniSSL::HAS_TLS1_3 + reused = run_session 'nil' + assert reused, 'TLSv1.3 session was not reused' + end +end if Puma::HAS_SSL && Puma::IS_MRI