How to Validate Ssl Certificate Chain in Ruby with Net/Http

Verify self-signed TLS certificate for HTTPS in Ruby

This worked for me.

require 'net/http'
require 'net/https'
require 'openssl'
require 'uri'
require 'json'

ContentURI = URI.parse("example.com")
@cert_raw = File.read('cert.pem')
TestDataPath = '.'

req = Net::HTTP::Get.new(ContentURI.path)
https = Net::HTTP.new(ContentURI.host, ContentURI.port)
https.use_ssl = true
https.cert = OpenSSL::X509::Certificate.new(@cert_raw)
https.key = OpenSSL::PKey::RSA.new(@cert_raw)
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
resp = https.start { |cx| cx.request(req) }
p resp
p resp.body

Ruby Net::HTTP responds with OpenSSL::SSL::SSLError certificate verify failed after certificate renewal

I would try to double-check the trusted certificate store if it contains the COMODO_RSA_Certification_Authority.pem certificate. In my (Linux) setup, the site works OK but when I temporarily remove the certificate of the COMODO cert authority from the cert store, I get exactly the same error as you (while in browsers it still works as they have their own cert stores).

BTW, the same error is also recognizable using curl as it also appears to use the same trusted cert store as ruby, so you might first ensure that the site works under curl.

In linux, the cert store is located usually in /etc/ssl/certs whereas under OSX it should probably be /System/Library/OpenSSL (see this article for other options).

You should see something like the following in the cert store directory:

root@apsara:/etc/ssl/certs$ ls -l | grep COMODO_RSA_Certification_Authority.pem
lrwxrwxrwx 1 root root 73 úno 28 10:24 COMODO_RSA_Certification_Authority.pem -> /usr/share/ca-certificates/mozilla/COMODO_RSA_Certification_Authority.crt
lrwxrwxrwx 1 root root 38 úno 28 10:24 d4c339cb.0 -> COMODO_RSA_Certification_Authority.pem
lrwxrwxrwx 1 root root 38 úno 28 10:24 d6325660.0 -> COMODO_RSA_Certification_Authority.pem

The following is a snipped of some attributes of this root CA certificate:

$ openssl x509 -in COMODO_RSA_Certification_Authority.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
4c:aa:f9:ca:db:63:6f:e0:1f:f7:4e:d8:5b:03:86:9d
Signature Algorithm: sha384WithRSAEncryption
Issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Certification Authority
Validity
Not Before: Jan 19 00:00:00 2010 GMT
Not After : Jan 18 23:59:59 2038 GMT
Subject: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Certification Authority
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:91:e8:54:92:d2:0a:56:b1:ac:0d:24:dd:c5:cf:
...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha384WithRSAEncryption
...

The certificate can be downloaded from Comodo here (index of all certs is here).

More info: while looking into it, it turns out that there are actually two distinct certification chains for certs by the Comodo CA. One, the older one, is the one with the root CA listed above. The newer validation chain uses "External CA root" certificates in the chain. This forum post explains further, with specific instructions for OSX for marking those certs as trusted.

Ruby Net HTTP Post: Certificate Verify Failed

openssl s_client -connect app.pilotpro.com:443 -CApath /etc/ssl/certs/ gives:

Certificate chain
0 s:/OU=Domain Control Validated/OU=PositiveSSL/CN=app.pilotpro.com
i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=PositiveSSL CA 2

That is the complete chain, which means, that your client expects to have PositiveSSL CA 2 as a trusted Root-CA in the CA store. But, this is not the case because this CA is just an intermediate CA so your HTTPS server should send all the intermediate CAs on the path to the trusted Root-CA.

So this is not a client-side, but a server-side problem. See also http://www.positivessl.com/ssl-certificate-support/cert_installation/apache-ssl.php

The reason you get no errors if you access it from browsers might be, that browsers cache intermediate CAs. So if the browser once connected to a host which provided the correct certificate chain it can from now on deal with hosts which omit the needed chain. With firefox you might try access with a fresh browser profile, which does not have any certificates cached.

Why is Ruby unable to verify an SSL certificate?

If you are only interested in how to make Ruby behave the same way as OpenSSL s_client or your browser does, you may skip to the very last section, I'll cover the fine print in what is following.

By default, the OpenSSL::X509::Store used for making the connection doesn't use any trusted certificates at all. Based on your knowledge of the application domain, you would typically feed an instance of X509::Store with the trusted certificate(s) that are relevant for your application. There are several options for this:

  • Store#add_file takes a path to a PEM/DER-encoded certificate
  • Store#add_cert takes an instance of X509::Certificate
  • Store#add_path takes a path to a directory where trusted certificates can be found

The "Browser" Approach

This is in contrast to the approach browsers, Java (cacerts), or Windows with its own internal store of trusted certificates, take. There the software is pre-equipped with a set of trusted certificates that is considered to be "good" in the opinion of the software vendor. Generally this is not a bad idea, but if you actually look into these sets, then you will soon notice that there are just too many certificates. An individual can't really tell whether all of these certificates should be trusted blindly or not.

The Ruby Approach

The requirements of your typical Ruby application on the other hand are a lot different than that of a browser. A browser must be be able to let you navigate to any "legitimate" web site that comes with a TLS certificate and is served over https. But in a typical Ruby application you will only have to deal with a few services that use TLS or would otherwise require certificate validation.

And there is the benefit of the Ruby approach - although it requires more manual work, you will end up with a hand-tailored solution that exactly trusts the certificates it should trust in your given application context. This is tedious, but security is much higher this way because you expose a lot less attack surface. Take recent events: if you never had to include DigiNotar or any other compromised root in your trust set, then there's no way such breaches can affect you.

The downside of this, however, as you have already noticed, is that by default, if you don't actively add trusted certificates, the OpenSSL extension will not be able to validate any peer certificate at all. In order to make things work, you have to set up the configuration manually.

This inconvenience has led to a lot of dubious measures to circumvent it, the worst of all being to globally set OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE. Please don't do this. We have even made jokes about adding code that lets your application crash randomly if we encounter that hack :)

If manual trust setup seems too complicated, I'll offer an easy alternative now that makes the OpenSSL extension behave exactly the same as OpenSSL CLI commands like s_client.

Why s_client can verify the certificate

OpenSSL uses a similar approach to browsers and Windows. A typical installation will put a bundle of trusted certificates somewhere on your hard disk (something like /etc/ssl/certs/ca-bundle.crt) and this will serve as the default set of trusted certificates. That's where s_client looks when it needs to verify peer certificates and that's why your experiment succeeded.

Making Ruby act like s_client

If you'd still like to have the same comfort when validating certificates with Ruby, you can tell it to use the OpenSSL bundle of trusted certificates if available on your system by calling OpenSSL::X509::Store#set_default_paths. Additional information can be found here. To use this with XMLRPC::Client, simply ensure that set_default_paths gets called on the X509::Store it uses.

Heroku Rails Net::HTTP: OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Heroku can't verify your server's certificate is validly signed by a CA root it recognizes. This can be because either:

  1. Your cert isn't signed by a CA or intermediate (ie, self-signed)
  2. Your cert is signed by a CA that Heroku doesn't know about (unlikely)
  3. The API server isn't providing the correct intermediate certs to help Heroku connect it to a valid CA root. (likely)

Try openssl s_client -showcerts -connect your-api-host.com:443 from your shell. You should see something like:

depth=3 C = SE, O = AddTrust AB, OU = AddTrust External TTP Network, CN = AddTrust External CA Root
verify return:1
depth=2 C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Certification Authority
verify return:1
depth=1 C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Domain Validation Secure Server CA
verify return:1
depth=0 OU = Domain Control Validated, OU = PositiveSSL, CN = www.coffeepowered.net
verify return:1

You're specifically looking to make sure that all certs in the chain return verify return: 1. If this works from your shell, then your machine likely has root certs installed that your Heroku instance doesn't.

Without knowing exactly what certs your API server is returning, it's hard to answer this definitively, but you probably need to be serving an intermediate cert bundle along with the SSL cert itself. This intermediate cert bundle will be provided by your SSL certificate signer, and can be provided in Apache via SSLCertificateChainFile, or in nginx by concatening the intermediates with your cert (per this documentation).

If you can't alter the configuration of the API server, then your "Manually overriding the certificate file location" solution is probably very close to correct (it's the same thing as the server providing the intermediate cert, except the client does it), but you are likely not providing the correct certificate chain bundle for your API server's certificates. Make sure that you have the correct intermediate certificate chain provided to OpenSSL, and it should work as desired.

How to properly use ca_file for self-signed certificates with Net::HTTP in ruby?

Somehow the certificates got messed. Not sure if I executed the wrong command, copied from the wrong shell or maybe the certificate of self-signed.badssl.com just changed. The certificate that is included in the original repl is actually not self-signed but is signed by an untrusted intermediate CA that was not included in the chain (i.e. I did not add it to the ca_file).

I have verified that by running the command

openssl x509 -text -in /path/to/badssl.crt

and observing the oputput

...
Serial Number:
cd:bc:5a:4a:ec:97:67:b1
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Intermediate Certificate Authority
Validity
Not Before: Aug 8 21:17:05 2016 GMT
Not After : Aug 8 21:17:05 2018 GMT
Subject: C = US, ST = California, L = San Francisco, O = BadSSL Fallback. Unknown subdomain or no SNI., CN = badssl-fallback-unknown-subdomain-or-no-sni
...

Today I grabbed the certificate again

openssl s_client -showcerts -verify 5 -connect self-signed.badssl.com:443 < /dev/null

After updating the certificate in my (forked) repl, the snippet passes:
https://repl.it/repls/UnselfishAvariciousDeletions



Related Topics



Leave a reply



Submit