Python 3.13.x SSL security changes

My organization currently uses Zscaler for security and VPN. On top of that I have no admin rights to my Windows machine.

I have tried a whole host of solutions which led me to pip-system-certs is this StackOverflow question that is quite similar to my situation here https://stackoverflow.com/questions/61635505/installing-zscaler-certificate-to-anaconda3

For Python 3.12, I was able to patch the cert by using autowrapt which was kindly provided by pip-system-certs.
Details for this solution and issue documented here Difference in behaviour of Conda and Base Python with Zscaler (#32) · Issues · alelec / pip-system-certs · GitLab

I am writing this now is because I have received an official Zscaler certificate from my organization. However, when I proceed to use verify on requests it fails saying “Basic Constraints of CA cert not marked critical” from my understanding all this means is that a cert may be missing an extension CA:TRUE under X509v3 format.

Upon inspection my cert does have X509v3 extensions

I have shared my observations to Zscaler as well as of writing this ticket because when Zscaler VPN is off everything runs as per normal. So my key questions here is what are the parameters needed to pass the “Basic Constraints of CA cert not marked critical” condition?

Traceback in text

`C:\Users\BYM1132>python
Python 3.13.2 (tags/v3.13.2:4f8bb39, Feb 4 2025, 15:23:48) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> requests.get('https://www.google.com',verify='C:\\_work\\Cert\\ZscalerRootCertificate-2048-SHA256.crt')
Traceback (most recent call last):
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connectionpool.py", line 464, in _make_request
self._validate_conn(~~^^
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connection.py", line 741, in connect
sock_and_verified = _ssl_wrap_socket_and_match_hostname(
sock=sock,
...<14 lines>...
assert_fingerprint=self.assert_fingerprint,
)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connection.py", line 920, in _ssl_wrap_socket_and_match_hostname
ssl_sock = ssl_wrap_socket(
sock=sock,
...<8 lines>...
tls_in_tls=tls_in_tls,
)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\util\ssl_.py", line 480, in ssl_wrap_socket
ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\util\ssl_.py", line 524, in _ssl_wrap_socket_impl
return ssl_context.wrap_socket(sock, server_hostname=server_hostname)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\Python313\Lib\ssl.py", line 455, in wrap_socket
return self.sslsocket_class._create(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
sock=sock,
^^^^^^^^^^
...<5 lines>...
session=session
^^^^^^^^^^^^^^^
)
^
File "C:\Program Files\Python313\Lib\ssl.py", line 1076, in _create
self.do_handshake()
~~~~~~~~~~~~~~~~~^^
File "C:\Program Files\Python313\Lib\ssl.py", line 1372, in do_handshake
self._sslobj.do_handshake()
~~~~~~~~~~~~~~~~~~~~~~~~~^^
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1028)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connectionpool.py", line 787, in urlopen
response = self._make_request(
conn,
...<10 lines>...
**response_kw,
)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connectionpool.py", line 488, in _make_request
raise new_e
urllib3.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1028)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\adapters.py", line 667, in send
resp = conn.urlopen(
method=request.method,
...<9 lines>...
chunked=chunked,
)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\connectionpool.py", line 841, in urlopen
retries = retries.increment(
method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\urllib3\util\retry.py", line 519, in increment
raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='www.google.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1028)')))
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
requests.get('https://www.google.com',verify='C:\\_work\\Cert\\ZscalerRootCertificate-2048-SHA256.crt')
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\api.py", line 73, in get
return request("get", url, params=params, **kwargs)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\api.py", line 59, in request
return session.request(method=method, url=url, **kwargs)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\sessions.py", line 589, in request
resp = self.send(prep, **send_kwargs)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\sessions.py", line 703, in send
r = adapter.send(request, **kwargs)
File "C:\Users\BYM1132\AppData\Roaming\Python\Python313\site-packages\requests\adapters.py", line 698, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='www.google.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1028)')))`

Is your extension marked as CRITICAL? It’s an extra flag on all X.590v3. The CA extension must be marked as critical extension. In the past OpenSSL was pretty lax. More recent OpenSSL are stricter.

Extension  ::=  SEQUENCE  {
     extnID      OBJECT IDENTIFIER,
     critical    BOOLEAN DEFAULT FALSE,
     extnValue   OCTET STRING  }

This are the available fields from my certificate. I removed all the data and posted the metadata only.
I have CA:True under Basic Constraints. I assume that Critical:True should be there too? I will let my vendor know about this.

ricson_chua [ ~ ]$ openssl x509 -text -in ZscalerRootCertificate-2048-SHA256.crt | grep ":" | awk -F':' '{ print $1 }'
Certificate
    Data
        Version
        Serial Number
        Signature Algorithm
        Issuer
            Not Before
            Not After 
        Subject
        Subject Public Key Info
            Public Key Algorithm
                Public-Key
                Modulus
                Exponent
        X509v3 extensions
            X509v3 Subject Key Identifier
            X509v3 Authority Key Identifier
                keyid
                DirName
                serial
            X509v3 Basic Constraints
    Signature Algorithm
    Signature Value

Yes, Basic Constraints must be marked as critical. They are also missing a Key Usage extension (must be critical as well) and they are using both key id and dirname+serial for AKI and SKI. They should only use key id. Vendors notoriously get DirName wrong, too.

Thanks Christian. I will send this information across and update on how the new certificate will look like atter they do necessary changes.

I’m running into the same Python + Zscaler issue as you. For my case this is in a Dockerfile running locally on my MacBook. Did you get anywhere on this?

1 Like

Hi Patrick,

I gave up on Zscaler’s support team. Their deadline to provide me a new cert was today. Instead of providing the cert they asked for logs again… Disappointing to say the least.

I did a work around instead. This is not really a workaround more of just building the cert from scratch. I am using this article as reference Solving the Dreadful Certificate Issues in Python Requests Module | HackerNoon

You will need a machine that can run openssl and google chrome for this.
The idea here is when you go to google chrome and open google website inspect and locate the certificate section. This is on the upper left beside the address bar (I am unable to post a photo due to being new here)

When you view the cert it looks something like this. There is one more for google.com that my screen capture did not get

What you will do is highlight one level and export. You should have a total of 4 files by the end.
Finally, follow the instructions in the link above to combine the files.

That worked for me so far. I am running a windows system, and I have a System Path for my certificate. I assume that you should set an ENV variable for your docker to use this cert and hopefully have a successful connection.

Funny that after I rebuilt the chain. none of the X509v3 extensions are marked critical anyway looks totally the same from the one that is not working, or I just don’t see the differences.

I think I found the issue and understand why the steps you followed solved the issue.

In your first post, you only reference the root certificate for validation. Later, after you followed the guide and exported the certificates via chrome, you reference both the root certificate AND the intermediate certificate. In the intermediate certificate, the attribute X509v3 Basic Constraints is actually set to “critical”, in root it is not.

From RFC 5280: Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile

Conforming CAs MUST include this extension in all CA certificates
that contain public keys used to validate digital signatures on
certificates and MUST mark the extension as critical in such
certificates.

So yes, the solution is to also include the intermediate certificate(s) in your trust chain, atleast for the current ZScaler certificates, as the intermediate cert has the correct attribute. I recommend to only include the first intermediate certificate along with the root cert, as it has a long validity in contrast to the second intermediate cert, which is only valid for ~2 weeks.

Here is how you can validate it yourself:

openssl x509 -text -noout -in zscaler_root.crt
...
 X509v3 Basic Constraints: 
     CA:TRUE
...
openssl x509 -text -noout -in zscaler_first_intermediate.crt
...
X509v3 Basic Constraints: critical
    CA:TRUE
...

And in Python:

>>> requests.get(some_url, verify="only_intermediate_1.crt")
<Response [200]>
>>> requests.get(some_url, verify="only_root.crt")
<ssl basic constraint not critical error>

Here is also the discussion where it was introduced in Python 3.13: `ssl`: changing the default `SSLContext.verify_flags`? - #15 by gpshead

1 Like

Hi Jan,

Thanks for this thorough explanation! This should have been something the Zscaler support should be telling me. Now I understand better why they said nothing changed.

Then as for my company’s IT support team they were also blindsided by Zscaler, because the cert in the downloads section of the admin page probably is only the root cert!

2 Likes

Technically it is correct to only deliver the root certificate, since that is the real “trustworthy” one. Trusting the intermediate cert kind of goes against the principle of a root cert, but I guess as a temporary workaround it is fine.

By the way, I noticed that ZScaler announced a fix for tomorrow - lets see what that brings: https://community.zscaler.com/s/question/0D5PJ00000Yzt5r0AB/python-313-zscaler-certificate-nonconform

Once fixed, it is safer to remove the intermediate cert from the trust store again :slight_smile:

I have validated that the regular cert from the admin page of Zscaler now works. Ask help from your organization’s network admin to get a copy of the cert and export it to your PATH or ENV variable.