diff --git a/.rubocop.yml b/.rubocop.yml index 0c01607f..161bf933 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,6 +18,8 @@ AllCops: - 'vendor/**/*' - 'www/**/*' - 'lib/httpx/extensions.rb' + # Do not lint ffi block, for openssl parity + - 'lib/httpx/io/tls/*.rb' Metrics/ClassLength: Max: 400 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0fb209a2..05e0dd7c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -36,4 +36,3 @@ Style/Documentation: Naming/AccessorMethodName: Enabled: false - \ No newline at end of file diff --git a/lib/httpx/io/tls.rb b/lib/httpx/io/tls.rb index 50c51fc5..5f35b9cc 100644 --- a/lib/httpx/io/tls.rb +++ b/lib/httpx/io/tls.rb @@ -100,7 +100,7 @@ module HTTPX # signals TLS invalid status / shutdown. def close_cb(msg = nil) log { "TLS Error: #{msg}, closing" } - raise TLSError, msg || "TLS Error" + raise TLSError, "TLSError: certificate verify failed (#{msg})" end # TLS callback. diff --git a/lib/httpx/io/tls/box.rb b/lib/httpx/io/tls/box.rb index da780f34..c920ded4 100644 --- a/lib/httpx/io/tls/box.rb +++ b/lib/httpx/io/tls/box.rb @@ -15,19 +15,28 @@ module HTTPX bio_out = SSL.BIO_new(SSL.BIO_s_mem) ret = SSL.PEM_write_bio_X509(bio_out, x509) - unless ret + if ret + 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 + cert = buffer.read_string(size) SSL.BIO_free(bio_out) - raise "Error reading certificate" + # InstanceLookup[ssl.address].verify(cert) || preverify_ok.zero? ? 1 : 0 + box = InstanceLookup[ssl.address] + hostname_verify = box.verify(cert) + if hostname_verify + 1 + else + SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_HOSTNAME_MISMATCH) + 0 + end + else + SSL.BIO_free(bio_out) + SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT) + 0 end - - 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 - cert = buffer.read_string(size) - SSL.BIO_free(bio_out) - InstanceLookup[ssl.address].verify(cert) || preverify_ok.zero? ? 1 : 0 end attr_reader :is_server, :context, :handshake_completed, :hosts, :ssl_version, :cipher, :verify_peer diff --git a/lib/httpx/io/tls/context.rb b/lib/httpx/io/tls/context.rb index 731000a5..d1059c20 100644 --- a/lib/httpx/io/tls/context.rb +++ b/lib/httpx/io/tls/context.rb @@ -60,9 +60,9 @@ module HTTPX set_alpn_negotiation(options[:protocols]) end - def cleanup return unless @ssl_ctx + SSL.SSL_CTX_free(@ssl_ctx) @ssl_ctx = nil end @@ -101,9 +101,9 @@ module HTTPX def set_min_version(version) return unless version + num = SSL.const_get("#{version}_VERSION") SSL.SSL_CTX_set_min_proto_version(@ssl_ctx, num) == 1 - puts "version done" rescue NameError raise Error, "#{version} is unsupported" end @@ -124,7 +124,6 @@ module HTTPX else protocols = Context.build_alpn_string(protocols) @alpn_set = SSL.SSL_CTX_set_alpn_protos(@ssl_ctx, protocols, protocols.length) == 0 - puts "alpn protocols: #{protocols}" end end else diff --git a/lib/httpx/io/tls/ffi.rb b/lib/httpx/io/tls/ffi.rb index 39274553..ff64fcba 100644 --- a/lib/httpx/io/tls/ffi.rb +++ b/lib/httpx/io/tls/ffi.rb @@ -148,16 +148,21 @@ module HTTPX attach_function :SSL_set_ex_data, %i[ssl int string], :int callback :verify_callback, %i[int x509], :int attach_function :SSL_set_verify, %i[ssl int verify_callback], :void + attach_function :SSL_CTX_set_verify, %i[ssl int verify_callback], :void attach_function :SSL_get_verify_result, %i[ssl], :long attach_function :SSL_connect, [:ssl], :int # Verify callback + X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT = 2 + X509_V_ERR_HOSTNAME_MISMATCH = 62 + X509_V_ERR_CERT_REJECTED = 28 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 :X509_STORE_CTX_get_error_depth, %i[x509], :int attach_function :PEM_write_bio_X509, %i[bio x509], :bool attach_function :X509_verify_cert_error_string, %i[long], :string + attach_function :X509_STORE_CTX_set_error, %i[ssl_ctx long], :void # SSL Context Class # OpenSSL before 1.1.0 do not have these methods @@ -230,7 +235,7 @@ module HTTPX def self.SSL_set_tlsext_host_name(ssl, host_name) name_ptr = FFI::MemoryPointer.from_string(host_name) - raise Error, "error setting SNI hostname" if SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name_ptr) == 0 + raise Error, "error setting SNI hostname" if SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name_ptr).zero? end # Server Name Indication (SNI) Support @@ -286,7 +291,7 @@ module HTTPX # Deconstructor attach_function :SSL_CTX_free, [:ssl_ctx], :void - PrivateMaterials = <<~keystr + PrivateMaterials = <<~KEYSTR -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDCYYhcw6cGRbhBVShKmbWm7UVsEoBnUf0cCh8AX+MKhMxwVDWV Igdskntn3cSJjRtmgVJHIK0lpb/FYHQB93Ohpd9/Z18pDmovfFF9nDbFF0t39hJ/ @@ -325,7 +330,7 @@ module HTTPX Df6hTAs7H3MWww62ddvR8l07AWfSzSP5L6mDsbvq7EmQsmPODwb6C+i2aF3EDL8j uw73m4YIGI0Zw2XdBpiOGkx2H56Kya6mJJe/5XORZedh1wpI7zki01tHYbcy -----END CERTIFICATE----- - keystr + KEYSTR BuiltinPasswdCB = FFI::Function.new(:int, %i[pointer int int pointer]) do |buffer, _len, _flag, _data| buffer.write_string("kittycat") diff --git a/test/https_test.rb b/test/https_test.rb index a7e8cbe8..21bee4d8 100644 --- a/test/https_test.rb +++ b/test/https_test.rb @@ -112,7 +112,8 @@ class HTTPSTest < Minitest::Test uri = build_uri("/get") response = HTTPX.with(ssl: { hostname: "great-gatsby.com" }).get(uri) assert response.is_a?(HTTPX::ErrorResponse), "response should contain errors" - assert response.error.message.include?("certificate verify failed") + assert response.error.message.include?("certificate verify failed"), + "unexpected error: #{response.error.message}" end private