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)
|
验证流程:
- 服务器证书由中间证书签名
- 中间证书由根证书签名
- 根证书预置于浏览器/操作系统中
- 逐级验证签名有效性
二、证书类型详解#
按验证级别分类#
| 类型 |
验证内容 |
显示效果 |
适用场景 |
| 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
|
参考来源#