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

一、前言

说这个问题之前,先说一下背景。是为了写一个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.
 */
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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Erorrs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值