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
|
@ -31,6 +31,7 @@ unless ENV["PUMA_DISABLE_SSL"]
|
||||||
|
|
||||||
# below is yes for 1.0.2 & later
|
# 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
|
# below are yes for 1.1.0 & later
|
||||||
have_func "TLS_server_method" , "openssl/ssl.h"
|
have_func "TLS_server_method" , "openssl/ssl.h"
|
||||||
|
|
|
@ -210,25 +210,28 @@ sslctx_alloc(VALUE klass) {
|
||||||
VALUE
|
VALUE
|
||||||
sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
||||||
SSL_CTX* ctx;
|
SSL_CTX* ctx;
|
||||||
|
|
||||||
#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
|
|
||||||
int min;
|
|
||||||
#endif
|
|
||||||
int ssl_options;
|
int ssl_options;
|
||||||
VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1,
|
VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1,
|
||||||
verification_flags, session_id_bytes, cert_pem, key_pem;
|
verification_flags, session_id_bytes, cert_pem, key_pem;
|
||||||
#ifndef HAVE_SSL_CTX_SET_DH_AUTO
|
|
||||||
DH *dh;
|
|
||||||
#endif
|
|
||||||
BIO *bio;
|
BIO *bio;
|
||||||
X509 *x509;
|
X509 *x509;
|
||||||
EVP_PKEY *pkey;
|
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
|
#if OPENSSL_VERSION_NUMBER < 0x10002000L
|
||||||
EC_KEY *ecdh;
|
EC_KEY *ecdh;
|
||||||
#endif
|
#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);
|
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);
|
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)) {
|
if (!NIL_P(cert)) {
|
||||||
StringValue(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_min_proto_version(ctx, min);
|
||||||
|
|
||||||
SSL_CTX_set_options(ctx, ssl_options);
|
|
||||||
|
|
||||||
#else
|
#else
|
||||||
/* As of 1.0.2f, SSL_OP_SINGLE_DH_USE key use is always on */
|
/* 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;
|
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)) {
|
if(RTEST(no_tlsv1_1)) {
|
||||||
ssl_options |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
|
ssl_options |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
|
||||||
}
|
}
|
||||||
SSL_CTX_set_options(ctx, ssl_options);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#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);
|
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)) {
|
if (!NIL_P(ssl_cipher_filter)) {
|
||||||
StringValue(ssl_cipher_filter);
|
StringValue(ssl_cipher_filter);
|
||||||
|
@ -340,8 +356,7 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if OPENSSL_VERSION_NUMBER < 0x10002000L
|
#if OPENSSL_VERSION_NUMBER < 0x10002000L
|
||||||
// Remove this case if OpenSSL 1.0.1 (now EOL) support is no
|
// Remove this case if OpenSSL 1.0.1 (now EOL) support is no longer needed.
|
||||||
// longer needed.
|
|
||||||
ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
|
ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
|
||||||
if (ecdh) {
|
if (ecdh) {
|
||||||
SSL_CTX_set_tmp_ecdh(ctx, 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
|
cert_flags = (cert = opts[:cert]) ? "cert=#{Puma::Util.escape(cert)}" : nil
|
||||||
key_flags = (key = opts[:key]) ? "&key=#{Puma::Util.escape(key)}" : 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}" \
|
"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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -454,6 +478,10 @@ module Puma
|
||||||
# Puma will assume you are using the +localhost+ gem and try to load the
|
# Puma will assume you are using the +localhost+ gem and try to load the
|
||||||
# appropriate files.
|
# 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
|
# @example
|
||||||
# ssl_bind '127.0.0.1', '9292', {
|
# ssl_bind '127.0.0.1', '9292', {
|
||||||
# cert: path_to_cert,
|
# cert: path_to_cert,
|
||||||
|
@ -461,6 +489,7 @@ module Puma
|
||||||
# ssl_cipher_filter: cipher_filter, # optional
|
# ssl_cipher_filter: cipher_filter, # optional
|
||||||
# verify_mode: verify_mode, # default 'none'
|
# verify_mode: verify_mode, # default 'none'
|
||||||
# verification_flags: flags, # optional, not supported by JRuby
|
# verification_flags: flags, # optional, not supported by JRuby
|
||||||
|
# reuse: true # optional
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# @example Using self-signed certificate with the +localhost+ gem:
|
# @example Using self-signed certificate with the +localhost+ gem:
|
||||||
|
@ -470,6 +499,7 @@ module Puma
|
||||||
# ssl_bind '127.0.0.1', '9292', {
|
# ssl_bind '127.0.0.1', '9292', {
|
||||||
# cert_pem: File.read(path_to_cert),
|
# cert_pem: File.read(path_to_cert),
|
||||||
# key_pem: File.read(path_to_key),
|
# key_pem: File.read(path_to_key),
|
||||||
|
# reuse: {size: 2_000, timeout: 20}
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# @example For JRuby, two keys are required: +keystore+ & +keystore_pass+
|
# @example For JRuby, two keys are required: +keystore+ & +keystore_pass+
|
||||||
|
|
|
@ -22,6 +22,7 @@ module Puma
|
||||||
@socket = socket
|
@socket = socket
|
||||||
@engine = engine
|
@engine = engine
|
||||||
@peercert = nil
|
@peercert = nil
|
||||||
|
@reuse = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# @!attribute [r] to_io
|
# @!attribute [r] to_io
|
||||||
|
@ -208,6 +209,9 @@ module Puma
|
||||||
@cert = nil
|
@cert = nil
|
||||||
@key_pem = nil
|
@key_pem = nil
|
||||||
@cert_pem = nil
|
@cert_pem = nil
|
||||||
|
@reuse = nil
|
||||||
|
@reuse_cache_size = nil
|
||||||
|
@reuse_timeout = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_file(file, desc)
|
def check_file(file, desc)
|
||||||
|
@ -279,6 +283,8 @@ module Puma
|
||||||
attr_accessor :ssl_cipher_filter
|
attr_accessor :ssl_cipher_filter
|
||||||
attr_accessor :verification_flags
|
attr_accessor :verification_flags
|
||||||
|
|
||||||
|
attr_reader :reuse, :reuse_cache_size, :reuse_timeout
|
||||||
|
|
||||||
def key=(key)
|
def key=(key)
|
||||||
check_file key, 'Key'
|
check_file key, 'Key'
|
||||||
@key = key
|
@key = key
|
||||||
|
@ -308,6 +314,35 @@ module Puma
|
||||||
raise "Key not configured" if @key.nil? && @key_pem.nil?
|
raise "Key not configured" if @key.nil? && @key_pem.nil?
|
||||||
raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
|
raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# disables TLSv1
|
# disables TLSv1
|
||||||
|
|
|
@ -54,10 +54,12 @@ module Puma
|
||||||
|
|
||||||
ctx.ca = params['ca'] if params['ca']
|
ctx.ca = params['ca'] if params['ca']
|
||||||
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
|
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
|
||||||
|
|
||||||
|
ctx.reuse = params['reuse'] if params['reuse']
|
||||||
end
|
end
|
||||||
|
|
||||||
ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
|
ctx.no_tlsv1 = params['no_tlsv1'] == 'true'
|
||||||
ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
|
ctx.no_tlsv1_1 = params['no_tlsv1_1'] == 'true'
|
||||||
|
|
||||||
if params['verify_mode']
|
if params['verify_mode']
|
||||||
ctx.verify_mode = case 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