欢迎来到德薄能鲜网

德薄能鲜网

Android13 系统/用户证书安装相关分析总结(一) 证书分类以及安装流程分析

时间:2025-06-24 12:52:16 阅读(143)

一、前言

说这个问题之前,先说一下背景。是为了写一个SDK的接口,需求大致是增加证书安装卸载的接口(系统、用户)。于是了解了一下证书相关的处理逻辑。

二、基本概念

1、系统中的证书安装、查看功能

入口:
安装证书:系统设置(Settings)–>安全–> 加密与凭据 --> 安装证书
查看证书:系统设置(Settings)–>安全–> 加密与凭据 -->信任的凭据
两个入口图如下
安装在这里插入图片描述
看到图又有两个概念,用户证书和系统证书。这两者的区别,笔者暂时只发现数据库中存放的type不同,证书存放的路径不同,其他的差异暂时也不是很清楚。而在wifi模块中的证书验证看到路径写死的是系统证书的路径。

2、证书的种类和存放路径

种类:
1.按照权限区分:可以分为系统证书和用户证书
2.按照用途区分:CA证书、VPN和应用用户证书、WiFi证书 (Android 8.1的系统在这个上面settings层面没有做区分,底层处理上不确定有没有作区分)

3、证书存放的路径和格式

系统证书: /system/etc/security/cacerts
用户证书:/data/misc/user/0/cacerts-added

证书格式:系统证书一般以 .0 后缀,用户证书settings中支持安装crt

三、证书安装流程整理

下面重点分析一下Ca证书安装的流程
安装入口:InstallCertificateFromStorage

安装流程写在前面,解释放在后面

Settings —> InstallCertificateFromStorage.java --> InstallCaCertificateWarning.java---->

/vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/security/InstallCaCertificateWarning.java

CertInstaller —> CertInstallerMain.java .onCreate() --> installingCaCertificate() —>confirmDeviceCredential()—>startOpenDocumentActivity()----> 选择证书文件后 ----> startInstallActivity() ---->CertInstaller.class onCreate() —>extractPkcs12OrInstall() —>installOthers() —>installCertificateOrShowNameDialog()—>InstallVpnAndAppsTrustAnchorsTask().execute()—>CredentialHelper.installVpnAndAppsTrustAnchors() —>IKeyChainService.installCaCertificate() ---->

packages/apps/KeyChain/src/com/android/keychain/KeyChainService.java
KeyChainService.installCaCertificate() ---->

external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
TrustedCertificateStore.installCertificate() ---->writeCertificate()

/** * Creates a warning dialog explaining the consequences of installing a CA certificate * This is displayed before a CA certificate can be installed from Settings. */publicclassInstallCaCertificateWarningextendsActivity{ @OverridepublicvoidonCreate(@NullableBundlesavedInstanceState){ super.onCreate(savedInstanceState);...mixin.setSecondaryButton(newFooterButton.Builder(this).setText(R.string.certificate_warning_install_anyway).setListener(installCaCertificate()).setButtonType(FooterButton.ButtonType.OTHER).setTheme(R.style.SudGlifButton_Secondary).build());mixin.getSecondaryButtonView().setFilterTouchesWhenObscured(true);...}privateView.OnClickListenerinstallCaCertificate(){ returnv ->{ finalIntentintent =newIntent();intent.setAction(Credentials.INSTALL_ACTION);intent.putExtra(Credentials.EXTRA_CERTIFICATE_USAGE,Credentials.CERTIFICATE_USAGE_CA);startActivity(intent);finish();};}//frameworks/base/keystore/java/android/security/Credentials.javapublicstaticfinalStringINSTALL_ACTION ="android.credentials.INSTALL";//packages/apps/CertInstaller/AndroidManifest.xml<activity android:name=".CertInstallerMain"android:theme="@style/Transparent"android:configChanges="orientation|keyboardHidden|screenSize"android:exported="true"><intent-filter><action android:name="android.credentials.INSTALL"/><category android:name="android.intent.category.DEFAULT"/></intent-filter><intent-filter><action android:name="android.intent.action.VIEW"/><category android:name="android.intent.category.DEFAULT"/><data android:mimeType="application/x-x509-ca-cert"/><data android:mimeType="application/x-x509-user-cert"/><data android:mimeType="application/x-x509-server-cert"/><data android:mimeType="application/x-pkcs12"/><data android:mimeType="application/x-pem-file"/><data android:mimeType="application/pkix-cert"/><data android:mimeType="application/x-wifi-config"/></intent-filter></activity>

从上面的来看settings 调用了CertInstaller 的CertInstallerMain 界面准备安装证书。
这里注意笔者发现了一个细节,那就是安装证书界面在选择文件的时候被限制了文件类型,比如系统证书中的.0后缀的都是灰显的,这是因为在上面的AndroidManifest.xml 中声明了mimeType,如果想安装.0的有两种方式:1、转换格式2、自己写demo调用证书安装的接口。笔者发现接口调用底层实现最终是把证书的数据变成byte数组,所以不受限制。
接着看调用链
CertInstallerMain 中最终调用startInstallActivity方法,跳转到CertInstaller,最终调用CredentialHelper.java 的installVpnAndAppsTrustAnchors方法,之后再次跨进程调用另一个服务KeyChainService的方法,流程代码如下:

// packages/apps/CertInstaller/src/com/android/certinstaller/CertInstallerMain.javaprivatevoidstartInstallActivity(StringmimeType,Uriuri){ if(mimeType ==null){ mimeType =getContentResolver().getType(uri);}Stringtarget =MIME_MAPPINGS.get(mimeType);if(target ==null){ Log.e(TAG,"Unknown MIME type: "+mimeType +". "+Log.getStackTraceString(newThrowable()));Toast.makeText(this,R.string.invalid_certificate_title,Toast.LENGTH_LONG).show();return;}if(WIFI_CONFIG.equals(target)){ startWifiInstallActivity(mimeType,uri);}else{ InputStreamin =null;try{ in =getContentResolver().openInputStream(uri);finalbyte[]raw =readWithLimit(in);Intentintent =getIntent();intent.putExtra(target,raw);startInstallActivity(intent);}catch(IOExceptione){ Log.e(TAG,"Failed to read certificate: "+e);Toast.makeText(this,R.string.cert_read_error,Toast.LENGTH_LONG).show();}finally{ IoUtils.closeQuietly(in);}}}//packages/apps/CertInstaller/src/com/android/certinstaller/CredentialHelper.javabooleaninstallVpnAndAppsTrustAnchors(Contextcontext,IKeyChainServicekeyChainService){ finalTrustedCertificateStoretrustedCertificateStore =newTrustedCertificateStore();for(X509CertificatecaCert :mCaCerts){ byte[]bytes =null;try{ bytes =caCert.getEncoded();}catch(CertificateEncodingExceptione){ thrownewAssertionError(e);}if(bytes !=null){ try{ keyChainService.installCaCertificate(bytes);}catch(RemoteExceptione){ Log.w(TAG,"installCaCertsToKeyChain(): "+e);returnfalse;}Stringalias =trustedCertificateStore.getCertificateAlias(caCert);if(alias ==null){ Log.e(TAG,"alias is null");returnfalse;}maybeApproveCaCert(context,alias);}}returntrue;}

到了KeyChainService这个方法,瞬间觉得好像马上找到了最终的答案。这里会调用 TrustedCertificateStore.installCertificate()方法,于是看一下TrustedCertificateStore的方法。看到最后发现,就是一个简单的像指定路径写文件。而写路径的user对应的路径就是我们前面提到的用户证书的路径/data/misc/user/0/cacerts-added。
这里简单注意一下,因为最重调用到了java的一些类,所以需要格外注意Android 的权限机制。为什么这么说,是因为Android调用者会被系统标记,不同的进程所拥有的权限是有差异的,比如我们平时读写存储需要权限,还有SElinux权限等等。至于为什么,笔者先卖个关子,后面再说

至此安装流程就结束了,代码如下所示:

//xqt552_sys/packages/apps/KeyChain/src/com/android/keychain/KeyChainService.java@OverridepublicStringinstallCaCertificate(byte[]caCertificate){ finalCallerIdentitycaller =getCaller();Preconditions.checkCallAuthorization(isSystemUid(caller)||isCertInstaller(caller),MSG_NOT_SYSTEM_OR_CERT_INSTALLER);finalStringalias;Stringsubject =null;finalbooleanisSecurityLoggingEnabled =mInjector.isSecurityLoggingEnabled();finalX509Certificatecert;try{ cert =parseCertificate(caCertificate);finalbooleanisDebugLoggable =Log.isLoggable(TAG,Log.DEBUG);subject =cert.getSubjectX500Principal().getName(X500Principal.CANONICAL);if(isDebugLoggable){ Log.d(TAG,String.format("Installing CA certificate: %s",subject));}synchronized(mTrustedCertificateStore){ mTrustedCertificateStore.installCertificate(cert);alias =mTrustedCertificateStore.getCertificateAlias(cert);}}catch(IOException|CertificateExceptione){ Log.w(TAG,"Failed installing CA certificate",e);if(isSecurityLoggingEnabled &&subject !=null){ mInjector.writeSecurityEvent(TAG_CERT_AUTHORITY_INSTALLED,0/*result*/,subject,UserHandle.myUserId());}thrownewIllegalStateException(e);}if(isSecurityLoggingEnabled &&subject !=null){ mInjector.writeSecurityEvent(TAG_CERT_AUTHORITY_INSTALLED,1/*result*/,subject,UserHandle.myUserId());}// If the caller is the cert installer, install the CA certificate into KeyStore.// This is a temporary solution to enable CA certificates to be used as VPN trust// anchors. Ultimately, the user should explicitly choose to install the VPN trust// anchor separately and independently of CA certificates, at which point this code// should be removed.if(CERT_INSTALLER_PACKAGE.equals(caller.mPackageName)||"android.uid.system:1000".equals(caller.mPackageName)){ try{ mKeyStore.setCertificateEntry(String.format("%s %s",subject,alias),cert);}catch(KeyStoreExceptione){ Log.e(TAG,String.format("Attempted installing %s (subject: %s) to KeyStore. Failed",alias,subject),e);}}broadcastLegacyStorageChange();broadcastTrustStoreChange();returnalias;}// external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/TrustedCertificateStore.java/**     * This non-{ @code KeyStoreSpi} public interface is used by the     * { @code KeyChainService} to install new CA certificates. It     * silently ignores the certificate if it already exists in the     * store.     */@libcore.api.CorePlatformApi(status =libcore.api.CorePlatformApi.Status.STABLE)publicvoidinstallCertificate(X509Certificatecert)throwsIOException,CertificateException{ if(cert ==null){ thrownewNullPointerException("cert == null");}Filesystem =getCertificateFile(systemDir,cert);if(system.exists()){ Filedeleted =getCertificateFile(deletedDir,cert);if(deleted.exists()){ // we have a system cert that was marked deleted.// remove the deleted marker to expose the originalif(!deleted.delete()){ thrownewIOException("Could not remove "+deleted);}return;}// otherwise we just have a dup of an existing system cert.// return taking no further action.return;}Fileuser =getCertificateFile(addedDir,cert);if(user.exists()){ // we have an already installed user cert, bail.return;}// install the user certwriteCertificate(user,cert);}privatevoidwriteCertificate(Filefile,X509Certificatecert)throwsIOException,CertificateException{ Filedir =file.getParentFile();dir.mkdirs();dir.setReadable(true,false);dir.setExecutable(true,false);OutputStreamos =null;try{ os =newFileOutputStream(file);os.write(cert.getEncoded());}finally{ IoUtils.closeQuietly(os);}file.setReadable(true,false);}

那么问题来了,大致搞懂了基本流程,安装用户证书的流程可以想办法调用后面的KeyChainService类的方法来实现,那如果安装成系统证书又该怎么办呢?遗憾的是,笔者找了半天发现没有办法,只能自己加接口了。

在思考了一下之后,笔者想到了两种方法,一种是把证书复制到系统证书的存放路径,另一种是创建一个新的目录,把这个目录在用到证书的时候多读一个路径。笔者选了后一种方法。原因是系统路径下变成可写的会有可能把系统内置的正式删除从而导致系统异常。那么如何解决这个问题,还有证书如何验证这些问题放在后续文章解决。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/fighting_2017/article/details/134581186

分享到:

温馨提示:以上内容和图片整理于网络,仅供参考,希望对您有帮助!如有侵权行为请联系删除!

友情链接: