in short
I got: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)
what is a much worse error than [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1129)
And I suspected python or my own code, but the same error occurs with pure openSSL:
openssl s_client -connect my-domain.com:443 -CAfile root.pem -verify 2
......
Verify return code: 21 (unable to verify the first certificate)
Extended master secret: no
Max Early Data: 0
---
......
If openSSL does not work, python will not work, too. Fore sure this is for any error at TLS handshake. ;-)
Comparing old and new certificates, the root cause was finally found in two new addtional Authority Key Identifier entries DirName and serial. They where not present before:
openssl x509 -in my-domain.pem -text
The only diff and root cause was Authority Key Identifier with two new entries:
X509v3 Authority Key Identifier:
keyid:1D:E8:38:95:85:65:A2:D9:44:99:96:30:D1:81:D5:5B:F7:38:CC:8C
DirName:/C=DE/O=My Company/OU=My OU/CN=My Sub-CA
serial:02
New and final question: Why openSSL doesn't work with DirName and serial within AKI?
I keep all the initial stuff, because that might help other to build up flexible python code.....
Now, all my approaches below are working fine, as expected.
If CA ist not in truststore I get the expected python error:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1129)
Setting truststore at context, session or requests level, take what you want, it works for me.
situation last 2 years
In past (2 years ago) I imported my private CA (root and sub-ca) into Win 10 local computer truststore
and used this code snippet to create my own urllib3 context to load the Windows store with context.load_default_certs()
That worked until this week, where I had to renew my CA and server certificate. :-(
I deleted old private CA bundle and imported new CA into Windows local computer truststore and firefox truststore as usual.
All browser are working fine with new truststore!
new Problem after update of my private CA
With python requests I now get again these annoying errors:
SSLError in steps code - Message: HTTPSConnectionPool(host='my-domain.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)')))
my approaches in code
My current code tries different ways to solve the issue, but nothing works.
I'm sure (or hope) the fault is at my side, but I don't see it anymore ;-)
import certifi
import json
import os
import requests
class MyHttpAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
# Google: "SSL failure on Windows using python requests" -- usage of Windows Truststore
# https://stackoverflow.com/questions/42981429/ssl-failure-on-windows-using-python-requests
context = create_urllib3_context()
I tried those 3 settings to load different ca bundles (only one code line is active per attempt)
## --------------------
# variant 1: usage of Windows Truststore where I imported my root-ca and sub-ca into local computer storage
# load_default_certs() --> this loads the OS defaults of Windows Truststore!!!!
context.load_default_certs()
# variant 2: my special ca bundle contains only my private CA only with 3 entries: root-ca, sub-ca and server cert
# load_verify_locations() --> this loads just a specific CA bundle !!!!
context.load_verify_locations(cafile="C:/Users/...../my_ca_bundle.pem")
# variant 3: I added my private CA with root-ca, sub-ca and server cert to certifi cacert.pem file as a hack
# certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
# load_verify_locations() --> this loads just a specific CA bundle !!!!
context.load_verify_locations(cafile=certifi.where())
## --------------------
To verify that my private CA is within the selected store, I do an assert on the CommonName of my CA, thus it would fail fast if not found. If it's fine, the context is added to the session.mount later on:
# get and print all CAs from loaded list to verify that my CA is within the list
# notice the server certificate of pem file is not loaded into the list, don't no why, maybe because it's no CA
json_ca_certs = context.get_ca_certs()
print("All current context CA certs: {}".format(json.dumps(json_ca_certs, indent=1, sort_keys=True)))
length=len(json_ca_certs)
print("Number of certificates in bundle: {}".format(length))
# fail fast:
assert "MY CommonName" in str(json_ca_certs), "private CA not found in used bundle"
kwargs['ssl_context'] = context
return super().init_poolmanager(*args, **kwargs)
I do not set those environment variables, they are None
def __init__(self, **kwargs):
print("REQUESTS_CA_BUNDLE: '{}'".format(os.environ.get('REQUESTS_CA_BUNDLE')))
print("CURL_CA_BUNDLE: '{}'".format(os.environ.get('CURL_CA_BUNDLE')))
I create a session and as creation of context with my truststore does not longer work, I try 3 different ways to use session.verify (one code line active per attempt only):
self.session = requests.Session()
# variant 1: default not working anymore:
self.session.verify = True
# variant 2: usage of my special ca bundle does not work
self.session.verify = "C:/Users/...../my_ca_bundle.pem"
# variant 3: certifi cacert.pem file contains my private CA and does not work
# certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
self.session.verify = certifi.where()
Here I finally mount my adapter to the session:
# mount with Schema only as prefix to use them for all my calls (this worked in past and should not be a new problem)
adapter = self.MyHttpAdapter()
self.session.mount("https://", adapter)
Now I do my self.session.get() calls. Once again I tried those settings, since context cafile nor session.verify works, direct at requests within session:
_api_url = 'https://my-domain.com/api/function'
###
# variant 1: does not longer work, no setting above for session.verify nor context.load...():
response = self.session.get(_api_url)
# variant 2: verify = my special ca bundle work around does not work:
response = self.session.get(_api_url, verify="C:/Users/...../my_ca_bundle.pem")
# variant 3: verify = certifi cacert.pem work around does not work:
response = self.session.get(_api_url, verify=certifi.where())
I added to requests/sessions.py some print debug output, to see what is used
sessions request() used final settings:
settings = self.merge_environment_settings(
prep.url, proxies, stream, verify, cert
)
print(f"DEBUG: request settings: {settings}")
sessions merge_environment_settings() right before the return ()
print(f"DEBUG: merge_environment_settings() before return: verify={verify}")
return {'verify': verify, 'proxies': proxies, 'stream': stream,
'cert': cert}
Just to see, that my settings are used:
DEBUG: merge_environment_settings() before return: verify=path_to_ca_bundle.pem
DEBUG: request settings: {'verify': 'path_to_ca_bundle.pem', 'proxies': OrderedDict(), 'stream': False, 'cert': None}
Python and installed modules are up-to-date:
Python 3.9.6 (tags/v3.9.6:db3ff76, Jun 28 2021, 15:26:21)
certifi==2021.10.8
requests==2.27.1
requests-toolbelt==0.9.1