证书固定(Certificate Pinning)是一种安全加固技术,通过将网站的身份与特定的证书或公钥绑定,防止攻击者使用伪造证书进行中间人攻击(MITM)。本文详细介绍证书固定的工作原理、实现方式和实战配置。
什么是证书固定#
传统 HTTPS 验证依赖证书链信任机制:由浏览器内置的根证书库验证网站证书是否由可信 CA 签发。然而,如果攻击者能控制用户的根证书(如通过恶意软件或某些网络审查工具),就可以使用任意 CA 签发的伪造证书实施中间人攻击。
证书固定的核心思想是:不仅验证证书是否可信,还验证证书是否是我们预期的那一个。通过预先在客户端保存服务器的公钥指纹或证书指纹,客户端只会接受固定的那个证书,其他任何证书(包括合法 CA 签发的)都会被拒绝。
固定方式#
1. 公钥指纹固定#
直接固定服务器的公钥指纹,这是最推荐的方式。公钥是证书中最稳定的部分,即使证书续期,只要不更换密钥,指纹就保持不变。
1
2
3
4
5
6
|
# 提取证书公钥并计算 SHA-256 指纹
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64
|
输出示例:
1
|
aqQ4L+Pac7Qy3Or7l6f9IypN8w1H64i48B4weiXJ2v4=
|
2. 证书指纹固定#
直接固定整个证书的 SHA-256 指纹:
1
2
3
|
# 提取证书 SHA-256 指纹
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -fingerprint -sha256 -noout
|
输出示例:
1
|
sha256 Fingerprint=0D:82:2C:9A:90:5A:EF:E9:8F:37:12:C0:E0:26:30:EE:95:33:2C:45:5F:E7:74:5D:F0:8D:BC:79:F4:B0:A1:49
|
3. SPKI 固定(Subject Public Key Info)#
固定 SPKI 哈希,兼容公钥指纹但更标准化:
1
2
3
4
5
6
|
# 提取 SPKI 并计算哈希
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64
|
HTTP 公钥固定(HPKP)#
HPKP 是曾经的标准机制,允许服务器通过 HTTP 响应头告诉浏览器固定哪个公钥:
1
|
Public-Key-Pins: pin-sha256="base64=="; max-age=2592000; includeSubDomains
|
⚠️ 注意:HPKP 已被主流浏览器废弃,原因是:
- 配置错误会导致网站永久无法访问
- 存在隐私风险(可以被用于追踪用户)
请勿在新项目中使用 HPKP,而应在客户端应用层面实现证书固定。
客户端实现示例#
Curl 验证固定#
使用 curl 配合公钥指纹验证连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 先获取并保存目标服务器的公钥指纹
EXPECTED_PIN="aqQ4L+Pac7Qy3Or7l6f9IypN8w1H64i48B4weiXJ2v4="
# 获取实际连接的公钥指纹
ACTUAL_PIN=$(openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64)
if [ "$EXPECTED_PIN" = "$ACTUAL_PIN" ]; then
echo "证书固定验证通过"
else
echo "警告:证书不匹配,可能存在中间人攻击!"
exit 1
fi
|
Nginx 配置(配合客户端证书)#
虽然 Nginx 本身不直接支持 HPKP,但可以配置客户端证书验证来实现类似效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /path/to/server.crt;
ssl_certificate_key /path/to/server.key;
# 启用客户端证书验证
ssl_verify_client on;
ssl_client_certificate /path/to/ca.crt;
# 验证客户端证书指纹
# 在 $ssl_client_cert 中获取证书,进行固定验证
}
|
多证书固定策略#
生产环境建议固定多个证书,防止单点故障:
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
|
# Python 示例:在应用层实现证书固定
import hashlib
import base64
# 允许的公钥指纹列表(包含当前证书和备用证书)
ALLOWED_PINS = [
"aqQ4L+Pac7Qy3Or7l6f9IypN8w1H64i48B4weiXJ2v4=", # 当前证书
"dGhpcyBpcyBhbiBleGFtcGxlIGZpbmdlcnByaW50IQ==", # 备用证书
]
def verify_certificate_pinning(cert_pem):
"""验证服务器证书是否在允许的固定列表中"""
# 提取公钥并计算 SHA-256 哈希
# 这里使用 cryptography 库处理
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import Encoding
cert = x509.load_pem_x509_certificate(cert_pem.encode())
public_key = cert.public_key()
public_key_der = public_key.public_bytes(
Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
)
pin = base64.b64encode(
hashlib.sha256(public_key_der).digest()
).decode()
return pin in ALLOWED_PINS
|
证书固定的最佳实践#
-
优先固定公钥而非证书:公钥在证书续期时保持不变,减少维护成本
-
提供备用固定点:至少固定两个证书,防止密钥轮换导致服务中断
-
固定时间不宜过长:建议 3-6 个月,过长会增加密钥泄露的风险
-
实施监控告警:监控固定验证失败的情况,及时发现攻击或配置问题
-
结合 HSTS 使用:配合 Strict-Transport-Security 头实现完整的安全加固
-
移动端优先实现:移动应用更容易控制客户端代码,是实现证书固定的最佳场景
验证固定是否生效#
使用以下命令模拟非固定证书的连接:
1
2
3
4
|
# 使用自签名证书测试客户端是否会拒绝
openssl s_client -connect example.com:443 \
-CAfile /path/to/untrusted-ca.crt \
-showcerts
|
如果客户端正确实现了证书固定,即使证书链验证通过,也应该拒绝连接。
证书固定是防御中间人攻击的有效手段,尤其适用于:
- 移动应用与后端 API 通信
- 金融交易类应用
- 企业内部系统
- 处理敏感数据的 API
虽然 HPKP 已废弃,但在应用层实现证书固定仍然是最佳安全实践。关键在于:提前规划、正确实现、提供备用方案。
参考来源