Skip to content

Commit

Permalink
Add support for managed certificates
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ZVNexus committed Mar 28, 2024
1 parent 59de319 commit 4230430
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 22 deletions.
93 changes: 82 additions & 11 deletions platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -268,6 +279,7 @@ public Date getCreationDate(String alias) {

public Set<String> aliases() {
Set<String> result = new HashSet<String>();
addAliases(result, PREFIX_MANAGED, managedDir);
addAliases(result, PREFIX_USER, addedDir);
addAliases(result, PREFIX_SYSTEM, systemDir);
return result;
Expand All @@ -292,6 +304,21 @@ private void addAliases(Set<String> result, String prefix, File dir) {
}
}

public Set<String> allManagedAliases() {
Set<String> result = new HashSet<String>();
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<String> allSystemAliases() {
Set<String> result = new HashSet<String>();
String[] files = systemDir.list();
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -445,6 +495,10 @@ public boolean match(X509Certificate ca) {
}
};
X500Principal issuer = c.getIssuerX500Principal();
Set<X509Certificate> managedCerts = findCertSet(managedDir, issuer, selector);
if (managedCerts != null) {
issuers = managedCerts;
}
Set<X509Certificate> userAddedCerts = findCertSet(addedDir, issuer, selector);
if (userAddedCerts != null) {
issuers = userAddedCerts;
Expand Down Expand Up @@ -609,7 +663,7 @@ private File file(File dir, String hash, int index) {
* 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");
}
Expand All @@ -628,6 +682,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.
Expand Down Expand Up @@ -667,7 +728,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();
Expand All @@ -678,22 +739,32 @@ 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('.');
if (dotIndex == -1) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -418,21 +419,23 @@ 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");
setupStore();
}

private void setupStore() {
dirManaged.mkdirs();
dirSystem.mkdirs();
cleanStore();
createStore();
}

private void createStore() {
System.setProperty("system.certs.enabled", mApexCertsEnabled);
store = new TrustedCertificateStore(dirSystem, dirAdded, dirDeleted);
store = new TrustedCertificateStore(dirManaged, dirSystem, dirAdded, dirDeleted);
}

@After
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
}
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down Expand Up @@ -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));
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 4230430

Please sign in to comment.