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.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 
     87     public static final boolean isSystem(String alias) {
     88         return alias.startsWith(PREFIX_SYSTEM);
     89     }
     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");
    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 
    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 
    120     public TrustedCertificateStore() {
    121         this(defaultCaCertsSystemDir, defaultCaCertsAddedDir, defaultCaCertsDeletedDir);
    122     }
    123 
    124     public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
    125         this.systemDir = systemDir;
    126         this.addedDir = addedDir;
    127         this.deletedDir = deletedDir;
    128     }
    129 
    130     public Certificate getCertificate(String alias) {
    131         return getCertificate(alias, false);
    132     }
    133 
    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 
    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 
    169     private boolean isTombstone(File file) {
    170         return file.length() == 0;
    171     }
    172 
    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 
    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 
    208     private boolean isDeletedSystemCertificate(X509Certificate x) {
    209         return getCertificateFile(deletedDir, x).exists();
    210     }
    211 
    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 
    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 
    236     public Set<String> userAliases() {
    237         Set<String> result = new HashSet<String>();
    238         addAliases(result, PREFIX_USER, addedDir);
    239         return result;
    240     }
    241 
    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 
    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 
    270     public boolean containsAlias(String alias) {
    271         return containsAlias(alias, false);
    272     }
    273 
    274     private boolean containsAlias(String alias, boolean includeDeletedSystem) {
    275         return getCertificate(alias, includeDeletedSystem) != null;
    276     }
    277 
    278     public String getCertificateAlias(Certificate c) {
    279         return getCertificateAlias(c, false);
    280     }
    281 
    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      */
    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     private File getCertificateFile(File dir, final X509Certificate x) {
    316         // compare X509Certificate.getEncoded values
    317         CertSelector selector = new CertSelector() {
    318             @Override
    319             public boolean match(X509Certificate cert) {
    320                 return cert.equals(x);
    321             }
    322         };
    323         return findCert(dir, x.getSubjectX500Principal(), selector, File.class);
    324     }
    325 
    326     /**
    327      * This non-{@code KeyStoreSpi} public interface is used by {@code
    328      * TrustManagerImpl} to locate a CA certificate with the same name
    329      * and public key as the provided {@code X509Certificate}. We
    330      * match on the name and public key and not the entire certificate
    331      * since a CA may be reissued with the same name and PublicKey but
    332      * with other differences (for example when switching signature
    333      * from md2WithRSAEncryption to SHA1withRSA)
    334      */
    335     public X509Certificate getTrustAnchor(final X509Certificate c) {
    336         // compare X509Certificate.getPublicKey values
    337         CertSelector selector = new CertSelector() {
    338             @Override
    339             public boolean match(X509Certificate ca) {
    340                 return ca.getPublicKey().equals(c.getPublicKey());
    341             }
    342         };
    343         X509Certificate user = findCert(addedDir,
    344                                         c.getSubjectX500Principal(),
    345                                         selector,
    346                                         X509Certificate.class);
    347         if (user != null) {
    348             return user;
    349         }
    350         X509Certificate system = findCert(systemDir,
    351                                           c.getSubjectX500Principal(),
    352                                           selector,
    353                                           X509Certificate.class);
    354         if (system != null && !isDeletedSystemCertificate(system)) {
    355             return system;
    356         }
    357         return null;
    358     }
    359 
    360     /**
    361      * This non-{@code KeyStoreSpi} public interface is used by {@code
    362      * TrustManagerImpl} to locate the CA certificate that signed the
    363      * provided {@code X509Certificate}.
    364      */
    365     public X509Certificate findIssuer(final X509Certificate c) {
    366         // match on verified issuer of Certificate
    367         CertSelector selector = new CertSelector() {
    368             @Override
    369             public boolean match(X509Certificate ca) {
    370                 try {
    371                     c.verify(ca.getPublicKey());
    372                     return true;
    373                 } catch (Exception e) {
    374                     return false;
    375                 }
    376             }
    377         };
    378         X500Principal issuer = c.getIssuerX500Principal();
    379         X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
    380         if (user != null) {
    381             return user;
    382         }
    383         X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class);
    384         if (system != null && !isDeletedSystemCertificate(system)) {
    385             return system;
    386         }
    387         return null;
    388     }
    389 
    390     private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) {
    391         final long ctx = cert.getContext();
    392         return NativeCrypto.X509_check_issued(ctx, ctx) == 0;
    393     }
    394 
    395     /**
    396      * Converts the {@code cert} to the internal OpenSSL X.509 format so we can
    397      * run {@link NativeCrypto} methods on it.
    398      */
    399     private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert)
    400             throws CertificateException {
    401         if (cert == null) {
    402             return null;
    403         }
    404 
    405         if (cert instanceof OpenSSLX509Certificate) {
    406             return (OpenSSLX509Certificate) cert;
    407         }
    408 
    409         try {
    410             return OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
    411         } catch (Exception e) {
    412             throw new CertificateException(e);
    413         }
    414     }
    415 
    416     /**
    417      * Attempt to build a certificate chain from the supplied {@code leaf}
    418      * argument through the chain of issuers as high up as known. If the chain
    419      * can't be completed, the most complete chain available will be returned.
    420      * This means that a list with only the {@code leaf} certificate is returned
    421      * if no issuer certificates could be found.
    422      *
    423      * @throws CertificateException if there was a problem parsing the
    424      *             certificates
    425      */
    426     public List<X509Certificate> getCertificateChain(X509Certificate leaf)
    427             throws CertificateException {
    428         final LinkedHashSet<OpenSSLX509Certificate> chain
    429                 = new LinkedHashSet<OpenSSLX509Certificate>();
    430         OpenSSLX509Certificate cert = convertToOpenSSLIfNeeded(leaf);
    431         chain.add(cert);
    432 
    433         while (true) {
    434             if (isSelfIssuedCertificate(cert)) {
    435                 break;
    436             }
    437             cert = convertToOpenSSLIfNeeded(findIssuer(cert));
    438             if (cert == null || chain.contains(cert)) {
    439                 break;
    440             }
    441             chain.add(cert);
    442         }
    443 
    444         return new ArrayList<X509Certificate>(chain);
    445     }
    446 
    447     // like java.security.cert.CertSelector but with X509Certificate and without cloning
    448     private static interface CertSelector {
    449         public boolean match(X509Certificate cert);
    450     }
    451 
    452     private <T> T findCert(
    453             File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) {
    454 
    455         String hash = hash(subject);
    456         for (int index = 0; true; index++) {
    457             File file = file(dir, hash, index);
    458             if (!file.isFile()) {
    459                 // could not find a match, no file exists, bail
    460                 if (desiredReturnType == Boolean.class) {
    461                     return (T) Boolean.FALSE;
    462                 }
    463                 if (desiredReturnType == File.class) {
    464                     // we return file so that caller that wants to
    465                     // write knows what the next available has
    466                     // location is
    467                     return (T) file;
    468                 }
    469                 return null;
    470             }
    471             if (isTombstone(file)) {
    472                 continue;
    473             }
    474             X509Certificate cert = readCertificate(file);
    475             if (cert == null) {
    476                 // skip problem certificates
    477                 continue;
    478             }
    479             if (selector.match(cert)) {
    480                 if (desiredReturnType == X509Certificate.class) {
    481                     return (T) cert;
    482                 }
    483                 if (desiredReturnType == Boolean.class) {
    484                     return (T) Boolean.TRUE;
    485                 }
    486                 if (desiredReturnType == File.class) {
    487                     return (T) file;
    488                 }
    489                 throw new AssertionError();
    490             }
    491         }
    492     }
    493 
    494     private String hash(X500Principal name) {
    495         int hash = NativeCrypto.X509_NAME_hash_old(name);
    496         return IntegralToString.intToHexString(hash, false, 8);
    497     }
    498 
    499     private File file(File dir, String hash, int index) {
    500         return new File(dir, hash + '.' + index);
    501     }
    502 
    503     /**
    504      * This non-{@code KeyStoreSpi} public interface is used by the
    505      * {@code KeyChainService} to install new CA certificates. It
    506      * silently ignores the certificate if it already exists in the
    507      * store.
    508      */
    509     public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
    510         if (cert == null) {
    511             throw new NullPointerException("cert == null");
    512         }
    513         File system = getCertificateFile(systemDir, cert);
    514         if (system.exists()) {
    515             File deleted = getCertificateFile(deletedDir, cert);
    516             if (deleted.exists()) {
    517                 // we have a system cert that was marked deleted.
    518                 // remove the deleted marker to expose the original
    519                 if (!deleted.delete()) {
    520                     throw new IOException("Could not remove " + deleted);
    521                 }
    522                 return;
    523             }
    524             // otherwise we just have a dup of an existing system cert.
    525             // return taking no further action.
    526             return;
    527         }
    528         File user = getCertificateFile(addedDir, cert);
    529         if (user.exists()) {
    530             // we have an already installed user cert, bail.
    531             return;
    532         }
    533         // install the user cert
    534         writeCertificate(user, cert);
    535     }
    536 
    537     /**
    538      * This could be considered the implementation of {@code
    539      * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we
    540      * consider {@code TrustedCertificateKeyStoreSpi} to be read
    541      * only. Instead, this is used by the {@code KeyChainService} to
    542      * delete CA certificates.
    543      */
    544     public void deleteCertificateEntry(String alias) throws IOException, CertificateException {
    545         if (alias == null) {
    546             return;
    547         }
    548         File file = fileForAlias(alias);
    549         if (file == null) {
    550             return;
    551         }
    552         if (isSystem(alias)) {
    553             X509Certificate cert = readCertificate(file);
    554             if (cert == null) {
    555                 // skip problem certificates
    556                 return;
    557             }
    558             File deleted = getCertificateFile(deletedDir, cert);
    559             if (deleted.exists()) {
    560                 // already deleted system certificate
    561                 return;
    562             }
    563             // write copy of system cert to marked as deleted
    564             writeCertificate(deleted, cert);
    565             return;
    566         }
    567         if (isUser(alias)) {
    568             // truncate the file to make a tombstone by opening and closing.
    569             // we need ensure that we don't leave a gap before a valid cert.
    570             new FileOutputStream(file).close();
    571             removeUnnecessaryTombstones(alias);
    572             return;
    573         }
    574         // non-existant user cert, nothing to delete
    575     }
    576 
    577     private void removeUnnecessaryTombstones(String alias) throws IOException {
    578         if (!isUser(alias)) {
    579             throw new AssertionError(alias);
    580         }
    581         int dotIndex = alias.lastIndexOf('.');
    582         if (dotIndex == -1) {
    583             throw new AssertionError(alias);
    584         }
    585 
    586         String hash = alias.substring(PREFIX_USER.length(), dotIndex);
    587         int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
    588 
    589         if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
    590             return;
    591         }
    592         while (lastTombstoneIndex >= 0) {
    593             File file = file(addedDir, hash, lastTombstoneIndex);
    594             if (!isTombstone(file)) {
    595                 break;
    596             }
    597             if (!file.delete()) {
    598                 throw new IOException("Could not remove " + file);
    599             }
    600             lastTombstoneIndex--;
    601         }
    602     }
    603 }
    604