Android 6/7下HttpURLConnection启用CRL证书吊销检查方案问询
我非常理解你遇到的这个痛点——Android 6和7默认关闭了证书吊销检查,而且直接照搬Java SE的CRL检查方案会因为系统TrustManagerFactory的限制报错。下面给你几个可行的解决方案,从应用层到root环境的都有:
一、应用层自定义TrustManager手动实现CRL检查
这是最可靠的非root方案,绕开系统TrustManagerFactory的参数限制,自己在代码里完成CRL验证:
步骤1:实现自定义X509TrustManager
我们可以包装系统默认的TrustManager,先完成基础的证书信任检查,再手动添加CRL吊销验证:
private static class CRLValidatingTrustManager implements X509TrustManager { private final X509TrustManager defaultTrustManager; private final CertPathValidator certPathValidator; private final PKIXParameters pkixParams; public CRLValidatingTrustManager(KeyStore trustedKeyStore) throws GeneralSecurityException { // 初始化系统默认TrustManager TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustedKeyStore); this.defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0]; // 配置CRL检查参数 certPathValidator = CertPathValidator.getInstance("PKIX"); pkixParams = new PKIXParameters(trustedKeyStore); pkixParams.setRevocationEnabled(true); // 设置偏好CRL而非OCSP,同时允许CRL获取失败时软失败(可根据需求调整) CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX"); PKIXRevocationChecker revocationChecker = (PKIXRevocationChecker) cpb.getRevocationChecker(); revocationChecker.setOptions(EnumSet.of( PKIXRevocationChecker.Option.PREFER_CRLS, PKIXRevocationChecker.Option.SOFT_FAIL )); pkixParams.addCertPathChecker(revocationChecker); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { defaultTrustManager.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 先做基础的证书信任校验 defaultTrustManager.checkServerTrusted(chain, authType); // 手动执行CRL吊销检查 try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); CertPath certPath = cf.generateCertPath(Arrays.asList(chain)); certPathValidator.validate(certPath, pkixParams); } catch (CertPathValidatorException e) { // 证书已吊销或CRL检查失败,抛出异常终止连接 throw new CertificateException("Certificate revoked or CRL validation failed", e); } catch (GeneralSecurityException e) { throw new CertificateException("CRL validation setup error", e); } } @Override public X509Certificate[] getAcceptedIssuers() { return defaultTrustManager.getAcceptedIssuers(); } }
步骤2:初始化SSLContext并绑定到HttpURLConnection
沿用你之前加载CA证书的代码,然后用自定义TrustManager初始化SSLContext:
// 加载自定义CA证书到KeyStore CertificateFactory cf = CertificateFactory.getInstance("X.509"); AssetManager am = getResources().getAssets(); Certificate caCert; try (InputStream caInput = am.open("ca_test.pem")) { caCert = cf.generateCertificate(caInput); } KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("trusted_ca", caCert); // 初始化自定义TrustManager和SSLContext CRLValidatingTrustManager customTm = new CRLValidatingTrustManager(keyStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{customTm}, null); // 给HttpURLConnection设置自定义SSLSocketFactory HttpURLConnection connection = (HttpURLConnection) yourTargetUrl.openConnection(); connection.setSSLSocketFactory(sslContext.getSocketFactory());
这个方案完全在应用层实现,不需要root,兼容性覆盖Android 6/7,而且你可以完全控制CRL检查的逻辑(比如是否软失败、是否偏好CRL)。
二、Root设备下全局开启CRL检查
如果你有设备的root权限,可以尝试修改系统配置来全局启用证书吊销检查:
- 首先通过adb获取root权限:
su - 设置系统属性(部分版本有效):
setprop security.crl.enable true setprop security.ocsp.enable true - 如果上述属性不生效,尝试修改系统的网络安全配置文件
/system/etc/security/network_security_config.xml:- 挂载系统分区为可写:
mount -o rw,remount /system - 编辑文件,在
<base-config>节点内添加:<revocation-checking enabled="true"/> - 恢复系统分区为只读:
mount -o ro,remount /system
- 挂载系统分区为可写:
- 注意:这个方法会影响所有应用,且不同厂商/版本的系统可能不兼容,重启设备后属性会重置,需要写入
/system/build.prop来持久化。
关于你遇到的参数兼容问题
你之前使用CertPathTrustManagerParameters报错是因为Android的默认RootTrustManagerFactorySpi只接受它自己的ApplicationConfigParameters(和network_security_config.xml绑定),不支持Java SE标准的PKIX参数。所以没法通过给系统TrustManagerFactory传参来开启CRL检查,只能用自定义TrustManager的方案。
谷歌Bug的现状
你提到的那个系统Issue确实是Android的长期遗留问题,目前官方还没有推出修复方案,所以应用层自定义TrustManager是当前最可行的解决方案。
内容的提问来源于stack exchange,提问作者IrM

