SSL 证书链顺序错误是 HTTPS 部署中最常见的问题之一。当浏览器或其他客户端无法正确验证服务器证书时,往往会显示安全警告,即使证书本身是有效的。本文将深入解析证书链的工作原理、常见错误原因,以及如何正确配置。

什么是证书链

SSL/TLS 证书采用链式信任机制。服务器证书由中间证书(Intermediate Certificate)签名,中间证书又由根证书(Root Certificate)签名,最终形成一条信任链:

1
2
3
4
5
6
7
浏览器内置根证书
    根证书 (Root CA)
    中间证书 (Intermediate CA)
    服务器证书 (Leaf Certificate)

在部署时,服务器只需要提供服务器证书中间证书(根证书由浏览器内置,无需提供)。

证书链顺序的重要性

证书链的顺序必须严格正确,否则:

  1. 客户端验证失败:浏览器无法建立信任链,显示"此连接不私密"或"NET::ERR_CERT_AUTHORITY_INVALID"
  2. 部分客户端信任失败:某些客户端(如旧版 Android、iOS)可能无法自动获取缺失的中间证书
  3. 性能问题:客户端需要额外查询来获取缺失的证书,增加握手延迟

正确的证书链顺序

证书链的正确顺序是:服务器证书 → 中间证书 → 根证书(可选)

1
2
3
4
5
6
7
8
9
-----BEGIN CERTIFICATE-----
服务器证书 (Your Server Certificate)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
中间证书 (Intermediate CA Certificate)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
根证书 (Root CA Certificate - 可选,通常不需要)
-----END CERTIFICATE-----

为什么是这个顺序

证书链的顺序遵循 RFC 5246 和 RFC 8446 的规定:

  • 从叶子到根:客户端从服务器证书开始,逐级向上验证直到根证书
  • 每一步都需要上级的证书:服务器证书需要中间证书来验证,中间证书需要根证书来验证
  • 顺序有助于解析:按顺序排列时,客户端可以依次读取并验证

常见错误:证书链顺序颠倒

最常见的错误是将证书链顺序颠倒,即把中间证书放在服务器证书之前:

1
2
3
4
5
6
7
错误示例:
-----BEGIN CERTIFICATE-----
中间证书 (Intermediate CA)  ← 错误:应该在后面
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
服务器证书 (Your Server Certificate)  ← 错误:应该在前面
-----END CERTIFICATE-----

这种错误会导致验证失败,因为客户端首先读到的是它无法直接信任的证书。

如何检查当前证书链顺序

使用 OpenSSL 查看证书链

1
2
3
4
5
# 查看服务器证书链(按显示顺序)
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -subject -issuer

# 完整证书链输出
echo | openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts 2>/dev/null

使用 OpenSSL 检查证书链顺序

1
2
3
4
5
# 将证书链保存到文件
echo | openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts 2>/dev/null | sed -n '/-----BEGIN/,/-----END/p' > chain.pem

# 逐个显示证书顺序
openssl crl2pkcs7 -nocrl -certchain CHAIN.pem | openssl pkcs7 -print_certs -noout -text | grep -A1 "Issuer:\|Subject:"

使用 testssl.sh 检测证书链

1
2
3
4
5
# 全面检测证书链配置
docker run --rm -ti drwetter/testssl.sh:latest --starttls xmpp(domain example.com) 2>/1 || true

# 或检测 HTTPS
docker run --rm -ti drwetter/testssl.sh:latest https://www.example.com/

正确配置证书链

Nginx 配置

在 Nginx 中,使用 ssl_certificate 指定服务器证书,ssl_certificate_key 指定私钥,证书链需要包含在服务器证书文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
server {
    listen 443 ssl http2;
    server_name example.com;

    # 服务器证书(包含证书链)
    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # ...
}

重要:Nginx 要求服务器证书文件中必须包含完整的证书链(服务器证书 + 中间证书)。

生成包含证书链的 .crt 文件

1
2
3
4
5
6
7
8
9
# 方法1:手动拼接(顺序很重要)
cat server.crt intermediate.crt > example.com.crt

# 方法2:从 fullchain.pem 复制(Let's Encrypt 使用)
cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/nginx/ssl/example.com.crt

# 验证最终文件包含正确数量的证书
openssl crl2pkcs7 -nocrl -certchain example.com.crt | openssl pkcs7 -print_certs -noout | grep "subject=" | wc -l
# 应该输出 2(服务器证书 + 中间证书)或更多

Apache 配置

1
2
3
4
5
6
7
8
<VirtualHost *:443>
    ServerName example.com
    
    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/example.com.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.com.key
    SSLCertificateChainFile /etc/apache2/ssl/ca-bundle.crt
</VirtualHost>

使用 OpenSSL 验证配置

1
2
3
4
# 测试 Nginx 配置中的证书链
openssl s_client -connect 127.0.0.1:443 -servername example.com -showcerts 2>/dev/null | grep "Verify return code"

# 应该输出:Verify return code: 0 (ok)

常见问题与解决方案

问题1:浏览器显示证书链不完整

症状:某些浏览器显示证书链错误,但使用 OpenSSL 验证正常。

原因:中间证书缺失或顺序错误。

解决

1
2
3
4
5
6
# 获取完整的证书链
# 1. 下载中间证书
wget -O intermediate.crt https://letsencrypt.org/certs/lets-encrypt-r3.pem

# 2. 追加到服务器证书
cat server.crt intermediate.crt > fullchain.crt

问题2:自签名证书部署后不被信任

原因:自签名证书的根证书未部署到客户端。

解决

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 生成完整的证书链(自签名场景)
# 1. 创建根 CA
openssl genrsa -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.crt

# 2. 用根 CA 签发服务器证书
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 365 -sha256

# 3. 打包证书链
cat server.crt rootCA.crt > server-chain.crt

# 4. 将 rootCA.crt 部署到客户端信任存储

问题3:多个中间证书的顺序

对于需要多个中间证书的情况(如交叉证书),顺序尤为重要:

1
2
# 正确的顺序:服务器证书 → 中间证书1 → 中间证书2 → 根证书
cat server.crt intermediate1.crt intermediate2.crt root.crt > fullchain.crt

自动修复证书链顺序

如果现有证书顺序错误,可以使用 OpenSSL 重新排序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# 修复证书链顺序

INPUT="$1"
OUTPUT="$2"

if [ -z "$INPUT" ] || [ -z "$OUTPUT" ]; then
    echo "用法: $0 <输入证书文件> <输出证书文件>"
    exit 1
fi

# 提取所有证书并按正确顺序写入
openssl crl2pkcs7 -nocrl -certchain "$INPUT" 2>/dev/null | \
    openssl pkcs7 -print_certs -noout -text | \
    grep -B1 "Issuer:\|Subject:" | \
    grep "Subject:" | \
    awk '{print $2}' > /tmp/_order.txt

# 重新拼接(简化的方法:先获取所有证书再按正确顺序拼接)
# 更可靠的方法是手动检查并按顺序拼接

更简单的方法是使用 Let’s Encrypt 的 fullchain.pem

1
2
3
# Let's Encrypt 证书已经包含完整证书链
# 直接使用 fullchain.pem
cp /etc/letsencrypt/live/example.com/fullchain.pem /path/to/server.crt

验证证书链配置

完成配置后,务必验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 检查证书链完整性
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | grep "Certificate chain" -A 20

# 2. 验证返回码(0 表示成功)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -verify_return_code

# 3. 检查各个证书的 Subject 和 Issuer
echo | openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
    openssl x509 -noout -subject -issuer

# 4. 使用在线工具检测
# https://www.ssllabs.com/ssltest/

总结

证书链顺序是 HTTPS 部署中的关键细节:

  1. 记住正确顺序:服务器证书 → 中间证书 → 根证书(可选)
  2. 始终验证:部署后使用 OpenSSL 测试验证
  3. 使用完整链:推荐使用 fullchain.pem 等已包含完整链的证书文件
  4. 测试多客户端:不同浏览器和客户端对证书链的处理可能有细微差异

正确的证书链配置是确保 HTTPS 服务稳定、可信的基础。


参考来源