数字证书与 PKI 体系:X.509 证书结构、CA 信任链、证书链验证
说到 SSL/TLS,绕不开的就是数字证书。我早年刚接触 Android 安全时,总觉得证书就是个「加密文件」,直到线上出了证书校验失败的崩溃,才老老实实把 X.509 啃了一遍。今天咱们就把它彻底说透。
X.509 证书长什么样?
X.509 是国际标准,定义了数字证书的格式。说白了,它就是一个「数字身份证」。你打开 Chrome 看任何 HTTPS 网站的证书信息,看到的那些字段,就是 X.509 的内容。
一个典型的 X.509 v3 证书包含以下核心字段:
| 字段 | 说明 | 我踩过的坑 |
|---|---|---|
| 版本号 (Version) | 目前主流是 v3 | v1/v2 不支持扩展字段,别用了 |
| 序列号 (Serial Number) | CA 分配的唯一编号 | 曾经遇到序列号重复的测试证书,排查了半天 |
| 签名算法 (Signature Algorithm) | 如 SHA256withRSA | 别用 SHA1,已经被攻破了 |
| 颁发者 (Issuer) | 签发该证书的 CA 名称 | — |
| 有效期 (Validity) | notBefore 和 notAfter | 手机时间不准会导致证书校验失败 |
| 主体 (Subject) | 证书持有者信息 | — |
| 公钥 (Public Key) | RSA/ECC 公钥 | Android 7.0+ 推荐 ECC 密钥 |
| 扩展 (Extensions) | v3 新增,如 SAN、Key Usage | SAN 字段缺失会导致域名校验失败 |
重点:Android 的 TrustManager 在校验证书时,会检查 Subject 或 SAN(Subject Alternative Name)是否匹配目标域名。我见过不少开发者只检查 Common Name,结果 SAN 里配了多个域名,直接崩了。
CA 信任链:谁给你的证书背书?
你想想看,如果每个 App 都自己签证书,那跟伪造身份证有什么区别?所以有了 CA(Certificate Authority,证书颁发机构)。
CA 是可信的第三方。它用自己的根证书(Root CA)给下级签发证书,下级再给网站或 App 签发。这就形成了一条链:
Root CA(自签名)
└── Intermediate CA(由 Root CA 签发)
└── 服务器证书(由 Intermediate CA 签发)
Android 系统内置了约 150 个根证书,存放在 /system/etc/security/cacerts/ 目录下。每个根证书都是一个 .0 文件,本质是 PEM 格式的 X.509 证书。
我的习惯:在项目里,我会把根证书和中间证书都打包进 App。因为有些 Android 设备厂商会魔改系统证书库,只依赖系统根证书可能出问题。
证书链验证:Android 是怎么检查的?
证书链验证,说白了就是「向上追溯,直到信任的根」。Android 的 TrustManagerImpl 会执行以下步骤:
- 提取证书链:从服务器拿到证书,以及它附带的中间证书。
- 构建信任锚:从系统证书库中加载所有根证书。
- 路径构建:从服务器证书开始,逐级向上找颁发者,直到某个根证书。
- 路径验证:检查每个证书的签名、有效期、Key Usage、是否被吊销等。
- 域名匹配:检查证书的 SAN 或 CN 是否匹配目标主机名。
这里有个关键点:Android 6.0 之后,默认只信任系统证书,不再信任用户安装的证书。这是为了防中间人攻击。我曾经在测试机上装了自己的抓包证书,结果发现 App 连不上——嗯,这就是原因。
注意:如果你在 App 里自定义了 TrustManager,一定要做证书链验证。我见过有人直接 trustAll(),那跟裸奔没区别。攻击者随便伪造个证书就能劫持你的通信。
证书链验证的代码实现
在 Android 中,你可以用 CertificateFactory 和 TrustManager 来手动验证证书链。下面是一个简化示例:
// 加载 CA 证书
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(
context.getAssets().open("ca.crt"));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
} finally {
caInput.close();
}
// 创建 KeyStore 并加载 CA
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// 创建 TrustManager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// 创建 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
这段代码的核心是:只信任我们指定的 CA 证书,而不是系统所有根证书。这样即使设备被植入了恶意根证书,我们的 App 也不会受影响。
避坑指南:我曾经在项目里直接用了 HttpsURLConnection 的默认校验,结果发现某些国产手机的系统证书库被厂商魔改过,导致我们的服务器证书校验失败。后来我改成固定证书指纹(Certificate Pinning),问题才解决。
证书链验证的 SVG 流程图
下面这张图展示了完整的证书链验证流程,从服务器握手到信任锚确认:
常见问题与我的经验
Q:为什么我的 App 在 Android 7.0 以上抓不到包?
A:因为 Android 7.0 开始,App 默认不信任用户安装的证书。你需要用 network_security_config.xml 来配置信任策略,或者用 Charles 的 SSL Proxying 配合 VirtualXposed 等工具。
Q:证书过期了怎么办?
A:我建议在 App 里做证书过期预警。可以在启动时检查服务器证书的 notAfter 字段,提前通知运维更换。别等到用户反馈连不上才去查。
Q:自签名证书能用吗?
A:开发测试可以,但生产环境绝对不要。自签名证书无法被系统信任,你需要手动把证书打包进 App,并自定义 TrustManager。而且一旦证书泄露,你没法吊销它。
总结一下:X.509 证书是 PKI 体系的基石。理解它的结构、信任链和验证流程,是做好 Android 安全通信的前提。我个人建议,每个 Android 开发者都应该亲手写一遍证书链验证的代码,踩一遍坑,才能真正理解它。