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