Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Refresh the bundled Mozilla CA certificates to the October 20, 2026 snapshot, building on [#444](https://github.com/nahi/httpclient/pull/444).

* Use `OpenSSL::X509::DEFAULT_CERT_FILE` by default on MRI. `SSLConfig#cacert_file=` can select another CA file, including the bundled certificates via `SSLConfig::BUNDLED_CACERT_FILE` ([#480](https://github.com/nahi/httpclient/issues/480)).

* With peer verification enabled and no custom verification callback, reject TLS connections when a server supplies certificates containing blocked eMudhra intermediate CA public keys, including cross-signed variants ([#480](https://github.com/nahi/httpclient/issues/480)).
Connections are also rejected when a server supplies an unused certificate containing a blocked public key.
`VERIFY_NONE`/`nil` disables this check; a custom verification callback replaces the default policy and is responsible for certificate validation.
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ You can install httpclient via rubygems: `gem install httpclient`
See [HTTPClient](http://www.rubydoc.info/gems/httpclient/frames) for documentation.
You can also check sample/howto.rb how to use APIs.

### TLS trust store

On MRI, HTTPClient uses `OpenSSL::X509::DEFAULT_CERT_FILE` by default. To
preserve compatibility with previous releases' default CA selection, select the
bundled Mozilla CA certificates explicitly:

```ruby
client = HTTPClient.new
client.ssl_config.cacert_file = HTTPClient::SSLConfig::BUNDLED_CACERT_FILE
```

Assigning `cacert_file` loads that file into a new certificate store, replacing
the current store and resetting existing sessions. You can assign it after a
connection has been made. `load_trust_ca` adds the selected file to the current
store and resets existing sessions. On JRuby, the default trust store remains
the JVM's; assigning `cacert_file` explicitly selects a certificate file. Use
`client.ssl_config.set_default_paths` on MRI to load OpenSSL's configured
default certificate file and directory.

### TLS certificate restrictions

Blocked public keys are identified by their SPKI SHA-256 hashes in
Expand All @@ -91,11 +110,10 @@ context. With peer verification enabled, its return value controls certificate
verification. The caller is responsible for the custom callback's validation
policy. Hostname verification is unchanged.

Note: MRI uses HTTPClient's bundled `cacert.pem` by default, while JRuby uses
the JVM's CA certificates and certificate validation. Secure operation on JRuby
depends on keeping the JVM and its CA certificates up to date. HTTPClient's
additional check may not detect blocked intermediates found only in a local
trust store or obtained through JVM issuer retrieval (such as AIA when enabled).
On JRuby, HTTPClient uses the JVM's CA certificates and validation by default.
Keep the JVM and its CA certificates up to date. The additional blocked-key
check may not detect intermediates found only in a local trust store or obtained
through JVM issuer retrieval (such as AIA when enabled).

Remove unused certificates containing blocked public keys from the server's
certificate chain configuration. If a certificate containing a blocked public key
Expand Down
55 changes: 34 additions & 21 deletions lib/httpclient/ssl_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,20 @@ class HTTPClient
#
# == Trust Anchor Control
#
# SSLConfig loads 'httpclient/cacert.pem' as a trust anchor
# (trusted certificate(s)) with add_trust_ca in initialization time.
# This means that HTTPClient instance trusts some CA certificates by default,
# like Web browsers. 'httpclient/cacert.pem' is downloaded from curl web
# site by the author and included in released package.
# On MRI, HTTPClient uses OpenSSL::X509::DEFAULT_CERT_FILE by default. To
# preserve compatibility with previous releases' default CA selection,
# explicitly select the bundled Mozilla CA certificates:
#
# On JRuby, HTTPClient uses Java runtime's trusted CA certificates, not
# cacert.pem by default. You can load cacert.pem by calling
# SSLConfig#load_trust_ca manually like:
# client = HTTPClient.new
# client.ssl_config.cacert_file = HTTPClient::SSLConfig::BUNDLED_CACERT_FILE
#
# HTTPClient.new { self.ssl_config.load_trust_ca }.get("https://...")
# Assigning cacert_file loads the selected file into a new certificate store,
# replacing the current store and resetting existing sessions. It can be
# changed after connections have been made. load_trust_ca loads the selected
# file into the current store, adding to its existing certificates.
#
# Secure operation on JRuby depends on keeping the JVM and its CA certificates
# up to date.
#
# You may want to change trust anchor by yourself. Call clear_cert_store
# then add_trust_ca for that purpose.
# On JRuby, HTTPClient uses the JVM's trusted CA certificates by default.
# Assign cacert_file explicitly to use a certificate file instead.
#
# == TLS certificate restrictions
#
Expand Down Expand Up @@ -116,6 +113,8 @@ def attr_config(symbol)


CIPHERS_DEFAULT = "ALL:!aNULL:!eNULL:!SSLv2" # OpenSSL >1.0.0 default
# Bundled Mozilla CA certificates, available for explicit selection.
BUNDLED_CACERT_FILE = File.join(File.dirname(__FILE__), 'cacert.pem').freeze

# Public keys whose SPKI SHA-256 hashes are listed below are considered blocked.
# The list covers eight eMudhra intermediate CA public keys whose corresponding
Expand Down Expand Up @@ -197,9 +196,12 @@ def self.blocked_public_key?(public_key_der)
# See ciphers(1) man in OpenSSL for more detail.
attr_config :ciphers

# OpenSSL::X509::X509::Store used for verification. You can reset the
# store with clear_cert_store and set the new store with cert_store=.
# OpenSSL::X509::Store used for verification. You can reset the store with
# clear_cert_store and set the new store with cert_store=.
attr_reader :cert_store # don't use if you don't know what it is.
# Selected CA certificate file. Defaults to OpenSSL::X509::DEFAULT_CERT_FILE
# on MRI.
attr_reader :cacert_file

# For server side configuration. Ignore this.
attr_config :client_ca # :nodoc:
Expand All @@ -214,6 +216,7 @@ def cert_store_items; @cert_store._httpclient_cert_store_items; end
def initialize(client)
return unless SSLEnabled
@client = client
@cacert_file = X509::DEFAULT_CERT_FILE
@cert_store = X509::Store.new
@cert_store_crl_items = []
@client_cert = @client_key = @client_key_pass = @client_ca = nil
Expand Down Expand Up @@ -309,6 +312,17 @@ def add_trust_ca(trust_ca_file_or_hashed_dir)
end
alias set_trust_ca add_trust_ca

# Selects a CA certificate file. Replaces the current certificate store
# with a new store loaded from file and resets existing sessions. If loading
# fails, the current store and selected file are unchanged.
def cacert_file=(file)
store = X509::Store.new
store._httpclient_cert_store_items.clear if defined?(JRUBY_VERSION)
add_trust_ca_to_store(store, file)
@cacert_file = file
self.cert_store = store
end

def add_trust_ca_to_store(cert_store, trust_ca_file_or_hashed_dir)
if FileTest.directory?(trust_ca_file_or_hashed_dir)
cert_store.add_path(trust_ca_file_or_hashed_dir)
Expand All @@ -317,8 +331,9 @@ def add_trust_ca_to_store(cert_store, trust_ca_file_or_hashed_dir)
end
end

# Loads default trust anchors.
# Calling this method resets all existing sessions.
# Loads the selected cacert_file into the current certificate store.
# Existing certificates in the store are kept. Calling this method resets
# all existing sessions.
def load_trust_ca
load_cacerts(@cert_store)
change_notify
Expand Down Expand Up @@ -521,10 +536,8 @@ def change_notify
nil
end

# Use 2048 bit certs trust anchor
def load_cacerts(cert_store)
file = File.join(File.dirname(__FILE__), 'cacert.pem')
add_trust_ca_to_store(cert_store, file)
add_trust_ca_to_store(cert_store, @cacert_file)
end
end

Expand Down
4 changes: 4 additions & 0 deletions test/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def test_blocked_peer_certificate_before_request

def test_blocked_intermediate_with_default_paths
escape_env do
# Nix's OpenSSL can prioritize this CA bundle path over SSL_CERT_FILE.
ENV.delete('NIX_SSL_CERT_FILE')
ENV['SSL_CERT_FILE'] = path('ca-chain.pem')
@client.ssl_config.set_default_paths
@server.ssl_context.extra_chain_cert = [cert('subca.cert')]
Expand Down Expand Up @@ -373,6 +375,8 @@ def test_set_default_paths
@client.get(@url)
end
escape_env do
# Nix's OpenSSL can prioritize this CA bundle path over SSL_CERT_FILE.
ENV.delete('NIX_SSL_CERT_FILE')
ENV['SSL_CERT_FILE'] = File.join(DIR, 'ca-chain.pem')
@client.ssl_config.set_default_paths
@client.get(@url)
Expand Down
43 changes: 43 additions & 0 deletions test/test_ssl_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,46 @@ def test_blocked_cross_signed_keys_without_der_export
end
end
end

# JRuby uses its JVM trust manager instead of SSLConfig#set_context.
unless defined?(JRUBY_VERSION)
class TestOpenSSLTrustStore < Test::Unit::TestCase
def setup
@client = HTTPClient.new
@config = @client.ssl_config
@loaded_files = []
loaded_files = @loaded_files
@config.define_singleton_method(:add_trust_ca_to_store) do |_store, file|
loaded_files << file
end
end

def teardown
@client.reset_all if @client
end

def test_default_cert_file_is_loaded_once
configure_context
configure_context
assert_equal([OpenSSL::X509::DEFAULT_CERT_FILE], @loaded_files)
end

def test_bundled_ca_can_be_selected_with_cacert_file
@config.cacert_file = bundled_ca_file
configure_context
assert_equal([bundled_ca_file], @loaded_files)
end

private

def configure_context
context = OpenSSL::SSL::SSLContext.new
@config.set_context(context)
assert_same(@config.cert_store, context.cert_store)
end

def bundled_ca_file
HTTPClient::SSLConfig::BUNDLED_CACERT_FILE
end
end
end
Loading