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