Android13 系统/用户证书安装相关分析总结(三) 增加安装系统证书的接口遇到的问题和坑
一、前言
接上回说到,修改了程序,增加了接口,却不知道有没有什么问题,于是心怀忐忑等了几天。果然过了几天,应用那边的小伙伴报过来了问题。用户证书安装没有问题,系统证书(新增的接口)还是出现了问题。调用了我提供的接口安装之后settings中可以查到,但是小伙伴的demo里调用的接口不行,而且网站证书安装之后,方位对应网站依然会弹出证书不受信任的弹窗。看到这里,笔者心里想着,这可又有事情干了
二、出现问题分析
写在前面,记录还原一下笔者的思路,由于当时卡在这个问题一些时间,虽然最后也解决了,但是现在想想也不算弯路,只能说笔者对Android的理解还不够。所以下文描述的是笔者还原的定位步骤,包含弯路。如果正常定位应该也不需要这么多步骤。所以完整记录一下吧,毕竟这过程中也了解了一些知识盲区
首先确认了一下,安装好查找证书的方法调用有差异,下面是应用的调用
public void readInstalledCertificates() {try {KeyStore ks = KeyStore.getInstance("AndroidCAStore");if (ks != null) {ks.load(null, null);Enumeration<String> aliases = ks.aliases();boolean certHere = false;while (aliases.hasMoreElements()) {String alias = (String) aliases.nextElement();java.security.cert.X509Certificate cert = (java.security.cert.X509Certificate) ks.getCertificate(alias);if (cert.getIssuerDN().getName().contains("xxxxx")) {System.out.println(cert.getIssuerDN().getName());certHere = true;}if (cert.getIssuerDN().getName().contains("CA Cert Signing")) {System.out.println(cert.getIssuerDN().getName());certHere = true;}//To print all certsString tmp = cert.getIssuerDN().getName();Log.e("tst", tmp);setText(tmp, false);}if (certHere) {Log.e("test", "cert found: xxxxx");Toast.makeText(this, "cert found: Root CA/xxxxx", Toast.LENGTH_SHORT).show();setText("", false);setText("cert found: Root CA/5ed36f99", false);} else {Log.e("test", "cert not found: xxxxx");Toast.makeText(this, "cert not found: Root CA/xxxxx", Toast.LENGTH_SHORT).show();setText("", false);setText("cert not found: Root CA/xxxxx", false);}}} catch (IOException e) {e.printStackTrace();} catch (KeyStoreException e) {e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (java.security.cert.CertificateException e) {e.printStackTrace();}}
看到上面的调用方法,笔者懵逼了。
于是开始了定位分析的漫长过程
1、最初以为是调用接口的差异,于是仔细看了一下自己增加的接口,发现有遗漏,安装证书代码如下:
//packages/apps/KeyChain/src/com/android/keychain/KeyChainService.java
if (CERT_INSTALLER_PACKAGE.equals(caller.mPackageName)) {try {mKeyStore.setCertificateEntry(String.format("%s %s", subject, alias), cert);} catch(KeyStoreException e) {Log.e(TAG, String.format("Attempted installing %s (subject: %s) to KeyStore. Failed", alias,subject), e);}
}
这里在调用安装接口安装之后限制了只有调用方为certinstaller 及证书安装器,可以调用keyStore调用setCertificateEntry方法,于是找了一下调用链
keyStore.setCertificateEntry -->keyStoreSpi.engineSetCertificateEntry—>AndroidKeyStoreSpi.java engineSetCertificateEntry —> KeyStore2.java.updateSubcomponents —>service.rs.updateSubcomponent—>service.rs.update_subcomponent
看了一些列调用链有点蒙,还看到了rust代码中的实现,更新数据库,于是乎笔者又找了一下这里对应的数据库,路径如下/data/misc/keystore/persistent.sqlite
找了一个数据库软件打开,发现settings安装用户证书之后,调用上面的函数,会在这个数据库中留下一条记录,在settings中的体现为下图:截图由scrcpy投屏软件中截图
而笔者当时觉得应该就是这里的问题于是看了一下数据库更新的逻辑,发现更新到数据库中的只有两种类型的证书用户证书和WiFi证书,而权限则在笔者的追踪下发现是通过selinux权限管控。
这一块儿的细节笔者不是很清晰了,后续有补充的可能。涉及到的文件如下
system/security/keystore2/src/database.rs
system/security/keystore2/src/service.rs
system/security/keystore2/src/permission.rs
当时追踪了这么多文件,加了很多日志想着一定是这里有问题,于是兴高采烈的改了,之后结果可以预料,还是有问题。于是这条路不通
2、安装证书之后进入网页还会提示证书不被信任
这里就网上搜了一下,在对应的系统源码里打印了调用堆栈,如下
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: java.lang.Exception: Stack trace
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at java.lang.Thread.dumpStack(Thread.java:1615)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:482)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:332)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:114)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:135)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at java.lang.reflect.Method.invoke(Native Method)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at android.net.http.X509TrustManagerExtensions.checkServerTrusted(X509TrustManagerExtensions.java:101)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at vE0.h(chromium-TrichromeWebViewGoogle.aab-stable-573506031:121)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: at org.chromium.net.AndroidNetworkLibrary.verifyServerCertificates(chromium-TrichromeWebViewGoogle.aab-stable-573506031:2)
于是乎发现了新大陆,原来网页证书验证会涉及到这里。仔细看一下发现了两个突破口:
一个是RootTrustManager.checkServerTrusted,另一个是RootTrustManager.checkServerTrusted。
先说第一个,RootTrustManager.checkServerTrusted,实现如下
@Overridepublic void checkServerTrusted(X509Certificate[] certs, String authType, Socket socket)throws CertificateException {if (socket instanceof SSLSocket) {SSLSocket sslSocket = (SSLSocket) socket;SSLSession session = sslSocket.getHandshakeSession();if (session == null) {throw new CertificateException("Not in handshake; no session available");}String host = session.getPeerHost();NetworkSecurityConfig config = mConfig.getConfigForHostname(host);config.getTrustManager().checkServerTrusted(certs, authType, socket);} else {// Not an SSLSocket, use the hostname unaware checkServerTrusted.checkServerTrusted(certs, authType);}}
可以看到这里边的两个关键变量mConfig和config。其中mConfig是frameworks/base/core/java/android/security/net/config/ApplicationConfig.java
查看源码我们了解到,c中两个重要的是
NetworkSecurityConfig mDefaultConfig;
ConfigSource mConfigSource;
这两个变量需要 ApplicationConfig初始化,其中NetworkSecurityConfig 需要通过ConfigSource 获取,而ConfigSource 又要通过ApplicationConfig构造函数传入。于是在源码网站全局搜索之后发现了frameworks/base/core/java/android/security/net/config/NetworkSecurityConfigProvider.java 在这个类中进行初始化的,可以看到初始化用到了ManifestConfigSource ,而ManifestConfigSource初始化时会调用 NetworkSecurityConfig.getDefaultBuilder方法
/** @hide */
public final class NetworkSecurityConfigProvider extends Provider {private static final String LOG_TAG = "nsconfig";private static final String PREFIX =NetworkSecurityConfigProvider.class.getPackage().getName() + ".";public NetworkSecurityConfigProvider() {// TODO: More clever name than thissuper("AndroidNSSP", 1.0, "Android Network Security Policy Provider");put("TrustManagerFactory.PKIX", PREFIX + "RootTrustManagerFactorySpi");put("Alg.Alias.TrustManagerFactory.X509", "PKIX");}public static void install(Context context) {ApplicationConfig config = new ApplicationConfig(new ManifestConfigSource(context));ApplicationConfig.setDefaultInstance(config);int pos = Security.insertProviderAt(new NetworkSecurityConfigProvider(), 1);if (pos != 1) {throw new RuntimeException("Failed to install provider as highest priority provider."+ " Provider was installed at position " + pos);}libcore.net.NetworkSecurityPolicy.setInstance(new ConfigNetworkSecurityPolicy(config));}/*** For a shared process, resolves conflicting values of usesCleartextTraffic.* 1. Throws a RuntimeException if the shared process with conflicting* usesCleartextTraffic values have per domain rules.* 2. Sets the default instance to the least strict config.*/public static void handleNewApplication(Context context) {ApplicationConfig config = new ApplicationConfig(new ManifestConfigSource(context));ApplicationConfig defaultConfig = ApplicationConfig.getDefaultInstance();String mProcessName = context.getApplicationInfo().processName;if (defaultConfig != null) {if (defaultConfig.isCleartextTrafficPermitted()!= config.isCleartextTrafficPermitted()) {Log.w(LOG_TAG, mProcessName+ ": New config does not match the previously set config.");if (defaultConfig.hasPerDomainConfigs()|| config.hasPerDomainConfigs()) {throw new RuntimeException("Found multiple conflicting per-domain rules");}config = defaultConfig.isCleartextTrafficPermitted() ? defaultConfig : config;}}ApplicationConfig.setDefaultInstance(config);}
}
接下来看一下frameworks/base/core/java/android/security/net/config/NetworkSecurityConfig.java的getDefaultBuilder
public static Builder getDefaultBuilder(ApplicationInfo info) {Builder builder = new Builder().setHstsEnforced(DEFAULT_HSTS_ENFORCED)// System certificate store, does not bypass static pins..addCertificatesEntryRef(new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));final boolean cleartextTrafficPermitted = info.targetSdkVersion < Build.VERSION_CODES.P&& !info.isInstantApp();builder.setCleartextTrafficPermitted(cleartextTrafficPermitted);// Applications targeting N and above must opt in into trusting the user added certificate// store.if (info.targetSdkVersion <= Build.VERSION_CODES.M && !info.isPrivilegedApp()) {// User certificate store, does not bypass static pins.builder.addCertificatesEntryRef(new CertificatesEntryRef(UserCertificateSource.getInstance(), false));}return builder;}
在这里发现了一个证书相关的配置类SystemCertificateSource。
这里不展示frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java的源码了,可以看到这里默认的配置指向的就是系统证书的路径,原来在这里也需要增加自定义的系统证书路径。可是由于很多方法是父类DirectoryCertificateSource实现,所以就在父类中修改了