证书固定(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

证书固定的最佳实践

  1. 优先固定公钥而非证书:公钥在证书续期时保持不变,减少维护成本

  2. 提供备用固定点:至少固定两个证书,防止密钥轮换导致服务中断

  3. 固定时间不宜过长:建议 3-6 个月,过长会增加密钥泄露的风险

  4. 实施监控告警:监控固定验证失败的情况,及时发现攻击或配置问题

  5. 结合 HSTS 使用:配合 Strict-Transport-Security 头实现完整的安全加固

  6. 移动端优先实现:移动应用更容易控制客户端代码,是实现证书固定的最佳场景

验证固定是否生效

使用以下命令模拟非固定证书的连接:

1
2
3
4
# 使用自签名证书测试客户端是否会拒绝
openssl s_client -connect example.com:443 \
  -CAfile /path/to/untrusted-ca.crt \
  -showcerts

如果客户端正确实现了证书固定,即使证书链验证通过,也应该拒绝连接。

总结

证书固定是防御中间人攻击的有效手段,尤其适用于:

  • 移动应用与后端 API 通信
  • 金融交易类应用
  • 企业内部系统
  • 处理敏感数据的 API

虽然 HPKP 已废弃,但在应用层实现证书固定仍然是最佳安全实践。关键在于:提前规划、正确实现、提供备用方案。


参考来源