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:
MSP-Greg 2022-09-13 17:32:39 -05:00 committed by GitHub
parent 06fa7432f2
commit 70c828031e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 243 additions and 22 deletions

View File

@ -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

View File

@ -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);

View File

@ -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+

View File

@ -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

View File

@ -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']

View 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