Python 3.13 enables the VERIFY_X509_STRICT flag by default in
ssl.create_default_context(). This enforces RFC 5280, which requires CA
certificates to include the Key Usage X.509v3 extension with at least
keyCertSign and cRLSign. Any CA certificate that omits this extension is
now rejected at the TLS handshake with:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: CA cert does not include key usage extension (_ssl.c:1000)
This affects corporate environments where an SSL-inspection proxy (e.g., Netskope, Zscaler, BlueCoat) injects its own root CA into the trust chain. If that root CA was issued without the Key Usage extension, all Python 3.13+ HTTPS connections through the proxy will fail.
Python 3.12 and below, curl, Node.js, Go, and browsers are not affected — they do not enforce this particular constraint.
- Any machine running Python >= 3.13 whose HTTPS traffic passes through an SSL-inspection proxy with a non-compliant root CA.
- All Python HTTP libraries are affected (
urllib,requests,httpx,aiohttp, …) because they all usessl.create_default_context()under the hood. - Setting
SSL_CERT_FILEorSSL_CERT_DIRdoes not help — the certificate is found and loaded, but then rejected during verification.
ssl.create_default_context() in Python 3.13+ sets verify_flags to include
ssl.VERIFY_X509_STRICT (value 0x20). This flag tells OpenSSL to enforce
the full set of RFC 5280 rules, including the requirement that CA certificates
carry the Key Usage extension.
You can verify this yourself:
>>> import ssl
>>> ctx = ssl.create_default_context()
>>> bool(ctx.verify_flags & ssl.VERIFY_X509_STRICT)
True # <-- Python 3.13+On Python 3.12 the same check returns False.
The script below creates two test CAs — one with and one without the Key Usage extension — signs a localhost server certificate with each, and proves that Python 3.13 rejects the non-compliant one.
Save this as test_keyusage.py and run it with python3 test_keyusage.py:
#!/usr/bin/env python3
"""
Reproducer for Python 3.13+ rejecting CA certs without Key Usage extension.
Creates two self-signed CAs:
- bad_ca: has basicConstraints but NO keyUsage (mimics broken proxy CA)
- good_ca: has basicConstraints AND keyUsage (compliant CA)
Signs a localhost server cert with each, then attempts a TLS handshake using
ssl.create_default_context() — the same code path used by urllib, requests, etc.
Expected output on Python 3.13+:
CA WITHOUT keyUsage : REJECTED - CA cert does not include key usage extension
CA WITH keyUsage : OK
Expected output on Python 3.12 and below:
CA WITHOUT keyUsage : OK
CA WITH keyUsage : OK
"""
import os
import socket
import ssl
import subprocess
import sys
import tempfile
import threading
def openssl(*args, stdin=None):
r = subprocess.run(
["openssl", *args],
input=stdin,
capture_output=True,
)
if r.returncode != 0:
raise RuntimeError(r.stderr.decode())
def make_ca(dir, name, include_key_usage):
key = os.path.join(dir, f"{name}.key")
cert = os.path.join(dir, f"{name}.pem")
exts = ["-addext", "basicConstraints=critical,CA:TRUE"]
if include_key_usage:
exts += ["-addext", "keyUsage=critical,keyCertSign,cRLSign"]
openssl(
"req", "-x509", "-newkey", "rsa:2048",
"-keyout", key, "-out", cert,
"-days", "1", "-nodes",
"-subj", f"/CN=Test {name}",
*exts,
)
return key, cert
def make_server_cert(dir, ca_key, ca_cert, name):
key = os.path.join(dir, f"{name}.key")
csr = os.path.join(dir, f"{name}.csr")
cert = os.path.join(dir, f"{name}.pem")
openssl(
"req", "-newkey", "rsa:2048",
"-keyout", key, "-out", csr, "-nodes",
"-subj", "/CN=localhost",
)
openssl(
"x509", "-req", "-in", csr,
"-CA", ca_cert, "-CAkey", ca_key,
"-CAcreateserial", "-out", cert, "-days", "1",
"-extfile", "/dev/stdin",
stdin=b"subjectAltName=DNS:localhost",
)
return key, cert
def test_handshake(ca_cert, server_cert, server_key):
"""Return (success: bool, detail: str)."""
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_ctx.load_cert_chain(server_cert, server_key)
client_ctx = ssl.create_default_context(cafile=ca_cert)
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
port = srv.getsockname()[1]
srv.listen(1)
def accept():
try:
conn, _ = srv.accept()
server_ctx.wrap_socket(conn, server_side=True).close()
except Exception:
pass
finally:
srv.close()
t = threading.Thread(target=accept)
t.start()
try:
with socket.create_connection(("127.0.0.1", port)) as sock:
with client_ctx.wrap_socket(sock, server_hostname="localhost"):
pass
return True, "OK"
except ssl.SSLCertVerificationError as exc:
return False, exc.verify_message
except Exception as exc:
return False, f"{type(exc).__name__}: {exc}"
finally:
t.join(timeout=5)
def main():
print(f"Python {sys.version}")
print(f"OpenSSL {ssl.OPENSSL_VERSION}")
strict = bool(
ssl.create_default_context().verify_flags & ssl.VERIFY_X509_STRICT
)
print(f"VERIFY_X509_STRICT enabled by default: {strict}")
print()
with tempfile.TemporaryDirectory() as d:
bad_ca_key, bad_ca_cert = make_ca(d, "bad_ca", include_key_usage=False)
good_ca_key, good_ca_cert = make_ca(d, "good_ca", include_key_usage=True)
bad_srv_key, bad_srv_cert = make_server_cert(
d, bad_ca_key, bad_ca_cert, "bad_srv",
)
good_srv_key, good_srv_cert = make_server_cert(
d, good_ca_key, good_ca_cert, "good_srv",
)
ok, detail = test_handshake(bad_ca_cert, bad_srv_cert, bad_srv_key)
status = "OK" if ok else f"REJECTED - {detail}"
print(f" CA WITHOUT keyUsage : {status}")
ok, detail = test_handshake(good_ca_cert, good_srv_cert, good_srv_key)
status = "OK" if ok else f"REJECTED - {detail}"
print(f" CA WITH keyUsage : {status}")
print()
if strict:
print("RESULT: This Python enforces strict X.509 — a CA cert without")
print(" keyUsage WILL be rejected. Apply the workaround below.")
else:
print("RESULT: This Python does NOT enforce strict X.509 — you are")
print(" not affected.")
if __name__ == "__main__":
main()Python 3.13.7 (main, ...) [GCC ...]
OpenSSL OpenSSL 3.5.3 16 Sep 2025
VERIFY_X509_STRICT enabled by default: True
CA WITHOUT keyUsage : REJECTED - CA cert does not include key usage extension
CA WITH keyUsage : OK
RESULT: This Python enforces strict X.509 — a CA cert without
keyUsage WILL be rejected. Apply the workaround below.
The fix is a single file that disables the strict flag process-wide. It is picked up automatically by every Python invocation — no code changes needed in any application, library, or virtualenv.
python3 -c "import site; print(site.ENABLE_USER_SITE and site.getusersitepackages())"If that prints False (user site disabled, e.g. inside a virtualenv), use the
system/venv site-packages instead:
python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))"Place the following file in the site-packages directory from Step 1. Use
usercustomize.py for a per-user fix, or sitecustomize.py for a system-wide
/ virtualenv-wide fix.
"""
Workaround for Python 3.13+ rejecting CA certificates that lack the
Key Usage X.509v3 extension (RFC 5280 strict enforcement).
This affects environments behind SSL-inspection proxies (Netskope, Zscaler,
etc.) whose root CA was issued without keyUsage=keyCertSign,cRLSign.
How it works: monkey-patches ssl.create_default_context() to clear the
VERIFY_X509_STRICT flag, restoring Python 3.12 behavior.
Remove this file once your proxy's root CA has been re-issued with the
correct extensions.
"""
import ssl as _ssl
if hasattr(_ssl, "VERIFY_X509_STRICT"):
_orig_create_default_context = _ssl.create_default_context
def _create_default_context(*args, **kwargs):
ctx = _orig_create_default_context(*args, **kwargs)
ctx.verify_flags &= ~_ssl.VERIFY_X509_STRICT
return ctx
_ssl.create_default_context = _create_default_contextpython3 -c "
import ssl
ctx = ssl.create_default_context()
strict = bool(ctx.verify_flags & ssl.VERIFY_X509_STRICT)
print('VERIFY_X509_STRICT:', strict)
assert not strict, 'Workaround not active'
print('Workaround is active.')
"# Determine site-packages and write the workaround in one shot:
python3 -c "
import site, sysconfig, pathlib
d = site.getusersitepackages() if site.ENABLE_USER_SITE else sysconfig.get_path('purelib')
p = pathlib.Path(d) / 'usercustomize.py'
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text('''import ssl as _ssl
if hasattr(_ssl, \"VERIFY_X509_STRICT\"):
_orig = _ssl.create_default_context
def _patched(*a, **kw):
ctx = _orig(*a, **kw)
ctx.verify_flags &= ~_ssl.VERIFY_X509_STRICT
return ctx
_ssl.create_default_context = _patched
''')
print(f'Written to {p}')
"This workaround does not disable certificate verification. The TLS handshake still validates the full certificate chain, hostname, and expiry. The only change is that CA certificates are no longer required to carry the Key Usage extension — the same behavior Python 3.12 and below had.
The proper fix is for the proxy vendor (Netskope) to re-issue their root CA certificate with the required extension:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
Once that is done, the usercustomize.py workaround should be removed.