lostisland/faraday-net_http

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
  1. 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' 
  2. Passing just a root CA (https://crt.sh/?id=853428) cert works

    http.ca_file = '/tmp/example_com_root.pem' 
  3. 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' 
  4. 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".