电脑打得开、手机 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 在两个系统上表现可能不同 |
所以不要用桌面浏览器判断这个问题。你自己电脑上能打开,恰恰是这个故障最典型的样子。
对应的错误信息,如果你手上只有一条日志,可以按这个对照:
| 报错来源 | 错误信息 |
|---|---|
curl | curl: (60) SSL certificate problem: unable to get local issuer certificate |
| OpenSSL | verify error:num=20:unable to get local issuer certificate 或 num=21:unable to verify the first certificate |
| Java | PKIX path building failed: unable to find valid certification path to requested target |
| Node.js | UNABLE_TO_VERIFY_LEAF_SIGNATURE |
| Python | certificate 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
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
# 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.pemCaddy / Traefik
自动申请证书时不会出现这个问题。手工指定证书文件的话,同样要给合并后的 fullchain.pem。
HAProxy
crt 指向的那一个文件里要依次放:叶子证书、中间证书、私钥。顺序不能颠倒。
Java / Tomcat
证书导入 keystore 时必须带整条链。用 PKCS#12 中转最稳妥:
openssl pkcs12 -export \
-in fullchain.pem -inkey privkey.pem \
-out keystore.p12 -name tomcatIIS / Windows
把中间证书导入本机的「中间证书颁发机构」存储(不是「受信任的根证书颁发机构」),IIS 会自己在握手时带上它。
CDN、负载均衡、云上的证书管理
证书上传到 CDN 或 SLB 控制台时,「证书内容」那个输入框里同样要粘贴叶子 + 中间两段,不是只有第一段。改完源站却忘了改边缘,是这个故障最常见的复发方式。
改完之后要重载服务
证书是在启动或重载时读进内存的,换掉文件本身不会生效。改完先测语法,再重载:
nginx -t && nginx -s reload # nginx
apachectl configtest && apachectl graceful # Apache然后用上面那条 grep -c 再数一次。返回 2 或更多,就修好了。
如果你用 certbot 自动续期,顺便确认一下 renew 的 --deploy-hook 里有重载命令。否则续期会成功、而线上还在用旧证书,直到下一次手工重启。
为什么有些检测工具说「没问题」
两个原因,都值得知道,因为它们会让你以为已经修好了:
- 工具用的客户端会自己补全。 任何基于桌面浏览器内核、或者开启了 AIA 下载的检测器,都会把缺失的中间证书下载回来,然后报告一切正常 —— 它替你的访客做了访客做不到的事。
- 工具在数链的长度。 验证成功之后,客户端会把自己信任库里的根证书补进链里。所以一台配置正确、只发两张证书的服务器,验证出来的链长度是 3。反过来,靠「链里有几张」判断缺不缺中间证书,会同时产生误报和漏报。
本站的检测器只认验证器返回的错误码(UNABLE_TO_VERIFY_LEAF_SIGNATURE),不数链长度,也不替服务器补下载。所以它给出的结论和你的访客实际遇到的一致 —— 这也是为什么结果页上的「验证链」一栏会特意注明,那个长度不等于服务器发送的证书数量。
想验证你手上的工具靠不靠得住,可以拿 incomplete-chain.badssl.com 试一下:一个诚实的检测器必须报错。
常见问题
检测一下你自己的域名
免登录,一次真实的 TLS 握手,直接给出结论:还有多少天到期、证书链是否完整、是否包含这个域名。
需要一张新证书?本站提供基于 Let's Encrypt 的免费签发,支持泛域名。
了解免费证书其他排错指南
- 证书过期了怎么办浏览器拦下访问,提示证书已过期或连接不是私密连接。
- 证书快到期了:先分清是没续期,还是续了没部署证书还没过期,但剩余天数已经不多了。
- 证书还没到生效时间:多半是时钟不对证书的生效时间还没到,被判定为无效。
- 证书透明度日志里出现了没见过的证书监控期间,域名下出现了不是本站签发的新证书。
- 无法连接:证书检测连不上服务器时的排查顺序检测连不上服务器,没能拿到证书。
- 「您的连接不是私密连接」:先找错误代码,再决定修哪里浏览器弹出红色警告拦下访问,但不知道属于哪一种证书问题。
- 关闭 TLS 1.0 / 1.1,只留 1.2 和 1.3服务器还在使用 TLS 1.0 或 1.1 协商连接。
- 证书不包含这个域名:ERR_CERT_COMMON_NAME_INVALID 的几种成因浏览器提示证书不适用于该网站,或者拿到了另一个站点的证书。
- OCSP stapling:开了能快一点,不开也不算故障没有启用 OCSP stapling,首次握手会稍慢一些。
- 证书被吊销,或者链验证没通过证书被吊销,或者链验证没有通过。
- 自签名证书:什么时候没问题,什么时候必须换证书是自己签给自己的,浏览器不信任。
- 证书链追溯不到受信任的根:新根交叉签名与私有 CA浏览器打得开,curl、旧安卓或 Java 客户端却说找不到颁发者证书。
- RSA 密钥长度不足:为什么现在才被提醒证书的 RSA 密钥长度低于 2048 位。
- 泛域名证书为什么不覆盖主域名子域名都正常,直接访问主域名却报证书错误。
Last updated on