Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
故障排查阅读约 11 分钟

SSL 证书验证失败:在 Python、curl 和 Web3 RPC 客户端中诊断信任链错误

一种可复现的方法,用于追踪从客户端信任存储经 TLS 握手到服务器证书链的 SSL: CERTIFICATE_VERIFY_FAILED 错误,并提供不关闭验证的修复方案。

TL;DR

SSL: CERTIFICATE_VERIFY_FAILED 表示客户端无法从服务器的叶证书构建到本地信任存储中某个根 CA 的可信路径。修复方法几乎从来不是关闭验证,而是找出链中四个环节里哪一个断了:客户端信任存储、服务器发送的中间证书、主机名/SNI 匹配,或时钟。本文给出了一套可复现的诊断流程,你可以用 openssl、curl 和 Python 运行,然后将同样的方法应用于针对 HTTPS 端点失败的 Web3 RPC 客户端。

错误实际报告了什么

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed 是 TLS 库(OpenSSL、BoringSSL、Secure Transport 或 SChannel)在路径验证失败时发出的。末尾的细节才是有用的部分:unable to get local issuer certificate 表示客户端无法在链中找到某个证书的签发者;certificate has expired 表示违反了有效期窗口;hostname mismatch 表示 SAN 列表不包含你请求的名称;self-signed certificate in certificate chain 表示提供了不受信任的根证书。

该错误是客户端关于信任的陈述,而不是服务器损坏的证明。一个服务器对浏览器完全有效,却可能对自带 CA 包的 Python 进程失败,或对未安装 ca-certificates 包的容器失败。请将该消息视为指向四个环节之一的指针:本地信任存储、服务器发送的中间证书、名称/SNI 一致性以及系统时间。

  • 信任存储:客户端是否拥有签署该链的根 CA?
  • 证书链:服务器是否发送了到达该根所需的所有中间证书?
  • 身份:证书是否覆盖主机名,SNI 是否正确发送?
  • 时间:客户端时钟是否在证书的 notBefore/notAfter 窗口内?

假设与前提条件

以下流程假设你可以在出错的客户端主机上运行命令,允许出站 TCP 443,并且你知道客户端连接的确切主机名。你需要 openssl(1.1.1 或 3.x)、curl,以及产生该错误的同一语言运行时。对于 Web3 工作,你需要应用程序使用的 RPC URL;将其保存在环境变量(如 RPC_URL)中,而不是硬编码。

如果你是在选择端点而不是调试端点,具体的提供商决策属于 RPC Assistant;本页重点介绍适用于任何 HTTPS 端点的诊断方法,包括你通过 OnFinality API 服务和专用节点产品访问的端点。

心智模型:四项独立检查

路径验证是一个序列,每一步都可能独立失败。首先,客户端解析呈现的证书链。其次,它从叶证书穿过中间证书,寻找它已经信任的签发者。第三,它检查叶证书的 subjectAltName 是否与请求的主机匹配。第四,它根据本地时钟检查有效期。任何失败都会产生同一标题错误的不同变体,这就是为什么从论坛帖子复制修复方案通常不起作用:该帖子解决的是另一个环节。

这也是为什么 pip install certifi 有时似乎能解决问题,有时却不能。Certifi 只有在 Python 进程实际配置为读取它时,才会改变该进程使用的包。如果出错的代码位于第三方库中,或者环境变量设置在服务不继承的 shell 中,那么替换包就没有效果。

步骤 1:检查服务器发送的证书链

从服务器开始。此命令打印叶证书和服务器呈现的每个中间证书,而不依赖客户端的信任存储:

  • 数一数证书数量。典型的公共链是叶证书加一两个中间证书;只有叶证书而没有中间证书是导致 unable to get local issuer certificate 的常见原因。
  • 记下最后一个证书的签发者。该名称就是客户端必须信任的根。
  • 使用 openssl x509 -noout -dates 检查每个证书的 notBefore 和 notAfter。
openssl s_client -connect your-rpc-host.example:443 -servername your-rpc-host.example -showcerts </dev/null 2>/dev/null | openssl crl2pkcs7 -nocrl -certfile /dev/stdin | openssl pkcs7 -print_certs -noout

步骤 2:与客户端信任存储比较

现在让客户端自己的 TLS 栈验证同一主机。curl 在大多数 Linux 发行版上使用系统存储,在 macOS 和 Windows 上使用平台存储:

  • 如果 curl 成功但 Python 失败,问题在于 Python 的 CA 包,而不是服务器。
  • 如果 curl 失败并显示 certificate has expired,请先检查客户端时钟。
  • 如果 curl 失败并显示 unable to get local issuer certificate,服务器可能没有发送中间证书。
curl -vI https://your-rpc-host.example 2>&1 | grep -E "SSL certificate|subject:|issuer:|expire|TLS"

步骤 3:在出错的运行时内复现

Python 的默认验证使用编译到解释器中或为解释器配置的包。打印它实际使用的路径,然后尝试一个将 TLS 层与应用程序代码隔离的最小连接:

  • 如果此代码片段失败但 curl 成功,解释器的包就是故障线。
  • 如果成功,则失败在于 HTTP 客户端层(requests、httpx、web3.py、ethers)或终止 TLS 的代理。
  • 记录确切的异常文本;冒号后的后缀是可操作的线索。
import ssl, socket, certifi

print("default verify paths:", ssl.get_default_verify_paths())
print("certifi bundle:", certifi.where())

host = "your-rpc-host.example"
ctx = ssl.create_default_context()
with socket.create_connection((host, 443), timeout=10) as sock:
    with ctx.wrap_socket(sock, server_hostname=host) as tls:
        print("negotiated:", tls.version())
        print("peer subject:", tls.getpeercert().get("subject"))

步骤 4:在不关闭验证的情况下修复信任存储

当解释器的包过时或缺失时,显式地将进程指向一个维护中的包。这与广泛共享的 SSL_CERT_FILE 变通方法使用相同的机制,但要有意应用,而不是猜测:

  • 在实际运行服务的环境中设置这些变量,而不仅仅是在交互式 shell 中。
  • 在 Debian/Ubuntu 主机上,apt-get install -y ca-certificates 和 update-ca-certificates 为 curl 和大多数运行时修复系统存储。
  • 在 Windows 上,某些运行时会使用平台存储,而其他运行时不会;certifi 路径是 Python 最可移植的修复方案。
  • 切勿将 verify=False 或 rejectUnauthorized: false 作为解决方案发布。它会把信任错误转化为未检测到的中间人攻击暴露。
pip install --upgrade certifi
python -c "import certifi; print(certifi.where())"
export SSL_CERT_FILE="$(python -c 'import certifi; print(certifi.where())')"
export SSL_CERT_DIR="$(dirname "$SSL_CERT_FILE")"

# verify the fix in the same shell
python -c "import urllib.request; urllib.request.urlopen('https://your-rpc-host.example', timeout=10); print('ok')"

将方法应用于 Web3 RPC 客户端

当通过 HTTPS 的 JSON-RPC 调用失败时,同样的四项检查也适用。节点提供商、负载均衡器和企业代理都位于你的客户端和链之间,其中任何一个都可能呈现你的运行时不信任的链。一个最小探测可以将传输失败与 RPC 失败分开:

  • ssl_verify_result=0 表示 TLS 握手已验证;非零值对应 OpenSSL 验证错误代码。
  • 如果 curl 返回 JSON-RPC 结果但你的应用程序失败,则应用程序的 HTTP 客户端具有不同的信任配置。
  • 如果路径中有代理,请检查代理的证书而不是源站的证书;客户端只能看到代理链。
  • 关于端点选择和网络覆盖,请参阅 OnFinality 网络目录和 RPC 端点指南,而不是猜测主机名。
curl -sS -X POST "$RPC_URL" \
  -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' \
  -w '\nhttp_code=%{http_code} tls=%{ssl_verify_result}\n'

验证:成功的样子

已解决的案例会在各层产生一致的证据。使用一个小型结果表记录修复前后的状态,以便修复可审计而不是轶事;填写你在自己主机上观察到的值。

  • | 检查 | 命令 | 修复后预期 |
  • | 服务器链 | openssl s_client -showcerts | 叶证书加中间证书,最后一个签发者是公共根 |
  • | 系统客户端 | curl -vI https://host | SSL certificate verify ok |
  • | 运行时 TLS | Python create_default_context() 代码片段 | 握手完成,打印对端主题 |
  • | RPC 传输 | 带 ssl_verify_result 的 curl POST | 0 和 JSON-RPC 结果 |
  • | 应用程序 | 你的正常请求路径 | 日志中没有 CERTIFICATE_VERIFY_FAILED |

排查基本修复后仍存在的问题

有些失败在包修正后仍然存在。使用 FROM scratch 或最小镜像构建的容器可能根本没有 CA 存储;将包复制进去并在镜像中设置 SSL_CERT_FILE。在 systemd 或 Kubernetes 下运行的服务可能不会继承 shell 导出;在单元文件或 Pod 规范中设置变量。企业 TLS 检查代理会呈现自己的根证书,必须有意识地并在安全团队知情的情况下将其添加到客户端包中。

在从快照恢复的虚拟机和嵌入式设备上,时钟偏差很容易被忽略;date -u 和 NTP 同步可以解决与证书无关的 certificate has expired 错误。最后,如果只有某个主机名失败,而同一提供商的其他主机名成功,则怀疑服务器端的 SNI 或虚拟主机配置错误,并附上 openssl s_client 输出进行报告。

限制与权衡

固定特定的包路径使行为可复现,但增加了维护任务:包会过期,根证书会退役,因此路径必须随运行时镜像刷新。依赖系统存储更简单,但因发行版和基础镜像而异,这就是为什么相同的代码可以在笔记本电脑上通过而在 CI 中失败。关闭验证从来不是修复;它移除了区分配置错误的代理和主动拦截的唯一信号。

对于生产 RPC 流量,持久的答案是标准化基础镜像、提供最新的 CA 包,并将 TLS 握手失败作为与 RPC 错误率不同的指标进行监控。这样,信任存储回归会在看起来像链中断之前暴露出来。

后续步骤

一旦握手干净,确认你使用的端点是你想要的端点,并且其网络覆盖与你的应用程序匹配。查看 OnFinality API 服务以获取托管 RPC 访问,在 RPC 定价页面比较计划,并在需要隔离吞吐量和可预测 TLS 终止时使用专用节点。如果你仍在选择提供商,RPC Assistant 可以返回具体建议,而本页仍是你的诊断参考。

  • 在基础镜像和 CI 运行器中标准化 CA 包。
  • 添加一个启动探测,对每个配置的 RPC 主机执行 TLS 握手。
  • 将 OpenSSL 验证结果与 RPC 错误一起记录,以免信任失败被错误分类。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始