mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
OpenSSL - allow session reuse via a 'reuse' ssl_bind method or bind string query parameter (#2846)
* minissl - add session reuse * Create test_integration_ssl_session.rb
This commit is contained in:
parent
06fa7432f2
commit
70c828031e
6 changed files with 243 additions and 22 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
138
test/test_integration_ssl_session.rb
Normal file
138
test/test_integration_ssl_session.rb
Normal file
|
@ -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)
|
||||
<<RUBY
|
||||
key = '#{File.expand_path '../examples/puma/client-certs/server.key', __dir__}'
|
||||
cert = '#{File.expand_path '../examples/puma/client-certs/server.crt', __dir__}'
|
||||
ca = '#{File.expand_path '../examples/puma/client-certs/ca.crt', __dir__}'
|
||||
|
||||
ssl_bind '#{HOST}', '#{bind_port}', {
|
||||
cert: cert,
|
||||
key: key,
|
||||
ca: ca,
|
||||
verify_mode: 'none',
|
||||
reuse: #{reuse}
|
||||
}
|
||||
|
||||
activate_control_app 'tcp://#{HOST}:#{control_tcp_port}', { auth_token: '#{TOKEN}' }
|
||||
|
||||
app do |env|
|
||||
[200, {}, [env['rack.url_scheme']]]
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def with_server(config)
|
||||
config_file = Tempfile.new %w(config .rb)
|
||||
config_file.write config
|
||||
config_file.close
|
||||
config_file.path
|
||||
|
||||
# start server
|
||||
cmd = "#{BASE} bin/puma -C #{config_file.path}"
|
||||
@server = IO.popen cmd, 'r'
|
||||
wait_for_server_to_boot
|
||||
@pid = @server.pid
|
||||
|
||||
yield
|
||||
|
||||
ensure
|
||||
# stop server
|
||||
sock = TCPSocket.new HOST, control_tcp_port
|
||||
@ios_to_close << sock
|
||||
sock.syswrite "GET /stop?token=#{TOKEN} HTTP/1.1\r\n\r\n"
|
||||
sock.read
|
||||
assert_match 'Goodbye!', @server.read
|
||||
end
|
||||
|
||||
def run_session(reuse, tls = nil)
|
||||
config = set_reuse reuse
|
||||
|
||||
with_server(config) do
|
||||
|
||||
uri = "https://#{HOST}:#{@bind_port}/"
|
||||
|
||||
curl_cmd = %(curl -k -v --http1.1 -H "Connection: close" #{tls} #{uri} #{uri})
|
||||
|
||||
curl = ''
|
||||
IO.popen(curl_cmd, :err=>[: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
|
Loading…
Reference in a new issue