diff --git a/lib/httpx/adapters/faraday.rb b/lib/httpx/adapters/faraday.rb index 6c5f85cc..a8e88b33 100644 --- a/lib/httpx/adapters/faraday.rb +++ b/lib/httpx/adapters/faraday.rb @@ -197,7 +197,7 @@ module Faraday response_headers.merge!(response.headers) end @app.call(env) - rescue OpenSSL::SSL::SSLError => e + rescue ::HTTPX::TLSError => e raise SSL_ERROR, e rescue Errno::ECONNABORTED, Errno::ECONNREFUSED, diff --git a/lib/httpx/connection.rb b/lib/httpx/connection.rb index 338ef3a8..2a55e444 100644 --- a/lib/httpx/connection.rb +++ b/lib/httpx/connection.rb @@ -444,7 +444,7 @@ module HTTPX throw(:jump_tick) rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, - OpenSSL::SSL::SSLError => e + TLSError => e # connect errors, exit gracefully handle_error(e) @state = :closed diff --git a/lib/httpx/io/ruby-tls.rb b/lib/httpx/io/ruby-tls.rb index 03268333..b0673249 100644 --- a/lib/httpx/io/ruby-tls.rb +++ b/lib/httpx/io/ruby-tls.rb @@ -149,7 +149,7 @@ module RubyTls attach_function :X509_STORE_CTX_get_current_cert, [:pointer], :x509 attach_function :SSL_get_ex_data_X509_STORE_CTX_idx, [], :int attach_function :X509_STORE_CTX_get_ex_data, %i[pointer int], :ssl - attach_function :PEM_write_bio_X509, %i[bio x509], :int + attach_function :PEM_write_bio_X509, %i[bio x509], :bool # SSL Context Class # OpenSSL before 1.1.0 do not have these methods @@ -219,9 +219,10 @@ module RubyTls attach_function :SSL_ctrl, %i[ssl int long pointer], :long SSL_CTRL_SET_TLSEXT_HOSTNAME = 55 + def self.SSL_set_tlsext_host_name(ssl, host_name) - name = FFI::MemoryPointer.from_string(host_name) - SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name) + name_ptr = FFI::MemoryPointer.from_string(host_name) + raise "error setting SNI hostname" if SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name_ptr) == 0 end # Server Name Indication (SNI) Support @@ -781,22 +782,29 @@ module RubyTls end end - VerifyCB = FFI::Function.new(:int, %i[int pointer]) do |_preverify_ok, x509_store| - x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store) - ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx) + VerifyCB = FFI::Function.new(:int, %i[int pointer]) do |preverify_ok, x509_store| + if preverify_ok.zero? + 1 + else + x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store) + ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx) - bio_out = SSL.BIO_new(SSL.BIO_s_mem) - SSL.PEM_write_bio_X509(bio_out, x509) + bio_out = SSL.BIO_new(SSL.BIO_s_mem) + ret = SSL.PEM_write_bio_X509(bio_out, x509) + unless ret + SSL.BIO_free(bio_out) + raise "Error reading certificate" + end - len = SSL.BIO_pending(bio_out) - buffer = FFI::MemoryPointer.new(:char, len, false) - size = SSL.BIO_read(bio_out, buffer, len) + len = SSL.BIO_pending(bio_out) + buffer = FFI::MemoryPointer.new(:char, len, false) + size = SSL.BIO_read(bio_out, buffer, len) - # THis is the callback into the ruby class - result = InstanceLookup[ssl.address].verify(buffer.read_string(size)) - - SSL.BIO_free(bio_out) - result + # THis is the callback into the ruby class + cert = buffer.read_string(size) + SSL.BIO_free(bio_out) + InstanceLookup[ssl.address].verify(cert) + end end def pending_data(bio) diff --git a/lib/httpx/io/ssl.rb b/lib/httpx/io/ssl.rb index 9d72aca1..8b64f287 100644 --- a/lib/httpx/io/ssl.rb +++ b/lib/httpx/io/ssl.rb @@ -3,6 +3,8 @@ require "openssl" module HTTPX + TLSError = OpenSSL::SSL::SSLError + class SSL < TCP TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols) { alpn_protocols: %w[h2 http/1.1] } diff --git a/lib/httpx/io/tls.rb b/lib/httpx/io/tls.rb index 65ab12fb..35157956 100644 --- a/lib/httpx/io/tls.rb +++ b/lib/httpx/io/tls.rb @@ -4,6 +4,7 @@ require "httpx/io/ruby-tls" require "openssl" module HTTPX + TLSError = Class.new(StandardError) class SSL < TCP def initialize(_, _, options) super @@ -96,8 +97,8 @@ module HTTPX # # signals TLS invalid status / shutdown. def close_cb - log { "TLS closing" } - transport_close + log { "Error, TLS closing" } + raise TLSError, "SSL Error" end # TLS callback. @@ -115,10 +116,11 @@ module HTTPX # passed the peer +cert+ to be verified. # def verify_cb(cert) - raise "Peer verification enabled, but no certificate received." if cert.nil? + raise TLSError, "Peer verification enabled, but no certificate received." if cert.nil? log { "TLS verifying #{cert}" } @peer_cert = OpenSSL::X509::Certificate.new(cert) + # by default one doesn't verify client certificates in the server verify_hostname(@sni_hostname) end @@ -126,8 +128,10 @@ module HTTPX # copied from: # https://github.com/ruby/ruby/blob/8cbf2dae5aadfa5d6241b0df2bf44d55db46704f/ext/openssl/lib/openssl/ssl.rb#L395-L409 # - def verify_hostname(peer_cert) - OpenSSL::SSL.verify_certificate_identity(peer_cert, @hostname) + def verify_hostname(host) + return false unless @ctx.verify_peer && @peer_cert + + OpenSSL::SSL.verify_certificate_identity(@peer_cert, host) end private @@ -147,8 +151,18 @@ module HTTPX options = {} options[:verify_peer] = !ssl_options.key?(:verify_mode) || ssl_options[:verify_mode] != OpenSSL::SSL::VERIFY_NONE options[:version] = ssl_options[:ssl_version] if ssl_options.key?(:ssl_version) - # options[:private_key] = tls[:key] if tls.key?(:key) - options[:cert_chain] = ssl_options[:cert_store] if ssl_options.key?(:cert_store) + + if ssl_options.key?(:key) + private_key = ssl_options[:key] + private_key = private_key.to_pem if private_key.respond_to?(:to_pem) + options[:private_key] = private_key + end + + if ssl_options.key?(:ca_path) || ssl_options.key?(:ca_file) + ca_path = ssl_options[:ca_path] || ssl_options[:ca_file].path + options[:cert_chain] = ca_path + end + options[:ciphers] = ssl_options[:ciphers] if ssl_options.key?(:ciphers) options[:protocols] = ssl_options.fetch(:alpn_protocols, %w[h2 http/1.1]) options[:fallback] = "http/1.1" diff --git a/lib/httpx/plugins/retries.rb b/lib/httpx/plugins/retries.rb index 98cc0589..b6abcbef 100644 --- a/lib/httpx/plugins/retries.rb +++ b/lib/httpx/plugins/retries.rb @@ -17,7 +17,7 @@ module HTTPX Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, - (OpenSSL::SSL::SSLError if defined?(OpenSSL)), + (TLSError if defined?(TLSError)), TimeoutError, Parser::Error, Errno::EINVAL, diff --git a/test/adapters/faraday_test.rb b/test/adapters/faraday_test.rb index d87b63a4..10389cb6 100644 --- a/test/adapters/faraday_test.rb +++ b/test/adapters/faraday_test.rb @@ -34,9 +34,9 @@ class FaradayTest < Minitest::Test assert JSON.parse(res.body.to_s)["gzipped"] end + SYSTEM_CERT_STORE_DIR = "/usr/share/ca-certificates/mozilla" def test_adapter_get_ssl_fails_with_bad_cert - fake_store = OpenSSL::X509::Store.new - conn = create_connection(ssl: { cert_store: fake_store, verify: OpenSSL::SSL::VERIFY_PEER }) + conn = create_connection(ssl: { ca_path: SYSTEM_CERT_STORE_DIR, verify: OpenSSL::SSL::VERIFY_PEER }) err = assert_raises Faraday::Adapter::HTTPX::SSL_ERROR do conn.get(build_path("/get")) end