Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix rejecting HTTP requests when TLS1.3 is used by server #2116

Merged
merged 3 commits into from May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions History.md
Expand Up @@ -26,6 +26,7 @@
* Configuration: `environment` is read from `RAILS_ENV`, if `RACK_ENV` can't be found (#2022)

* Bugfixes
* Close client http connections made to an ssl server with TLSv1.3 (#2116)
* Do not set user_config to quiet by default to allow for file config (#2074)
* Always close SSL connection in Puma::ControlCLI (#2211)
* Windows update extconf.rb for use with ssp and varied Ruby/MSYS2 combinations (#2069)
Expand Down
14 changes: 12 additions & 2 deletions ext/puma_http11/mini_ssl.c
Expand Up @@ -301,6 +301,7 @@ void raise_error(SSL* ssl, int result) {
char msg[512];
const char* err_str;
int err = errno;
int mask = 4095;
int ssl_err = SSL_get_error(ssl, result);
int verify_err = (int) SSL_get_verify_result(ssl);

Expand All @@ -317,8 +318,8 @@ void raise_error(SSL* ssl, int result) {
} else {
err = (int) ERR_get_error();
ERR_error_string_n(err, buf, sizeof(buf));
snprintf(msg, sizeof(msg), "OpenSSL error: %s - %d", buf, err);

int errexp = err & mask;
snprintf(msg, sizeof(msg), "OpenSSL error: %s - %d", buf, errexp);
}
} else {
snprintf(msg, sizeof(msg), "Unknown OpenSSL error: %d", ssl_err);
Expand Down Expand Up @@ -462,6 +463,13 @@ VALUE engine_peercert(VALUE self) {
return rb_cert_buf;
}

static VALUE
engine_ssl_vers_st(VALUE self) {
ms_conn* conn;
Data_Get_Struct(self, ms_conn, conn);
return rb_ary_new3(2, rb_str_new2(SSL_get_version(conn->ssl)), rb_str_new2(SSL_state_string(conn->ssl)));
}

VALUE noop(VALUE self) {
return Qnil;
}
Expand Down Expand Up @@ -533,6 +541,8 @@ void Init_mini_ssl(VALUE puma) {
rb_define_method(eng, "init?", engine_init, 0);

rb_define_method(eng, "peercert", engine_peercert, 0);

rb_define_method(eng, "ssl_vers_st", engine_ssl_vers_st, 0);
}

#else
Expand Down
33 changes: 31 additions & 2 deletions lib/puma/minissl.rb
Expand Up @@ -5,8 +5,18 @@
rescue LoadError
end

# need for Puma::MiniSSL::OPENSSL constants used in `HAS_TLS1_3`
require 'puma/puma_http11'

module Puma
module MiniSSL

# define constant at runtime, as it's easy to determine at built time,
# but Puma could (it shouldn't) be loaded with an older OpenSSL version
HAS_TLS1_3 = !IS_JRUBY &&
(OPENSSL_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) != -1 &&
(OPENSSL_LIBRARY_VERSION[/ \d+\.\d+\.\d+/].split('.').map(&:to_i) <=> [1,1,1]) !=-1

class Socket
def initialize(socket, engine)
@socket = socket
Expand All @@ -22,6 +32,24 @@ def closed?
@socket.closed?
end

# returns a two element array
# first is protocol version (SSL_get_version)
# second is 'handshake' state (SSL_state_string)
#
# used for dropping tcp connections to ssl
# see OpenSSL ssl/ssl_stat.c SSL_state_string for info
#
def ssl_version_state
IS_JRUBY ? [nil, nil] : @engine.ssl_vers_st
end

# used to check the handshake status, in particular when a TCP connection
# is made with TLSv1.3 as an available protocol
def bad_tlsv1_3?
HAS_TLS1_3 && @engine.ssl_vers_st == ['TLSv1.3', 'SSLERR']
end
private :bad_tlsv1_3?

def readpartial(size)
while true
output = @engine.read
Expand All @@ -41,6 +69,7 @@ def readpartial(size)

def engine_read_all
output = @engine.read
raise SSLError.exception "HTTP connection?" if bad_tlsv1_3?
while output and additional_output = @engine.read
output << additional_output
end
Expand Down Expand Up @@ -167,7 +196,7 @@ def peercert
end
end

if defined?(JRUBY_VERSION)
if IS_JRUBY
class SSLError < StandardError
# Define this for jruby even though it isn't used.
end
Expand All @@ -184,7 +213,7 @@ def initialize
@no_tlsv1_1 = false
end

if defined?(JRUBY_VERSION)
if IS_JRUBY
# jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
attr_reader :keystore
attr_accessor :keystore_pass
Expand Down
38 changes: 38 additions & 0 deletions test/test_puma_server_ssl.rb
Expand Up @@ -203,6 +203,44 @@ def test_tls_v1_1_rejection
assert_match(msg, @events.error.message) if @events.error
end
end

def test_http_rejection
body_http = nil
body_https = nil

start_server

http = Net::HTTP.new @host, @server.connected_ports[0]
http.use_ssl = false
http.read_timeout = 6

tcp = Thread.new do
req_http = Net::HTTP::Get.new "/", {}
assert_raises(Errno::ECONNREFUSED, EOFError) do
http.start.request(req_http) { |rep| body_http = rep.body }
end
end

ssl = Thread.new do
@http.start do
req_https = Net::HTTP::Get.new "/", {}
@http.request(req_https) { |rep_https| body_https = rep_https.body }
end
end

tcp.join
ssl.join
http.finish
sleep 1.0

assert_nil body_http
assert_equal "https", body_https

thread_pool = @server.instance_variable_get(:@thread_pool)
busy_threads = thread_pool.spawned - thread_pool.waiting

assert busy_threads.zero?, "Our connection is wasn't dropped"
end
end unless DISABLE_SSL

# client-side TLS authentication tests
Expand Down