Posted over 2 years ago. Visible to the public. Repeats.

RestClient / Net::HTTP: How to communicate with self-signed or misconfigured HTTPS endpoints

Occasionally, you have to talk to APIs via HTTPS that use a custom certificate or a misconfigured certificate chain (like missing an intermediate certificate).

Using RestClient will then raise RestClient::SSLCertificateNotVerified errors, or when using plain Net::HTTP:

Copy
OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Here is how to fix that in your application.

Important: Do not disable certificate checks for production. The interwebs are full of people saying that you should just set verify_ssl: false or similar. This is only okay-ish for testing something in development.

Note that there are services like badssl.com to test against weird SSL behavior.

Self-signed certificates

Talking to a host using a self-signed certificate will fail because the certificate can not be verified.

Copy
>> RestClient.get('https://self-signed.badssl.com/') RestClient::SSLCertificateNotVerified: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Self-signed certificates are basically fine, they were simply not signed by a "popular" certificate authority. While this is not a good solution for websites (because browsers complain), it may be okay for an API. As long as you know which certificate to use, you can communicate safely. (See important caveat below!)

You need to have the correct certificate (put it into your application) and tell RestClient to use it:

Copy
>> RestClient::Resource.new('https://self-signed.badssl.com/', ssl_ca_file: 'lib/certs/self-signed.crt').get => <RestClient::Response 200 "hello">

Note that you need to use RestClient::Resource or RestClient::Request for that.

It's a bit more difficult for Net::HTTP, but works similarly:

Copy
http = Net::HTTP.new('self-signed.badssl.com', 443) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.ca_file = 'lib/certs/self-signed.crt' http.start.get('/') => #<Net::HTTPOK 200 OK readbody=true>

Caveat: If the remote application installs a new certificate, your application will fail again. You need to update it with the new certificate which is why this is painful if the remote application is managed by a third party.

Note: You could also set an environment variable SSL_CERT_FILE. Depending on your setup, this may be alright, but a configured certificate store is more clearly.

Misconfigured certificate chains

Some system administrators do not send intermediate certificates for their CA while they should. Often times, you won't see a problem with Browsers like Chrome, simply because they include some well-known intermediate certificates (like those of Comodo).

Copy
>> RestClient.get('https://incomplete-chain.badssl.com/') RestClient::SSLCertificateNotVerified: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Best case scenario: the remote system's administrator understands and fixes this issue.
If the remote system's certificate chain can not be fixed, you may place intermediate or root certificates into your application and use them. This will also allow the remote application's certificate to renewed (as long as they don't switch to another CA).

You can not pass an intermediate certificate to RestClient or Net::HTTP like above. Instead, you need to use your own certificate store – which is simpler than it sounds.

First, prepare your certificate store. Note that it is essential to call set_default_paths to include your system's default certificate store which contains root CAs. You may omit this but must then configure all required root/intermediate certificates yourself.

Copy
store = OpenSSL::X509::Store.new store.set_default_paths store.add_file('lib/certs/ca-intermediate.crt')

You can then tell RestClient to use that store:

Copy
>> RestClient::Resource.new('https://incomplete-chain.badssl.com/', ssl_cert_store: store).get => <RestClient::Response 200 "hello">

Or Net::HTTP:

Copy
http = Net::HTTP.new('incomplete-chain.badssl.com', 443) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.cert_store = store http.start.get('/') => #<Net::HTTPOK 200 OK readbody=true>

Note that the SSL_CERT_FILE env variable would work in this case, too. Still, a certificate store is clearer and more versatile.

Protip: You can test certificate chains with services like whatsmychaincert.com.


Bonus info: when using curl on the command line, either case can be resolved by using the --cacert switch to specify the self-signed or intermediate certificate file.

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Avatar
Arne Hartherz
Last edit:
over 2 years ago
by Arne Hartherz
Keywords:
exception, TLS, curl, cacert
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Arne Hartherz to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more