• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.conscrypt;
18 
19 import java.io.BufferedInputStream;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.security.cert.Certificate;
27 import java.security.cert.CertificateException;
28 import java.security.cert.CertificateFactory;
29 import java.security.cert.X509Certificate;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Date;
33 import java.util.HashSet;
34 import java.util.LinkedHashSet;
35 import java.util.List;
36 import java.util.Set;
37 import javax.security.auth.x500.X500Principal;
38 import libcore.io.IoUtils;
39 
40 /**
41  * A source for trusted root certificate authority (CA) certificates
42  * supporting an immutable system CA directory along with mutable
43  * directories allowing the user addition of custom CAs and user
44  * removal of system CAs. This store supports the {@code
45  * TrustedCertificateKeyStoreSpi} wrapper to allow a traditional
46  * KeyStore interface for use with {@link
47  * javax.net.ssl.TrustManagerFactory.init}.
48  *
49  * <p>The CAs are accessed via {@code KeyStore} style aliases. Aliases
50  * are made up of a prefix identifying the source ("system:" vs
51  * "user:") and a suffix based on the OpenSSL X509_NAME_hash_old
52  * function of the CA's subject name. For example, the system CA for
53  * "C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification
54  * Authority" could be represented as "system:7651b327.0". By using
55  * the subject hash, operations such as {@link #getCertificateAlias
56  * getCertificateAlias} can be implemented efficiently without
57  * scanning the entire store.
58  *
59  * <p>In addition to supporting the {@code
60  * TrustedCertificateKeyStoreSpi} implementation, {@code
61  * TrustedCertificateStore} also provides the additional public
62  * methods {@link #isTrustAnchor} and {@link #findIssuer} to allow
63  * efficient lookup operations for CAs again based on the file naming
64  * convention.
65  *
66  * <p>The KeyChainService users the {@link installCertificate} and
67  * {@link #deleteCertificateEntry} to install user CAs as well as
68  * delete those user CAs as well as system CAs. The deletion of system
69  * CAs is performed by placing an exact copy of that CA in the deleted
70  * directory. Such deletions are intended to persist across upgrades
71  * but not intended to mask a CA with a matching name or public key
72  * but is otherwise reissued in a system update. Reinstalling a
73  * deleted system certificate simply removes the copy from the deleted
74  * directory, reenabling the original in the system directory.
75  *
76  * <p>Note that the default mutable directory is created by init via
77  * configuration in the system/core/rootdir/init.rc file. The
78  * directive "mkdir /data/misc/keychain 0775 system system"
79  * ensures that its owner and group are the system uid and system
80  * gid and that it is world readable but only writable by the system
81  * user.
82  *
83  * @hide
84  */
85 @Internal
86 public class TrustedCertificateStore {
87 
88     private static final String PREFIX_SYSTEM = "system:";
89     private static final String PREFIX_USER = "user:";
90 
isSystem(String alias)91     public static final boolean isSystem(String alias) {
92         return alias.startsWith(PREFIX_SYSTEM);
93     }
isUser(String alias)94     public static final boolean isUser(String alias) {
95         return alias.startsWith(PREFIX_USER);
96     }
97 
98     private static class PreloadHolder {
99         private static File defaultCaCertsSystemDir;
100         private static File defaultCaCertsAddedDir;
101         private static File defaultCaCertsDeletedDir;
102 
103         static {
104             String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
105             String ANDROID_DATA = System.getenv("ANDROID_DATA");
106             defaultCaCertsSystemDir = new File(ANDROID_ROOT + "/etc/security/cacerts");
setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"))107             setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"));
108         }
109     }
110 
111     private static final CertificateFactory CERT_FACTORY;
112     static {
113         try {
114             CERT_FACTORY = CertificateFactory.getInstance("X509");
115         } catch (CertificateException e) {
116             throw new AssertionError(e);
117         }
118     }
119 
setDefaultUserDirectory(File root)120     public static void setDefaultUserDirectory(File root) {
121         PreloadHolder.defaultCaCertsAddedDir = new File(root, "cacerts-added");
122         PreloadHolder.defaultCaCertsDeletedDir = new File(root, "cacerts-removed");
123     }
124 
125     private final File systemDir;
126     private final File addedDir;
127     private final File deletedDir;
128 
TrustedCertificateStore()129     public TrustedCertificateStore() {
130         this(PreloadHolder.defaultCaCertsSystemDir, PreloadHolder.defaultCaCertsAddedDir,
131                 PreloadHolder.defaultCaCertsDeletedDir);
132     }
133 
TrustedCertificateStore(File systemDir, File addedDir, File deletedDir)134     public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
135         this.systemDir = systemDir;
136         this.addedDir = addedDir;
137         this.deletedDir = deletedDir;
138     }
139 
getCertificate(String alias)140     public Certificate getCertificate(String alias) {
141         return getCertificate(alias, false);
142     }
143 
getCertificate(String alias, boolean includeDeletedSystem)144     public Certificate getCertificate(String alias, boolean includeDeletedSystem) {
145 
146         File file = fileForAlias(alias);
147         if (file == null || (isUser(alias) && isTombstone(file))) {
148             return null;
149         }
150         X509Certificate cert = readCertificate(file);
151         if (cert == null || (isSystem(alias)
152                              && !includeDeletedSystem
153                              && isDeletedSystemCertificate(cert))) {
154             // skip malformed certs as well as deleted system ones
155             return null;
156         }
157         return cert;
158     }
159 
fileForAlias(String alias)160     private File fileForAlias(String alias) {
161         if (alias == null) {
162             throw new NullPointerException("alias == null");
163         }
164         File file;
165         if (isSystem(alias)) {
166             file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
167         } else if (isUser(alias)) {
168             file = new File(addedDir, alias.substring(PREFIX_USER.length()));
169         } else {
170             return null;
171         }
172         if (!file.exists() || isTombstone(file)) {
173             // silently elide tombstones
174             return null;
175         }
176         return file;
177     }
178 
isTombstone(File file)179     private boolean isTombstone(File file) {
180         return file.length() == 0;
181     }
182 
readCertificate(File file)183     private X509Certificate readCertificate(File file) {
184         if (!file.isFile()) {
185             return null;
186         }
187         InputStream is = null;
188         try {
189             is = new BufferedInputStream(new FileInputStream(file));
190             return (X509Certificate) CERT_FACTORY.generateCertificate(is);
191         } catch (IOException e) {
192             return null;
193         } catch (CertificateException e) {
194             // reading a cert while its being installed can lead to this.
195             // just pretend like its not available yet.
196             return null;
197         } finally {
198             IoUtils.closeQuietly(is);
199         }
200     }
201 
writeCertificate(File file, X509Certificate cert)202     private void writeCertificate(File file, X509Certificate cert)
203             throws IOException, CertificateException {
204         File dir = file.getParentFile();
205         dir.mkdirs();
206         dir.setReadable(true, false);
207         dir.setExecutable(true, false);
208         OutputStream os = null;
209         try {
210             os = new FileOutputStream(file);
211             os.write(cert.getEncoded());
212         } finally {
213             IoUtils.closeQuietly(os);
214         }
215         file.setReadable(true, false);
216     }
217 
isDeletedSystemCertificate(X509Certificate x)218     private boolean isDeletedSystemCertificate(X509Certificate x) {
219         return getCertificateFile(deletedDir, x).exists();
220     }
221 
getCreationDate(String alias)222     public Date getCreationDate(String alias) {
223         // containsAlias check ensures the later fileForAlias result
224         // was not a deleted system cert.
225         if (!containsAlias(alias)) {
226             return null;
227         }
228         File file = fileForAlias(alias);
229         if (file == null) {
230             return null;
231         }
232         long time = file.lastModified();
233         if (time == 0) {
234             return null;
235         }
236         return new Date(time);
237     }
238 
aliases()239     public Set<String> aliases() {
240         Set<String> result = new HashSet<String>();
241         addAliases(result, PREFIX_USER, addedDir);
242         addAliases(result, PREFIX_SYSTEM, systemDir);
243         return result;
244     }
245 
userAliases()246     public Set<String> userAliases() {
247         Set<String> result = new HashSet<String>();
248         addAliases(result, PREFIX_USER, addedDir);
249         return result;
250     }
251 
addAliases(Set<String> result, String prefix, File dir)252     private void addAliases(Set<String> result, String prefix, File dir) {
253         String[] files = dir.list();
254         if (files == null) {
255             return;
256         }
257         for (String filename : files) {
258             String alias = prefix + filename;
259             if (containsAlias(alias)) {
260                 result.add(alias);
261             }
262         }
263     }
264 
allSystemAliases()265     public Set<String> allSystemAliases() {
266         Set<String> result = new HashSet<String>();
267         String[] files = systemDir.list();
268         if (files == null) {
269             return result;
270         }
271         for (String filename : files) {
272             String alias = PREFIX_SYSTEM + filename;
273             if (containsAlias(alias, true)) {
274                 result.add(alias);
275             }
276         }
277         return result;
278     }
279 
containsAlias(String alias)280     public boolean containsAlias(String alias) {
281         return containsAlias(alias, false);
282     }
283 
containsAlias(String alias, boolean includeDeletedSystem)284     private boolean containsAlias(String alias, boolean includeDeletedSystem) {
285         return getCertificate(alias, includeDeletedSystem) != null;
286     }
287 
getCertificateAlias(Certificate c)288     public String getCertificateAlias(Certificate c) {
289         return getCertificateAlias(c, false);
290     }
291 
getCertificateAlias(Certificate c, boolean includeDeletedSystem)292     public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) {
293         if (c == null || !(c instanceof X509Certificate)) {
294             return null;
295         }
296         X509Certificate x = (X509Certificate) c;
297         File user = getCertificateFile(addedDir, x);
298         if (user.exists()) {
299             return PREFIX_USER + user.getName();
300         }
301         if (!includeDeletedSystem && isDeletedSystemCertificate(x)) {
302             return null;
303         }
304         File system = getCertificateFile(systemDir, x);
305         if (system.exists()) {
306             return PREFIX_SYSTEM + system.getName();
307         }
308         return null;
309     }
310 
311     /**
312      * Returns true to indicate that the certificate was added by the
313      * user, false otherwise.
314      */
isUserAddedCertificate(X509Certificate cert)315     public boolean isUserAddedCertificate(X509Certificate cert) {
316         return getCertificateFile(addedDir, cert).exists();
317     }
318 
319     /**
320      * Returns a File for where the certificate is found if it exists
321      * or where it should be installed if it does not exist. The
322      * caller can disambiguate these cases by calling {@code
323      * File.exists()} on the result.
324      *
325      * @VisibleForTesting
326      */
getCertificateFile(File dir, final X509Certificate x)327     public File getCertificateFile(File dir, final X509Certificate x) {
328         // compare X509Certificate.getEncoded values
329         CertSelector selector = new CertSelector() {
330             @Override
331             public boolean match(X509Certificate cert) {
332                 return cert.equals(x);
333             }
334         };
335         return findCert(dir, x.getSubjectX500Principal(), selector, File.class);
336     }
337 
338     /**
339      * This non-{@code KeyStoreSpi} public interface is used by {@code
340      * TrustManagerImpl} to locate a CA certificate with the same name
341      * and public key as the provided {@code X509Certificate}. We
342      * match on the name and public key and not the entire certificate
343      * since a CA may be reissued with the same name and PublicKey but
344      * with other differences (for example when switching signature
345      * from md2WithRSAEncryption to SHA1withRSA)
346      */
getTrustAnchor(final X509Certificate c)347     public X509Certificate getTrustAnchor(final X509Certificate c) {
348         // compare X509Certificate.getPublicKey values
349         CertSelector selector = new CertSelector() {
350             @Override
351             public boolean match(X509Certificate ca) {
352                 return ca.getPublicKey().equals(c.getPublicKey());
353             }
354         };
355         X509Certificate user = findCert(addedDir,
356                                         c.getSubjectX500Principal(),
357                                         selector,
358                                         X509Certificate.class);
359         if (user != null) {
360             return user;
361         }
362         X509Certificate system = findCert(systemDir,
363                                           c.getSubjectX500Principal(),
364                                           selector,
365                                           X509Certificate.class);
366         if (system != null && !isDeletedSystemCertificate(system)) {
367             return system;
368         }
369         return null;
370     }
371 
372     /**
373      * This non-{@code KeyStoreSpi} public interface is used by {@code
374      * TrustManagerImpl} to locate the CA certificate that signed the
375      * provided {@code X509Certificate}.
376      */
findIssuer(final X509Certificate c)377     public X509Certificate findIssuer(final X509Certificate c) {
378         // match on verified issuer of Certificate
379         CertSelector selector = new CertSelector() {
380             @Override
381             public boolean match(X509Certificate ca) {
382                 try {
383                     c.verify(ca.getPublicKey());
384                     return true;
385                 } catch (Exception e) {
386                     return false;
387                 }
388             }
389         };
390         X500Principal issuer = c.getIssuerX500Principal();
391         X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
392         if (user != null) {
393             return user;
394         }
395         X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class);
396         if (system != null && !isDeletedSystemCertificate(system)) {
397             return system;
398         }
399         return null;
400     }
401 
findAllIssuers(final X509Certificate c)402     public Set<X509Certificate> findAllIssuers(final X509Certificate c) {
403         Set<X509Certificate> issuers = null;
404         CertSelector selector = new CertSelector() {
405             @Override
406             public boolean match(X509Certificate ca) {
407                 try {
408                     c.verify(ca.getPublicKey());
409                     return true;
410                 } catch (Exception e) {
411                     return false;
412                 }
413             }
414         };
415         X500Principal issuer = c.getIssuerX500Principal();
416         Set<X509Certificate> userAddedCerts = findCert(addedDir, issuer, selector, Set.class);
417         if (userAddedCerts != null) {
418             issuers = userAddedCerts;
419         }
420         selector = new CertSelector() {
421             @Override
422             public boolean match(X509Certificate ca) {
423                 try {
424                     if (isDeletedSystemCertificate(ca)) {
425                         return false;
426                     }
427                     c.verify(ca.getPublicKey());
428                     return true;
429                 } catch (Exception e) {
430                     return false;
431                 }
432             }
433         };
434         Set<X509Certificate> systemCerts = findCert(systemDir, issuer, selector, Set.class);
435         if (systemCerts != null) {
436             if (issuers != null) {
437                 issuers.addAll(systemCerts);
438             } else {
439                 issuers = systemCerts;
440             }
441         }
442         return (issuers != null) ? issuers : Collections.<X509Certificate>emptySet();
443     }
444 
isSelfIssuedCertificate(OpenSSLX509Certificate cert)445     private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) {
446         final long ctx = cert.getContext();
447         return NativeCrypto.X509_check_issued(ctx, ctx) == 0;
448     }
449 
450     /**
451      * Converts the {@code cert} to the internal OpenSSL X.509 format so we can
452      * run {@link NativeCrypto} methods on it.
453      */
convertToOpenSSLIfNeeded(X509Certificate cert)454     private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert)
455             throws CertificateException {
456         if (cert == null) {
457             return null;
458         }
459 
460         if (cert instanceof OpenSSLX509Certificate) {
461             return (OpenSSLX509Certificate) cert;
462         }
463 
464         try {
465             return OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
466         } catch (Exception e) {
467             throw new CertificateException(e);
468         }
469     }
470 
471     /**
472      * Attempt to build a certificate chain from the supplied {@code leaf}
473      * argument through the chain of issuers as high up as known. If the chain
474      * can't be completed, the most complete chain available will be returned.
475      * This means that a list with only the {@code leaf} certificate is returned
476      * if no issuer certificates could be found.
477      *
478      * @throws CertificateException if there was a problem parsing the
479      *             certificates
480      */
getCertificateChain(X509Certificate leaf)481     public List<X509Certificate> getCertificateChain(X509Certificate leaf)
482             throws CertificateException {
483         final LinkedHashSet<OpenSSLX509Certificate> chain
484                 = new LinkedHashSet<OpenSSLX509Certificate>();
485         OpenSSLX509Certificate cert = convertToOpenSSLIfNeeded(leaf);
486         chain.add(cert);
487 
488         while (true) {
489             if (isSelfIssuedCertificate(cert)) {
490                 break;
491             }
492             cert = convertToOpenSSLIfNeeded(findIssuer(cert));
493             if (cert == null || chain.contains(cert)) {
494                 break;
495             }
496             chain.add(cert);
497         }
498 
499         return new ArrayList<X509Certificate>(chain);
500     }
501 
502     // like java.security.cert.CertSelector but with X509Certificate and without cloning
503     private static interface CertSelector {
match(X509Certificate cert)504         public boolean match(X509Certificate cert);
505     }
506 
findCert( File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType)507     private <T> T findCert(
508             File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) {
509 
510         Set<X509Certificate> certs = null;
511         String hash = hash(subject);
512         for (int index = 0; true; index++) {
513             File file = file(dir, hash, index);
514             if (!file.isFile()) {
515                 // could not find a match, no file exists, bail
516                 if (desiredReturnType == Boolean.class) {
517                     return (T) Boolean.FALSE;
518                 }
519                 if (desiredReturnType == File.class) {
520                     // we return file so that caller that wants to
521                     // write knows what the next available has
522                     // location is
523                     return (T) file;
524                 }
525                 if (desiredReturnType == Set.class) {
526                     return (T) certs;
527                 }
528                 return null;
529             }
530             if (isTombstone(file)) {
531                 continue;
532             }
533             X509Certificate cert = readCertificate(file);
534             if (cert == null) {
535                 // skip problem certificates
536                 continue;
537             }
538             if (selector.match(cert)) {
539                 if (desiredReturnType == X509Certificate.class) {
540                     return (T) cert;
541                 } else if (desiredReturnType == Boolean.class) {
542                     return (T) Boolean.TRUE;
543                 } else if (desiredReturnType == File.class) {
544                     return (T) file;
545                 } else if (desiredReturnType == Set.class) {
546                     if (certs == null) {
547                         certs = new HashSet<X509Certificate>();
548                     }
549                     certs.add((X509Certificate) cert);
550                 } else {
551                     throw new AssertionError();
552                 }
553             }
554         }
555     }
556 
hash(X500Principal name)557     private String hash(X500Principal name) {
558         int hash = NativeCrypto.X509_NAME_hash_old(name);
559         return Hex.intToHexString(hash, 8);
560     }
561 
file(File dir, String hash, int index)562     private File file(File dir, String hash, int index) {
563         return new File(dir, hash + '.' + index);
564     }
565 
566     /**
567      * This non-{@code KeyStoreSpi} public interface is used by the
568      * {@code KeyChainService} to install new CA certificates. It
569      * silently ignores the certificate if it already exists in the
570      * store.
571      */
installCertificate(X509Certificate cert)572     public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
573         if (cert == null) {
574             throw new NullPointerException("cert == null");
575         }
576         File system = getCertificateFile(systemDir, cert);
577         if (system.exists()) {
578             File deleted = getCertificateFile(deletedDir, cert);
579             if (deleted.exists()) {
580                 // we have a system cert that was marked deleted.
581                 // remove the deleted marker to expose the original
582                 if (!deleted.delete()) {
583                     throw new IOException("Could not remove " + deleted);
584                 }
585                 return;
586             }
587             // otherwise we just have a dup of an existing system cert.
588             // return taking no further action.
589             return;
590         }
591         File user = getCertificateFile(addedDir, cert);
592         if (user.exists()) {
593             // we have an already installed user cert, bail.
594             return;
595         }
596         // install the user cert
597         writeCertificate(user, cert);
598     }
599 
600     /**
601      * This could be considered the implementation of {@code
602      * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we
603      * consider {@code TrustedCertificateKeyStoreSpi} to be read
604      * only. Instead, this is used by the {@code KeyChainService} to
605      * delete CA certificates.
606      */
deleteCertificateEntry(String alias)607     public void deleteCertificateEntry(String alias) throws IOException, CertificateException {
608         if (alias == null) {
609             return;
610         }
611         File file = fileForAlias(alias);
612         if (file == null) {
613             return;
614         }
615         if (isSystem(alias)) {
616             X509Certificate cert = readCertificate(file);
617             if (cert == null) {
618                 // skip problem certificates
619                 return;
620             }
621             File deleted = getCertificateFile(deletedDir, cert);
622             if (deleted.exists()) {
623                 // already deleted system certificate
624                 return;
625             }
626             // write copy of system cert to marked as deleted
627             writeCertificate(deleted, cert);
628             return;
629         }
630         if (isUser(alias)) {
631             // truncate the file to make a tombstone by opening and closing.
632             // we need ensure that we don't leave a gap before a valid cert.
633             new FileOutputStream(file).close();
634             removeUnnecessaryTombstones(alias);
635             return;
636         }
637         // non-existant user cert, nothing to delete
638     }
639 
removeUnnecessaryTombstones(String alias)640     private void removeUnnecessaryTombstones(String alias) throws IOException {
641         if (!isUser(alias)) {
642             throw new AssertionError(alias);
643         }
644         int dotIndex = alias.lastIndexOf('.');
645         if (dotIndex == -1) {
646             throw new AssertionError(alias);
647         }
648 
649         String hash = alias.substring(PREFIX_USER.length(), dotIndex);
650         int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
651 
652         if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
653             return;
654         }
655         while (lastTombstoneIndex >= 0) {
656             File file = file(addedDir, hash, lastTombstoneIndex);
657             if (!isTombstone(file)) {
658                 break;
659             }
660             if (!file.delete()) {
661                 throw new IOException("Could not remove " + file);
662             }
663             lastTombstoneIndex--;
664         }
665     }
666 }
667