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