Skip to content

Commit aed0c9f

Browse files
committed
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.
1 parent 59de319 commit aed0c9f

File tree

2 files changed

+106
-22
lines changed

2 files changed

+106
-22
lines changed

platform/src/main/java/org/conscrypt/TrustedCertificateStore.java

+90-11
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@
8282
*/
8383
@Internal
8484
public class TrustedCertificateStore implements ConscryptCertStore {
85+
private static final String PREFIX_MANAGED = "managed:";
8586
private static String PREFIX_SYSTEM = "system:";
8687
private static final String PREFIX_USER = "user:";
8788

89+
public static final boolean isManaged(String alias) {
90+
return alias.startsWith(PREFIX_MANAGED);
91+
}
8892
public static final boolean isSystem(String alias) {
8993
return alias.startsWith(PREFIX_SYSTEM);
9094
}
@@ -93,6 +97,7 @@ public static final boolean isUser(String alias) {
9397
}
9498

9599
private static class PreloadHolder {
100+
private static File defaultCaCertsManagedDir;
96101
private static File defaultCaCertsSystemDir;
97102
private static File defaultCaCertsAddedDir;
98103
private static File defaultCaCertsDeletedDir;
@@ -101,6 +106,7 @@ private static class PreloadHolder {
101106
String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
102107
String ANDROID_DATA = System.getenv("ANDROID_DATA");
103108
File updatableDir = new File("/apex/com.android.conscrypt/cacerts");
109+
defaultCaCertsManagedDir = new File(ANDROID_DATA + "/misc/cacerts_managed");
104110
if (shouldUseApex(updatableDir)) {
105111
defaultCaCertsSystemDir = updatableDir;
106112
} else {
@@ -147,20 +153,23 @@ public static void setDefaultUserDirectory(File root) {
147153
PreloadHolder.defaultCaCertsDeletedDir = new File(root, "cacerts-removed");
148154
}
149155

156+
private final File managedDir;
150157
private final File systemDir;
151158
private final File addedDir;
152159
private final File deletedDir;
153160

154161
public TrustedCertificateStore() {
155-
this(PreloadHolder.defaultCaCertsSystemDir, PreloadHolder.defaultCaCertsAddedDir,
156-
PreloadHolder.defaultCaCertsDeletedDir);
162+
this(PreloadHolder.defaultCaCertsManagedDir, PreloadHolder.defaultCaCertsSystemDir,
163+
PreloadHolder.defaultCaCertsAddedDir, PreloadHolder.defaultCaCertsDeletedDir);
157164
}
158165

159166
public TrustedCertificateStore(File baseDir) {
160-
this(baseDir, PreloadHolder.defaultCaCertsAddedDir, PreloadHolder.defaultCaCertsDeletedDir);
167+
this(PreloadHolder.defaultCaCertsManagedDir, baseDir, PreloadHolder.defaultCaCertsAddedDir,
168+
PreloadHolder.defaultCaCertsDeletedDir);
161169
}
162170

163-
public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
171+
public TrustedCertificateStore(File managedDir, File systemDir, File addedDir, File deletedDir) {
172+
this.managedDir = managedDir;
164173
this.systemDir = systemDir;
165174
this.addedDir = addedDir;
166175
this.deletedDir = deletedDir;
@@ -173,7 +182,7 @@ public Certificate getCertificate(String alias) {
173182
public Certificate getCertificate(String alias, boolean includeDeletedSystem) {
174183

175184
File file = fileForAlias(alias);
176-
if (file == null || (isUser(alias) && isTombstone(file))) {
185+
if (file == null || (isUser(alias) && isTombstone(file)) || (isManaged(alias) && isTombstone(file))) {
177186
return null;
178187
}
179188
X509Certificate cert = readCertificate(file);
@@ -193,6 +202,8 @@ private File fileForAlias(String alias) {
193202
File file;
194203
if (isSystem(alias)) {
195204
file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
205+
} else if (isManaged(alias)) {
206+
file = new File(managedDir, alias.substring(PREFIX_MANAGED.length()));
196207
} else if (isUser(alias)) {
197208
file = new File(addedDir, alias.substring(PREFIX_USER.length()));
198209
} else {
@@ -268,6 +279,7 @@ public Date getCreationDate(String alias) {
268279

269280
public Set<String> aliases() {
270281
Set<String> result = new HashSet<String>();
282+
addAliases(result, PREFIX_MANAGED, managedDir);
271283
addAliases(result, PREFIX_USER, addedDir);
272284
addAliases(result, PREFIX_SYSTEM, systemDir);
273285
return result;
@@ -292,6 +304,21 @@ private void addAliases(Set<String> result, String prefix, File dir) {
292304
}
293305
}
294306

307+
public Set<String> allManagedAliases() {
308+
Set<String> result = new HashSet<String>();
309+
String[] files = managedDir.list();
310+
if (files == null) {
311+
return result;
312+
}
313+
for (String filename : files) {
314+
String alias = PREFIX_MANAGED + filename;
315+
if (containsAlias(alias, true)) {
316+
result.add(alias);
317+
}
318+
}
319+
return result;
320+
}
321+
295322
public Set<String> allSystemAliases() {
296323
Set<String> result = new HashSet<String>();
297324
String[] files = systemDir.list();
@@ -324,6 +351,10 @@ public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) {
324351
return null;
325352
}
326353
X509Certificate x = (X509Certificate) c;
354+
File managed = getCertificateFile(managedDir, x);
355+
if (managed.exists()) {
356+
return PREFIX_MANAGED + managed.getName();
357+
}
327358
File user = getCertificateFile(addedDir, x);
328359
if (user.exists()) {
329360
return PREFIX_USER + user.getName();
@@ -338,6 +369,14 @@ public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) {
338369
return null;
339370
}
340371

372+
/**
373+
* Returns true to indicate that the certificate was added by the
374+
* device owner, false otherwise.
375+
*/
376+
public boolean isManagedCertificate(X509Certificate cert) {
377+
return getCertificateFile(managedDir, cert).exists();
378+
}
379+
341380
/**
342381
* Returns true to indicate that the certificate was added by the
343382
* user, false otherwise.
@@ -383,6 +422,13 @@ public boolean match(X509Certificate ca) {
383422
return ca.getPublicKey().equals(c.getPublicKey());
384423
}
385424
};
425+
X509Certificate managed = findCert(managedDir,
426+
c.getSubjectX500Principal(),
427+
selector,
428+
X509Certificate.class);
429+
if (managed != null) {
430+
return managed;
431+
}
386432
X509Certificate user = findCert(addedDir,
387433
c.getSubjectX500Principal(),
388434
selector,
@@ -419,6 +465,10 @@ public boolean match(X509Certificate ca) {
419465
}
420466
};
421467
X500Principal issuer = c.getIssuerX500Principal();
468+
X509Certificate managed = findCert(managedDir, issuer, selector, X509Certificate.class);
469+
if (managed != null) {
470+
return managed;
471+
}
422472
X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
423473
if (user != null) {
424474
return user;
@@ -445,6 +495,10 @@ public boolean match(X509Certificate ca) {
445495
}
446496
};
447497
X500Principal issuer = c.getIssuerX500Principal();
498+
Set<X509Certificate> managedCerts = findCertSet(managedDir, issuer, selector);
499+
if (managedCerts != null) {
500+
issuers = managedCerts;
501+
}
448502
Set<X509Certificate> userAddedCerts = findCertSet(addedDir, issuer, selector);
449503
if (userAddedCerts != null) {
450504
issuers = userAddedCerts;
@@ -603,13 +657,21 @@ private File file(File dir, String hash, int index) {
603657
return new File(dir, hash + '.' + index);
604658
}
605659

660+
/**
661+
* @deprecated Use {@link #installCertificate(boolean[], java.security.cert.X509Certificate)} instead.
662+
*/
663+
@Deprecated
664+
public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
665+
installCertificate(false, cert);
666+
}
667+
606668
/**
607669
* This non-{@code KeyStoreSpi} public interface is used by the
608670
* {@code KeyChainService} to install new CA certificates. It
609671
* silently ignores the certificate if it already exists in the
610672
* store.
611673
*/
612-
public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
674+
public void installCertificate(boolean isManaged, X509Certificate cert) throws IOException, CertificateException {
613675
if (cert == null) {
614676
throw new NullPointerException("cert == null");
615677
}
@@ -628,6 +690,13 @@ public void installCertificate(X509Certificate cert) throws IOException, Certifi
628690
// return taking no further action.
629691
return;
630692
}
693+
File managed = getCertificateFile(managedDir, cert);
694+
if (isManaged) {
695+
if (!managed.exists()) {
696+
writeCertificate(managed, cert);
697+
}
698+
return;
699+
}
631700
File user = getCertificateFile(addedDir, cert);
632701
if (user.exists()) {
633702
// we have an already installed user cert, bail.
@@ -667,7 +736,7 @@ public void deleteCertificateEntry(String alias) throws IOException, Certificate
667736
writeCertificate(deleted, cert);
668737
return;
669738
}
670-
if (isUser(alias)) {
739+
if (isUser(alias) || isManaged(alias)) {
671740
// truncate the file to make a tombstone by opening and closing.
672741
// we need ensure that we don't leave a gap before a valid cert.
673742
new FileOutputStream(file).close();
@@ -678,22 +747,32 @@ public void deleteCertificateEntry(String alias) throws IOException, Certificate
678747
}
679748

680749
private void removeUnnecessaryTombstones(String alias) throws IOException {
681-
if (!isUser(alias)) {
750+
if (!isUser(alias) && !isManaged(alias)) {
682751
throw new AssertionError(alias);
683752
}
684753
int dotIndex = alias.lastIndexOf('.');
685754
if (dotIndex == -1) {
686755
throw new AssertionError(alias);
687756
}
688757

689-
String hash = alias.substring(PREFIX_USER.length(), dotIndex);
758+
File dir = null;
759+
String hash = null;
760+
if (isUser(alias)) {
761+
dir = addedDir;
762+
hash = alias.substring(PREFIX_USER.length(), dotIndex);
763+
} else if (isManaged(alias)) {
764+
dir = managedDir;
765+
hash = alias.substring(PREFIX_MANAGED.length(), dotIndex);
766+
} else {
767+
throw new AssertionError(alias);
768+
}
690769
int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
691770

692-
if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
771+
if (file(dir, hash, lastTombstoneIndex + 1).exists()) {
693772
return;
694773
}
695774
while (lastTombstoneIndex >= 0) {
696-
File file = file(addedDir, hash, lastTombstoneIndex);
775+
File file = file(dir, hash, lastTombstoneIndex);
697776
if (!isTombstone(file)) {
698777
break;
699778
}

platform/src/test/java/org/conscrypt/TrustedCertificateStoreTest.java

+16-11
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class TrustedCertificateStoreTest {
6969
private static final Random tempFileRandom = new Random();
7070

7171
private static File dirTest;
72+
private static File dirManaged;
7273
private static File dirSystem;
7374
private static File dirAdded;
7475
private static File dirDeleted;
@@ -418,21 +419,23 @@ public static Object[] data() {
418419
@Before
419420
public void setUp() throws Exception {
420421
dirTest = Files.createTempDirectory("cert-store-test").toFile();
422+
dirManaged = new File(dirTest, "managed");
421423
dirSystem = new File(dirTest, "system");
422424
dirAdded = new File(dirTest, "added");
423425
dirDeleted = new File(dirTest, "removed");
424426
setupStore();
425427
}
426428

427429
private void setupStore() {
430+
dirManaged.mkdirs();
428431
dirSystem.mkdirs();
429432
cleanStore();
430433
createStore();
431434
}
432435

433436
private void createStore() {
434437
System.setProperty("system.certs.enabled", mApexCertsEnabled);
435-
store = new TrustedCertificateStore(dirSystem, dirAdded, dirDeleted);
438+
store = new TrustedCertificateStore(dirManaged, dirSystem, dirAdded, dirDeleted);
436439
}
437440

438441
@After
@@ -441,7 +444,7 @@ public void tearDown() {
441444
}
442445

443446
private void cleanStore() {
444-
for (File dir : new File[] { dirSystem, dirAdded, dirDeleted, dirTest }) {
447+
for (File dir : new File[] { dirManaged, dirSystem, dirAdded, dirDeleted, dirTest }) {
445448
File[] files = dir.listFiles();
446449
if (files == null) {
447450
continue;
@@ -543,7 +546,7 @@ private void assertEmpty() throws Exception {
543546
assertNull(store.findIssuer(getCa1()));
544547

545548
try {
546-
store.installCertificate(null);
549+
store.installCertificate(false, null);
547550
fail();
548551
} catch (NullPointerException expected) {
549552
}
@@ -617,7 +620,7 @@ private void testTwo(X509Certificate x1, String alias1,
617620
@Test
618621
public void testOneSystemOneUserOneDeleted() throws Exception {
619622
install(getCa1(), getAliasSystemCa1());
620-
store.installCertificate(getCa2());
623+
store.installCertificate(false, getCa2());
621624
store.deleteCertificateEntry(getAliasSystemCa1());
622625
assertDeleted(getCa1(), getAliasSystemCa1());
623626
assertRootCa(getCa2(), getAliasUserCa2());
@@ -627,7 +630,7 @@ public void testOneSystemOneUserOneDeleted() throws Exception {
627630
@Test
628631
public void testOneSystemOneUserOneDeletedSameSubject() throws Exception {
629632
install(getCa1(), getAliasSystemCa1());
630-
store.installCertificate(getCa3WithCa1Subject());
633+
store.installCertificate(false, getCa3WithCa1Subject());
631634
store.deleteCertificateEntry(getAliasSystemCa1());
632635
assertDeleted(getCa1(), getAliasSystemCa1());
633636
assertRootCa(getCa3WithCa1Subject(), getAliasUserCa3());
@@ -705,7 +708,7 @@ public void testIsTrustAnchorWithReissuedgetCa() throws Exception {
705708
resetStore();
706709

707710
String userAlias = alias(true, ca1, 0);
708-
store.installCertificate(ca1);
711+
store.installCertificate(false, ca1);
709712
assertRootCa(ca1, userAlias);
710713
assertNotNull(store.getTrustAnchor(ca2));
711714
assertEquals(ca1, store.findIssuer(ca2));
@@ -714,12 +717,12 @@ public void testIsTrustAnchorWithReissuedgetCa() throws Exception {
714717

715718
@Test
716719
public void testInstallEmpty() throws Exception {
717-
store.installCertificate(getCa1());
720+
store.installCertificate(false, getCa1());
718721
assertRootCa(getCa1(), getAliasUserCa1());
719722
assertAliases(getAliasUserCa1());
720723

721724
// reinstalling should not change anything
722-
store.installCertificate(getCa1());
725+
store.installCertificate(false, getCa1());
723726
assertRootCa(getCa1(), getAliasUserCa1());
724727
assertAliases(getAliasUserCa1());
725728
}
@@ -731,7 +734,7 @@ public void testInstallEmptySystemExists() throws Exception {
731734
assertAliases(getAliasSystemCa1());
732735

733736
// reinstalling should not affect system CA
734-
store.installCertificate(getCa1());
737+
store.installCertificate(false, getCa1());
735738
assertRootCa(getCa1(), getAliasSystemCa1());
736739
assertAliases(getAliasSystemCa1());
737740
}
@@ -744,7 +747,7 @@ public void testInstallEmptyDeletedSystemExists() throws Exception {
744747
assertDeleted(getCa1(), getAliasSystemCa1());
745748

746749
// installing should restore deleted system CA
747-
store.installCertificate(getCa1());
750+
store.installCertificate(false, getCa1());
748751
assertRootCa(getCa1(), getAliasSystemCa1());
749752
assertAliases(getAliasSystemCa1());
750753
}
@@ -758,7 +761,7 @@ public void testDeleteEmpty() throws Exception {
758761

759762
@Test
760763
public void testDeleteUser() throws Exception {
761-
store.installCertificate(getCa1());
764+
store.installCertificate(false, getCa1());
762765
assertRootCa(getCa1(), getAliasUserCa1());
763766
assertAliases(getAliasUserCa1());
764767

@@ -1035,6 +1038,8 @@ private File file(String alias) {
10351038
File dir;
10361039
if (TrustedCertificateStore.isSystem(alias)) {
10371040
dir = dirSystem;
1041+
} else if (TrustedCertificateStore.isManaged(alias)) {
1042+
dir = dirManaged;
10381043
} else if (TrustedCertificateStore.isUser(alias)) {
10391044
dir = dirAdded;
10401045
} else {

0 commit comments

Comments
 (0)