SSL证书验证失败修复说明(通用bash版)

Python(requests)访问 HTTPS 报证书验证失败 —— 通用修复说明(纯 bash 版)

适用:Linux / WSL / macOS(bash 环境)
特征:公司网络存在 HTTPS 流量审计(TLS 中间人)代理
依赖:openssl、curl、awk、grep、Python 的 certifi(多数环境默认已具备)


一、问题现象

执行需要访问 HTTPS 的命令(例如下载 qlib 数据)时报错,关键信息:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate

requests.exceptions.SSLError: HTTPSConnectionPool(host='github.com', port=443):
Max retries exceeded ...
(Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed ...')))

含义:Python 在 TLS 握手时无法把服务器证书链回溯到受信任的根证书,连接被中止。


二、问题原因(根因)

1. 网络出口存在 HTTPS 流量审计代理(TLS 中间人)

用下面的命令检查服务器证书链:

openssl s_client -connect github.com:443 -servername github.com </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:|Verify return code'

在本类环境中会看到签发者是公司内部 CA(出现公司名/内部域名/DC= AD 域结构),而不是
DigiCert、Let's Encrypt、ISRG、GlobalSign 等公共 CA,例如:

 0 s:..., CN=github.com
   i:..., CN=<公司>-CA            ← 叶子证书由公司中间 CA 签发
 1 s:..., CN=<公司>-CA
   i:..., CN=<公司根 CA>          ← 再向上是公司根
Verify return code: 20 (unable to get local issuer certificate)

公司代理会解开并重新签名所有 HTTPS 流量;客户端只有信任了该公司根 CA 才能验证通过。

2. 系统 / Python 的信任库不含公司根 CA

  • Windows 通常已由 IT 安装并信任公司根证书,所以 Windows 浏览器访问正常;
  • 但 WSL/Linux 是独立系统,其系统 CA 库以及 Python 的 certifi 证书包
    (requests 默认使用的信任库)都不包含公司根 CA;
  • 因此 Python 报 unable to get local issuer certificate。

关键细节(实测):代理在握手时通常只下发"叶子证书 + 中间证书",不下发自签名根证书。
OpenSSL 3 / Python 默认要求信任到自签名根,所以仅把抓到的中间证书加入信任库仍然会失败
(错误变为 unable to get issuer certificate)。必须额外获得根证书。

结论:这不是应用本身(如 qlib)的问题,也不是网络不通,而是 Linux/Python 缺少公司代理的根证书。

补充:很多下载链接会 302 重定向到资源站(如 release-assets.githubusercontent.com),
这些站点可能使用标准公共 CA(ISRG/Let's Encrypt),certifi 已默认信任;把公司根加入后两类站点都能访问。


三、修改策略(全程 bash,不改源码、不关闭证书校验)

  1. 用 openssl s_client -showcerts 抓取代理下发的证书链;
  2. 找到链顶证书;若其不是自签名根,则从证书的 AIA(Authority Information Access)
    的 CA-Issuers(http)地址
    用 curl 下载根证书(http 地址无需证书校验);
  3. 下载内容若为 DER 二进制,则用 openssl x509 -inform DER 转成 PEM;
  4. 构造合并证书包 = certifi 公共 CA + 公司根证书;
  5. 通过环境变量 REQUESTS_CA_BUNDLE / SSL_CERT_FILE 指向合并包,并写入 ~/.bashrc 永久生效;
  6. (可选)把公司根安装到系统 CA 库,使 curl、git、apt 等系统工具同样信任。

该方案保留证书校验(不使用 verify=False),安全、通用、可重复执行。


四、具体操作步骤

步骤 1(推荐,一键完成):运行通用脚本

将随附脚本保存为 setup_corporate_ca.sh,然后执行:

bash setup_corporate_ca.sh github.com ~/.corporate_ca_bundle.pem

参数:第 1 个为目标域名(默认 github.com),第 2 个为合并包输出路径(默认 ~/.corporate_ca_bundle.pem)。
脚本会自动:抓链 → 找链顶 → 必要时从 AIA 下载根并 DER→PEM → 合并 certifi → 写 ~/.bashrc → 连通性验证。

步骤 2(理解原理时可手工执行的等价命令)

# (1) 抓取链并拆分成单张 PEM(只保留 BEGIN/END 之间的内容)
openssl s_client -connect github.com:443 -servername github.com -showcerts </dev/null 2>/dev/null \
  | awk '/-----BEGIN CERTIFICATE-----/{n++;f=sprintf("chain%02d.pem",n);c=1} c{print>f}
         /-----END CERTIFICATE-----/{close(f);c=0}'

# (2) 查看链顶证书的 AIA,找到 http 形式的 CA Issuers 地址
openssl x509 -in chain02.pem -noout -text | grep -A4 'Authority Information Access'

# (3) 下载根证书;若是 DER 二进制则转 PEM
curl -sS -L -o root_raw 'http://<AIA中的CA-Issuers地址>'
openssl x509 -inform DER -in root_raw -outform PEM -out root.pem   # 若已是PEM则跳过本转换

# (4) 合并 certifi + 根证书
python -c 'import certifi;print(certifi.where())'                  # 查看 certifi 路径
cat "$(python -c 'import certifi;print(certifi.where())')" root.pem > ~/.corporate_ca_bundle.pem

# (5) 当前终端立即生效,并写入 ~/.bashrc 永久生效
export REQUESTS_CA_BUNDLE=~/.corporate_ca_bundle.pem
export SSL_CERT_FILE=~/.corporate_ca_bundle.pem
grep -q REQUESTS_CA_BUNDLE ~/.bashrc || \
  printf '\nexport REQUESTS_CA_BUNDLE=%s\nexport SSL_CERT_FILE=%s\n' \
    "$HOME/.corporate_ca_bundle.pem" "$HOME/.corporate_ca_bundle.pem" >> ~/.bashrc

步骤 3(可选):安装到系统 CA 库

让 curl、git、apt 等系统级工具也信任公司根(Debian/Ubuntu/WSL):

sudo cp root.pem /usr/local/share/ca-certificates/corporate-root.crt
sudo update-ca-certificates

步骤 4:使配置生效并重新执行原命令

source ~/.bashrc          # 或重新打开一个终端
python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn

五、验证方法(如何确认修复成功)

# 1) openssl:返回码应为 0 (ok)
openssl s_client -connect github.com:443 -servername github.com \
  -CAfile ~/.corporate_ca_bundle.pem </dev/null 2>/dev/null | grep 'Verify return code'

# 2) Python:应打印 200
REQUESTS_CA_BUNDLE=~/.corporate_ca_bundle.pem python -c \
  "import requests;print(requests.get('https://github.com',timeout=30).status_code)"

实测输出示例:

Verify return code: 0 (ok)
verify status = 200

实际数据文件(跟随 302)也应可下载:

download = 200 | host = release-assets.githubusercontent.com | zip magic ok = True

返回码速查

Verify return code 含义 处置
0 (ok) 验证成功 —
20 unable to get local issuer certificate 信任库里没有根证书 需加入自签名根(本文方案)
2 unable to get issuer certificate 只信任到中间证书、缺上级根 说明只加了中间证书,仍需补根
21 unable to verify the first certificate 服务器未下发中间证书 需补全中间证书

关于版本文件 404(如出现,属正常)

部分程序(如 qlib)会先请求带版本号的文件,不存在(404)后自动回退到 latest,
这是应用层逻辑,与 SSL 问题无关。


六、注意事项

  1. 本文适用于“证书链由公司代理 CA 签发”的场景;若是普通缺 CA,
    可先 sudo apt-get install --reinstall ca-certificates 或升级 certifi。
  2. 脚本幂等,可重复执行;~/.bashrc 已含配置时不会重复写入。
  3. 使用其他 shell(zsh/fish)时,把同样的 export 写入对应配置文件(如 ~/.zshrc)。
  4. certifi 升级后如需同步最新公共 CA,重新运行一次步骤 1 的脚本即可。
  5. 不要用 verify=False、--no-check-certificate 等方式关闭校验,存在安全风险。
  6. 若证书未提供可用的 http CA-Issuers 地址,则需向 IT 安全部门索取公司根 CA(PEM/DER),
    再从“步骤 2 的第(4)小步”开始手工合并。

七、文件清单

文件 说明
setup_corporate_ca.sh 一键发现/下载根 CA、生成合并包、写环境变量的通用脚本
~/.corporate_ca_bundle.pem certifi 公共 CA + 公司根证书的合并包(Python 实际使用,勿删)
root.pem 从 AIA 下载并转换得到的公司自签名根证书(安装系统 CA 时使用)
posted @ 2026-09-21 14:04  lisacumt  阅读(18)  评论(0)    收藏  举报