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