I've come across this issue many times, and each time I have to search through layers of stackoverflow.com answers to find the one that is applicable to me, so now I am recording how I resolve this error in a gist so that maybe I will remember to just look at the gist next time.
Set environment variable
REQUESTS_CA_BUNDLE=/usr/local/etc/ca-certificates/cert.pem
when executing any program that uses the requests package.
The environment in which this occurs has these features:
- MacOS (Monterey currently, but it's happened prior to Monterey)
- Python 3.9 installed with Homebrew
- in a virtual environment, these are installed:
- requests 2.27.1
- certifi 2021.10.8
I try to send an HTTP request to a domain such as drive.google.com
that I am
darn sure has a valid TLS certificate, and this exception is thrown:
>>> requests.get("https://drive.google.com/")
HTTPSConnectionPool(host='drive.google.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)')))
Note that using Python's built-in HTTP facility, this does not happen:
>>> with urllib.request.urlopen('https://drive.google.com/') as response:
... print(response.status, response.reason)
200 OK
Some clues can be found by examining certificate paths as follows.
The certificates urllib trusts when it interacts with a server are:
>>> import ssl
>>> ssl.get_default_verify_paths()
DefaultVerifyPaths(cafile='/usr/local/etc/[email protected]/cert.pem', capath='/usr/local/etc/[email protected]/certs', openssl_cafile_env='SSL_CERT_FILE', openssl_cafile='/usr/local/etc/[email protected]/cert.pem', openssl_capath_env='SSL_CERT_DIR', openssl_capath='/usr/local/etc/[email protected]/certs')
(Note that with the bundled-with-the-OS system Python interpreter
/usr/bin/python3
, urllib uses /etc/ssl/cert.pem
.)
The certificates requests trusts when it interacts with a server are:
>>> import certifi
>>> certifi.where()
'$PATH_TO_VIRTUAL_ENVIRONMENT_DIR/lib/python3.9/site-packages/certifi/cacert.pem'
What we conclude from this is that for whatever reason the certificate trust store that is bundled with certifi is inadequate.
The requests library will read the value of environment variable
REQUESTS_CA_BUNDLE
and interpret it as the pathname of a certificate trust
store file.
So we can set this variable to a known-good trust store, e.g.
$ REQUESTS_CA_BUNDLE=/usr/local/etc/ca-certificates/cert.pem ./fetch.py
and execute without error.
I hestitate to define the variable in .bash_profile
because I'm not sure how
frequently /usr/local/etc/ca-certificates
will be updated, and I guess I'd
rather trust the OS's default decisions about the best certificate trust store.
This could be the wrong decision.