httpclient 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -128,6 +128,8 @@ unless defined?(SSLSocket)
128
128
  @bufstr << String.from_java_bytes(@buf, Encoding::BINARY)[0, size]
129
129
  end
130
130
  size
131
+ rescue java.net.SocketTimeoutException => e
132
+ raise HTTPClient::ReceiveTimeoutError.new(e.getMessage)
131
133
  rescue java.io.IOException => e
132
134
  raise OpenSSL::SSL::SSLError.new("#{e.class}: #{e.getMessage}")
133
135
  end
@@ -164,6 +166,10 @@ unless defined?(SSLSocket)
164
166
  @cert.getSubjectDN
165
167
  end
166
168
 
169
+ def public_key
170
+ @cert.getPublicKey
171
+ end
172
+
167
173
  def to_text
168
174
  @cert.toString
169
175
  end
@@ -570,6 +576,9 @@ unless defined?(SSLSocket)
570
576
  def ssl_connect(hostname)
571
577
  @ssl_socket.startHandshake
572
578
  ssl_session = @ssl_socket.getSession
579
+ if @config.verify? && !@config.verify_callback
580
+ @config.check_peer_certificates(ssl_session.getPeerCertificates)
581
+ end
573
582
  @peer_cert = JavaCertificate.new(ssl_session.getPeerCertificates.first)
574
583
  if $DEBUG
575
584
  warn("Protocol version: #{ssl_session.getProtocol}")
@@ -32,8 +32,36 @@ class HTTPClient
32
32
  #
33
33
  # HTTPClient.new { self.ssl_config.load_trust_ca }.get("https://...")
34
34
  #
35
+ # Secure operation on JRuby depends on keeping the JVM and its CA certificates
36
+ # up to date.
37
+ #
35
38
  # You may want to change trust anchor by yourself. Call clear_cert_store
36
39
  # then add_trust_ca for that purpose.
40
+ #
41
+ # == TLS certificate restrictions
42
+ #
43
+ # Blocked public keys are identified by their SPKI SHA-256 hashes in
44
+ # BLOCKED_SPKI_SHA256. The list covers eight eMudhra intermediate CA public
45
+ # keys whose corresponding private keys were compromised.
46
+ #
47
+ # With peer verification enabled and no custom verify_callback, HTTPClient
48
+ # rejects a connection before sending an HTTP request if any certificate
49
+ # supplied by the server contains a blocked eMudhra public key. This includes
50
+ # cross-signed certificates and extra certificates that are not used in the
51
+ # verification path, even when another valid path exists.
52
+ #
53
+ # Setting verify_mode to OpenSSL::SSL::VERIFY_NONE or nil disables this check.
54
+ # A custom verify_callback replaces the default verification policy, including
55
+ # the blocked public key check. The callback receives the underlying verifier's
56
+ # result and context. With peer verification enabled, its return value controls
57
+ # certificate verification.
58
+ # The caller is responsible for the custom callback's validation policy.
59
+ # Hostname verification is unchanged.
60
+ #
61
+ # On JRuby, this additional check may not detect blocked intermediates found
62
+ # only in a local trust store or obtained through JVM issuer retrieval (such as
63
+ # AIA when enabled). These paths depend on the JVM's certificate validation.
64
+ # See https://github.com/nahi/httpclient/issues/480 for background.
37
65
  class SSLConfig
38
66
  include HTTPClient::Util
39
67
  if SSLEnabled
@@ -89,6 +117,39 @@ class HTTPClient
89
117
 
90
118
  CIPHERS_DEFAULT = "ALL:!aNULL:!eNULL:!SSLv2" # OpenSSL >1.0.0 default
91
119
 
120
+ # Public keys whose SPKI SHA-256 hashes are listed below are considered blocked.
121
+ # The list covers eight eMudhra intermediate CA public keys whose corresponding
122
+ # private keys were compromised. Hashes use DER-encoded SubjectPublicKeyInfo,
123
+ # covering cross-signed variants.
124
+ # Includes six TLS keys and two code-signing keys.
125
+ # CA names are from CCADB's public intermediate certificate inventory:
126
+ # https://ccadb.my.salesforce-sites.com/mozilla/PublicAllIntermediateCertsWithPEMCSV
127
+ # Matches distrustedSPKIHashes in Go's crypto/x509 at:
128
+ # https://github.com/golang/go/blob/7f86f957633ee31a1c17a1eb8452ca7a3d9bc141/src/crypto/x509/verify.go
129
+ # Related Go mitigation: https://go.dev/issue/82345 (CVE-2026-111943).
130
+ BLOCKED_SPKI_SHA256 = [
131
+ # EM OV TLS CA - G3B
132
+ '43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b',
133
+ # EM DV TLS CA - G3C
134
+ '49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f',
135
+ # EM EV TLS CA - G3C
136
+ '5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0',
137
+ # EM OV TLS CA - G3C
138
+ '63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8',
139
+ # EM OV CS CA - G3C
140
+ '92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30',
141
+ # EM EV TLS CA - G3B
142
+ 'e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41',
143
+ # EM DV TLS CA - G3B
144
+ 'ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105',
145
+ # EM OV CS CA - G3B
146
+ 'ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6',
147
+ ].map(&:freeze).freeze
148
+
149
+ def self.blocked_public_key?(public_key_der)
150
+ BLOCKED_SPKI_SHA256.include?(OpenSSL::Digest::SHA256.hexdigest(public_key_der))
151
+ end
152
+
92
153
  # Which TLS protocol version (also called method) will be used. Defaults
93
154
  # to :auto which means that OpenSSL decides (In my tests this resulted
94
155
  # with always the highest available protocol being used).
@@ -109,6 +170,7 @@ class HTTPClient
109
170
 
110
171
  # A number which represents OpenSSL's verify mode. Default value is
111
172
  # OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT.
173
+ # VERIFY_NONE or nil disables the default blocked public key check.
112
174
  attr_config :verify_mode
113
175
  # A number of verify depth. Certification path which length is longer than
114
176
  # this depth is not allowed.
@@ -119,6 +181,9 @@ class HTTPClient
119
181
  # OpenSSL's verification. handler.call is invoked with 2 arguments,
120
182
  # ok and ctx; ok is a result of general OpenSSL's verification. ctx is a
121
183
  # OpenSSL::X509::StoreContext.
184
+ # Setting a handler replaces the default verification policy, including its
185
+ # blocked public key check. The caller is responsible for the
186
+ # handler's validation policy. Hostname verification is unchanged.
122
187
  attr_config :verify_callback
123
188
  # SSL timeout in sec. nil by default.
124
189
  attr_config :timeout
@@ -311,6 +376,35 @@ class HTTPClient
311
376
  ctx.ssl_version = @ssl_version unless @ssl_version == :auto
312
377
  end
313
378
 
379
+ # Reject blocked public keys in the given certificate collection.
380
+ # Peer certificates are checked even when unused in the verification path.
381
+ def check_peer_certificates(certificates) # :nodoc:
382
+ certificates = certificates.to_a if certificates.respond_to?(:to_a)
383
+ certificates.each do |certificate|
384
+ public_key = if certificate.respond_to?(:public_key)
385
+ certificate.public_key
386
+ else
387
+ certificate.getPublicKey
388
+ end
389
+ public_key_der = if public_key.respond_to?(:getEncoded)
390
+ String.from_java_bytes(public_key.getEncoded)
391
+ elsif public_key.respond_to?(:public_to_der)
392
+ public_key.public_to_der
393
+ elsif public_key.respond_to?(:to_der)
394
+ public_key.to_der
395
+ else
396
+ # Older ruby-openssl cannot export generic PKey objects.
397
+ fields = OpenSSL::ASN1.decode(certificate.to_der).value[0].value
398
+ # The optional version field shifts SubjectPublicKeyInfo by one.
399
+ spki_index = fields[0].tag_class == :CONTEXT_SPECIFIC ? 6 : 5
400
+ fields[spki_index].to_der
401
+ end
402
+ if self.class.blocked_public_key?(public_key_der)
403
+ raise SSL::SSLError, 'certificate chain contains a blocked eMudhra public key'
404
+ end
405
+ end
406
+ end
407
+
314
408
  # post connection check proc for ruby < 1.8.5.
315
409
  # this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
316
410
  def post_connection_check(peer_cert, hostname) # :nodoc:
@@ -340,8 +434,13 @@ class HTTPClient
340
434
  raise SSL::SSLError, "hostname was not match with the server certificate"
341
435
  end
342
436
 
343
- # Default callback for verification: only dumps error.
437
+ # Default callback: reject blocked public keys and report verification errors.
344
438
  def default_verify_callback(is_ok, ctx)
439
+ begin
440
+ check_peer_certificates(ctx.chain) if verify?
441
+ rescue SSL::SSLError
442
+ return false
443
+ end
345
444
  if $DEBUG
346
445
  if is_ok
347
446
  warn("ok: #{ctx.current_cert.subject.to_s.dump}")
@@ -101,6 +101,9 @@ class HTTPClient
101
101
  @ssl_socket.hostname = hostname
102
102
  end
103
103
  @ssl_socket.connect
104
+ if @config.verify? && !@config.verify_callback
105
+ @config.check_peer_certificates(@ssl_socket.peer_cert_chain)
106
+ end
104
107
  if $DEBUG
105
108
  if @ssl_socket.respond_to?(:ssl_version)
106
109
  warn("Protocol version: #{@ssl_socket.ssl_version}")
@@ -1,3 +1,3 @@
1
1
  class HTTPClient
2
- VERSION = '2.9.0'
2
+ VERSION = '2.10.0'
3
3
  end
@@ -0,0 +1,47 @@
1
+ # EM DV TLS CA - G3B, signed by emSign Root TLS CA - G3 and AAA Certificate Services.
2
+ # Public certificates from https://emudhra.we-have-a-problem.com/chains.zip
3
+ # SPKI SHA-256: ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105
4
+ -----BEGIN CERTIFICATE-----
5
+ MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw
6
+ CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
7
+ ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1
8
+ NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
9
+ ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
10
+ AQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J
11
+ hZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p
12
+ 1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
13
+ WUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG
14
+ CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
15
+ KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
16
+ BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
17
+ aHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
18
+ b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f
19
+ BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
20
+ cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
21
+ AwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO
22
+ Ibh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8
23
+ gt2TlfJbHii+KNvK
24
+ -----END CERTIFICATE-----
25
+ -----BEGIN CERTIFICATE-----
26
+ MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw
27
+ ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
28
+ A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
29
+ BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
30
+ ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
31
+ MRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
32
+ IgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO
33
+ L4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2
34
+ PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
35
+ BgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG
36
+ A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
37
+ MDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
38
+ AQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
39
+ QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
40
+ BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
41
+ AQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE
42
+ wqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM
43
+ QkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ
44
+ 7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl
45
+ bN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V
46
+ GUyRCEIDhF5TBUuKF4E3kf1h
47
+ -----END CERTIFICATE-----
data/test/helper.rb CHANGED
@@ -9,6 +9,38 @@ require 'stringio'
9
9
  require 'cgi'
10
10
  require 'webrick/httputils'
11
11
 
12
+ if RUBY_ENGINE == 'jruby'
13
+ require 'delegate'
14
+
15
+ module JRubyWEBrickSSLReadBuffer
16
+ class ReadableIO < SimpleDelegator
17
+ def initialize(socket)
18
+ @ssl_socket = socket
19
+ super(socket.to_io)
20
+ end
21
+
22
+ def wait_readable(timeout = nil)
23
+ # JRuby may buffer request bytes while completing the TLS handshake.
24
+ @ssl_socket.pending > 0 || super
25
+ end
26
+ end
27
+
28
+ module HTTPServerPatch
29
+ def run(sock)
30
+ if sock.respond_to?(:pending)
31
+ readable_io = ReadableIO.new(sock)
32
+ sock.define_singleton_method(:to_io) { readable_io }
33
+ end
34
+ super
35
+ ensure
36
+ sock.singleton_class.remove_method(:to_io) if readable_io
37
+ end
38
+ end
39
+ end
40
+
41
+ WEBrick::HTTPServer.prepend(JRubyWEBrickSSLReadBuffer::HTTPServerPatch)
42
+ end
43
+
12
44
 
13
45
  module Helper
14
46
  Port = 17171
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../helper', File.dirname(__FILE__))
2
+
3
+
4
+ class JRubySocketTimeoutTest < Test::Unit::TestCase
5
+ def test_read_timeout_is_reported_as_receive_timeout
6
+ socket = HTTPClient::JavaSocketWrap.allocate
7
+ input = Object.new
8
+ input.define_singleton_method(:read) do |_buffer|
9
+ raise Java::JavaNet::SocketTimeoutException.new('Read timed out')
10
+ end
11
+ socket.instance_variable_set(:@instr, input)
12
+ socket.instance_variable_set(:@buf, Java::byte[HTTPClient::JavaSocketWrap::BUF_SIZE].new)
13
+
14
+ assert_raise(HTTPClient::ReceiveTimeoutError) do
15
+ socket.send(:fill)
16
+ end
17
+ end
18
+ end
data/test/test_ssl.rb CHANGED
@@ -61,6 +61,129 @@ class TestSSL < Test::Unit::TestCase
61
61
  assert_instance_of(OpenSSL::X509::Store, cfg.cert_store)
62
62
  end
63
63
 
64
+ def test_emudhra_cross_signed_public_key
65
+ certificates = read_path('fixtures/emudhra-dv-g3b.pem').scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m).map { |pem|
66
+ OpenSSL::X509::Certificate.new(pem)
67
+ }
68
+ assert_equal(2, certificates.size)
69
+ assert_not_equal(certificates[0].issuer, certificates[1].issuer)
70
+ assert_equal(8, HTTPClient::SSLConfig::BLOCKED_SPKI_SHA256.size)
71
+ certificates.each do |certificate|
72
+ assert(HTTPClient::SSLConfig.blocked_public_key?(certificate.public_key.to_der))
73
+ end
74
+ assert_equal(false, HTTPClient::SSLConfig.blocked_public_key?(cert('ca.cert').public_key.to_der))
75
+ end
76
+
77
+ def test_blocked_peer_certificate_before_request
78
+ cfg = @client.ssl_config
79
+ cfg.add_trust_ca(path('ca-chain.pem'))
80
+ requests = Queue.new
81
+ @server.mount_proc('/blocked') { |req, res| requests << req; res.body = 'unexpected' }
82
+ intermediate = cert('subca.cert')
83
+ # This unrelated self-signed certificate cannot be part of the valid path.
84
+ unused = cert('fixtures/verify.foo.cert')
85
+ @server.ssl_context.extra_chain_cert = [intermediate, unused]
86
+ assert_equal('hello', @client.get_content(@url))
87
+
88
+ @client.reset_all
89
+ with_blocked_public_key(intermediate) do
90
+ error = assert_raise(OpenSSL::SSL::SSLError) do
91
+ @client.post("https://localhost:#{serverport}/blocked", 'secret', 'Authorization' => 'secret')
92
+ end
93
+ assert_match(/certificate verify failed|verifycallback failed|revoked|blocked eMudhra/, error.message)
94
+ end
95
+ assert(requests.empty?, 'HTTP request reached a server supplying an intermediate containing a blocked public key')
96
+
97
+ @client.reset_all
98
+ with_blocked_public_key(unused) do
99
+ error = assert_raise(OpenSSL::SSL::SSLError) do
100
+ @client.post("https://localhost:#{serverport}/blocked", 'secret', 'Authorization' => 'secret')
101
+ end
102
+ assert_match(/certificate verify failed|verifycallback failed|revoked|blocked eMudhra/, error.message)
103
+ end
104
+ assert(requests.empty?, 'HTTP request reached a server supplying an unused certificate containing a blocked public key')
105
+ end
106
+
107
+ def test_blocked_intermediate_with_default_paths
108
+ escape_env do
109
+ ENV['SSL_CERT_FILE'] = path('ca-chain.pem')
110
+ @client.ssl_config.set_default_paths
111
+ @server.ssl_context.extra_chain_cert = [cert('subca.cert')]
112
+ assert_equal('hello', @client.get_content(@url))
113
+ @client.reset_all
114
+ with_blocked_public_key(cert('subca.cert')) do
115
+ assert_raise(OpenSSL::SSL::SSLError) { @client.get(@url) }
116
+ end
117
+ end
118
+ end
119
+
120
+ def test_blocked_intermediate_with_verification_overrides
121
+ cfg = @client.ssl_config
122
+ cfg.add_trust_ca(path('ca-chain.pem'))
123
+ @server.ssl_context.extra_chain_cert = [cert('subca.cert')]
124
+ assert_equal('hello', @client.get_content(@url))
125
+ @client.reset_all
126
+ with_blocked_public_key(cert('subca.cert')) do
127
+ cfg.verify_mode = OpenSSL::SSL::VERIFY_NONE
128
+ assert_equal('hello', @client.get_content(@url))
129
+
130
+ cfg.verify_mode = nil
131
+ assert_equal('hello', @client.get_content(@url))
132
+
133
+ cfg.verify_mode = OpenSSL::SSL::VERIFY_PEER
134
+ callback_results = []
135
+ cfg.verify_callback = proc { |ok, ctx| callback_results << ok; true }
136
+ assert_equal('hello', @client.get_content(@url))
137
+ assert(!callback_results.empty?, 'custom verification callback was not invoked')
138
+ assert(callback_results.all?, 'custom callback did not receive the original verification result')
139
+
140
+ cfg.verify_callback = proc { |ok, ctx| false }
141
+ assert_raise(OpenSSL::SSL::SSLError) { @client.get_content(@url) }
142
+
143
+ cfg.verify_callback = cfg.method(:default_verify_callback)
144
+ assert_raise(OpenSSL::SSL::SSLError) { @client.get_content(@url) }
145
+
146
+ cfg.verify_callback = nil
147
+ assert_raise(OpenSSL::SSL::SSLError) { @client.get_content(@url) }
148
+ end
149
+ end
150
+
151
+ data('VERIFY_NONE with nil' => [OpenSSL::SSL::VERIFY_NONE, nil],
152
+ 'VERIFY_NONE with false' => [OpenSSL::SSL::VERIFY_NONE, false],
153
+ 'nil with nil' => [nil, nil],
154
+ 'nil with false' => [nil, false])
155
+ def test_verification_disabled_ignores_callback_result(settings)
156
+ verify_mode, callback_result = settings
157
+ cfg = @client.ssl_config
158
+ cfg.verify_mode = verify_mode
159
+ callback_certificates = []
160
+ cfg.verify_callback = proc { |ok, ctx|
161
+ callback_certificates << ctx.current_cert
162
+ callback_result
163
+ }
164
+ assert_equal('hello', @client.get_content(@url))
165
+ assert(!callback_certificates.empty?, 'custom verification callback was not invoked')
166
+ end
167
+
168
+ def test_custom_verify_callback_with_untrusted_blocked_intermediate
169
+ cfg = @client.ssl_config
170
+ cfg.clear_cert_store
171
+ @server.ssl_context.extra_chain_cert = [cert('subca.cert')]
172
+ callback_results = []
173
+ cfg.verify_callback = proc { |ok, ctx|
174
+ callback_results << [ok, ctx.error]
175
+ true
176
+ }
177
+ with_blocked_public_key(cert('subca.cert')) do
178
+ assert_equal('hello', @client.get_content(@url))
179
+ assert(callback_results.any? { |ok, error| !ok && error && error != 0 },
180
+ 'custom callback did not receive the underlying verification error')
181
+
182
+ cfg.verify_callback = proc { |ok, ctx| ok }
183
+ assert_raise(OpenSSL::SSL::SSLError) { @client.get_content(@url) }
184
+ end
185
+ end
186
+
64
187
  unless defined?(HTTPClient::JRubySSLSocket)
65
188
  # JRubySSLSocket does not support sync mode.
66
189
  def test_sync
@@ -209,7 +332,10 @@ if defined?(HTTPClient::JRubySSLSocket)
209
332
  assert_match(/No appropriate protocol/, ssle.message)
210
333
  end
211
334
  #
212
- cfg.ciphers = %w(TLS_RSA_WITH_AES_128_CBC_SHA)
335
+ # CI's Temurin 21 disables static-RSA suites such as
336
+ # TLS_RSA_WITH_AES_128_CBC_SHA in jdk.tls.disabledAlgorithms. Use an enabled
337
+ # ECDHE suite to verify explicitly configured ciphers can be negotiated.
338
+ cfg.ciphers = %w(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
213
339
  assert_equal("hello", @client.get_content(@url))
214
340
  #
215
341
  cfg.ciphers = HTTPClient::SSLConfig::CIPHERS_DEFAULT
@@ -263,7 +389,15 @@ end
263
389
  end
264
390
  end
265
391
 
266
- if OPENSSL_VERSION < 3
392
+ if RUBY_ENGINE == 'jruby'
393
+ def test_disallow_tlsv1
394
+ @client.ssl_config.ssl_version = :TLSv1
395
+ @client.ssl_config.verify_mode = nil
396
+ assert_raise(OpenSSL::SSL::SSLError) do
397
+ @client.get(@url)
398
+ end
399
+ end
400
+ elsif OPENSSL_VERSION < 3
267
401
  def test_allow_tlsv1
268
402
  teardown_server
269
403
  setup_server_with_ssl_version(:TLSv1)
@@ -285,7 +419,7 @@ end
285
419
  end
286
420
 
287
421
  def test_use_higher_TLS
288
- omit('TODO: it does not pass with Java 7 or old openssl')
422
+ omit('TODO: it does not pass with Java 7 or old openssl') unless RUBY_ENGINE == 'jruby'
289
423
  teardown_server
290
424
  setup_server_with_ssl_version('TLSv1_2')
291
425
  assert_nothing_raised do
@@ -323,6 +457,11 @@ end
323
457
  assert_raises(OpenSSL::SSL::SSLError) do
324
458
  @client.get("https://localhost:#{serverport}/hello")
325
459
  end
460
+ @client.ssl_config.verify_callback = proc { |ok, ctx| true }
461
+ assert_raises(OpenSSL::SSL::SSLError) do
462
+ @client.get("https://localhost:#{serverport}/hello")
463
+ end
464
+ @client.ssl_config.verify_callback = nil
326
465
  @client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
327
466
  assert_nothing_raised do
328
467
  @client.get("https://localhost:#{serverport}/hello")
@@ -392,6 +531,20 @@ end
392
531
 
393
532
  private
394
533
 
534
+ def with_blocked_public_key(certificate)
535
+ config = HTTPClient::SSLConfig
536
+ original = config::BLOCKED_SPKI_SHA256
537
+ digest = OpenSSL::Digest::SHA256.hexdigest(certificate.public_key.to_der)
538
+ config.send(:remove_const, :BLOCKED_SPKI_SHA256)
539
+ config.const_set(:BLOCKED_SPKI_SHA256, original + [digest])
540
+ yield
541
+ ensure
542
+ if original
543
+ config.send(:remove_const, :BLOCKED_SPKI_SHA256)
544
+ config.const_set(:BLOCKED_SPKI_SHA256, original)
545
+ end
546
+ end
547
+
395
548
  def cert(filename)
396
549
  OpenSSL::X509::Certificate.new(File.read(File.join(DIR, filename)))
397
550
  end
@@ -0,0 +1,51 @@
1
+ require 'test/unit'
2
+ require 'httpclient'
3
+
4
+ class TestSSLConfig < Test::Unit::TestCase
5
+ # Older ruby-openssl returns generic PKey objects without DER export methods.
6
+ CertificateWithoutKeyExport = Struct.new(:to_der) do
7
+ def public_key
8
+ Object.new
9
+ end
10
+ end
11
+
12
+ data('X.509 v1' => false, 'X.509 v3' => true)
13
+ def test_public_key_without_der_export(explicit_version)
14
+ certificate = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, 'ca.cert')))
15
+ public_key_der = certificate.public_key.to_der
16
+ encoded = OpenSSL::ASN1.decode(certificate.to_der)
17
+ unless explicit_version
18
+ # Only the DER layout matters here; the checker does not verify signatures.
19
+ fields = encoded.value[0].value
20
+ fields.shift
21
+ fields.reject! { |field| field.tag_class == :CONTEXT_SPECIFIC && field.tag == 3 }
22
+ end
23
+ peer = CertificateWithoutKeyExport.new(encoded.to_der)
24
+ config = HTTPClient::SSLConfig.new(nil)
25
+ assert_nothing_raised { config.check_peer_certificates([peer]) }
26
+
27
+ original = HTTPClient::SSLConfig::BLOCKED_SPKI_SHA256
28
+ HTTPClient::SSLConfig.send(:remove_const, :BLOCKED_SPKI_SHA256)
29
+ HTTPClient::SSLConfig.const_set(:BLOCKED_SPKI_SHA256,
30
+ original + [OpenSSL::Digest::SHA256.hexdigest(public_key_der)])
31
+ assert_raise(OpenSSL::SSL::SSLError) { config.check_peer_certificates([peer]) }
32
+ ensure
33
+ if original
34
+ HTTPClient::SSLConfig.send(:remove_const, :BLOCKED_SPKI_SHA256)
35
+ HTTPClient::SSLConfig.const_set(:BLOCKED_SPKI_SHA256, original)
36
+ end
37
+ end
38
+
39
+ def test_blocked_cross_signed_keys_without_der_export
40
+ pem = File.read(File.join(__dir__, 'fixtures/emudhra-dv-g3b.pem'))
41
+ certificates = pem.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
42
+ assert_equal(2, certificates.size)
43
+ certificates.each do |encoded|
44
+ certificate = OpenSSL::X509::Certificate.new(encoded)
45
+ peer = CertificateWithoutKeyExport.new(certificate.to_der)
46
+ assert_raise(OpenSSL::SSL::SSLError) do
47
+ HTTPClient::SSLConfig.new(nil).check_peer_certificates([peer])
48
+ end
49
+ end
50
+ end
51
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi Nakamura
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-02-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: mutex_m
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- description:
28
26
  email: nahi@ruby-lang.org
29
27
  executables:
30
28
  - httpclient
@@ -83,6 +81,7 @@ files:
83
81
  - test/client-pass.key
84
82
  - test/client.cert
85
83
  - test/client.key
84
+ - test/fixtures/emudhra-dv-g3b.pem
86
85
  - test/fixtures/verify.alt.cert
87
86
  - test/fixtures/verify.foo.cert
88
87
  - test/fixtures/verify.key
@@ -91,6 +90,7 @@ files:
91
90
  - test/htdigest
92
91
  - test/htpasswd
93
92
  - test/jruby_ssl_socket/test_pemutils.rb
93
+ - test/jruby_ssl_socket/test_socket_timeout.rb
94
94
  - test/runner.rb
95
95
  - test/server.cert
96
96
  - test/server.key
@@ -106,12 +106,12 @@ files:
106
106
  - test/test_include_client.rb
107
107
  - test/test_jsonclient.rb
108
108
  - test/test_ssl.rb
109
+ - test/test_ssl_config.rb
109
110
  - test/test_webagent-cookie.rb
110
111
  homepage: https://github.com/nahi/httpclient
111
112
  licenses:
112
113
  - ruby
113
114
  metadata: {}
114
- post_install_message:
115
115
  rdoc_options: []
116
116
  require_paths:
117
117
  - lib
@@ -126,8 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0'
128
128
  requirements: []
129
- rubygems_version: 3.5.3
130
- signing_key:
129
+ rubygems_version: 4.0.3
131
130
  specification_version: 4
132
131
  summary: gives something like the functionality of libwww-perl (LWP) in Ruby
133
132
  test_files: []