6

I'm working on a box that's running CentOS (Linux), and I'm running into the following error when try to access a particular subdomain for work:

Traceback (most recent call last):
  ... # My code, relevant call is requests.get(url)
  File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 60, in get
    return request('get', url, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 49, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 457, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 569, in send
    r = adapter.send(request, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/adapters.py", line 420, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [Errno 1] _ssl.c:504: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

According to https://www.digicert.com/help/, the subdomain "is not sending the required intermediate certificate" (and that's the only problem DigiCert found). However, my code handles this without problem when I run it from my Mac laptop, and so do both Chrome and Safari. I'm running Python 2.7.5 on both my laptop and the linux box. I was running requests 1.2.0 on the linux box and 2.2.1 on my laptop, but I upgraded both to 2.4.3 and they still don't have the same behavior.

Also possibly relevant - the same certificate is being used with some other subdomains where the intermediate certificate is being sent, and neither my laptop nor the linux box has any problems with those, so it shouldn't be that my laptop has a root CA that the linux box doesn't have.

Does anyone know why it isn't working from my linux box and how I can fix it?

Rob Watts
  • 6,866
  • 3
  • 39
  • 58
  • Oh, and I don't have control over the subdomain, so fixing the certificate issue might not be feasible. – Rob Watts Nov 21 '14 at 18:44

3 Answers3

10

I spent a day to understand and fix this issue completely, so I thought it will be nice to share my findings with everybody :-)! Here are my results:

It is a common flaw in SSL server configurations to provide an incomplete chain of certificates, often omitting intermediate certificates. For instance, a site I was working with did not include the common DigiCert "intermediate" certificate "DigiCert TLS RSA SHA256 2020 CA1" in the server's response.

Because this configuration flaw is common, most but not all modern browsers implement a technique called "AIA Fetching" to fix this on the fly (see e.g. https://www.thesslstore.com/blog/aia-fetching/).

Python's SSL support does not support AIA Fetching and depends on a complete chain of certificates from the server; otherwise it throws an exception, like so

SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1124)')))

There is an ongoing discussion about whether AIA Fetching should be added to Python, e.g. in this thread: https://bugs.python.org/issue18617#msg293894.

My impression is that this will remain an open issue for the foreseeable future.

Now, how can we fix that?

  1. Install certifi, if you have not done so, or update it

pip install certifi

or

pip install certifi --upgrade

Many (but not all) Python modules can use the certificates from certifi, and certifi takes them from the Mozilla CA Certificate initiative (https://wiki.mozilla.org/CA). Basically, certifi creates a clean *.pem file from the Mozilla site and provides a lightweight Python interface for accessing that file.

  1. Download the missing certificate as a file in PEM syntax, e.g. from https://www.digicert.com/kb/digicert-root-certificates.htm, or from a trusted browser.

  2. Locate the certifi *.PEM certificate file with

     import certifi
     print(certifi.where())
    

Note: I recommend to first activate the virtual environment (e.g. conda activate <envname>) you want to use the certificate with. The file path will differ. If you apply this to your base environment, any potential flawed certificate will put the entire SSL mechanism for all your code at risk.

Example: /Users/username/anaconda3/envs/environment_name/lib/python3.8/site-packages/certifi/cacert.pem

Take a simple text editor, open that file, and insert the missing certificate at the beginning right after the header, like so

##
## Bundle of CA Root Certificates
##
...
-----BEGIN CERTIFICATE-----
+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB
Afr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6J
---> This is the additional certificate.
+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB
Afr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6J
-----END CERTIFICATE-----

It is important to include the begin and end markers.

Save the file and you should be all set!.

You can test that it works with the following few lines:

# Python 3
import urllib.request import certifi import requests

 
URL = 'https://www.the_url_that_caused_the_trouble.org' 
print('Trying urllib.request.urlopen().') 
r = urllib.request.urlopen(URL)
print(f'urllib.request.urlopen\n================\n {r.read()[:80]}')
print('Trying requests.get().') 
r = requests.get(URL)
print(f'requests.get()\n================\n {r.text[:80]}')

Note: The general SSL certificates, e.g. for openssl, might be located elsewhere, so you may have to try the same approach there:

/Users/username/anaconda3/envs/environment_name/ssl

Voila!

Notes:

  1. When you update certifi or create a new virtual environment, the changes will likely be lost, but I think that is actually good design, because it does not perpetuate a temporary security tweak to your entire system.
  2. Naturally, the process of downloading the certificate is a potential security risk - if that download is compromised, your entire SSL chain might be, too.
  3. The maintenance of certifi lags behind the Mozilla releases of certificates. If you want to use the most current version of the Mozilla CA bundles with certifi, you can use my script from https://github.com/mfhepp/update_certifi_certificates.
Martin Hepp
  • 1,380
  • 12
  • 20
2

I still don't understand why it's working one place but not another, but I did find a somewhat acceptable workaround that's much better than turning off certificate verification.

According to the requests library documentation, it will use certifi if it is installed on the system. So I installed certifi

sudo pip install certifi

and then modified the .pem file it uses. You can find the file location using certifi.where():

>>> import certifi
>>> certifi.where()
'/usr/local/lib/python2.7/site-packages/certifi/cacert.pem'

I added the intermediate key to that .pem file, and it works now. FYI, the .pem file expects certificates to show up like

-----BEGIN CERTIFICATE-----
<certificate here>
-----END CERTIFICATE-----

WARNING: This is not really a solution, only a workaround. Telling your system to trust a certificate can be dangerous from a security point of view. If you don't understand certificates then don't use this workaround unless your other option is to turn off certificate verification entirely.

Also, from the requests documentation:

For the sake of security we recommend upgrading certifi frequently!

I assume that when you upgrade certifi you'll have to redo any changes you made to the file. I haven't looked at it enough to see how to make a change that won't be overwritten when certifi gets updated.

Rob Watts
  • 6,866
  • 3
  • 39
  • 58
0

If you are on *nix and your intermediate or self-signed certificate is installed in SSL (i.e. you can hit the URL successfully from CURL but not from Python), you can set the environment variable REQUESTS_CA_BUNDLE to where your ca-certificates are stored (ex. /etc/ssl/certs/ca-certificates.crt).

Credit here.

Jason
  • 941
  • 1
  • 10
  • 19