Net::HTTP adapter trusts all system root CAs when a ca_file is specified
aetherknight opened this issue · 5 comments
Problem
Faraday always trusts the OpenSSL system root CAs, even when a :ca_file
or a :ca_path
are specified, eg to implement CA pinning, or to reduce the number of trusted certificates.
Example
Faraday.new('https://www.google.com', ssl: { ca_file: '/not/used/by/google/ca.pem' }).get('/') # => #<Faraday::Response:0x007ffd580b19d8 ...
Expected behavior:
An error about server certificate certificate validation, because the website's certificate does not match the :ca_file
Root Cause:
Within the net_http
adapter, ssl_cert_store
will create a certificate store that includes the OpenSSL system root CAs if :cert_store
is not specified:
https://github.com/lostisland/faraday/blob/master/lib/faraday/adapter/net_http.rb#L105
def ssl_cert_store(ssl)
return ssl[:cert_store] if ssl[:cert_store]
# Use the default cert store by default, i.e. system ca certs
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
cert_store
end
I would think that Faraday should only set a default :cert_store
if there is no :ca_file
, no :ca_path
, and no :cert_store
specified.
Thanks for the nice report. I get the problem; however, I have the feeling that people right now are using ca_file
to provide an extra custom certificate on top of system CA certs. Flipping the switch on this behavior would be backwards-incompatible.
How about that you can choose to disable the default cert store if you deliberately want to do CA pinning? E.g.
Faraday.new('...', ssl: { ca_file: 'ca.pem', cert_store: false })
Would that satisfy your needs?
We would need to investigate how current HTTP libs (including net/http) behave in this regard: are we able to turn off the default system certs by passing no cert store object? If you have time and will to test this, it would be great.
Hey @mislav thanks for the response.
One immediate remediation I am currently using is to set the :cert_store
to an empty certificate store:
cert_store = OpenSSL::X509::Store.new
Faraday.new('...', ssl: { ca_file: 'ca.pem', cert_store: cert_store })
Or to just use a cert_store:
cert_store = OpenSSL::X509::Store.new
cert_store.add_file('ca.pem')
Faraday.new('...', ssl: { cert_store: cert_store })
However, the only adapters that currently support :cert_store
are:
httpclient
net_http
net_http_persistant
The httpclient
adapter does not add a default certificate store, and the net_http_persistant
adapter does the same thing as net_http
(although it currently supports fewer SSL config options).
I'll take a little time to see how the other HTTP libs behave when just a :ca_file
is specified, eg to see if they still trust the CA root. (The curl
command, when built with openssl support, disables the system root when I specify a CA file, but I haven't tested the other Ruby HTTP libs yet aside from net/http).
I'll take a little time to see how the other HTTP libs behave when just a :ca_file is specified, eg to see if they still trust the CA root. (The curl command, when built with openssl support, disables the system root when I specify a CA file, but I haven't tested the other Ruby HTTP libs yet aside from net/http).
Have tested situation on CRuby 2.7 & 3.0 (both with OpenSSL's 1.1.1) with Net::HTTP
.
http = Net::HTTP.new('example.com', 443) # Certificate https://crt.sh/?id=3704614715
http.use_ssl = true
http.start
-
Passing concatenation of CA and Root (https://crt.sh/?id=853428 + https://crt.sh/?id=3427370830) certs works
http.ca_file = '/tmp/example_com_chained.pem'
-
Passing just a root CA (https://crt.sh/?id=853428) cert works
http.ca_file = '/tmp/example_com_root.pem'
-
Passing just a single CA (https://crt.sh/?id=3427370830) cert doesn't work (
SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get issuer certificate) (OpenSSL::SSL::SSLError)
)http.ca_file = '/tmp/example_com_ca.pem'
-
Passing concatenation of CA and Root (https://crt.sh/?id=853428 + https://crt.sh/?id=3427370830) certs doesn't work for a page served with different CA/Root combination (e.g., "google.com") - fails with
SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get issuer certificate) (OpenSSL::SSL::SSLError)
http = Net::HTTP.new('google.com', 443) http.ca_file = '/tmp/example_com_chained.pem'
Thanks for jumping in on this @aleksandrs-ledovskis, would you please clarify out of the 4 points above which ones are working as expected and which ones are not?
would you please clarify out of the 4 points above which ones are working as expected and which ones are not?
The examples are from Net::HTTP
standard library and as such these are all working as expected. I have just provided an example/reference regading "how others do it".