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