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