Search code examples
pythonubuntusslhttpspython-requests

Why python requests not use the system ssl cert by default?


Background:

I'm working on Ubuntu 18.04.1 LTS, using next to install the self-signed certs:

$ cp -rf my.crt /usr/local/share/ca-certificates/
$ update-ca-certificates

Everything works fine, because now I could use next to visit my web successfully:

$ curl https://example.com

Problem:

However, when I use python3 requests to visit, it reports next error:

>>> requests.get("https://example.com")
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/urllib3/connectionpool.py", line 706, in urlopen
    chunked=chunked,
  File "/usr/local/lib/python3.6/dist-packages/urllib3/connectionpool.py", line 382, in _make_request
    self._validate_conn(conn)
  File "/usr/local/lib/python3.6/dist-packages/urllib3/connectionpool.py", line 1010, in _validate_conn
    conn.connect()
  File "/usr/local/lib/python3.6/dist-packages/urllib3/connection.py", line 421, in connect
    tls_in_tls=tls_in_tls,
  File "/usr/local/lib/python3.6/dist-packages/urllib3/util/ssl_.py", line 429, in ssl_wrap_socket
    sock, context, tls_in_tls, server_hostname=server_hostname
  File "/usr/local/lib/python3.6/dist-packages/urllib3/util/ssl_.py", line 472, in _ssl_wrap_socket_impl
    return ssl_context.wrap_socket(sock, server_hostname=server_hostname)
  File "/usr/lib/python3.6/ssl.py", line 407, in wrap_socket
    _context=self, _session=session)
  File "/usr/lib/python3.6/ssl.py", line 817, in __init__
    self.do_handshake()
  File "/usr/lib/python3.6/ssl.py", line 1077, in do_handshake
    self._sslobj.do_handshake()
  File "/usr/lib/python3.6/ssl.py", line 689, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:847)

Additional:

But, if I explicitly specify the crt, it works successfully:

>>> requests.get("https://example.com", verify='/etc/ssl/certs/ca-certificates.crt')
<Response [200]>

Question:

I'm using a third-party code, which means I can't change the python code.

So, my question is: why python requests not default to find verify in /etc/ssl/certs/ca-certificates.crt? It's the default certification on ubuntu I think, as curl successfully find this by default...

And, if possible I could set any variable with export which outside of python code, then python requests will default to find the certification?


Solution

  • I find the root cause, the requests in fact will call where using from certifi import where to find the proper CA on this pc.

    But, there are 2 kinds of ways to install certifi module in ubuntu:

    Option 1:

    apt-get install -y python3-certifi
    

    Option 2:

    pip3 install certifi
    

    NOTE: if directly using pip3 install requests, it will implicitly install certifi using pip3 if no debian package installed for python3-certifi.

    However, looks Canonical (the backers of Ubuntu) made some changes for certifi, so if using python3-certifi from apt, the code of def where is next or similar varies from different versions:

    root@4e1aab76e082:/# cat /usr/lib/python3/dist-packages/certifi/core.py
    def where():
        return "/etc/ssl/certs/ca-certificates.crt"
    

    But if using pip3 to install, it will be:

    root@4e1aab76e082:/# cat /usr/local/lib/python3.6/dist-packages/certifi/core.py
    def where():
        _CACERT_CTX = get_path("certifi", "cacert.pem")
    

    That's /usr/local/lib/python3.6/dist-packages/certifi/cacert.pem in this package.

    So the solution on ubuntu is: use apt-get install python3-certifi to install the ubuntu variant of certifi, uninstall the pip one. Then, we can no change anything of app code.

    UPDATE:

    I find another way which works with official certifi, using next variable, I could also let python requests module go to fetch the CA from where I specified without change app code:

    export REQUESTS_CA_BUNDLE='/etc/ssl/certs/ca-certificates.crt'