一、前言

接着上回说,最初是为了写一个SDK的接口,需求大致是增加证书安装卸载的接口(系统、用户)。于是了解了一下证书相关的处理逻辑,在了解了功能和流程之后,发现settings中支持安装的证书,只能安装到指定路径,并且是user 证书。那么到目前为止,安装用户证书的需求算是可行,可以完成。但是还遗留着一个问题,如何安装系统证书呢?

在上篇文章里边笔者给了两个方案:
1、一种是把证书复制到系统证书的存放路径 /system/etc/security/cacerts
2、另一种是创建一个新的目录

下面开始分析两个方案的可行性

二、可行性分析

1、把证书复制到系统证书的存放路径 /system/etc/security/cacerts

我们可以发现该路径是system分区,system分区一般是只读的,而参考了一下settings里边对系统证书的处理,没有删除,只有禁用和启用。所以不推荐在原有路径下操作。我们顺便看一下对应路径的selinux权限。
Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口-LMLPHP
可以看到证书路径下除了root 用户都没有写权限,且selinux 的域为system_security_cacerts_file。我们也看一下这域的对dir 和file 权限范围的定义:
Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口-LMLPHP
我们可以看到,te文件中对该路径文件和目录权限范围的约束和linux的一致,都是只读权限,所以放弃在该路径创建删除自定义删除是正确的。另外如果选择了该方案,也有误删系统证书的风险。

2、另一种是创建一个新的目录

这种方案相对来说就比较安全,但也存在一个问题,那就是比较复杂。

说复杂是因为,我们在接到这个需求的时候,对证书这一块儿的了解并不多。对系统证书和应用证书的使用场景几乎也不了解,这样在增加一个额外的证书路径是就会存在一个问题,那就是增加后,对对应流程的处理必然会存在遗漏。

令人遗憾的是,这个问题没有办法解决,只能先按照这个方案实现。然后把自己已经知道的流程和现象做验证,遇到问题的时候,再根据问题,修修补补。

所以下面开始梳理一下如何按照这个方案对接口进行实验

三、具体实现步骤

1、系统接口路径证书选择

首先这个问题,其实也不算个很难的问题。笔者选择的路径是/system/etc/security/cacerts。
至于原因是因为data分区可以读写,其次路径保持和系统证书路径方便记忆

2、实现步骤

确定了证书路径,下一步就是实现了。那么首先我们要在一个特定的时候创建它。在这个系列的上篇文章,我们提到安装、卸载最终的实现了是TrustedCertificateStore.java
那么我们就看一下这个类的实现

//external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
public class TrustedCertificateStore implements ConscryptCertStore {

    private static final String PREFIX_SYSTEM = "system:";
    private static final String PREFIX_USER = "user:";

    public static final boolean isSystem(String alias) {
        return alias.startsWith(PREFIX_SYSTEM);
    }
    public static final boolean isUser(String alias) {
        return alias.startsWith(PREFIX_USER);
    }

    private static class PreloadHolder {
        private static File defaultCaCertsSystemDir;
        private static File defaultCaCertsAddedDir;
        private static File defaultCaCertsDeletedDir;

        static {
            String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
            String ANDROID_DATA = System.getenv("ANDROID_DATA");
            defaultCaCertsSystemDir = new File(ANDROID_ROOT + "/etc/security/cacerts");
            setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"));
        }
    }

    private static final CertificateFactory CERT_FACTORY;
    static {
        try {
            CERT_FACTORY = CertificateFactory.getInstance("X509");
        } catch (CertificateException e) {
            throw new AssertionError(e);
        }
    }

    public static void setDefaultUserDirectory(File root) {
        PreloadHolder.defaultCaCertsAddedDir = new File(root, "cacerts-added");
        PreloadHolder.defaultCaCertsDeletedDir = new File(root, "cacerts-removed");
    }

    private final File systemDir;
    private final File addedDir;
    private final File deletedDir;

这里只选取了一下开头的部分,可以看到这里说明了 system user 证书的路径。我们可以在这里加一个自己定义的系统证书的路径。不过值得注意的是,除了在这里增加文件目录创建,还要看一下其他的。比如说证书别名,证书个数这些根据分类扫描目录的方法也需要增加相关的处理。

那么下面就先贴出修改

diff --git a/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java b/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
index 333450d..e12a88c 100644
--- a/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
+++ b/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
@@ -122,7 +122,10 @@
     private final File systemDir;
     private final File addedDir;
     private final File deletedDir;
+    private final File systemEditDir = new File("/data/etc/security/cacerts");
 
+    @android.compat.annotation.UnsupportedAppUsage
+    @libcore.api.CorePlatformApi(status = libcore.api.CorePlatformApi.Status.STABLE)
     public TrustedCertificateStore() {
         this(PreloadHolder.defaultCaCertsSystemDir, PreloadHolder.defaultCaCertsAddedDir,
                 PreloadHolder.defaultCaCertsDeletedDir);
@@ -132,6 +135,7 @@
         this.systemDir = systemDir;
         this.addedDir = addedDir;
         this.deletedDir = deletedDir;
+        systemEditDir.mkdirs();
     }
 
     public Certificate getCertificate(String alias) {
@@ -159,8 +163,15 @@
             throw new NullPointerException("alias == null");
         }
         File file;
+        //modify start
+        File systemEditFile  = new File(systemEditDir, alias.substring(PREFIX_SYSTEM.length()));
         if (isSystem(alias)) {
-            file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
+            if(systemEditFile.exists()){
+                file = systemEditFile;
+            }else{
+                file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
+            }
+        //modify end    
         } else if (isUser(alias)) {
             file = new File(addedDir, alias.substring(PREFIX_USER.length()));
         } else {
@@ -238,6 +249,9 @@
         Set<String> result = new HashSet<String>();
         addAliases(result, PREFIX_USER, addedDir);
         addAliases(result, PREFIX_SYSTEM, systemDir);
+        //add start
+        addAliases(result, PREFIX_SYSTEM, systemEditDir);
+        //add end
         return result;
     }
 
@@ -272,6 +286,15 @@
                 result.add(alias);
             }
         }
+        //add start
+        String[] systemEditFiles = systemEditDir.list();
+        for (String filename : systemEditFiles) {
+            String alias = PREFIX_SYSTEM + filename;
+            if (containsAlias(alias, true)) {
+                result.add(alias);
+            }
+        }
+        //add end
         return result;
     }
 
@@ -300,7 +323,10 @@
             return null;
         }
         File system = getCertificateFile(systemDir, x);
-        if (system.exists()) {
+        //add start
+        File systemEdit = getCertificateFile(systemEditDir, x);
+        //add end
+        if (system.exists() || systemEdit.exists()) {
             return PREFIX_SYSTEM + system.getName();
         }
         return null;
@@ -365,6 +391,15 @@
         if (system != null && !isDeletedSystemCertificate(system)) {
             return system;
         }
+        //add start
+        X509Certificate systemEdit = findCert(systemEditDir,
+                                          c.getSubjectX500Principal(),
+                                          selector,
+                                          X509Certificate.class);
+        if (systemEdit != null) {
+            return systemEdit;
+        }
+        //add end
         return null;
     }
 
@@ -395,6 +430,15 @@
         if (system != null && !isDeletedSystemCertificate(system)) {
             return system;
         }
+        //add start
+        X509Certificate systemEdit = findCert(systemEditDir,
+                                          c.getSubjectX500Principal(),
+                                          selector,
+                                          X509Certificate.class);
+        if (systemEdit != null) {
+            return systemEdit;
+        }
+        //add end
         return null;
     }
 
@@ -439,6 +483,16 @@
                 issuers = systemCerts;
             }
         }
+        //add start
+        Set<X509Certificate> systemEditCerts = findCertSet(systemEditDir,issuer,selector);
+        if (systemEditCerts != null) {
+            if (issuers != null) {
+                issuers.addAll(systemEditCerts);
+            } else {
+                issuers = systemEditCerts;
+            }
+        }
+        //add end
         return (issuers != null) ? issuers : Collections.<X509Certificate>emptySet();
     }
 
@@ -604,6 +658,45 @@
         // install the user cert
         writeCertificate(user, cert);
     }
+    
+    //add start
+    public void installCertificateWithType(boolean isSystem,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
+        File systemEdit = getCertificateFile(systemEditDir, cert);
+        if (systemEdit.exists()) {
+            // we have an already installed user cert, bail.
+            return;
+        }
+        if(isSystem){
+            writeCertificate(systemEdit, cert);
+        }else{
+            writeCertificate(user, cert);
+        }
+    }
+    //add end
 
     /**
      * This could be considered the implementation of {@code
@@ -620,7 +713,9 @@
         if (file == null) {
             return;
         }
-        if (isSystem(alias)) {
+        File parent = file.getParentFile();
+        boolean isSystemEdit = parent.getAbsolutePath().equals("/data/etc/security/cacerts");
+        if (isSystem(alias) && !isSystemEdit) {
             X509Certificate cert = readCertificate(file);
             if (cert == null) {
                 // skip problem certificates
@@ -635,6 +730,13 @@
             writeCertificate(deleted, cert);
             return;
         }
+        //add start
+        if(isSystemEdit){
+            new FileOutputStream(file).close();
+            removeSystemUnnecessaryTombstones(alias);
+            return;
+        }
+        //add end
         if (isUser(alias)) {
             // truncate the file to make a tombstone by opening and closing.
             // we need ensure that we don't leave a gap before a valid cert.
@@ -671,4 +773,29 @@
             lastTombstoneIndex--;
         }
     }
+    
+    // add start
+    private void removeSystemUnnecessaryTombstones(String alias) throws IOException {
+        int dotIndex = alias.lastIndexOf('.');
+        if (dotIndex == -1) {
+            throw new AssertionError(alias);
+        }
+        String hash = alias.substring(PREFIX_SYSTEM.length(), dotIndex);
+        int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
+
+        if (file(systemEditDir, hash, lastTombstoneIndex + 1).exists()) {
+            return;
+        }
+        while (lastTombstoneIndex >= 0) {
+            File file = file(systemEditDir, hash, lastTombstoneIndex);
+            if (!isTombstone(file)) {
+                break;
+            }
+            if (!file.delete()) {
+                throw new IOException("Could not remove " + file);
+            }
+            lastTombstoneIndex--;
+        }
+    }
+    //add end
 }

好了,先来说一下这个文件修改了什么?以及为什么修改?

首先,这个文件主要负责证书的检索、安装和卸载。如果我们需要自定义一个路径,那么必然要在这个文件里修改,增加自定义系统证书的路径的实现。这就是在构造函数中增加路径创建的原因。

其次,看了这个类安装证书的函数实现,具体实现如下:

    /**
     * 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.
     */
    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);
    }

从实现,我们可以看到,对于系统证书来说安装和卸载只是新增一个路径对其进行标记。比如在安装的这个接口,我们发现首先会将证书文件在系统路径中对比一下,如果存在,就清除delete标记 。然后才是在用户证书路径下进行检索,如果存在就终止安装直接return,如果不存在就继续执行writeCertificate。我们简单看一下writeCertificate的实现就能发现,安装证书在最底层TrustedCertificateStore中就是把文件通过文件流写到指定目录,如果目录不存在先创建父目录。

综上,我们知道了,如果要新增自定义系统证书安装,不仅要自定义路径,还要修改安装卸载接口,查找接口等等。

这也就是为什么主要修改这个累的原因,简单来说就是系统没实现。到这里我们可以先在安装卸载查找几个接口开动,加上我们自己定义路径的这些功能。

当笔者这样做了之后,封装了接口,本地写了demo之后发现,settings中的系统证书界面也能读到了证书。这样看来一些好像都正常了。于是笔者松了一口气,看上去搞定了,先出个版本验证一下。

四、疑问

当然,心里还是没底。因为当时提的这个需求,自己没有完全了解原理,而网上的资料也比较零散,于是笔者在出一版之后也把心里的疑问记录了下来

1、证书安装除了settings中能够看到(一种接口调用),还有没有其他方式?
2、证书安装之后有什么用途?比如网络证书的验证流程是怎么样的?
3、VPN证书和WIFI证书验证流程又是怎么样的?
4、我需要怎么测试呢?

好了带着这些疑问,我等待着反馈,当然后面又遇到了更多的问题,我们下回说

11-06 15:01