排错指南

电脑打得开、手机 App 报证书错误:证书链缺少中间证书

电脑浏览器打得开,手机 App、curl 或 Java 客户端却报证书错误。

如果你的网站在电脑浏览器上一切正常,但手机 App、curl、Java 或 Go 写的客户端访问时报证书错误,绝大多数情况下不是证书本身有问题,而是服务器只发送了自己的证书,没有把中间证书一起发出去

修复通常只需要改一个文件路径:把服务器配置里的 cert.pem 换成 fullchain.pem,然后重载服务。下面解释为什么会出现「只有一部分人打不开」,以及怎么确认和修改。

为什么只有一部分客户端报错

一张证书要被信任,客户端必须能从它一路验证到操作系统里预置的根证书。中间通常还有一张「中间证书」(intermediate CA),这张证书必须由服务器在握手时一起发给客户端 —— 它不在任何人的信任库里。

服务器只发叶子证书时,不同客户端的表现不一样,这才是这个故障最难定位的地方:

客户端表现原因
Chrome / Edge / Safari(桌面)多数情况下正常会按证书里的 AIA 扩展主动下载缺失的中间证书,并缓存下来
Firefox多数情况下正常不主动下载,但内置并缓存了常见的中间证书
curl / OpenSSL报错不做 AIA 下载,缺一张就是缺一张
Java(HttpClient / OkHttp)报错同上,且错误信息是 PKIX 相关,看不出是链的问题
Go / Python requests报错同上
手机 App 内的网络库不一定取决于走系统栈还是自带的 OpenSSL,同一个 App 在两个系统上表现可能不同

所以不要用桌面浏览器判断这个问题。你自己电脑上能打开,恰恰是这个故障最典型的样子。

对应的错误信息,如果你手上只有一条日志,可以按这个对照:

报错来源错误信息
curlcurl: (60) SSL certificate problem: unable to get local issuer certificate
OpenSSLverify error:num=20:unable to get local issuer certificatenum=21:unable to verify the first certificate
JavaPKIX path building failed: unable to find valid certification path to requested target
Node.jsUNABLE_TO_VERIFY_LEAF_SIGNATURE
Pythoncertificate verify failed: unable to get local issuer certificate
微信 / 支付宝回调多数只回一句「证书校验失败」,需要用下面的命令自查

怎么确认是这个问题

最快的办法是数一下服务器到底发了几张证书。把下面命令里的域名换成你自己的:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"
  • 结果是 1 —— 服务器只发了自己的证书,就是本文说的问题。
  • 结果是 2 或更多 —— 链是完整的,报错另有原因,可以先跑一次证书检测看看结论。

想看得更清楚,去掉 grep 直接读输出。开头那段 Certificate chain 会按顺序列出服务器发来的每一张证书,s: 是主题、i: 是颁发者:

openssl s_client -connect example.com:443 -servername example.com </dev/null

# 链完整时,看起来是这样(0 的颁发者出现在 1 的主题上):
# Certificate chain
#  0 s:CN=example.com
#    i:C=US, O=Let's Encrypt, CN=R13
#  1 s:C=US, O=Let's Encrypt, CN=R13
#    i:C=US, O=Internet Security Research Group, CN=ISRG Root X1

手边没有 openssl 的话,本站的证书检测会直接给出结论 —— 它读的是验证器返回的错误码,判定方式和上面的命令一致。

修复:用 fullchain,不要用 cert

证书颁发机构给你的文件里,通常同时有「只有叶子证书」和「叶子 + 中间证书」两个版本。用错的那个就是这个故障的全部原因。以 Let's Encrypt / certbot 的输出为例:

文件内容该不该用
cert.pem只有你的证书❌ 用了就是本文的故障
chain.pem只有中间证书❌ 单独用不行
fullchain.pem你的证书 + 中间证书✅ 这个
privkey.pem私钥✅ 配在 key 那一项

nginx

nginx.conf
ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

ssl_trusted_certificate 不是用来发送证书链的 —— 它只用于 OCSP stapling 的验证,配在那里不会让服务器多发一张证书。这是这个问题最常见的一种「我明明配了中间证书」。

Apache

httpd.conf / vhost
# Apache 2.4.8 及以上:直接给合并好的文件
SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

# 2.4.8 以下:中间证书要单独指定
# SSLCertificateFile      .../cert.pem
# SSLCertificateChainFile .../chain.pem

Caddy / Traefik

自动申请证书时不会出现这个问题。手工指定证书文件的话,同样要给合并后的 fullchain.pem

HAProxy

crt 指向的那一个文件里要依次放:叶子证书、中间证书、私钥。顺序不能颠倒。

Java / Tomcat

证书导入 keystore 时必须带整条链。用 PKCS#12 中转最稳妥:

openssl pkcs12 -export \
  -in fullchain.pem -inkey privkey.pem \
  -out keystore.p12 -name tomcat

IIS / Windows

把中间证书导入本机的「中间证书颁发机构」存储(不是「受信任的根证书颁发机构」),IIS 会自己在握手时带上它。

CDN、负载均衡、云上的证书管理

证书上传到 CDN 或 SLB 控制台时,「证书内容」那个输入框里同样要粘贴叶子 + 中间两段,不是只有第一段。改完源站却忘了改边缘,是这个故障最常见的复发方式。

改完之后要重载服务

证书是在启动或重载时读进内存的,换掉文件本身不会生效。改完先测语法,再重载:

nginx -t && nginx -s reload        # nginx
apachectl configtest && apachectl graceful   # Apache

然后用上面那条 grep -c 再数一次。返回 2 或更多,就修好了。

如果你用 certbot 自动续期,顺便确认一下 renew 的 --deploy-hook 里有重载命令。否则续期会成功、而线上还在用旧证书,直到下一次手工重启。

为什么有些检测工具说「没问题」

两个原因,都值得知道,因为它们会让你以为已经修好了:

  1. 工具用的客户端会自己补全。 任何基于桌面浏览器内核、或者开启了 AIA 下载的检测器,都会把缺失的中间证书下载回来,然后报告一切正常 —— 它替你的访客做了访客做不到的事。
  2. 工具在数链的长度。 验证成功之后,客户端会把自己信任库里的根证书补进链里。所以一台配置正确、只发两张证书的服务器,验证出来的链长度是 3。反过来,靠「链里有几张」判断缺不缺中间证书,会同时产生误报和漏报。

本站的检测器只认验证器返回的错误码(UNABLE_TO_VERIFY_LEAF_SIGNATURE),不数链长度,也不替服务器补下载。所以它给出的结论和你的访客实际遇到的一致 —— 这也是为什么结果页上的「验证链」一栏会特意注明,那个长度不等于服务器发送的证书数量。

想验证你手上的工具靠不靠得住,可以拿 incomplete-chain.badssl.com 试一下:一个诚实的检测器必须报错。

常见问题

检测一下你自己的域名

免登录,一次真实的 TLS 握手,直接给出结论:还有多少天到期、证书链是否完整、是否包含这个域名。

需要一张新证书?本站提供基于 Let's Encrypt 的免费签发,支持泛域名。

了解免费证书

其他排错指南

Last updated on