1. HTTPS 证书固定原理
证书固定(Certificate Pinning)是一种将客户端(支付 SDK)与特定服务器证书或公钥绑定的安全策略。在支付场景中,它能够有效防止中间人攻击(MITM),确保交易数据只被真实的支付网关处理。理解其原理需要先掌握 TLS 握手流程、证书链验证机制,以及与动态信任的差异。
1.1 TLS 握手核心流程
TLS 1.2/1.3 握手的主要步骤(忽略密钥协定细节):
- ClientHello:客户端发送支持的 TLS 版本、密码套件和随机数。
- ServerHello + Certificate:服务端选定版本/套件,发送其数字证书(含公钥)。
- 证书链验证:客户端内置的 CA 信任存储对服务端证书链逐级认证。
- 密钥交换:客户端生成预主密钥,用服务端公钥加密后发送;服务端解密。
- Finished:双方完成握手,开始加密通信。
其中第 3 步是静态信任的核心——客户端依赖系统 CA 列表判断证书是否由受信任的根签发。但这种方式存在风险:如果攻击者能控制系统 CA 列表(如植入恶意根证书),则可冒充支付服务器。
1.2 证书链验证机制
| 层级 | 角色 | 验证要点 |
|---|---|---|
| 叶子证书 | 最终服务器证书 | 域名匹配、有效期、密钥用法 |
| 中间证书 | 签发叶子证书的 CA | 必须由受信任的根 CA 签发 |
| 根证书 | CA 信任锚点 | 客户端预置的根证书(自签信任) |
标准验证流程:客户端从叶子证书向上构建链,直到找到一个本地信任的根证书;同时检查每级证书的签名、有效期和 CRL/OCSP 吊销状态。如果链中任何一环失败,握手终止。
支付场景的难点:支付网关的证书可能由多家 CA 签发(如备用证书轮换),而系统 CA 列表会随操作系统更新变化。单纯信任系统 CA 无法防范“中间人使用了合法的被信任 CA 颁发的假证书”的攻击。
1.3 固定与动态信任的区别
| 对比维度 | 动态信任(标准 TLS) | 证书固定(Pinning) |
|---|---|---|
| 信任基础 | 系统 CA 存储 | 硬编码的证书指纹或公钥 |
| 灵活性 | 高,CA 可随时签发新证书 | 低,变更需升级 SDK |
| 安全性 | 受 CA 生态影响(CA 被攻陷则危险) | 强,仅接受特定证书/公钥 |
| 运维成本 | 低,证书自动续期 | 需要预置多个备用指纹并定期更新 |
| 对中间人攻击的防护 | 弱(若 CA 被滥用) | 强(即使 CA 信任链被破坏) |
1.4 证书固定策略的两种形式
- 证书固定(Certificate Pinning):直接固定服务器叶子证书的 SHA-256 指纹。证书过期或更换时需要更新 SDK。
- 公钥固定(Public Key Pinning):固定证书中公钥(SPKI)的哈希。证书更换但公钥不变时仍可正常通信,更灵活。
⚠ 关键警告:支付 SDK 中固定公钥时需要同时预置多个备用公钥(至少一个生产、一个灾备),避免因服务端证书轮换导致所有支付请求失败。推荐使用 HPKP(HTTP Public Key Pinning)的备份机制,但注意 HPKP 已被弃用,现代移动端建议使用本地代码硬编码。
1.5 TLS 握手与证书固定交互流程图
1.6 支付 SDK 中的固定实现要求
- 固定内容:推荐固定公钥的 SPKI 指纹(SHA-256 编码),而非整个证书。
- 备份机制:至少预置 2-3 个备用公钥,允许服务端在证书轮换时顺利过渡。
- 更新通道:通过应用商店或热修复更新固定的指纹列表,避免长期依赖同一组。
- 调试模式:开发阶段可临时关闭固定检查,但生产包绝不能去掉该逻辑。
💡 最佳实践:使用 Android 的
CertificatePinner(OkHttp)或 iOS 的 URLSession 的 SecTrustEvaluate 自定义验证回调来实施固定。避免自己实现复杂的证书链解析,利用框架提供的接口更安全。
1.7 常见误区
| 误区 | 说明 |
|---|---|
| 固定域名而非证书 | 域名固定无法抵御 DNS 劫持与伪造证书攻击 |
| 只固定叶子证书指纹 | 证书更换时支付功能立即中断;应同时固定多个指纹 |
| 混淆固定与双向验证 | 固定是客户端验证服务器,双向验证还需要服务端验证客户端证书 |