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