26. Android 中的 HPKP(HTTP Public Key Pinning):已被弃用,但需了解
说实话,每次讲到 HPKP,我都觉得有点唏嘘。这个技术当年被寄予厚望,结果因为太容易把自己玩死,最后被各大浏览器和 Android 系统给弃用了。但作为安全工程师,了解它为什么失败,比了解它怎么用更重要。
HPKP 的全称是 HTTP Public Key Pinning,翻译过来就是「HTTP 公钥固定」。它的核心思想很简单:客户端第一次访问服务器时,记住服务器证书链中的某个公钥,后续连接只信任这个公钥签发的证书。说白了,就是给服务器证书加了一把「指纹锁」。
核心概念:HPKP 允许 HTTPS 服务器在响应头中指定一个或多个公钥哈希值,客户端在后续连接中只接受匹配这些哈希值的证书链。
26.1 HPKP 的工作原理
我习惯把 HPKP 比作「第一次见面就记住你的长相」。客户端第一次访问服务器时,服务器通过 HTTP 响应头告诉客户端:「记住我的公钥指纹,以后别人冒充我,你就拒绝。」
具体流程是这样的:
- 客户端发起 HTTPS 请求到服务器
- 服务器返回证书链,同时在响应头中携带
Public-Key-Pins字段 - 客户端验证证书链有效后,提取指定公钥的 SHA256 哈希值并缓存
- 后续连接中,客户端检查服务器证书链中是否包含缓存的公钥哈希
- 如果不匹配,客户端直接拒绝连接
嗯,这里要注意:HPKP 只检查公钥,不检查证书的其他信息。这意味着即使证书过期、域名变更,只要公钥没变,HPKP 就认为连接是安全的。
26.2 HPKP 响应头格式
服务器需要在 HTTPS 响应头中设置 Public-Key-Pins 或 Public-Key-Pins-Report-Only。格式如下:
Public-Key-Pins:
pin-sha256="base64_encoded_hash1";
pin-sha256="base64_encoded_hash2";
max-age=expire_time_in_seconds;
includeSubDomains;
report-uri="https://report.example.com/hpkp-violation"
每个参数的含义:
| 参数 | 说明 |
|---|---|
pin-sha256 |
指定公钥的 SHA256 哈希值,至少需要两个:一个作为主 pin,一个作为备份 pin |
max-age |
客户端缓存 pin 的时间(秒),建议至少 60 天(5184000 秒) |
includeSubDomains |
可选,是否对子域名也生效 |
report-uri |
可选,当 pin 校验失败时,向该 URL 发送报告 |
⚠️ 重要警告:HPKP 必须至少指定两个 pin。一个用于当前证书的公钥,另一个作为备份。如果只指定一个 pin,一旦证书更换或私钥泄露,客户端将无法访问你的服务器,直到 max-age 过期。这就是所谓的「把自己锁在门外」。
26.3 在 Android 中实现 HPKP
虽然 Android 从 7.0(API 24)开始弃用了 HPKP,但了解它的实现方式仍然有价值。你想想看,很多老旧系统还在用,而且它的设计思路影响了后来的证书透明度(Certificate Transparency)和 Expect-CT 等机制。
在 Android 中,HPKP 可以通过两种方式实现:
方式一:使用 Network Security Config(推荐)
Android 7.0 引入的 Network Security Config 提供了更安全的证书固定方式。虽然它不叫 HPKP,但原理类似:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">base64_encoded_hash1</pin>
<pin digest="SHA-256">base64_encoded_hash2</pin>
</pin-set>
</domain-config>
</network-security-config>
我个人习惯把 Network Security Config 放在 res/xml/network_security_config.xml,然后在 AndroidManifest.xml 中引用:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
方式二:使用 OkHttp 的 CertificatePinner
如果你用的是 OkHttp 网络库,它提供了更灵活的证书固定机制:
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build())
.build();
我曾经在一个金融类项目里用过 OkHttp 的 CertificatePinner。当时客户要求必须固定证书,但又不愿意升级到 Android 7.0 以上。嗯,那会儿真是折腾了好一阵子。
26.4 HPKP 的致命缺陷
为什么 HPKP 会被弃用?说白了,它有几个设计上的硬伤:
- 锁死风险:一旦设置错误的 pin,或者备份 pin 丢失,客户端将永久无法访问服务器。我曾经见过一个案例,某公司更换了 CDN 服务商,结果忘了更新 HPKP 头,导致大量用户无法访问,整整瘫痪了两天。
- 恢复困难:即使发现错误,也只能等 max-age 过期。如果设置了 60 天的 max-age,那就得等 60 天才能恢复。
- 中间人攻击:攻击者可以在第一次连接时注入恶意 pin,后续所有连接都会被劫持。这就是所谓的「首次使用信任」(TOFU)问题。
- 运维复杂:证书轮换、备份 pin 管理、多 CDN 场景下的 pin 同步,都是噩梦。
💡 我的建议:如果你现在还在用 HPKP,赶紧迁移到 Certificate Transparency(CT)和 Expect-CT 头。CT 通过日志机制保证证书的公开可审计性,不会出现锁死问题。Android 从 7.0 开始强制要求 CT 验证,这才是正道。
26.5 知识体系图
下面这张图展示了 HPKP 的核心逻辑和替代方案:
26.6 避坑指南
如果你还在维护老旧系统,不得不使用 HPKP,记住这几条:
- 永远保留至少一个备份 pin:备份 pin 对应的私钥要离线保存,不要放在生产服务器上。我曾经见过有人把备份 pin 和主 pin 放在同一台服务器上,结果服务器被入侵,两个私钥全丢了。
- max-age 从短到长逐步增加:刚开始设置 5 分钟,确认没问题后再逐步增加到 1 天、7 天、30 天。别一上来就设 60 天,万一出问题哭都来不及。
- 使用 Report-Only 模式先测试:
Public-Key-Pins-Report-Only头只报告不拦截,可以用来验证 pin 是否正确。我建议至少运行一周的 Report-Only 模式,收集足够的报告后再切换到强制模式。 - 监控报告端点:如果设置了
report-uri,一定要有专人监控。很多团队设了报告端点却没人看,等于白设。
⚠️ 最后的忠告:HPKP 已经被 Android 7.0 弃用,Google 强烈建议不要使用。如果你还在用,尽快迁移到 Network Security Config 的 pin-set 或者 Certificate Transparency。别等到用户无法访问了才后悔。
好了,关于 HPKP 就讲这么多。虽然它已经退出了历史舞台,但它的设计思路和教训,对我们理解安全通信的本质很有帮助。记住:安全方案的设计,永远要在「安全」和「可用」之间找到平衡。