15. Keystore密钥链:证书链管理、自签名证书、CA证书导入、信任锚点配置

说到密钥链,很多人第一反应就是「把证书串起来」。嗯,这个理解没错,但实际做起来坑不少。我最早接触证书链是在做企业级应用的时候,那时候要对接内部CA,结果证书链没配好,SSL握手一直报错,排查了整整一个下午。说白了,证书链就是一条信任的传递路径——从你的设备证书,一路追溯到根CA。

15.1 证书链的核心逻辑

证书链,也叫证书路径。它由三部分组成:

  • 终端实体证书:你的App或设备持有的证书
  • 中间CA证书:签发终端证书的机构
  • 根CA证书:整个信任链的起点,自签名

你想想看,如果只有终端证书,对方怎么验证你是可信的?它需要沿着证书链往上找,直到找到一个它信任的根证书。这个过程就是证书链验证。

关键点:Android Keystore支持存储完整的证书链,但你必须自己构建好链,再导入。系统不会自动帮你补全中间证书。

15.2 自签名证书的生成与导入

自签名证书,说白了就是自己给自己签发。开发环境、内部测试经常用。我习惯用KeyTool命令行生成,然后导入Keystore。

// 生成自签名证书(命令行)
keytool -genkey -v -keystore mykeystore.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -alias myalias -keyalg RSA -keysize 2048 -validity 3650 -storepass password -keypass password -dname "CN=MyApp, OU=Dev, O=Company, L=City, ST=State, C=CN"

注意,Android Keystore不支持直接导入PKCS12或JKS格式的密钥库。你需要先把证书和私钥分开,再通过KeyChain API导入。

// Android代码导入自签名证书
byte[] certBytes = ...; // 从文件读取
X509Certificate cert = CertificateFactory.getInstance("X.509")
        .generateCertificate(new ByteArrayInputStream(certBytes));

Intent intent = KeyChain.createInstallIntent();
intent.putExtra(KeyChain.EXTRA_CERTIFICATE, cert.getEncoded());
startActivityForResult(intent, REQUEST_INSTALL_CERT);

个人经验:自签名证书的CN字段一定要写对。我曾经因为CN写成了IP地址,结果在Android 9以上版本直接报错「No subject alternative names present」。建议用域名,或者加上SAN扩展。

15.3 CA证书导入:信任链的搭建

CA证书导入,说白了就是把第三方CA的根证书或中间证书装进设备。Android系统预置了上百个根证书,但企业自建CA的证书不在其中。这时候就需要手动导入。

我遇到过最典型的情况:公司内部App要访问内网服务器,服务器证书由内部CA签发。如果不导入CA证书,SSL握手直接失败。

// 导入CA证书到信任存储
try {
    // 读取CA证书文件
    InputStream caInput = new BufferedInputStream(
            getAssets().open("internal_ca.cer"));
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    Certificate ca = cf.generateCertificate(caInput);
    
    // 获取系统信任存储
    KeyStore trustStore = KeyStore.getInstance("AndroidCAStore");
    trustStore.load(null, null);
    
    // 注意:Android不允许直接修改系统信任存储
    // 你需要使用Network Security Config或自定义TrustManager
} catch (Exception e) {
    e.printStackTrace();
}

避坑指南:Android 4.2以上不允许App直接修改系统信任存储。我曾经试图用反射去添加证书,结果在Android 7上直接崩溃。正确的做法是用Network Security Config,或者自定义SSLSocketFactory。

15.4 信任锚点配置

信任锚点,就是验证链的起点。说白了,你信任谁,谁就是锚点。Android的信任锚点默认是系统预置的根证书。但你可以通过两种方式自定义:

  • Network Security Config:声明式配置,简单安全
  • 自定义TrustManager:代码控制,灵活但容易出错

我个人更推荐Network Security Config,因为它不需要修改代码,而且Google官方维护。

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <base-config>
        <trust-anchors>
            <!-- 系统预置证书 -->
            <certificates src="system" />
            <!-- 自定义CA证书 -->
            <certificates src="@raw/my_ca" />
        </trust-anchors>
    </base-config>
    
    <domain-config>
        <domain includeSubdomains="true">internal.company.com</domain>
        <trust-anchors>
            <!-- 仅信任自定义CA -->
            <certificates src="@raw/internal_ca" />
        </trust-anchors>
    </domain-config>
</network-security-config>

注意:如果你同时配置了系统证书和自定义证书,Android会先检查自定义证书,再检查系统证书。这个顺序在Android 10以上有变化,建议显式指定。

15.5 证书链验证的完整流程

我画了一张图,帮你理清证书链验证的完整流程:

证书链验证流程 终端实体证书 步骤1:验证签名是否由中间CA签发 中间CA证书 步骤2:验证中间CA签名是否由根CA签发 根CA证书(自签名) 关键说明 • 根CA证书自签名 • 中间CA可有多级 • 验证是自底向上 • 信任锚点是根CA • 任何一环失败则拒绝 常见失败原因: 1. 证书过期 2. 证书被吊销 3. 中间证书缺失 4. 根证书不受信任

15.6 实战中的证书链管理

在实际项目中,我总结了几条经验:

  1. 证书链要完整:导入时确保包含所有中间证书,缺一个就验证失败
  2. 注意证书格式:Android支持DER和PEM格式,但建议用DER二进制格式,解析更快
  3. 证书过期处理:提前30天预警,我习惯在证书到期前两个月就开始提醒
  4. 信任锚点不要乱改:除非必要,否则不要禁用系统证书验证

我的习惯:每次导入证书链后,我都会写一个单元测试,用代码验证整个链的完整性。这样部署到生产环境前就能发现问题,而不是等到用户反馈才去排查。

15.7 证书链的存储与更新

Android Keystore本身不直接管理证书链的更新。你需要自己实现更新逻辑。我常用的方案是:

  • 把证书链文件放在assets目录或远程服务器
  • App启动时检查证书有效期
  • 如果即将过期,从服务器下载新证书链
  • 通过KeyChain API更新
// 检查证书是否即将过期
X509Certificate cert = ...;
Date expiryDate = cert.getNotAfter();
Date now = new Date();
long daysUntilExpiry = (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);

if (daysUntilExpiry < 30) {
    // 触发证书更新流程
    downloadAndInstallNewCertChain();
}

曾经踩过的坑:证书更新时,如果旧证书还没过期,新证书就导入,会导致系统里有两份相同的证书。我后来加了一个去重逻辑,导入前先检查别名是否已存在。

证书链管理,说白了就是信任的传递。你只要把根证书、中间证书、终端证书这条链搭好,Android系统就能自动完成验证。但要注意,这条链上任何一个环节出问题,整个信任就崩塌了。所以,证书链的完整性和时效性,是你在开发中必须时刻关注的。

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