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
相关产品推荐
相关产品推荐

