Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口
一、前言
接着上回说,最初是为了写一个SDK的接口,需求大致是增加证书安装卸载的接口(系统、用户)。于是了解了一下证书相关的处理逻辑,在了解了功能和流程之后,发现settings中支持安装的证书,只能安装到指定路径,并且是user 证书。那么到目前为止,安装用户证书的需求算是可行,可以完成。但是还遗留着一个问题,如何安装系统证书呢?
在上篇文章里边笔者给了两个方案:
1、一种是把证书复制到系统证书的存放路径 /system/etc/security/cacerts
2、另一种是创建一个新的目录
下面开始分析两个方案的可行性
二、可行性分析
1、把证书复制到系统证书的存放路径 /system/etc/security/cacerts
我们可以发现该路径是system分区,system分区一般是只读的,而参考了一下settings里边对系统证书的处理,没有删除,只有禁用和启用。所以不推荐在原有路径下操作。我们顺便看一下对应路径的selinux权限。
可以看到证书路径下除了root 用户都没有写权限,且selinux 的域为system_security_cacerts_file。我们也看一下这域的对dir 和file 权限范围的定义:
我们可以看到,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、我需要怎么测试呢?
好了带着这些疑问,我等待着反馈,当然后面又遇到了更多的问题,我们下回说