• 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 public class TrustedCertificateStore {
84 
85     private static final String PREFIX_SYSTEM = "system:";
86     private static final String PREFIX_USER = "user:";
87 
isSystem(String alias)88     public static final boolean isSystem(String alias) {
89         return alias.startsWith(PREFIX_SYSTEM);
90     }
isUser(String alias)91     public static final boolean isUser(String alias) {
92         return alias.startsWith(PREFIX_USER);
93     }
94 
95     private static File defaultCaCertsSystemDir;
96     private static File defaultCaCertsAddedDir;
97     private static File defaultCaCertsDeletedDir;
98     private static final CertificateFactory CERT_FACTORY;
99     static {
100         String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
101         String ANDROID_DATA = System.getenv("ANDROID_DATA");
102         defaultCaCertsSystemDir = new File(ANDROID_ROOT + "/etc/security/cacerts");
setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"))103         setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"));
104 
105         try {
106             CERT_FACTORY = CertificateFactory.getInstance("X509");
107         } catch (CertificateException e) {
108             throw new AssertionError(e);
109         }
110     }
111 
setDefaultUserDirectory(File root)112     public static void setDefaultUserDirectory(File root) {
113         defaultCaCertsAddedDir = new File(root, "cacerts-added");
114         defaultCaCertsDeletedDir = new File(root, "cacerts-removed");
115     }
116 
117     private final File systemDir;
118     private final File addedDir;
119     private final File deletedDir;
120 
TrustedCertificateStore()121     public TrustedCertificateStore() {
122         this(defaultCaCertsSystemDir, defaultCaCertsAddedDir, defaultCaCertsDeletedDir);
123     }
124 
TrustedCertificateStore(File systemDir, File addedDir, File deletedDir)125     public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
126         this.systemDir = systemDir;
127         this.addedDir = addedDir;
128         this.deletedDir = deletedDir;
129     }
130 
getCertificate(String alias)131     public Certificate getCertificate(String alias) {
132         return getCertificate(alias, false);
133     }
134 
getCertificate(String alias, boolean includeDeletedSystem)135     public Certificate getCertificate(String alias, boolean includeDeletedSystem) {
136 
137         File file = fileForAlias(alias);
138         if (file == null || (isUser(alias) && isTombstone(file))) {
139             return null;
140         }
141         X509Certificate cert = readCertificate(file);
142         if (cert == null || (isSystem(alias)
143                              && !includeDeletedSystem
144                              && isDeletedSystemCertificate(cert))) {
145             // skip malformed certs as well as deleted system ones
146             return null;
147         }
148         return cert;
149     }
150 
fileForAlias(String alias)151     private File fileForAlias(String alias) {
152         if (alias == null) {
153             throw new NullPointerException("alias == null");
154         }
155         File file;
156         if (isSystem(alias)) {
157             file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
158         } else if (isUser(alias)) {
159             file = new File(addedDir, alias.substring(PREFIX_USER.length()));
160         } else {
161             return null;
162         }
163         if (!file.exists() || isTombstone(file)) {
164             // silently elide tombstones
165             return null;
166         }
167         return file;
168     }
169 
isTombstone(File file)170     private boolean isTombstone(File file) {
171         return file.length() == 0;
172     }
173 
readCertificate(File file)174     private X509Certificate readCertificate(File file) {
175         if (!file.isFile()) {
176             return null;
177         }
178         InputStream is = null;
179         try {
180             is = new BufferedInputStream(new FileInputStream(file));
181             return (X509Certificate) CERT_FACTORY.generateCertificate(is);
182         } catch (IOException e) {
183             return null;
184         } catch (CertificateException e) {
185             // reading a cert while its being installed can lead to this.
186             // just pretend like its not available yet.
187             return null;
188         } finally {
189             IoUtils.closeQuietly(is);
190         }
191     }
192 
writeCertificate(File file, X509Certificate cert)193     private void writeCertificate(File file, X509Certificate cert)
194             throws IOException, CertificateException {
195         File dir = file.getParentFile();
196         dir.mkdirs();
197         dir.setReadable(true, false);
198         dir.setExecutable(true, false);
199         OutputStream os = null;
200         try {
201             os = new FileOutputStream(file);
202             os.write(cert.getEncoded());
203         } finally {
204             IoUtils.closeQuietly(os);
205         }
206         file.setReadable(true, false);
207     }
208 
isDeletedSystemCertificate(X509Certificate x)209     private boolean isDeletedSystemCertificate(X509Certificate x) {
210         return getCertificateFile(deletedDir, x).exists();
211     }
212 
getCreationDate(String alias)213     public Date getCreationDate(String alias) {
214         // containsAlias check ensures the later fileForAlias result
215         // was not a deleted system cert.
216         if (!containsAlias(alias)) {
217             return null;
218         }
219         File file = fileForAlias(alias);
220         if (file == null) {
221             return null;
222         }
223         long time = file.lastModified();
224         if (time == 0) {
225             return null;
226         }
227         return new Date(time);
228     }
229 
aliases()230     public Set<String> aliases() {
231         Set<String> result = new HashSet<String>();
232         addAliases(result, PREFIX_USER, addedDir);
233         addAliases(result, PREFIX_SYSTEM, systemDir);
234         return result;
235     }
236 
userAliases()237     public Set<String> userAliases() {
238         Set<String> result = new HashSet<String>();
239         addAliases(result, PREFIX_USER, addedDir);
240         return result;
241     }
242 
addAliases(Set<String> result, String prefix, File dir)243     private void addAliases(Set<String> result, String prefix, File dir) {
244         String[] files = dir.list();
245         if (files == null) {
246             return;
247         }
248         for (String filename : files) {
249             String alias = prefix + filename;
250             if (containsAlias(alias)) {
251                 result.add(alias);
252             }
253         }
254     }
255 
allSystemAliases()256     public Set<String> allSystemAliases() {
257         Set<String> result = new HashSet<String>();
258         String[] files = systemDir.list();
259         if (files == null) {
260             return result;
261         }
262         for (String filename : files) {
263             String alias = PREFIX_SYSTEM + filename;
264             if (containsAlias(alias, true)) {
265                 result.add(alias);
266             }
267         }
268         return result;
269     }
270 
containsAlias(String alias)271     public boolean containsAlias(String alias) {
272         return containsAlias(alias, false);
273     }
274 
containsAlias(String alias, boolean includeDeletedSystem)275     private boolean containsAlias(String alias, boolean includeDeletedSystem) {
276         return getCertificate(alias, includeDeletedSystem) != null;
277     }
278 
getCertificateAlias(Certificate c)279     public String getCertificateAlias(Certificate c) {
280         return getCertificateAlias(c, false);
281     }
282 
getCertificateAlias(Certificate c, boolean includeDeletedSystem)283     public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) {
284         if (c == null || !(c instanceof X509Certificate)) {
285             return null;
286         }
287         X509Certificate x = (X509Certificate) c;
288         File user = getCertificateFile(addedDir, x);
289         if (user.exists()) {
290             return PREFIX_USER + user.getName();
291         }
292         if (!includeDeletedSystem && isDeletedSystemCertificate(x)) {
293             return null;
294         }
295         File system = getCertificateFile(systemDir, x);
296         if (system.exists()) {
297             return PREFIX_SYSTEM + system.getName();
298         }
299         return null;
300     }
301 
302     /**
303      * Returns true to indicate that the certificate was added by the
304      * user, false otherwise.
305      */
isUserAddedCertificate(X509Certificate cert)306     public boolean isUserAddedCertificate(X509Certificate cert) {
307         return getCertificateFile(addedDir, cert).exists();
308     }
309 
310     /**
311      * Returns a File for where the certificate is found if it exists
312      * or where it should be installed if it does not exist. The
313      * caller can disambiguate these cases by calling {@code
314      * File.exists()} on the result.
315      *
316      * @VisibleForTesting
317      */
getCertificateFile(File dir, final X509Certificate x)318     public File getCertificateFile(File dir, final X509Certificate x) {
319         // compare X509Certificate.getEncoded values
320         CertSelector selector = new CertSelector() {
321             @Override
322             public boolean match(X509Certificate cert) {
323                 return cert.equals(x);
324             }
325         };
326         return findCert(dir, x.getSubjectX500Principal(), selector, File.class);
327     }
328 
329     /**
330      * This non-{@code KeyStoreSpi} public interface is used by {@code
331      * TrustManagerImpl} to locate a CA certificate with the same name
332      * and public key as the provided {@code X509Certificate}. We
333      * match on the name and public key and not the entire certificate
334      * since a CA may be reissued with the same name and PublicKey but
335      * with other differences (for example when switching signature
336      * from md2WithRSAEncryption to SHA1withRSA)
337      */
getTrustAnchor(final X509Certificate c)338     public X509Certificate getTrustAnchor(final X509Certificate c) {
339         // compare X509Certificate.getPublicKey values
340         CertSelector selector = new CertSelector() {
341             @Override
342             public boolean match(X509Certificate ca) {
343                 return ca.getPublicKey().equals(c.getPublicKey());
344             }
345         };
346         X509Certificate user = findCert(addedDir,
347                                         c.getSubjectX500Principal(),
348                                         selector,
349                                         X509Certificate.class);
350         if (user != null) {
351             return user;
352         }
353         X509Certificate system = findCert(systemDir,
354                                           c.getSubjectX500Principal(),
355                                           selector,
356                                           X509Certificate.class);
357         if (system != null && !isDeletedSystemCertificate(system)) {
358             return system;
359         }
360         return null;
361     }
362 
363     /**
364      * This non-{@code KeyStoreSpi} public interface is used by {@code
365      * TrustManagerImpl} to locate the CA certificate that signed the
366      * provided {@code X509Certificate}.
367      */
findIssuer(final X509Certificate c)368     public X509Certificate findIssuer(final X509Certificate c) {
369         // match on verified issuer of Certificate
370         CertSelector selector = new CertSelector() {
371             @Override
372             public boolean match(X509Certificate ca) {
373                 try {
374                     c.verify(ca.getPublicKey());
375                     return true;
376                 } catch (Exception e) {
377                     return false;
378                 }
379             }
380         };
381         X500Principal issuer = c.getIssuerX500Principal();
382         X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
383         if (user != null) {
384             return user;
385         }
386         X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class);
387         if (system != null && !isDeletedSystemCertificate(system)) {
388             return system;
389         }
390         return null;
391     }
392 
findAllIssuers(final X509Certificate c)393     public Set<X509Certificate> findAllIssuers(final X509Certificate c) {
394         Set<X509Certificate> issuers = null;
395         CertSelector selector = new CertSelector() {
396             @Override
397             public boolean match(X509Certificate ca) {
398                 try {
399                     c.verify(ca.getPublicKey());
400                     return true;
401                 } catch (Exception e) {
402                     return false;
403                 }
404             }
405         };
406         X500Principal issuer = c.getIssuerX500Principal();
407         Set<X509Certificate> userAddedCerts = findCert(addedDir, issuer, selector, Set.class);
408         if (userAddedCerts != null) {
409             issuers = userAddedCerts;
410         }
411         selector = new CertSelector() {
412             @Override
413             public boolean match(X509Certificate ca) {
414                 try {
415                     if (isDeletedSystemCertificate(ca)) {
416                         return false;
417                     }
418                     c.verify(ca.getPublicKey());
419                     return true;
420                 } catch (Exception e) {
421                     return false;
422                 }
423             }
424         };
425         Set<X509Certificate> systemCerts = findCert(systemDir, issuer, selector, Set.class);
426         if (systemCerts != null) {
427             if (issuers != null) {
428                 issuers.addAll(systemCerts);
429             } else {
430                 issuers = systemCerts;
431             }
432         }
433         return (issuers != null) ? issuers : Collections.<X509Certificate>emptySet();
434     }
435 
isSelfIssuedCertificate(OpenSSLX509Certificate cert)436     private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) {
437         final long ctx = cert.getContext();
438         return NativeCrypto.X509_check_issued(ctx, ctx) == 0;
439     }
440 
441     /**
442      * Converts the {@code cert} to the internal OpenSSL X.509 format so we can
443      * run {@link NativeCrypto} methods on it.
444      */
convertToOpenSSLIfNeeded(X509Certificate cert)445     private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert)
446             throws CertificateException {
447         if (cert == null) {
448             return null;
449         }
450 
451         if (cert instanceof OpenSSLX509Certificate) {
452             return (OpenSSLX509Certificate) cert;
453         }
454 
455         try {
456             return OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
457         } catch (Exception e) {
458             throw new CertificateException(e);
459         }
460     }
461 
462     /**
463      * Attempt to build a certificate chain from the supplied {@code leaf}
464      * argument through the chain of issuers as high up as known. If the chain
465      * can't be completed, the most complete chain available will be returned.
466      * This means that a list with only the {@code leaf} certificate is returned
467      * if no issuer certificates could be found.
468      *
469      * @throws CertificateException if there was a problem parsing the
470      *             certificates
471      */
getCertificateChain(X509Certificate leaf)472     public List<X509Certificate> getCertificateChain(X509Certificate leaf)
473             throws CertificateException {
474         final LinkedHashSet<OpenSSLX509Certificate> chain
475                 = new LinkedHashSet<OpenSSLX509Certificate>();
476         OpenSSLX509Certificate cert = convertToOpenSSLIfNeeded(leaf);
477         chain.add(cert);
478 
479         while (true) {
480             if (isSelfIssuedCertificate(cert)) {
481                 break;
482             }
483             cert = convertToOpenSSLIfNeeded(findIssuer(cert));
484             if (cert == null || chain.contains(cert)) {
485                 break;
486             }
487             chain.add(cert);
488         }
489 
490         return new ArrayList<X509Certificate>(chain);
491     }
492 
493     // like java.security.cert.CertSelector but with X509Certificate and without cloning
494     private static interface CertSelector {
match(X509Certificate cert)495         public boolean match(X509Certificate cert);
496     }
497 
findCert( File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType)498     private <T> T findCert(
499             File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) {
500 
501         Set<X509Certificate> certs = null;
502         String hash = hash(subject);
503         for (int index = 0; true; index++) {
504             File file = file(dir, hash, index);
505             if (!file.isFile()) {
506                 // could not find a match, no file exists, bail
507                 if (desiredReturnType == Boolean.class) {
508                     return (T) Boolean.FALSE;
509                 }
510                 if (desiredReturnType == File.class) {
511                     // we return file so that caller that wants to
512                     // write knows what the next available has
513                     // location is
514                     return (T) file;
515                 }
516                 if (desiredReturnType == Set.class) {
517                     return (T) certs;
518                 }
519                 return null;
520             }
521             if (isTombstone(file)) {
522                 continue;
523             }
524             X509Certificate cert = readCertificate(file);
525             if (cert == null) {
526                 // skip problem certificates
527                 continue;
528             }
529             if (selector.match(cert)) {
530                 if (desiredReturnType == X509Certificate.class) {
531                     return (T) cert;
532                 } else if (desiredReturnType == Boolean.class) {
533                     return (T) Boolean.TRUE;
534                 } else if (desiredReturnType == File.class) {
535                     return (T) file;
536                 } else if (desiredReturnType == Set.class) {
537                     if (certs == null) {
538                         certs = new HashSet<X509Certificate>();
539                     }
540                     certs.add((X509Certificate) cert);
541                 } else {
542                     throw new AssertionError();
543                 }
544             }
545         }
546     }
547 
hash(X500Principal name)548     private String hash(X500Principal name) {
549         int hash = NativeCrypto.X509_NAME_hash_old(name);
550         return Hex.intToHexString(hash, 8);
551     }
552 
file(File dir, String hash, int index)553     private File file(File dir, String hash, int index) {
554         return new File(dir, hash + '.' + index);
555     }
556 
557     /**
558      * This non-{@code KeyStoreSpi} public interface is used by the
559      * {@code KeyChainService} to install new CA certificates. It
560      * silently ignores the certificate if it already exists in the
561      * store.
562      */
installCertificate(X509Certificate cert)563     public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
564         if (cert == null) {
565             throw new NullPointerException("cert == null");
566         }
567         File system = getCertificateFile(systemDir, cert);
568         if (system.exists()) {
569             File deleted = getCertificateFile(deletedDir, cert);
570             if (deleted.exists()) {
571                 // we have a system cert that was marked deleted.
572                 // remove the deleted marker to expose the original
573                 if (!deleted.delete()) {
574                     throw new IOException("Could not remove " + deleted);
575                 }
576                 return;
577             }
578             // otherwise we just have a dup of an existing system cert.
579             // return taking no further action.
580             return;
581         }
582         File user = getCertificateFile(addedDir, cert);
583         if (user.exists()) {
584             // we have an already installed user cert, bail.
585             return;
586         }
587         // install the user cert
588         writeCertificate(user, cert);
589     }
590 
591     /**
592      * This could be considered the implementation of {@code
593      * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we
594      * consider {@code TrustedCertificateKeyStoreSpi} to be read
595      * only. Instead, this is used by the {@code KeyChainService} to
596      * delete CA certificates.
597      */
deleteCertificateEntry(String alias)598     public void deleteCertificateEntry(String alias) throws IOException, CertificateException {
599         if (alias == null) {
600             return;
601         }
602         File file = fileForAlias(alias);
603         if (file == null) {
604             return;
605         }
606         if (isSystem(alias)) {
607             X509Certificate cert = readCertificate(file);
608             if (cert == null) {
609                 // skip problem certificates
610                 return;
611             }
612             File deleted = getCertificateFile(deletedDir, cert);
613             if (deleted.exists()) {
614                 // already deleted system certificate
615                 return;
616             }
617             // write copy of system cert to marked as deleted
618             writeCertificate(deleted, cert);
619             return;
620         }
621         if (isUser(alias)) {
622             // truncate the file to make a tombstone by opening and closing.
623             // we need ensure that we don't leave a gap before a valid cert.
624             new FileOutputStream(file).close();
625             removeUnnecessaryTombstones(alias);
626             return;
627         }
628         // non-existant user cert, nothing to delete
629     }
630 
removeUnnecessaryTombstones(String alias)631     private void removeUnnecessaryTombstones(String alias) throws IOException {
632         if (!isUser(alias)) {
633             throw new AssertionError(alias);
634         }
635         int dotIndex = alias.lastIndexOf('.');
636         if (dotIndex == -1) {
637             throw new AssertionError(alias);
638         }
639 
640         String hash = alias.substring(PREFIX_USER.length(), dotIndex);
641         int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
642 
643         if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
644             return;
645         }
646         while (lastTombstoneIndex >= 0) {
647             File file = file(addedDir, hash, lastTombstoneIndex);
648             if (!isTombstone(file)) {
649                 break;
650             }
651             if (!file.delete()) {
652                 throw new IOException("Could not remove " + file);
653             }
654             lastTombstoneIndex--;
655         }
656     }
657 }
658