SSL Pinning — 一种安全技术,应用程序基于事先已知的指纹或证书来验证服务器证书,而不是依赖CA信任链。与标准验证不同,pinning可防止通过伪造根证书颁发机构拦截流量。根据OWASP移动安全测试指南(2025),该技术位列抵御MITM攻击的前三项推荐控制措施。如果没有pinning,拥有伪造根证书的攻击者可以解密应用程序的所有HTTPS流量。
要点
SSL Pinning — 是一种安全机制,移动或Web应用程序记住服务器的可信证书或公钥,并拒绝任何证书与存储不匹配的连接。在标准HTTPS方案中,客户端通过信任链验证证书直到根CA——任何CA都可以为任何域签署证书。SSL Pinning消除了这一弱点:应用程序不信任数百个CA,而只信任一个特定的证书。
标准验证的问题在于,数百个根CA中的任何一个都可以为您的域颁发有效证书——无论是偶然的还是被迫的。获得具有自己根证书的企业代理访问权限的攻击者可以在没有浏览器警告的情况下执行MITM攻击。SSL Pinning堵住了这个漏洞:即使CA颁发了伪造证书,应用程序也会拒绝它,因为指纹与记录的不匹配。
在移动应用中,SSL Pinning尤为重要,因为设备经常在不安全的网络中运行——公共WiFi、进行流量检查的企业代理、受感染的接入点。根据Verizon移动安全指数(2025),移动应用中超过60%的数据泄露与传输层的流量拦截有关。
移动应用传输敏感数据——身份验证令牌、支付信息、用户的个人数据。如果没有额外的保护,HTTPS可能因设备上根证书的替换而受到威胁——例如在安装企业配置文件或恶意应用程序之后。SSL Pinning保证,即使设备上安装了伪造的根CA,应用程序也会继续根据其自身的白名单验证证书。
SSL Pinning过程包括三个阶段:获取指纹、连接时验证和错误处理。在开发阶段,工程师获取服务器证书的SHA-256指纹(openssl x509 -fingerprint -sha256),并将其嵌入应用程序代码或配置文件中。在每个HTTPS请求中,应用程序计算接收到的证书的指纹,并将其与存储的进行比较——如果值不匹配,连接将中断。
第一阶段 — 构建阶段的pinning:开发人员事先知道服务器证书并嵌入其哈希值。第二阶段 — 首次连接时的pinning(首次使用时信任,TOFU):应用程序在第一次请求时记住证书,并用它来验证所有后续请求。TOFU适用于动态环境,但在首次攻击时容易受到攻击——如果第一次连接已被拦截,伪造的证书将被视为可信。
关键细节——备用指纹(backup pins)。证书有有效期,更换时未更新的应用程序将失去与服务器的连接。工程师添加2–3个额外的指纹——例如备用证书的指纹和根CA的指纹。如果主证书更改,应用程序根据备用pin进行验证,连接继续工作。
# 获取证书的SHA-256指纹
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64
实现pinning有两种主要方法:绑定到整个证书(certificate pinning)和绑定到公钥(public key pinning)。每种方法都有其优点和局限性,影响安全性和维护便利性。
| 类型 | 固定对象 | 灵活性 | 安全性 |
|---|---|---|---|
| Certificate Pinning | 整个X.509证书 | 低 — 更换证书时需要更新 | 高 — 精确绑定 |
| Public Key Pinning | 证书的公钥 | 中 — 密钥可能在新证书中 | 高 — 对证书细节不太敏感 |
| Hash Pinning | 证书或密钥的SHA-256哈希 | 高 — 可以更换证书而不更改密钥 | 中 — 取决于哈希的强度 |
证书绑定 — 最严格的方法。应用程序存储可信证书的副本或其SHA-256指纹,并在每次HTTPS连接时与服务器证书进行比较。这种方法提供最大的安全性,但在轮换时会产生问题——证书通常有效1–2年,之后需要强制更新应用程序。推荐用于具有受控更新周期的关键系统。
公钥固定 — 一种更灵活的方法。应用程序不是记住整个证书,而是只记住服务器的RSA或ECDSA公钥。如果公司使用相同的密钥对,密钥在重新颁发证书时可以保持不变。这降低了应用程序更新的频率。但是,如果密钥被泄露,则需要在所有客户端进行级联替换。
在Apple平台上,SSL Pinning通过URLSession委托实现。开发人员创建一个实现URLSessionDelegate协议的类,并重写didReceive challenge方法,在其中手动验证服务器证书与存储的指纹。另一种方法——使用Alamofire及其ServerTrustManager,它简化了配置。
class SSLPinningDelegate: NSObject, URLSessionDelegate {
let pinnedHash = "sha256/Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg="
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust
else { return completionHandler(.cancelAuthenticationChallenge, nil) }
if validate(serverTrust, pinnedHash) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
在示例中,委托从URLSession接收身份验证请求,从challenge中提取serverTrust,并将证书的SHA-256指纹与存储的进行比较。如果指纹匹配——连接继续,否则challenge被拒绝。对于生产环境,建议添加多个备用pin的检查和错误记录以进行监控。
从iOS 14开始,Apple通过Info.plist添加了对Certificate Pinning的内置支持。开发人员在NSAppTransportSecurity键中指定受信任的证书,并使用NSPinnedDomains子字典。这种方法不需要编写代码,但灵活性较低——无法动态更改pin或记录验证错误。
在Android上,有三种主要方法可以实现SSL Pinning:通过OkHttp库的CertificatePinner、通过XML中的Network Security Config以及通过HttpsURLConnection中的自定义验证。OkHttp是最流行和推荐的方法,用于Retrofit和其他HTTP客户端。
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com",
"sha256/Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg=")
.add("api.example.com",
"sha256/FiPq6ePEsVRqZ1yW0F6wLgWi24BE7j5qLk0iL=") // 备用pin
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
在OkHttp配置中,开发人员指定域和一个或多个SHA-256指纹。在第一个指纹处,OkHttp将服务器证书与指定的pin进行比较。如果没有匹配,客户端抛出SSLPeerUnverifiedException。备用pin是必需的——没有它,更换证书时API请求将立即开始失败。
Android从API 24开始通过XML配置支持声明式Certificate Pinning。res/xml/network_security_config.xml文件包含域及其指纹的列表。这种方法适用于静态配置,但不允许实现TOFU或带有异常记录的自定义验证逻辑。
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-12-31">
<pin digest="SHA-256">
Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg=</pin>
<pin digest="SHA-256">
FiPq6ePEsVRqZ1yW0F6wLgWi24BE7j5qLk0iL=</pin>
</pin-set>
</domain-config>
</network-security-config>
SSL Pinning显著提高了移动应用程序的安全性,但带来了操作复杂性。主要优点——即使在根CA被攻破的情况下也能抵御MITM攻击。应用程序只信任开发人员明确指定的证书,而不是整个公共证书颁发机构基础设施。这对于金融应用程序、即时通讯工具和包含敏感数据的应用程序尤其关键。
主要缺点——证书轮换的复杂性。如果证书过期或被撤销,未更新应用程序的用户将失去连接。通过备用pin和逐步更新机制解决:新应用程序知道旧证书和新证书,在用户完全更新后,旧pin从代码中删除。建议放置至少2个备用pin——一个用于当前证书,一个用于未来证书。
另一个折衷——在不禁用pinning的情况下无法使用公共代理进行流量调试(Charles Proxy、Burp Suite)。这使开发阶段的网络请求调试变得复杂。解决方案——条件编译:在debug构建中pinning被禁用,在release构建中启用。OWASP建议使用BuildConfig.DEBUG标志进行切换。
| 方面 | 优点 | 缺点 |
|---|---|---|
| 安全性 | 通过伪造CA抵御MITM | 密钥泄露时的复杂性 |
| 维护 | 明确的信任控制 | 轮换需要应用程序更新 |
| 调试 | 保证连接到正确的服务器 | 调试代理被阻止 |
常见问题
标准HTTPS验证信任由已知根CA签署的任何证书。SSL Pinning只信任特定证书或密钥——如果CA颁发伪造证书,应用程序将拒绝它。
证书通常有效1–2年。建议在当前证书到期前3–6个月更新pin,将新指纹添加为备用pin,轮换后删除旧pin。
可以,但需要考虑CDN在边缘服务器之间切换时可能会更改证书。建议绑定到公钥而不是特定证书,并使用多个备用pin。
连接中断并报错——在Android上是SSLPeerUnverifiedException,在iOS上challenge以.cancelAuthenticationChallenge被拒绝。应用程序应正确处理此错误并通知用户。
不,但OWASP建议处理敏感数据的应用程序使用:银行、医疗、企业系统。对于简单的只读应用程序,带有EV证书的标准HTTPS验证通常就足够了。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。