数字证书与 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 会执行以下步骤:

  1. 提取证书链:从服务器拿到证书,以及它附带的中间证书。
  2. 构建信任锚:从系统证书库中加载所有根证书。
  3. 路径构建:从服务器证书开始,逐级向上找颁发者,直到某个根证书。
  4. 路径验证:检查每个证书的签名、有效期、Key Usage、是否被吊销等。
  5. 域名匹配:检查证书的 SAN 或 CN 是否匹配目标主机名。

这里有个关键点:Android 6.0 之后,默认只信任系统证书,不再信任用户安装的证书。这是为了防中间人攻击。我曾经在测试机上装了自己的抓包证书,结果发现 App 连不上——嗯,这就是原因。

注意:如果你在 App 里自定义了 TrustManager,一定要做证书链验证。我见过有人直接 trustAll(),那跟裸奔没区别。攻击者随便伪造个证书就能劫持你的通信。

证书链验证的代码实现

在 Android 中,你可以用 CertificateFactoryTrustManager 来手动验证证书链。下面是一个简化示例:

// 加载 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 流程图

下面这张图展示了完整的证书链验证流程,从服务器握手到信任锚确认:

Android 证书链验证流程 1. 服务器发送证书链 2. 提取证书列表 3. 路径构建:逐级找颁发者 4. 路径验证:签名/有效期/用途 5. 域名匹配:检查 SAN/CN 服务器证书 中间证书 根证书(信任锚) 系统证书库 /system/etc/security/cacerts/ 向上追溯 直到根证书

常见问题与我的经验

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 开发者都应该亲手写一遍证书链验证的代码,踩一遍坑,才能真正理解它。

公众号:蓝海资料掘金营,微信 deep3321