You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python/aiohttp报SSLCertVerificationError但OpenSSL验证正常

TLS证书校验异常排查方案

根因前置说明

你当前测试代码存在两个明显缺陷,这也是连google.com都报自签名证书错误的核心原因:

  • 你用os.path.relpath()处理certifi的CA路径,相对路径是基于脚本执行时的工作目录解析的,只要执行脚本的目录不是脚本所在目录,CA文件就会加载失败,Python会使用空的信任存储,自然所有证书都会被判定为不可信,报自签名错误。
  • 你调用wrap_socket时没有传server_hostname参数,没有开启SNI扩展,访问多域名托管的端点时,服务端可能返回默认的错误证书,直接导致校验失败。
    另外你贴的openssl输出显示服务端返回了4张证书,包含了自签名的根证书,正常TLS配置下服务端只需要返回终端实体证书+中间证书即可,多余的根证书也可能触发部分客户端的校验异常。

问题1:Python3环境下验证TLS端点证书配置的可靠方法

不要手写裸socket+无参数wrap_socket的逻辑,容易遗漏SNI、主机名校验等关键配置,推荐两种稳妥方案:

  • 通用TLS场景:使用标准库提供的ssl.create_default_context()创建上下文,该方法默认开启严格证书校验、主机名校验,自动配置安全的TLS协议版本和加密套件,自动加载系统默认信任的CA库,不需要手动设置verify_mode。如果需要使用certifi提供的CA库,直接加载certifi返回的绝对路径即可。
  • HTTPS场景:直接使用标准库urllib.request或者成熟的HTTP客户端库发请求,这类库已经覆盖了TLS校验的所有边缘场景,默认开启严格证书校验,不需要手动处理SSL层逻辑。

正确的最简验证代码如下:

import certifi
import socket
import ssl

SERVER = "myhost.example.com"
PORT = 443

# 创建默认安全上下文,不要直接裸调ssl.SSLContext()
context = ssl.create_default_context()
# 加载certifi的CA库,直接用certifi返回的绝对路径,不要转相对路径
context.load_verify_locations(certifi.where())

# 建立连接时必须传server_hostname开启SNI
with socket.create_connection((SERVER, PORT)) as sock:
    with context.wrap_socket(sock, server_hostname=SERVER) as ssock:
        print("TLS协议版本:", ssock.version())
        print("使用的加密套件:", ssock.cipher())
        # 可直接获取服务端返回的完整证书链做进一步核查
        print("服务端证书链:", ssock.getpeercert(chain=True))

问题2:获取详细证书校验错误信息的方法

Python默认抛出的SSL异常信息比较简略,可以通过以下方式拿到足够的排查细节:

  • 开启openssl调试日志,在代码最开头添加以下两行,即可输出完整的TLS握手过程日志,包含证书链每一步的校验情况:
    import logging
    logging.basicConfig(level=logging.DEBUG)
    
  • 先临时关闭证书校验完成握手,拿到服务端返回的完整证书链单独排查,再单独做校验捕获详细错误:
    import certifi
    import socket
    import ssl
    
    SERVER = "myhost.example.com"
    PORT = 443
    
    # 先不校验证书,完整握手拿到服务端返回的证书
    temp_ctx = ssl._create_unverified_context()
    with socket.create_connection((SERVER, PORT)) as sock:
        with temp_ctx.wrap_socket(sock, server_hostname=SERVER) as ssock:
            cert_chain = ssock.getpeercert(chain=True)
            print(f"服务端返回证书数量: {len(cert_chain)}")
            for idx, cert in enumerate(cert_chain):
                print(f"证书{idx} 主题: {cert.get('subject')}")
                print(f"证书{idx} 颁发者: {cert.get('issuer')}")
    
    # 再做严格校验,捕获详细错误信息
    verify_ctx = ssl.create_default_context()
    verify_ctx.load_verify_locations(certifi.where())
    try:
        with socket.create_connection((SERVER, PORT)) as sock:
            with verify_ctx.wrap_socket(sock, server_hostname=SERVER) as ssock:
                print("证书校验通过")
    except ssl.SSLCertVerificationError as e:
        print(f"校验错误码: {e.errno}")
        print(f"OpenSSL原始错误原因: {e.reason}")
        print(f"错误来源模块: {e.library}")
    
  • Python 3.10及以上版本可以在握手后调用ssock.get_verify_result()拿到OpenSSL返回的原始校验结果码,对应具体的校验失败原因(比如证书过期、域名不匹配、缺少中间证书、根证书不受信等)。

针对你当前场景的优先排查项

  • 去掉代码里的os.path.relpath包装,直接使用certifi.where()返回的绝对路径加载CA库。
  • 给wrap_socket方法添加server_hostname=SERVER参数开启SNI。
  • 检查运行环境是否配置了全局HTTPS/ TLS透明代理,这类代理通常会做TLS中间人替换证书,如果代理的自签名根证书只导入了系统信任库,没有包含在certifi的CA库中,就会出现openssl验证正常、Python加载certifi库验证失败的情况。
  • 调整服务端证书链配置,删除证书文件里的根证书,只保留终端实体证书+中间证书,多余的根证书可能触发部分客户端的校验逻辑异常。

内容的提问来源于stack exchange,提问作者Scott Crooks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:06:22