bigywater.tech
常见报错 · 16

Python SSL: CERTIFICATE_VERIFY_FAILED:几种原因对应不同修法,别用 CERT_NONE

“ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed” 后面跟着的原因决定了该怎么修,但教程里的万能办法常常是“关掉验证”。这篇在本机用 openssl 造出自签名、私有 CA、过期、主机名错、缺中间证书、缺 AKI 几种证书,逐个看 Python 的报错和每种的正确修法。

188 → 1
Python 默认加载的根证书数,与设 SSL_CERT_FILE 为单个根证书之后的数目
85
手工签发、缺 Authority Key Identifier 的证书被 VERIFY_X509_STRICT 拒绝的 verify_code
62 / 10
Hostname mismatch 与 certificate has expired 的 verify_code,这两种加载任何 CA 都修不好

信任库在哪

实测 · 本机执行输出
  ssl.get_default_verify_paths(): cafile=/opt/homebrew/etc/openssl@4/cert.pem
      capath=/opt/homebrew/etc/openssl@4/certs  环境变量名: SSL_CERT_FILE、SSL_CERT_DIR
  cafile 文件存在: True;create_default_context() 加载的根证书数 = 188
  设置 SSL_CERT_FILE=<只有一个根证书的文件> 之后(子进程里): 加载的根证书数 = 1

默认从 OpenSSL 指定位置的 cert.pem 加载根证书,可由 SSL_CERT_FILE / SSL_CERT_DIR 覆盖。

同一个报错的不同原因

实测 · 本机执行输出
  自签名证书(selfsig):
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1144)
      verify_code=18 verify_message='self-signed certificate'
  私有 CA 签发、客户端不认识这个 CA:
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)
      verify_code=20 verify_message='unable to get local issuer certificate'
  连接的名字不在证书里(证书只给 other.test,却用 localhost 去连):
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)
      verify_code=20 verify_message='unable to get local issuer certificate'
  用 IP 地址 127.0.0.1 去连,证书的 SAN 里有 IP:127.0.0.1(对照,仍因 CA 不认识而失败):
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)
      verify_code=20 verify_message='unable to get local issuer certificate'
  create_default_context().verify_flags 里带 VERIFY_X509_STRICT: True
  再换成“客户端信任了根证书”后的几种失败(这时只剩证书本身的问题):
  证书过期: 
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1144)
      verify_code=10 verify_message='certificate has expired'
  主机名对不上(证书只给 other.test): 
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1144)
      verify_code=62 verify_message="Hostname mismatch, certificate is not valid for 'localhost'."
  服务器只发了叶子证书、没发中间证书(根已信任): 
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)
      verify_code=20 verify_message='unable to get local issuer certificate'
  手工签的证书缺少 Authority Key Identifier 扩展: 
      [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Authority Key Identifier (_ssl.c:1144)
      verify_code=85 verify_message='Missing Authority Key Identifier'
  对照:发了完整链: 成功,收到 'hello'
  对照:用 IP 连、证书 SAN 里有这个 IP: 成功,收到 'hello'

用 verify_code 和 verify_message 区分:自签名、找不到签发者、过期、主机名不对、缺 AKI。unable to get local issuer certificate 既可能是 CA 不被信任,也可能是服务器没发中间证书。

对症修法

实测 · 本机执行输出
  私有 CA:cafile=根证书 -> 成功
  缺 Authority Key Identifier:cafile=根证书 -> 失败:Missing Authority Key Identifier
  自签名:cafile=它自己的证书 -> 成功
  只发叶子证书:cafile=根证书 -> 失败:unable to get local issuer certificate
  只发叶子证书:cafile=根证书 + 把中间证书也加进信任 -> 成功
  过期的证书:cafile=根证书 -> 失败:certificate has expired
  主机名不对:cafile=根证书 -> 失败:Hostname mismatch, certificate is not valid for 'localhost'.
  缺 Authority Key Identifier + 去掉 VERIFY_X509_STRICT 标志 -> 成功(能连,但这是在放宽校验,正确做法是重新签发带 AKI 的证书)
  不改代码,只设环境变量 SSL_CERT_FILE=<根证书> -> 成功 b'hello'
  不设环境变量 -> ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:114

CA 不认识就加载 CA;自签名就信任它自己;缺中间证书就让服务器发完整链;过期和主机名不对要换证书或用对的名字;缺 AKI 要重新签发。

别关校验

实测 · 本机执行输出
  关闭校验去连一个“冒充 localhost”的服务器:连接成功,收到 'hello';对方证书 subject=CN=attacker.example;  issuer=CN=attacker.example
  同一个冒充者,开着校验(信任了我们的根证书)-> 失败:self-signed certificate
  CERT_NONE 下 getpeercert() 返回 {},要看对方证书得用 getpeercert(binary_form=True)(拿到 791 字节的 DER)

CERT_NONE 会接受冒充者的证书,等于不再检查对方是谁。

urllib 里的样子

实测 · 本机执行输出
  urllib.request.urlopen(url) -> URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1144)>
      e.reason 的类型 = SSLCertVerificationError
  urlopen(url, context=信任自签名证书的上下文) -> b'hi'

异常被包在 URLError 里,真正的 SSLCertVerificationError 在 .reason 上;可用 context= 指定信任。

分步演示

证书校验失败
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.10.0 与 Redis 8.10.2(Homebrew,每个实验自己起实例,端口 9150 以上,只监听 127.0.0.1,不碰本机已有的实例)/ Python 3.15.0 + PyMySQL(按各篇注明用到哪些及版本);报错文本都是真实复现时原样捕获的;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 Python 3.15.0(OpenSSL 4.0.3)的标准库 ssl / urllib,证书由 OpenSSL 4.0.3 命令行在本机生成(根 CA、中间 CA、叶子证书、自签名证书、冒充者证书),服务端是本机起的 TLS 套接字和 http.server;实验结束后证书目录整体删除
    • 没有验证:macOS 钥匙串里手工信任的证书是否影响 Python;requests / certifi / httpx / aiohttp 等第三方库各自的信任库与报错文本;企业代理做 TLS 拦截时的证书链;Python 3.12 及以前(没有默认 VERIFY_X509_STRICT)上缺 AKI 的证书是否会被接受;Python.org 安装包的 Install Certificates.command 的作用;证书吊销(CRL / OCSP)检查
    京ICP备2026067502号-1京公网安备11010502063487号