Search code examples
pythonsslfedora-25

Python throwing an SSL certificate validation error w/o consulting system CA bundles?


With two largely identical systems (both running Fedora 25, both with similar package versions installed), one system is failing with an SSL certicate verification error while another is not. That is, if I run:

import requests
r = requests.get('https://insidehost.corp.example.com')

One one system it works, while on the other it fails:

requests.exceptions.SSLError: ("bad handshake: Error([('SSL routines', 'ssl3_get_server_certificate', 'certificate verify failed')],)",)

At first I figure that I was simply missing the necessary CA certificates, but running python under strace reveals that on the failing system, python is never attempting to open a ca bundle. That is, on the system that works:

strace -e trace=open,stat python testscript.py |& grep /etc/pki

Yields only:

open("/etc/pki/tls/legacy-settings", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/etc/pki/tls/certs/ca-bundle.crt", {st_mode=S_IFREG|0444, st_size=257079, ...}) = 0
open("/etc/pki/tls/certs/ca-bundle.crt", O_RDONLY) = 4

But on the failing system yields:

open("/etc/pki/tls/legacy-settings", O_RDONLY) = -1 ENOENT (No such file or directory)

Furthermore, running the same test script with python3 on the failing system...works!

In both cases, python is /usr/bin/python from python-2.7.13-1.fc25.x86_64. Neither system is setting any *_CA_BUNDLE environment variable.


Solution

  • After some additional investigation I've figured it out, and I thought I would post the solution here because it's not necessarily obvious.

    The requests module includes its own certificate bundle, and it will fall back on that if it can't find another one to use. The way it looks for a certificate bundle is like this, in requests/certs.py:

    try:
        from certifi import where
    except ImportError:
        def where():
            """Return the preferred certificate bundle."""
            # vendored bundle inside Requests
            return os.path.join(os.path.dirname(__file__), 'cacert.pem')
    

    You can see the result of this by running:

    $ python -m requests.certs
    /etc/pki/tls/certs/ca-bundle.crt
    

    As you can see from the above code, requests uses the certifi module to locate an appropriate bundle. On the failing system, the certifi module had been installed via pip rather than using the system package, which meant it was lacking the appropriate configuration.

    The solution was yum install python2-certifi.