小小小幸运 发表于 2025-3-9 05:54:15

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

一、前言

说这个问题之前,先说一下配景。是为了写一个SDK的接口,需求大抵是增加证书安装卸载的接口(系统、用户)。于是了解了一下证书相关的处置处罚逻辑。
二、根本概念

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

入口:
安装证书:系统设置(Settings)–>安全–> 加密与凭据 --> 安装证书
查察证书:系统设置(Settings)–>安全–> 加密与凭据 -->信任的凭据
两个入口图如下
https://i-blog.csdnimg.cn/direct/9540707922eb4f18b8cbfb352103001b.pnghttps://i-blog.csdnimg.cn/direct/28e7a06d89184e9386e6ead8c648bbd2.png
看到图又有两个概念,用户证书和系统证书。这两者的区别,笔者临时只发现数据库中存放的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.
*/
public class InstallCaCertificateWarning extends Activity {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ...
            mixin.setSecondaryButton(
                new FooterButton.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);
      ...
    }

    private View.OnClickListener installCaCertificate() {
      return v -> {
            final Intent intent = new Intent();
            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.java
public static final String INSTALL_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.java
    private void startInstallActivity(String mimeType, Uri uri) {
      if (mimeType == null) {
            mimeType = getContentResolver().getType(uri);
      }
      String target = MIME_MAPPINGS.get(mimeType);
      if (target == null) {
            Log.e(TAG, "Unknown MIME type: " + mimeType + ". "
                  + Log.getStackTraceString(new Throwable()));
            Toast.makeText(this, R.string.invalid_certificate_title, Toast.LENGTH_LONG).show();
            return;
      }
      if (WIFI_CONFIG.equals(target)) {
            startWifiInstallActivity(mimeType, uri);
      }
      else {
            InputStream in = null;
            try {
                in = getContentResolver().openInputStream(uri);
                final byte[] raw = readWithLimit(in);
                Intent intent = getIntent();
                intent.putExtra(target, raw);
                startInstallActivity(intent);
            } catch (IOException e) {
                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.java
    boolean installVpnAndAppsTrustAnchors(Context context, IKeyChainService keyChainService) {
      final TrustedCertificateStore trustedCertificateStore = new TrustedCertificateStore();
      for (X509Certificate caCert : mCaCerts) {
            byte[] bytes = null;
            try {
                bytes = caCert.getEncoded();
            } catch (CertificateEncodingException e) {
                throw new AssertionError(e);
            }
            if (bytes != null) {
                try {
                  keyChainService.installCaCertificate(bytes);
                } catch (RemoteException e) {
                  Log.w(TAG, "installCaCertsToKeyChain(): " + e);
                  return false;
                }

                String alias = trustedCertificateStore.getCertificateAlias(caCert);
                if (alias == null) {
                  Log.e(TAG, "alias is null");
                  return false;
                }

                maybeApproveCaCert(context, alias);
            }
      }
      return true;
    }
到了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
      @Override public String installCaCertificate(byte[] caCertificate) {
            final CallerIdentity caller = getCaller();
            Preconditions.checkCallAuthorization(isSystemUid(caller) || isCertInstaller(caller),
                  MSG_NOT_SYSTEM_OR_CERT_INSTALLER);
            final String alias;
            String subject = null;
            final boolean isSecurityLoggingEnabled = mInjector.isSecurityLoggingEnabled();
            final X509Certificate cert;
            try {
                cert = parseCertificate(caCertificate);

                final boolean isDebugLoggable = 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 | CertificateException e) {
                Log.w(TAG, "Failed installing CA certificate", e);
                if (isSecurityLoggingEnabled && subject != null) {
                  mInjector.writeSecurityEvent(
                            TAG_CERT_AUTHORITY_INSTALLED, 0 /*result*/, subject,
                            UserHandle.myUserId());
                }
                throw new IllegalStateException(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(KeyStoreException e) {
                  Log.e(TAG, String.format(
                            "Attempted installing %s (subject: %s) to KeyStore. Failed", alias,
                            subject), e);
                }
            }

            broadcastLegacyStorageChange();
            broadcastTrustStoreChange();
            return alias;
      }

// 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)
    public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
      if (cert == null) {
            throw new NullPointerException("cert == null");
      }
      File system = getCertificateFile(systemDir, cert);
      if (system.exists()) {
            File deleted = getCertificateFile(deletedDir, cert);
            if (deleted.exists()) {
                // we have a system cert that was marked deleted.
                // remove the deleted marker to expose the original
                if (!deleted.delete()) {
                  throw new IOException("Could not remove " + deleted);
                }
                return;
            }
            // otherwise we just have a dup of an existing system cert.
            // return taking no further action.
            return;
      }
      File user = getCertificateFile(addedDir, cert);
      if (user.exists()) {
            // we have an already installed user cert, bail.
            return;
      }
      // install the user cert
      writeCertificate(user, cert);
    }
    private void writeCertificate(File file, X509Certificate cert)
            throws IOException, CertificateException {
      File dir = file.getParentFile();
      dir.mkdirs();
      dir.setReadable(true, false);
      dir.setExecutable(true, false);
      OutputStream os = null;
      try {
            os = new FileOutputStream(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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Android13 系统/用户证书安装相关分析总结(一) 证书分类以及安装流程分析