7、Android证书绑定基础:Certificate Pinning概念、为什么需要证书绑定、证书绑定的实现方式。
大家好,我是你们的老朋友。今天我们来聊聊Android安全里一个非常核心的话题——证书绑定,也就是Certificate Pinning。
说实话,我在做移动安全这些年,见过太多App因为没做证书绑定,结果被中间人攻击搞得体无完肤。我自己也踩过坑,所以今天把这些经验都掏出来,咱们好好捋一捋。
什么是Certificate Pinning?
先问个问题:你的App真的信任了正确的服务器吗?
传统的HTTPS连接,系统会检查服务器证书是不是由受信任的CA(证书颁发机构)签发的。但问题是,CA也可能被攻破,或者被恶意签发证书。这时候,中间人攻击者就能用一张假证书骗过你的App。
证书绑定,说白了就是让你的App只信任特定的证书或公钥。它不再依赖系统根证书列表,而是直接锁定你服务器的证书指纹。这样一来,就算攻击者拿到了CA签发的假证书,你的App也不会买账。
核心思想: 信任白名单,而不是信任整个CA体系。
为什么需要证书绑定?
我遇到过不少开发者问:“HTTPS不是已经加密了吗?为什么还要多此一举?”
嗯,这里有个误区。HTTPS确实加密了通信内容,但它并不能保证你连接到的服务器就是你想连的那个。中间人攻击的核心,就是让客户端和服务器都以为自己是在和对方通信,实际上中间人把两边的数据都截获并转发了。
具体来说,不做证书绑定会有这些风险:
- CA被攻破: 历史上发生过多次CA被入侵,签发假证书的事件。比如2011年的DigiNotar事件,Google、Facebook等网站都被签发过假证书。
- 设备被植入恶意根证书: 用户手机如果被安装了恶意CA证书(比如某些公司监控员工手机),攻击者就能轻松发起中间人攻击。
- 公共Wi-Fi钓鱼: 攻击者在公共Wi-Fi上伪造一个和你服务器一模一样的域名,配合假证书,你的App可能毫无防备地连接上去。
我记得有一次,我们团队审计一个金融类App。它的API接口全部走HTTPS,但没做证书绑定。我们用一台笔记本开了个热点,装了个中间人代理,不到5分钟就把用户的登录凭证和交易数据全抓到了。嗯,那个项目后来紧急上线了证书绑定补丁。
警告: 不做证书绑定的App,在公共网络环境下基本等于裸奔。这不是危言耸听。
证书绑定的实现方式
证书绑定主要有两种实现思路:证书绑定和公钥绑定。我个人更推荐公钥绑定,因为证书会过期,但公钥可以保持不变。
下面我给大家展示几种常见的实现方式。
1. 使用OkHttp实现证书绑定
OkHttp是Android开发中最常用的网络库之一。它内置了证书绑定支持,用起来非常方便。
// 1. 获取服务器证书的SHA-256哈希值
// 可以用openssl命令获取:
// openssl s_client -connect yourserver.com:443 -servername yourserver.com | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
// 2. 在OkHttp中配置证书绑定
String hostname = "yourserver.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 替换为你的公钥哈希
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
// 3. 发起请求
Request request = new Request.Builder()
.url("https://yourserver.com/api/data")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 证书不匹配时会回调这里
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 正常响应
}
});
小技巧: 建议绑定至少两个公钥——一个主用,一个备用。这样主证书更换时,备用公钥还能保证服务不中断。我曾经就因为只绑了一个公钥,证书轮换时App大面积报错,那叫一个惨。
2. 使用HttpsURLConnection实现证书绑定
如果你还在用原生的HttpsURLConnection,也可以实现证书绑定,不过代码会稍微复杂一些。
// 自定义TrustManager,只信任指定的证书
TrustManager[] trustManagers = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// 客户端证书校验,一般不需要实现
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
if (chain == null || chain.length == 0) {
throw new CertificateException("证书链为空");
}
// 获取服务器证书的公钥
PublicKey serverKey = chain[0].getPublicKey();
String serverKeyHash = hashPublicKey(serverKey);
// 与预置的公钥哈希比较
if (!PINNED_PUBLIC_KEY_HASH.equals(serverKeyHash)) {
throw new CertificateException("证书绑定失败:公钥不匹配");
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
URL url = new URL("https://yourserver.com/api/data");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
// 发起请求...
3. 使用Android Network Security Config
从Android 7.0(API 24)开始,Google提供了Network Security Config,可以在XML中配置证书绑定,无需修改代码。
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">yourserver.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- 备用公钥 -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
然后在AndroidManifest.xml中引用:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
推荐做法: 我个人习惯用Network Security Config做基础绑定,再配合OkHttp的CertificatePinner做双重校验。这样即使XML配置被篡改,代码层还有一道防线。
证书绑定的知识体系
为了让大家更直观地理解证书绑定的整体逻辑,我画了一张图:
避坑指南
最后,分享几个我踩过的坑,希望能帮大家少走弯路。
- 不要只绑定一个证书: 证书会过期,一旦过期你的App就废了。至少准备两个公钥,一个主用,一个备用。
- 注意调试环境: 我曾经在调试版里关闭了证书绑定,结果上线时忘记打开,App在用户手机上直接崩溃。建议用BuildConfig.DEBUG做区分。
- 公钥哈希要定期更新: 服务器证书轮换后,记得同步更新App里的公钥哈希。最好有个服务端接口动态下发新的哈希值。
- 不要绑定整个证书链: 绑定叶子证书的公钥就够了。绑定中间CA证书的话,一旦CA更换,你的App也会受影响。
个人建议: 证书绑定不是一劳永逸的。它需要配合证书监控、动态更新机制一起使用。我现在的做法是:App内置两个公钥,同时通过一个加密接口定期拉取最新的公钥列表,做到动态更新。
好了,关于证书绑定的基础概念、为什么需要它、以及几种实现方式,今天就聊到这里。记住一句话:HTTPS保证的是传输加密,证书绑定保证的是连接可信。两者缺一不可。