SSL/TLS 证书是互联网信任体系的基石。本文深入剖析证书的工作原理、类型选择、配置最佳实践,以及常见问题的排查方法。

一、证书工作原理

TLS 握手过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
客户端                                    服务器
   |                                        |
   |  1. ClientHello                        |
   |  (支持的加密套件、TLS版本、随机数)      |
   | --------------------------------------> |
   |                                        |
   |  2. ServerHello                        |
   |  (选定加密套件、TLS版本、随机数)        |
   |  + Certificate (服务器证书链)           |
   |  + ServerKeyExchange (可选)            |
   |  + CertificateRequest (可选,双向认证)   |
   | <-------------------------------------- |
   |                                        |
   |  3. 客户端验证证书                       |
   |    - 检查证书有效期                     |
   |    - 验证证书链                         |
   |    - 检查吊销状态 (CRL/OCSP)            |
   |                                        |
   |  4. ClientKeyExchange                  |
   |  (预主密钥加密发送)                     |
   |  + CertificateVerify (双向认证时)       |
   |  + ChangeCipherSpec                    |
   |  + Finished                            |
   | --------------------------------------> |
   |                                        |
   |  5. ChangeCipherSpec + Finished        |
   | <-------------------------------------- |
   |                                        |
   |  ========== 加密通道建立 ============   |

证书链验证

证书链是一个信任链条:

1
2
3
根证书 (Root CA)
    └── 中间证书 (Intermediate CA)
            └── 服务器证书 (Leaf Certificate)

验证流程:

  1. 服务器证书由中间证书签名
  2. 中间证书由根证书签名
  3. 根证书预置于浏览器/操作系统中
  4. 逐级验证签名有效性

二、证书类型详解

按验证级别分类

类型 验证内容 显示效果 适用场景
DV (域名验证) 仅验证域名所有权 锁图标 个人博客、测试环境
OV (组织验证) 验证组织身份 锁图标+公司名 企业网站、电商平台
EV (扩展验证) 严格身份验证 地址栏显示公司名 金融、支付系统

按功能分类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 单域名证书
CN = www.example.com

# 通配符证书
CN = *.example.com
# 保护: www.example.com, api.example.com, mail.example.com
# 不保护: sub.api.example.com (多级子域名)

# 多域名证书 (SAN)
Subject Alternative Name:
    DNS:example.com
    DNS:www.example.com
    DNS:api.example.com
    DNS:shop.example.net

# 通配符+多域名
*.example.com + www.example.org + api.example.io

自签名证书 vs CA 签发

1
2
3
4
5
6
# 自签名证书(仅用于测试)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"

# 优点:免费、快速
# 缺点:浏览器不信任、需要手动信任

三、使用 OpenSSL 管理证书

查看证书信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查看证书详情
openssl x509 -in cert.pem -text -noout

# 查看证书主体
openssl x509 -in cert.pem -subject -noout

# 查看证书颁发者
openssl x509 -in cert.pem -issuer -noout

# 查看证书有效期
openssl x509 -in cert.pem -dates -noout

# 查看证书公钥
openssl x509 -in cert.pem -pubkey -noout

# 查看 PEM 格式证书指纹
openssl x509 -in cert.pem -fingerprint -sha256 -noout

# 查看远程服务器证书
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -text -noout

# 提取远程证书
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -out remote_cert.pem

# 查看 PKCS#12 (.p12/.pfx) 文件
openssl pkcs12 -in file.p12 -info -noout

证书格式转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# PEM → DER
openssl x509 -in cert.pem -outform DER -out cert.der

# DER → PEM
openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM

# PEM → PKCS#12
openssl pkcs12 -export -out cert.p12 -inkey key.pem -in cert.pem -certfile chain.pem

# PKCS#12 → PEM
openssl pkcs12 -in cert.p12 -out cert_and_key.pem -nodes

# 仅提取私钥
openssl pkcs12 -in cert.p12 -nocerts -out key.pem -nodes

# 仅提取证书
openssl pkcs12 -in cert.p12 -nokeys -out cert.pem

# PEM → PKCS#7
openssl crl2pkcs7 -nocrl -certfile cert.pem -certfile chain.pem -out cert.p7b

# PKCS#7 → PEM
openssl pkcs7 -in cert.p7b -print_certs -out cert.pem

验证证书链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 验证证书
openssl verify cert.pem

# 使用指定 CA 验证
openssl verify -CAfile ca.pem cert.pem

# 验证证书链
openssl verify -CAfile root.pem -untrusted intermediate.pem cert.pem

# 验证私钥和证书是否匹配
# 方法1:比较公钥指纹
openssl x509 -in cert.pem -pubkey -noout | openssl md5
openssl rsa -in key.pem -pubout | openssl md5

# 方法2:直接验证
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5

证书吊销检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 检查 OCSP
openssl ocsp -issuer ca.pem -cert cert.pem -url http://ocsp.example.com -resp_text

# 从证书提取 OCSP URL
openssl x509 -in cert.pem -ocsp_uri -noout

# 从证书提取 CRL URL
openssl x509 -in cert.pem -text -noout | grep -A5 "CRL Distribution"

# 下载并验证 CRL
openssl x509 -in cert.pem -text -noout | grep -A4 "CRL Distribution" | grep http | xargs wget -O crl.pem
openssl crl -in crl.pem -inform DER -outform PEM -out crl_conv.pem
openssl verify -crl_check -CAfile ca.pem -CRLfile crl_conv.pem cert.pem

四、使用 Let’s Encrypt 免费证书

Certbot 基本使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 安装 Certbot
apt update && apt install -y certbot python3-certbot-nginx

# Webroot 方式申请证书
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

# Standalone 方式(需要停止 Web 服务)
certbot certonly --standalone -d example.com

# Nginx 自动配置
certbot --nginx -d example.com -d www.example.com

# Apache 自动配置
certbot --apache -d example.com

# 申请通配符证书(需要 DNS 验证)
certbot certonly --manual --preferred-challenges dns -d '*.example.com' -d example.com

# 使用 DNS 插件(Cloudflare 示例)
pip install certbot-dns-cloudflare
certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d '*.example.com'

证书续期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 测试续期(dry-run)
certbot renew --dry-run

# 手动续期
certbot renew

# 强制续期
certbot renew --force-renewal

# 续期特定证书
certbot renew --cert-name example.com

# 设置自动续期(Cron)
echo "0 0,12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | tee -a /etc/crontab > /dev/null

证书文件位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Let's Encrypt 证书目录
/etc/letsencrypt/live/example.com/
├── cert.pem      # 服务器证书
├── chain.pem     # 中间证书
├── fullchain.pem # 完整证书链(服务器+中间)
└── privkey.pem   # 私钥

# Nginx 配置示例
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# Apache 配置示例
SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

五、TLS 配置最佳实践

Nginx TLS 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
server {
    listen 443 ssl http2;
    server_name example.com;

    # 证书配置
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 协议版本
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # 加密套件
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # ECDH 曲线
    ssl_ecdh_curve secp384r1;

    # 会话缓存
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    # 其他安全头
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
}

# HTTP 重定向 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

Apache TLS 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

    # 协议和加密套件
    SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    SSLHonorCipherOrder off

    # HSTS
    Header always set Strict-Transport-Security "max-age=63072000"
    
    # OCSP Stapling
    SSLUseStapling on
    SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
</VirtualHost>

测试 TLS 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 使用 testssl.sh
git clone --depth 1 https://github.com/drwetter/testssl.sh.git
cd testssl.sh
./testssl.sh https://example.com

# 使用 nmap 脚本
nmap --script ssl-enum-ciphers -p 443 example.com

# 使用 openssl 测试协议
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# 测试特定加密套件
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'

# 在线测试
# https://www.ssllabs.com/ssltest/

六、证书问题排查

常见错误及解决

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 错误1:证书链不完整
# 症状:浏览器显示证书有效,但 curl/移动端报错
# 解决:使用 fullchain.pem 而非 cert.pem

# 检查证书链
openssl s_client -connect example.com:443 -servername example.com

# 正确的输出应包含完整的证书链:
# s:CN = example.com
# i:C = US, O = Let's Encrypt, CN = R3
# s:C = US, O = Let's Encrypt, CN = R3
# i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

# 错误2:证书过期
# 检查有效期
openssl x509 -in cert.pem -dates -noout
# notBefore=Mar 24 00:00:00 2024 GMT
# notAfter=Jun 22 00:00:00 2024 GMT

# 错误3:域名不匹配
# 检查证书包含的域名
openssl x509 -in cert.pem -text -noout | grep -A1 "Subject Alternative Name"

# 错误4:私钥不匹配
# 验证私钥和证书的模数是否一致
openssl x509 -noout -modulus -in cert.pem | md5sum
openssl rsa -noout -modulus -in key.pem | md5sum

# 错误5:时间不同步
# 检查系统时间
date
# 同步时间
ntpdate pool.ntp.org
# 或
timedatectl set-ntp true

吊销状态检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 在线检查证书状态
# https://crt.sh/ - 查看证书透明度日志
# https://www.ssllabs.com/ssltest/ - 综合测试

# 命令行 OCSP 检查
# 1. 获取 OCSP URL
openssl x509 -in cert.pem -ocsp_uri -noout

# 2. 获取颁发者证书
openssl x509 -in cert.pem -issuer -noout

# 3. 执行 OCSP 查询
openssl ocsp -issuer issuer.pem -cert cert.pem -url http://ocsp.int-x3.letsencrypt.org -resp_text

七、证书自动化管理

acme.sh 脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 安装
curl https://get.acme.sh | sh -s email=admin@example.com

# 申请证书
acme.sh --issue -d example.com -d www.example.com --nginx

# DNS API 方式(支持通配符)
export CF_Key="your_cloudflare_api_key"
export CF_Email="your_email@example.com"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

# 安装证书到指定位置
acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/example.com/key.pem \
  --fullchain-file /etc/nginx/ssl/example.com/cert.pem \
  --reloadcmd "nginx -s reload"

# 自动续期
# acme.sh 会自动添加 cron 任务
# 查看: crontab -l

证书监控脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# cert_check.sh - 证书过期监控

DOMAINS=("example.com" "api.example.com" "shop.example.com")
ALERT_DAYS=30

for domain in "${DOMAINS[@]}"; do
    expiry_date=$(openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
    
    expiry_epoch=$(date -d "$expiry_date" +%s)
    current_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - current_epoch) / 86400 ))
    
    echo "域名: $domain, 剩余天数: $days_left"
    
    if [ $days_left -lt $ALERT_DAYS ]; then
        echo "警告: $domain 证书将在 $days_left 天后过期!"
        # 发送告警(邮件/钉钉/Slack)
        # curl -X POST "webhook_url" -d "text=证书告警: $domain 即将过期"
    fi
done

参考来源