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

Posted . Visible to the public.

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:

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.

>> 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:

>> 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:

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).

>> 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.

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:

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

Or Net::HTTP:

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 Show archive.org snapshot .


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.

Arne Hartherz
Last edit
Arne Hartherz
Keywords
exception, TLS, curl, cacert
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2017-03-08 16:06)