From aed0c9f716f377a0a18d5fb9dfa5f648155c0223 Mon Sep 17 00:00:00 2001 From: Alexander Koskovich Date: Thu, 28 Mar 2024 14:43:03 -0400 Subject: [PATCH] Add support for managed certificates Currently for Android we have two certificate prefixes, "system:" and "user:". System certificates are baked into the build and cannot be changed after the fact. User certificates can be installed after the fact, but they only get used for that user. For a device owner that wants to control certificates across the entire device this is problematic. Introduce a new "managed" store that can be used across all users. Certificates that get installed via the DevicePolicyManager APIs should be placed here. --- .../conscrypt/TrustedCertificateStore.java | 101 ++++++++++++++++-- .../TrustedCertificateStoreTest.java | 27 +++-- 2 files changed, 106 insertions(+), 22 deletions(-) diff --git a/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java b/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java index 14df3764d..a42a4fc26 100644 --- a/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java +++ b/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java @@ -82,9 +82,13 @@ */ @Internal public class TrustedCertificateStore implements ConscryptCertStore { + private static final String PREFIX_MANAGED = "managed:"; private static String PREFIX_SYSTEM = "system:"; private static final String PREFIX_USER = "user:"; + public static final boolean isManaged(String alias) { + return alias.startsWith(PREFIX_MANAGED); + } public static final boolean isSystem(String alias) { return alias.startsWith(PREFIX_SYSTEM); } @@ -93,6 +97,7 @@ public static final boolean isUser(String alias) { } private static class PreloadHolder { + private static File defaultCaCertsManagedDir; private static File defaultCaCertsSystemDir; private static File defaultCaCertsAddedDir; private static File defaultCaCertsDeletedDir; @@ -101,6 +106,7 @@ private static class PreloadHolder { String ANDROID_ROOT = System.getenv("ANDROID_ROOT"); String ANDROID_DATA = System.getenv("ANDROID_DATA"); File updatableDir = new File("/apex/com.android.conscrypt/cacerts"); + defaultCaCertsManagedDir = new File(ANDROID_DATA + "/misc/cacerts_managed"); if (shouldUseApex(updatableDir)) { defaultCaCertsSystemDir = updatableDir; } else { @@ -147,20 +153,23 @@ public static void setDefaultUserDirectory(File root) { PreloadHolder.defaultCaCertsDeletedDir = new File(root, "cacerts-removed"); } + private final File managedDir; private final File systemDir; private final File addedDir; private final File deletedDir; public TrustedCertificateStore() { - this(PreloadHolder.defaultCaCertsSystemDir, PreloadHolder.defaultCaCertsAddedDir, - PreloadHolder.defaultCaCertsDeletedDir); + this(PreloadHolder.defaultCaCertsManagedDir, PreloadHolder.defaultCaCertsSystemDir, + PreloadHolder.defaultCaCertsAddedDir, PreloadHolder.defaultCaCertsDeletedDir); } public TrustedCertificateStore(File baseDir) { - this(baseDir, PreloadHolder.defaultCaCertsAddedDir, PreloadHolder.defaultCaCertsDeletedDir); + this(PreloadHolder.defaultCaCertsManagedDir, baseDir, PreloadHolder.defaultCaCertsAddedDir, + PreloadHolder.defaultCaCertsDeletedDir); } - public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) { + public TrustedCertificateStore(File managedDir, File systemDir, File addedDir, File deletedDir) { + this.managedDir = managedDir; this.systemDir = systemDir; this.addedDir = addedDir; this.deletedDir = deletedDir; @@ -173,7 +182,7 @@ public Certificate getCertificate(String alias) { public Certificate getCertificate(String alias, boolean includeDeletedSystem) { File file = fileForAlias(alias); - if (file == null || (isUser(alias) && isTombstone(file))) { + if (file == null || (isUser(alias) && isTombstone(file)) || (isManaged(alias) && isTombstone(file))) { return null; } X509Certificate cert = readCertificate(file); @@ -193,6 +202,8 @@ private File fileForAlias(String alias) { File file; if (isSystem(alias)) { file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length())); + } else if (isManaged(alias)) { + file = new File(managedDir, alias.substring(PREFIX_MANAGED.length())); } else if (isUser(alias)) { file = new File(addedDir, alias.substring(PREFIX_USER.length())); } else { @@ -268,6 +279,7 @@ public Date getCreationDate(String alias) { public Set aliases() { Set result = new HashSet(); + addAliases(result, PREFIX_MANAGED, managedDir); addAliases(result, PREFIX_USER, addedDir); addAliases(result, PREFIX_SYSTEM, systemDir); return result; @@ -292,6 +304,21 @@ private void addAliases(Set result, String prefix, File dir) { } } + public Set allManagedAliases() { + Set result = new HashSet(); + String[] files = managedDir.list(); + if (files == null) { + return result; + } + for (String filename : files) { + String alias = PREFIX_MANAGED + filename; + if (containsAlias(alias, true)) { + result.add(alias); + } + } + return result; + } + public Set allSystemAliases() { Set result = new HashSet(); String[] files = systemDir.list(); @@ -324,6 +351,10 @@ public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) { return null; } X509Certificate x = (X509Certificate) c; + File managed = getCertificateFile(managedDir, x); + if (managed.exists()) { + return PREFIX_MANAGED + managed.getName(); + } File user = getCertificateFile(addedDir, x); if (user.exists()) { return PREFIX_USER + user.getName(); @@ -338,6 +369,14 @@ public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) { return null; } + /** + * Returns true to indicate that the certificate was added by the + * device owner, false otherwise. + */ + public boolean isManagedCertificate(X509Certificate cert) { + return getCertificateFile(managedDir, cert).exists(); + } + /** * Returns true to indicate that the certificate was added by the * user, false otherwise. @@ -383,6 +422,13 @@ public boolean match(X509Certificate ca) { return ca.getPublicKey().equals(c.getPublicKey()); } }; + X509Certificate managed = findCert(managedDir, + c.getSubjectX500Principal(), + selector, + X509Certificate.class); + if (managed != null) { + return managed; + } X509Certificate user = findCert(addedDir, c.getSubjectX500Principal(), selector, @@ -419,6 +465,10 @@ public boolean match(X509Certificate ca) { } }; X500Principal issuer = c.getIssuerX500Principal(); + X509Certificate managed = findCert(managedDir, issuer, selector, X509Certificate.class); + if (managed != null) { + return managed; + } X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class); if (user != null) { return user; @@ -445,6 +495,10 @@ public boolean match(X509Certificate ca) { } }; X500Principal issuer = c.getIssuerX500Principal(); + Set managedCerts = findCertSet(managedDir, issuer, selector); + if (managedCerts != null) { + issuers = managedCerts; + } Set userAddedCerts = findCertSet(addedDir, issuer, selector); if (userAddedCerts != null) { issuers = userAddedCerts; @@ -603,13 +657,21 @@ private File file(File dir, String hash, int index) { return new File(dir, hash + '.' + index); } + /** + * @deprecated Use {@link #installCertificate(boolean[], java.security.cert.X509Certificate)} instead. + */ + @Deprecated + public void installCertificate(X509Certificate cert) throws IOException, CertificateException { + installCertificate(false, cert); + } + /** * 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 { + public void installCertificate(boolean isManaged, X509Certificate cert) throws IOException, CertificateException { if (cert == null) { throw new NullPointerException("cert == null"); } @@ -628,6 +690,13 @@ public void installCertificate(X509Certificate cert) throws IOException, Certifi // return taking no further action. return; } + File managed = getCertificateFile(managedDir, cert); + if (isManaged) { + if (!managed.exists()) { + writeCertificate(managed, cert); + } + return; + } File user = getCertificateFile(addedDir, cert); if (user.exists()) { // we have an already installed user cert, bail. @@ -667,7 +736,7 @@ public void deleteCertificateEntry(String alias) throws IOException, Certificate writeCertificate(deleted, cert); return; } - if (isUser(alias)) { + if (isUser(alias) || isManaged(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. new FileOutputStream(file).close(); @@ -678,7 +747,7 @@ public void deleteCertificateEntry(String alias) throws IOException, Certificate } private void removeUnnecessaryTombstones(String alias) throws IOException { - if (!isUser(alias)) { + if (!isUser(alias) && !isManaged(alias)) { throw new AssertionError(alias); } int dotIndex = alias.lastIndexOf('.'); @@ -686,14 +755,24 @@ private void removeUnnecessaryTombstones(String alias) throws IOException { throw new AssertionError(alias); } - String hash = alias.substring(PREFIX_USER.length(), dotIndex); + File dir = null; + String hash = null; + if (isUser(alias)) { + dir = addedDir; + hash = alias.substring(PREFIX_USER.length(), dotIndex); + } else if (isManaged(alias)) { + dir = managedDir; + hash = alias.substring(PREFIX_MANAGED.length(), dotIndex); + } else { + throw new AssertionError(alias); + } int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); - if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { + if (file(dir, hash, lastTombstoneIndex + 1).exists()) { return; } while (lastTombstoneIndex >= 0) { - File file = file(addedDir, hash, lastTombstoneIndex); + File file = file(dir, hash, lastTombstoneIndex); if (!isTombstone(file)) { break; } diff --git a/platform/src/test/java/org/conscrypt/TrustedCertificateStoreTest.java b/platform/src/test/java/org/conscrypt/TrustedCertificateStoreTest.java index 8108d4404..914cd6a57 100644 --- a/platform/src/test/java/org/conscrypt/TrustedCertificateStoreTest.java +++ b/platform/src/test/java/org/conscrypt/TrustedCertificateStoreTest.java @@ -69,6 +69,7 @@ public class TrustedCertificateStoreTest { private static final Random tempFileRandom = new Random(); private static File dirTest; + private static File dirManaged; private static File dirSystem; private static File dirAdded; private static File dirDeleted; @@ -418,6 +419,7 @@ public static Object[] data() { @Before public void setUp() throws Exception { dirTest = Files.createTempDirectory("cert-store-test").toFile(); + dirManaged = new File(dirTest, "managed"); dirSystem = new File(dirTest, "system"); dirAdded = new File(dirTest, "added"); dirDeleted = new File(dirTest, "removed"); @@ -425,6 +427,7 @@ public void setUp() throws Exception { } private void setupStore() { + dirManaged.mkdirs(); dirSystem.mkdirs(); cleanStore(); createStore(); @@ -432,7 +435,7 @@ private void setupStore() { private void createStore() { System.setProperty("system.certs.enabled", mApexCertsEnabled); - store = new TrustedCertificateStore(dirSystem, dirAdded, dirDeleted); + store = new TrustedCertificateStore(dirManaged, dirSystem, dirAdded, dirDeleted); } @After @@ -441,7 +444,7 @@ public void tearDown() { } private void cleanStore() { - for (File dir : new File[] { dirSystem, dirAdded, dirDeleted, dirTest }) { + for (File dir : new File[] { dirManaged, dirSystem, dirAdded, dirDeleted, dirTest }) { File[] files = dir.listFiles(); if (files == null) { continue; @@ -543,7 +546,7 @@ private void assertEmpty() throws Exception { assertNull(store.findIssuer(getCa1())); try { - store.installCertificate(null); + store.installCertificate(false, null); fail(); } catch (NullPointerException expected) { } @@ -617,7 +620,7 @@ private void testTwo(X509Certificate x1, String alias1, @Test public void testOneSystemOneUserOneDeleted() throws Exception { install(getCa1(), getAliasSystemCa1()); - store.installCertificate(getCa2()); + store.installCertificate(false, getCa2()); store.deleteCertificateEntry(getAliasSystemCa1()); assertDeleted(getCa1(), getAliasSystemCa1()); assertRootCa(getCa2(), getAliasUserCa2()); @@ -627,7 +630,7 @@ public void testOneSystemOneUserOneDeleted() throws Exception { @Test public void testOneSystemOneUserOneDeletedSameSubject() throws Exception { install(getCa1(), getAliasSystemCa1()); - store.installCertificate(getCa3WithCa1Subject()); + store.installCertificate(false, getCa3WithCa1Subject()); store.deleteCertificateEntry(getAliasSystemCa1()); assertDeleted(getCa1(), getAliasSystemCa1()); assertRootCa(getCa3WithCa1Subject(), getAliasUserCa3()); @@ -705,7 +708,7 @@ public void testIsTrustAnchorWithReissuedgetCa() throws Exception { resetStore(); String userAlias = alias(true, ca1, 0); - store.installCertificate(ca1); + store.installCertificate(false, ca1); assertRootCa(ca1, userAlias); assertNotNull(store.getTrustAnchor(ca2)); assertEquals(ca1, store.findIssuer(ca2)); @@ -714,12 +717,12 @@ public void testIsTrustAnchorWithReissuedgetCa() throws Exception { @Test public void testInstallEmpty() throws Exception { - store.installCertificate(getCa1()); + store.installCertificate(false, getCa1()); assertRootCa(getCa1(), getAliasUserCa1()); assertAliases(getAliasUserCa1()); // reinstalling should not change anything - store.installCertificate(getCa1()); + store.installCertificate(false, getCa1()); assertRootCa(getCa1(), getAliasUserCa1()); assertAliases(getAliasUserCa1()); } @@ -731,7 +734,7 @@ public void testInstallEmptySystemExists() throws Exception { assertAliases(getAliasSystemCa1()); // reinstalling should not affect system CA - store.installCertificate(getCa1()); + store.installCertificate(false, getCa1()); assertRootCa(getCa1(), getAliasSystemCa1()); assertAliases(getAliasSystemCa1()); } @@ -744,7 +747,7 @@ public void testInstallEmptyDeletedSystemExists() throws Exception { assertDeleted(getCa1(), getAliasSystemCa1()); // installing should restore deleted system CA - store.installCertificate(getCa1()); + store.installCertificate(false, getCa1()); assertRootCa(getCa1(), getAliasSystemCa1()); assertAliases(getAliasSystemCa1()); } @@ -758,7 +761,7 @@ public void testDeleteEmpty() throws Exception { @Test public void testDeleteUser() throws Exception { - store.installCertificate(getCa1()); + store.installCertificate(false, getCa1()); assertRootCa(getCa1(), getAliasUserCa1()); assertAliases(getAliasUserCa1()); @@ -1035,6 +1038,8 @@ private File file(String alias) { File dir; if (TrustedCertificateStore.isSystem(alias)) { dir = dirSystem; + } else if (TrustedCertificateStore.isManaged(alias)) { + dir = dirManaged; } else if (TrustedCertificateStore.isUser(alias)) { dir = dirAdded; } else {