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

> 用 OpenSSL 3.x 验证证书链：-CAfile 与 -untrusted 的区别、主机名和用途检查、补齐缺失的中间证书、检查证书顺序、确认证书与私钥是否匹配。

Source: https://ssl.sh/zh/cookbook/verify-chains/
Last updated: 2026-11-10

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

## 用指定的根证书验证证书

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

```bash
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

```text
leaf.pem: OK
```

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

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

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

```bash
openssl verify -CAfile root.pem leaf.pem
```

```text
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.pem: verification failed
```

这句话的意思是“在深度 0（服务器证书）找不到给它签名的那张证书”。深度 1 是中间证书，深度 2 是根证书。真正的修复在服务器端：配置完整证书链（见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)），或者用[证书链补全工具](https://ssl.sh/zh/tools/chain-builder/)生成。

## 打印 OpenSSL 实际拼出的证书链

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

```bash
openssl verify -show_chain -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

```text
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` 下的默认信任库，它不一定和操作系统的信任库是同一份。

```bash
openssl version -d
openssl verify -untrusted chain.pem leaf.pem
```

```text
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` 传一次。

```bash
cat leaf.pem intermediate.pem > fullchain.pem
openssl verify -CAfile root.pem fullchain.pem
openssl verify -CAfile root.pem -untrusted fullchain.pem fullchain.pem
```

```text
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 的行为一致。

```bash
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
```

```text
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。

```bash
openssl verify -CAfile root.pem -untrusted intermediate.pem -purpose sslclient leaf.pem
openssl x509 -in leaf.pem -noout -ext extendedKeyUsage
```

```text
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 被移除](https://ssl.sh/zh/errors/client-auth-eku-removed/)和 [mTLS 原理](https://ssl.sh/zh/learn/mtls/)。

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

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

```bash
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
```

```text
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）。

```bash
openssl storeutl -noout -text -certs fullchain.pem | grep -E '^[0-9]+: |Subject:|Issuer:'
```

```text
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](https://ssl.sh/zh/errors/nginx-key-values-mismatch/)）。在宝塔面板里手动粘贴证书时，也要把自己的域名证书放在最上面。

## 确认证书和私钥是一对

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

```bash
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
```

```text
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
```

这个方法对 RSA、ECDSA、Ed25519 都适用，比只适用于 RSA 的 `-modulus` 老办法通用。第三行顺便核对 CSR。不想在命令行处理私钥，也可以用在浏览器本地运行的[密钥匹配工具](https://ssl.sh/zh/tools/key-matcher/)，私钥不会上传。

## 按指定日期验证

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

```bash
openssl verify -attime $(date -d 2028-01-01 +%s) \
  -CAfile root.pem -untrusted intermediate.pem leaf.pem
```

```text
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` 让验证失败直接中断握手。

```bash
openssl s_client -connect www.example.com:443 -servername www.example.com \
  -verify_return_error < /dev/null
```

```text
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 连接](https://ssl.sh/zh/cookbook/test-connections/)。

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

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

```bash
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
```

```text
subject=CN=www.example.com
subject=C=CN, O=Example Corp, CN=Example Intermediate CA
```

`cert-1.pem` 是服务器证书，其余的可以作为 `-untrusted` 离线验证，也可以贴到[证书解析工具](https://ssl.sh/zh/tools/certificate-decoder/)里查看。最后一个文件如果是自签名证书，说明服务器把根证书也发出来了，这没有必要。
