验证与连接测试

OpenSSL 验证证书链:中间证书、主机名与私钥匹配

作者: · 最后核实 实测 macOS · OpenSSL 3.6.1

下列命令回答四个问题:证书能不能链到受信任的根、主机名对不对、用途(EKU)对不对、证书和私钥是不是一对。命令基于 OpenSSL 3.x(输出取自 3.6.1),除特别说明外在 1.1.1 上也能用。示例里 leaf.pem 是服务器证书,intermediate.pem 是签发它的中间证书,root.pem 是你信任的根证书。

用指定的根证书验证证书

根证书用 -CAfile 传入,中间证书用 -untrusted 传入,这和 TLS 客户端看到的情形一致。

openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
leaf.pem: OK

-CAfile 里的证书会被无条件信任,是“信任锚”;-untrusted 里的证书只是拼链用的材料,本身还得能链到信任锚。多张中间证书可以放在同一个 -untrusted 文件里。如果把中间证书误放进 -CAfile,OpenSSL 会停在中间证书并报 error 2 at 1 depth lookup: unable to get issuer certificate,除非再加 -partial_chain 允许以非自签名证书作为信任锚。

复现“缺少中间证书”的报错

不给中间证书,就会在深度 0 得到错误 20,和服务器只发了服务器证书时客户端看到的一样。

openssl verify -CAfile root.pem leaf.pem
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.pem: verification failed

这句话的意思是“在深度 0(服务器证书)找不到给它签名的那张证书”。深度 1 是中间证书,深度 2 是根证书。真正的修复在服务器端:配置完整证书链(见缺少中间证书),或者用证书链补全工具生成。

打印 OpenSSL 实际拼出的证书链

-show_chain 会列出验证路径上的每一张证书,并标出哪些来自不受信任的输入。

openssl verify -show_chain -CAfile root.pem -untrusted intermediate.pem leaf.pem
leaf.pem: OK
Chain:
depth=0: CN=www.example.com (untrusted)
depth=1: C=CN, O=Example Corp, CN=Example Intermediate CA (untrusted)
depth=2: C=CN, O=Example Corp, CN=Example Root CA

遇到交叉签名、存在多条路径的情况,用它最快看清最终链到了哪个根。存在多条路径时,OpenSSL 会优先使用信任库里的证书,而不是 -untrusted 里的同名证书。

用系统信任库验证

不写 -CAfile 时,OpenSSL 使用 OPENSSLDIR 下的默认信任库,它不一定和操作系统的信任库是同一份。

openssl version -d
openssl verify -untrusted chain.pem leaf.pem
OPENSSLDIR: "/opt/homebrew/etc/openssl@3"
leaf.pem: OK

Debian/Ubuntu 的默认信任库在 /etc/ssl/certs(由 update-ca-certificates 维护),CentOS/Rocky 等 RHEL 系在 /etc/pki/tls,Homebrew 自带一份 cert.pem。只想信任某一个根时,把 -no-CAfile -no-CApath -no-CAstore 和 -CAfile 一起用。老系统的信任库可能还没有新发布的根证书,即便浏览器已经信任它;国内一些多年不升级的 CentOS 7 服务器就常见这种情况。

验证整个证书链文件

openssl verify 对一个文件只检查第一张证书,所以要把同一个文件再用 -untrusted 传一次。

cat leaf.pem intermediate.pem > fullchain.pem
openssl verify -CAfile root.pem fullchain.pem
openssl verify -CAfile root.pem -untrusted fullchain.pem fullchain.pem
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error fullchain.pem: verification failed
fullchain.pem: OK

第一条失败,是因为同一文件里的中间证书被忽略了;第二条通过,说明这个证书链文件完整、顺序可用。上传到宝塔面板或云厂商负载均衡之前,用第二条命令自查一遍很省事。

像客户端那样检查主机名

-verify_hostname 按 SAN(使用者备用名称)检查域名,和浏览器、curl 的行为一致。

openssl verify -CAfile root.pem -untrusted intermediate.pem \
  -verify_hostname shop.example.com leaf.pem
openssl x509 -in leaf.pem -noout -checkhost www.example.com
CN=www.example.com
error 62 at 0 depth lookup: hostname mismatch
error leaf.pem: verification failed
Hostname www.example.com does match certificate

现代客户端在证书有 SAN 时完全不看 CN,所以只写在 CN 里的域名会校验失败。通配符 *.example.com 能匹配 shop.example.com,但不包括裸域 example.com,也不包括 a.b.example.com。IP 证书用 -verify_ip 203.0.113.10 检查。

检查证书用途(EKU)

-purpose sslserver 要求证书带 serverAuth 扩展密钥用法,sslclient 要求带 clientAuth。

openssl verify -CAfile root.pem -untrusted intermediate.pem -purpose sslclient leaf.pem
openssl x509 -in leaf.pem -noout -ext extendedKeyUsage
CN=www.example.com
error 26 at 0 depth lookup: unsuitable certificate purpose
error leaf.pem: verification failed
X509v3 Extended Key Usage: 
    TLS Web Server Authentication

这一点对双向 TLS 很关键:Chrome 根证书计划自 2026-06-15 起开始把 clientAuth 从公网 TLS 体系中剔除(2027-03-15 起签发的证书只能声明 serverAuth),Let's Encrypt 的 Generation Y 证书也已不含客户端认证用途,所以公网证书越来越多地过不了 -purpose sslclient。详见客户端认证 EKU 被移除和 mTLS 原理。

找到并下载缺失的中间证书

AIA 扩展里的 CA Issuers 地址指向签发者证书,通常以 DER 格式提供。

openssl x509 -in leaf.pem -noout -ext authorityInfoAccess
curl -s -o issuer.der http://ca.example.net/issuing-ca.crt
openssl x509 -inform DER -in issuer.der -out intermediate.pem
openssl x509 -in intermediate.pem -noout -subject -issuer
Authority Information Access: 
    CA Issuers - URI:http://ca.example.net/issuing-ca.crt
subject=C=US, O=Example CA, CN=Example TLS Issuing CA
issuer=C=US, O=Example CA, CN=Example Root R1

下载后确认它的 subject 正好等于服务器证书的 issuer。文件以 -----BEGIN 开头说明已经是 PEM,用 file issuer.der 也能看出来。少数 CA 提供的是 PKCS#7 包(.p7c),用 openssl pkcs7 -inform DER -in issuer.p7c -print_certs -out intermediate.pem 转换。一直往上找到你信任的根为止,但根证书不要放进证书链文件。

检查证书文件里的顺序

从服务器证书开始,每张证书的签发者(Issuer)都应该是下一张证书的主体(Subject)。

openssl storeutl -noout -text -certs fullchain.pem | grep -E '^[0-9]+: |Subject:|Issuer:'
0: Certificate
        Issuer: C=CN, O=Example Corp, CN=Example Intermediate CA
        Subject: CN=www.example.com
1: Certificate
        Issuer: C=CN, O=Example Corp, CN=Example Root CA
        Subject: C=CN, O=Example Corp, CN=Example Intermediate CA

没有 storeutl 的老版本可以用 openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout。Nginx、HAProxy 会把文件里的第一张证书当作服务器证书,顺序颠倒就会报私钥不匹配(见 nginx key values mismatch)。在宝塔面板里手动粘贴证书时,也要把自己的域名证书放在最上面。

确认证书和私钥是一对

分别计算两者公钥的哈希,相同就说明匹配。

openssl x509 -in leaf.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256
openssl req -in request.csr -noout -pubkey | openssl sha256
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458

这个方法对 RSA、ECDSA、Ed25519 都适用,比只适用于 RSA 的 -modulus 老办法通用。第三行顺便核对 CSR。不想在命令行处理私钥,也可以用在浏览器本地运行的密钥匹配工具,私钥不会上传。

按指定日期验证

-attime 接受 Unix 时间戳,按那个时刻判断有效期,适合在计划换证之前预演。

openssl verify -attime $(date -d 2028-01-01 +%s) \
  -CAfile root.pem -untrusted intermediate.pem leaf.pem
CN=www.example.com
error 10 at 0 depth lookup: certificate has expired
error leaf.pem: verification failed

macOS 上把日期命令换成 date -j -f %Y-%m-%d 2028-01-01 +%s。错误 9 是“证书尚未生效”,多半是本机时钟不对而不是证书有问题。只想知道会不会快到期,用 openssl x509 -in leaf.pem -noout -checkend 2592000,30 天内到期时返回非零。

检查线上服务器实际发送的证书链

s_client 能看到服务器发了哪些证书,-verify_return_error 让验证失败直接中断握手。

openssl s_client -connect www.example.com:443 -servername www.example.com \
  -verify_return_error < /dev/null
depth=0 CN=www.example.com
verify error:num=20:unable to get local issuer certificate
...:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:...
---
Certificate chain
 0 s:CN=www.example.com
   i:C=CN, O=Example Corp, CN=Example Intermediate CA

不加 -verify_return_error 时,s_client 只打印错误但仍完成握手,最后显示 Verify return code: 21 (unable to verify the first certificate)。“Certificate chain”下面只有一张证书,就证实服务器没发中间证书。更多用法见测试 TLS 连接。

把服务器发来的证书逐张保存

-showcerts 配合一小段 awk,把整条链拆成 cert-1.pem、cert-2.pem……

openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null 2>/dev/null \
  | awk '/BEGIN CERT/{n++; f="cert-" n ".pem"} f{print > f} /END CERT/{f=""}'
for f in cert-*.pem; do openssl x509 -in "$f" -noout -subject; done
subject=CN=www.example.com
subject=C=CN, O=Example Corp, CN=Example Intermediate CA

cert-1.pem 是服务器证书,其余的可以作为 -untrusted 离线验证,也可以贴到证书解析工具里查看。最后一个文件如果是自签名证书,说明服务器把根证书也发出来了,这没有必要。

常见问题

浏览器打开正常,openssl verify 却报错,是谁的问题?

多半是服务器没发中间证书。浏览器会缓存见过的中间证书,部分平台还会按 AIA 地址自动下载,所以能掩盖证书链不完整;openssl verify 只用你给它的东西。加上 -untrusted 中间证书后能通过,就说明服务器配置里也缺这一张,参见缺少中间证书。

fullchain.pem 里要不要放根证书?

不用。客户端信任库里本来就有根证书,再发一遍只会让每次握手多几百字节到上千字节。证书链文件应当是服务器证书在前、中间证书在后。个别严格的客户端在链里看到不受信任的根证书时,还会报 self-signed certificate in certificate chain。

阿里云、腾讯云下载的 Nginx 证书包需要自己拼中间证书吗?

一般不需要。云厂商提供的 Nginx 格式 .pem/.crt 文件通常已经包含服务器证书和中间证书,直接配置到 ssl_certificate 即可。拿不准时用“检查证书文件里的顺序”一节的命令列一下,看到两张证书且第一张是你的域名就没问题。

资料来源

  1. OpenSSL 手册:openssl-verify
  2. OpenSSL 手册:openssl-verification-options
  3. OpenSSL 手册:openssl-s_client
  4. RFC 5280:X.509 证书与 CRL 规范