SSL: CERTIFICATE_VERIFY_FAILED in Python (tested)

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed has at least five different causes, and the fix depends on which one. We generated each broken certificate locally, recorded the exact reason Python 3.13.5 gives, and tested which trust setting fixes urllib and which fixes requests.

Setup for every block: a test root CA and leaf certificates made with openssl 3.5.7, served by a small helper (tlsserver.py: http.server wrapped in ssl.SSLContext.wrap_socket, one port per certificate). The part after certificate verify failed: is the reason; it is also on the exception as e.reason.verify_message.

1. Read the reason, not just CERTIFICATE_VERIFY_FAILED

import urllib.request, ssl
from tlsserver import serve

servers = {"self-signed": "selfsigned.pem", "private CA": "leaf.pem",
           "wrong name": "wrongname.pem", "expired": "expired.pem"}
ca_ctx = ssl.create_default_context(cafile="ca.pem")   # trusts our test CA
for port, (label, cert) in enumerate(servers.items(), start=8963):
    serve(cert, port)
    ctx = None if label in ("self-signed", "private CA") else ca_ctx
    try:
        urllib.request.urlopen(f"https://localhost:{port}/", context=ctx, timeout=5)
        print(f"{label}: ok")
    except urllib.error.URLError as e:
        print(f"{label}: {e.reason.verify_message}")
        print(f"    {e.reason}")
self-signed: self-signed certificate
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)
private CA: unable to get local issuer certificate
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1029)
wrong name: CA cert does not include key usage extension
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: CA cert does not include key usage extension (_ssl.c:1029)
expired: CA cert does not include key usage extension
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: CA cert does not include key usage extension (_ssl.c:1029)

Measured: the same [SSL: CERTIFICATE_VERIFY_FAILED] prefix, four different reasons:

2. New in Python 3.13: “CA cert does not include key usage extension”

import urllib.request, ssl
from tlsserver import serve

serve("leaf.pem", 8963)    # leaf signed by a CA made with plain `openssl req -x509`
serve("leaf2.pem", 8964)   # leaf signed by a CA that has keyUsage=keyCertSign
for port, cafile in [(8963, "ca.pem"), (8964, "ca2.pem")]:
    ctx = ssl.create_default_context(cafile=cafile)
    try:
        urllib.request.urlopen(f"https://localhost:{port}/", context=ctx, timeout=5)
        print(cafile, "ok")
    except urllib.error.URLError as e:
        print(cafile, e.reason.verify_message)
    ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT
    urllib.request.urlopen(f"https://localhost:{port}/", context=ctx, timeout=5)
    print(cafile, "ok without VERIFY_X509_STRICT")
ca.pem CA cert does not include key usage extension
ca.pem ok without VERIFY_X509_STRICT
ca2.pem ok
ca2.pem ok without VERIFY_X509_STRICT

Measured: Python 3.13 turned on ssl.VERIFY_X509_STRICT in create_default_context(). A root CA made with plain openssl req -x509 has no keyUsage extension and is now rejected, even though cafile= points straight at it and the same code worked on older Pythons. Clearing the flag makes it pass, but the real fix is to reissue the CA with -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign": the second CA, made that way, passed with strict mode on. Public CAs already comply; this mostly hits home-made, test and internal CAs.

3. Hostname mismatch and expired

import urllib.request, ssl
from tlsserver import serve

serve("wrongname2.pem", 8965)
serve("expired2.pem", 8966)
ctx = ssl.create_default_context(cafile="ca2.pem")
for port in (8965, 8966):
    try:
        urllib.request.urlopen(f"https://localhost:{port}/", context=ctx, timeout=5)
    except urllib.error.URLError as e:
        print(e.reason.verify_message)
        print("   ", e.reason)
Hostname mismatch, certificate is not valid for 'localhost'.
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1029)
certificate has expired
    [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1029)

Measured: with a valid chain, the reason names the real problem. A hostname mismatch means the URL's host is not in the certificate's subjectAltName list; our test certificate had CN=localhost and was still rejected, because the CN is ignored when subjectAltName is present; connect with the name on the certificate, or reissue it with the right names. certificate has expired also appears when the client's clock is wrong, so check the date on the machine running Python before blaming the server.

4. Fix: trust your CA — urllib and requests read different settings

import os, subprocess, sys

client = """
import urllib.request, requests
from tlsserver import serve
serve("leaf2.pem", 8967)
url = "https://localhost:8967/"
for name, get in [("urllib", lambda: urllib.request.urlopen(url, timeout=5)),
                  ("requests", lambda: requests.get(url, timeout=5))]:
    try:
        get(); print(f"  {name}: ok")
    except Exception as e:
        print(f"  {name}: failed")
"""
for env_var in [None, "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"]:
    env = {k: v for k, v in os.environ.items()
           if k not in ("SSL_CERT_FILE", "REQUESTS_CA_BUNDLE")}
    if env_var:
        env[env_var] = "ca2.pem"
    print(env_var or "no env var")
    subprocess.run([sys.executable, "-c", client], env=env)
no env var
  urllib: failed
  requests: failed
SSL_CERT_FILE
  urllib: ok
  requests: failed
REQUESTS_CA_BUNDLE
  urllib: failed
  requests: ok

Measured: SSL_CERT_FILE fixed urllib (and anything using ssl.create_default_context()) but not requests. REQUESTS_CA_BUNDLE fixed requests but not urllib. requests uses its own CA bundle (certifi on a pip install; Debian's packaged requests, which we used, points at /etc/ssl/certs/ca-certificates.crt). In code, pass cafile= to ssl.create_default_context() for urllib, or verify="/path/ca.pem" for requests. The file needs the CA (or the self-signed certificate itself), in PEM format. On a corporate network, export the proxy's root CA and point these at a bundle that includes it.

What not to do

verify=False or ssl._create_unverified_context() make the error go away by switching off the check that tells you whether you're talking to the right server. They are fine for a one-off against localhost; in anything that sends credentials or runs unattended, use one of the fixes above.

On Python 3.13 with a private CA, see also CA cert does not include key usage extension.

Frequently asked questions

What does SSL: CERTIFICATE_VERIFY_FAILED mean in Python?

Python couldn't verify the server's certificate. The text after 'certificate verify failed:' is the reason; we measured self-signed certificate, unable to get local issuer certificate, Hostname mismatch, certificate has expired and, new in 3.13, CA cert does not include key usage extension.

How do I fix unable to get local issuer certificate in Python?

Give Python the CA that signed the server's certificate: ssl.create_default_context(cafile='ca.pem') or SSL_CERT_FILE for urllib, verify='ca.pem' or REQUESTS_CA_BUNDLE for requests. We measured that each setting only affects its own library.

Why did my self-made CA stop working on Python 3.13?

Python 3.13 enables VERIFY_X509_STRICT by default. We measured a CA made with plain openssl req -x509 failing with 'CA cert does not include key usage extension'; reissuing it with keyUsage=critical,keyCertSign,cRLSign fixed it.

Does requests use SSL_CERT_FILE?

No. We measured SSL_CERT_FILE fixing urllib but not requests; requests reads REQUESTS_CA_BUNDLE (or verify=).

Is verify=False safe?

It removes the check that you're talking to the real server. Only use it for a throwaway test against localhost; otherwise trust the right CA instead.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 (OpenSSL 3.5.7) urllib and requests 2.32.3 against local HTTPS servers using six certificates we generated with openssl: self-signed, signed by a private CA, wrong hostname, expired, and signed by CAs with and without keyUsage. Also tested SSL_CERT_FILE vs REQUESTS_CA_BUNDLE. Every code block was run verbatim and its output pasted from that run. macOS and Windows trust stores were not tested. PlainHeaders is our extension; ssl and urllib are built in, requests is a third-party library.