HTTPS依靠CA机构的信任链条来防止中间人攻击,但这条信任链本身并不完美。如果某个CA被入侵或者误签发了证书,攻击者就能拿到一张对你域名“合法有效”的证书,从而悄悄截获用户流量。证书锁定就是为了解决这个问题而生的:客户端不再盲目信任系统内置的几百个CA,而是只认准服务器预先约定好的那一把“锁”。这篇文章就来详细聊聊Certificate Pinning的原理、实现方式以及落地时的坑。

证书锁定的核心原理:从信任链到信任锚
标准的HTTPS验证流程是这样的:客户端收到服务器证书后,会沿着证书链向上追溯,直到找到一个是系统或浏览器内置信任的根证书为止。只要链条完整且域名匹配,验证就通过。问题在于全球有上百个受信任的CA,任何一个CA都可能有安全疏漏。2011年DigiNotar被黑客攻陷的事件就是典型案例,攻击者用伪造的Google证书监听了伊朗用户的Gmail流量。
证书锁定的思路是把验证逻辑反过来:客户端在开发阶段就把服务器证书的公钥哈希(通常叫pin)写死到代码或配置里。每次建立连接时,客户端计算服务器实际返回的证书公钥哈希,与本地存储的pin做比对,只有完全一致才放行连接。这样一来,哪怕攻击者从CA手里拿到了合法签发的证书,公钥对不上,连接照样会被拒绝。
需要注意的一点是,锁定的一般是公钥哈希而不是整张证书。原因很简单:证书有有效期,到期换发新证书时,如果公钥对继续沿用,pin就不用变。这张公钥的哈希计算方式通常是SHA-256,再做一次Base64编码,得到一个27到28字节左右的字符串,业界标准格式叫SPKI SHA-256指纹。
在Android和iOS上实现证书锁定
移动端是证书锁定应用最广泛的场景,因为App分发渠道可控,可以把pin打包进客户端。Android上如果使用OkHttp,配置非常直接,调用CertificatePinner即可:
String pin1 = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("api.ipipp.com", pin1)
// 强烈建议配置备用pin,避免证书轮换时App无法联网
.add("api.ipipp.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build())
.build();如果项目没用OkHttp,Android 7.0以上还提供了系统级的Network Security Config方案,在res/xml/network_security_config.xml中声明pin,这样所有走系统网络栈的请求都会生效:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.ipipp.com</domain>
<pin-set expiration="2026-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- 备用pin,防止主证书更换导致锁死 -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>取pin值可以用openssl一条命令搞定:openssl s_client -connect api.ipipp.com:443拿到证书后,再用openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64计算出SPKI哈希。iOS上则在NSURLSession的代理回调didReceiveChallenge里比对serverTrust中的公钥指纹,逻辑相同。
Web端的困境与HPKP的教训
浏览器端的证书锁定走的是另一条路,历史上有个叫HPKP(HTTP Public Key Pinning)的标准,通过响应头Public-Key-Pins向浏览器声明pin。但它有个致命缺陷:如果服务器私钥丢失或者配置写错,浏览器会在缓存期内(可能长达几个月)拒绝所有用户访问,而且恶意者甚至可以利用HPKP头对网站实施“反向勒索”——向用户浏览器植入错误pin导致正常站点无法访问。Chrome最终在2018年移除了HPKP支持。
目前Web端更主流的替代方案是Expect-CT和Certificate Transparency日志监控。CT要求CA把所有签发的证书记录到公开透明的日志中,站点运营者可以订阅监控服务,一旦发现自己域名下出现了陌生证书,立即报警并申请吊销。这不算严格意义上的锁定,但从风险发现的角度看,实际效果并不差。
对于企业内部系统或者高安全需求的Web应用,还可以考虑自建CA加私有信任的方案:客户端安装企业根证书,服务端只使用这个CA签发的证书,相当于人为把信任面收窄到一个CA。这比pinning运维成本低,但前提是你能控制所有客户端的证书安装。
证书轮换与降级方案:别把自己锁在门外
证书锁定最大的风险不是技术实现,而是运维事故。一旦你锁定了pin,而证书到期更换时生成了新密钥对,所有旧版本客户端都会立即断连,而且这种故障无法通过服务端热修复——App必须发新版。历史上不少知名App都栽过这个跟头。
规避这类事故有几个成熟做法。第一,配置至少两个pin:一个是当前证书的公钥,另一个是备份密钥对的公钥,备份私钥放在离线保险库中。换证书时直接启用备份密钥对,pin不变,客户端无需更新。第二,让新证书沿用同一对密钥,只更新有效期,很多证书颁发流程支持renew时复用CSR。第三,搭建远程配置下发机制,pin列表不写死在代码里,而是通过一个可独立更新的接口下发,同时内置一份兜底pin防止配置接口本身失联。
另外建议在客户端实现锁定失败的上报逻辑,当用户因为pin不匹配而连接失败时,把错误信息连同App版本号一起上报到监控平台。一旦出现大规模失败,你能第一时间判断是证书更换失误还是真的遭遇了中间人攻击,处置思路完全不同。安全性和可用性的平衡,从来都不是一句空话,而是需要这些工程细节来托底。
HTTPS证书锁定Certificate PinningSSL Pinning修改时间:2026-09-13 14:40:43