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.
- checksums.yaml +4 -4
- data/README.md +31 -0
- data/lib/httpclient/cacert.pem +2079 -3015
- data/lib/httpclient/jruby_ssl_socket.rb +9 -0
- data/lib/httpclient/ssl_config.rb +100 -1
- data/lib/httpclient/ssl_socket.rb +3 -0
- data/lib/httpclient/version.rb +1 -1
- data/test/fixtures/emudhra-dv-g3b.pem +47 -0
- data/test/helper.rb +32 -0
- data/test/jruby_ssl_socket/test_socket_timeout.rb +18 -0
- data/test/test_ssl.rb +156 -3
- data/test/test_ssl_config.rb +51 -0
- metadata +6 -7
|
@@ -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
|
|
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}")
|
data/lib/httpclient/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
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: []
|