Home | History | Annotate | Download | only in jsse
      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.apache.harmony.xnet.provider.jsse;
     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.KeyStoreSpi;
     27 import java.security.PublicKey;
     28 import java.security.cert.CertSelector;
     29 import java.security.cert.Certificate;
     30 import java.security.cert.CertificateException;
     31 import java.security.cert.CertificateFactory;
     32 import java.security.cert.X509Certificate;
     33 import java.util.Collections;
     34 import java.util.Date;
     35 import java.util.HashSet;
     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 final 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 final File CA_CERTS_DIR_SYSTEM;
     96     private static final File CA_CERTS_DIR_ADDED;
     97     private static final File CA_CERTS_DIR_DELETED;
     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         CA_CERTS_DIR_SYSTEM = new File(ANDROID_ROOT + "/etc/security/cacerts");
    103         CA_CERTS_DIR_ADDED = new File(ANDROID_DATA + "/misc/keychain/cacerts-added");
    104         CA_CERTS_DIR_DELETED = new File(ANDROID_DATA + "/misc/keychain/cacerts-removed");
    105 
    106         try {
    107             CERT_FACTORY = CertificateFactory.getInstance("X509");
    108         } catch (CertificateException e) {
    109             throw new AssertionError(e);
    110         }
    111     }
    112 
    113     private final File systemDir;
    114     private final File addedDir;
    115     private final File deletedDir;
    116 
    117     public TrustedCertificateStore() {
    118         this(CA_CERTS_DIR_SYSTEM, CA_CERTS_DIR_ADDED, CA_CERTS_DIR_DELETED);
    119     }
    120 
    121     public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
    122         this.systemDir = systemDir;
    123         this.addedDir = addedDir;
    124         this.deletedDir = deletedDir;
    125     }
    126 
    127     public Certificate getCertificate(String alias) {
    128         return getCertificate(alias, false);
    129     }
    130 
    131     public Certificate getCertificate(String alias, boolean includeDeletedSystem) {
    132 
    133         File file = fileForAlias(alias);
    134         if (file == null || (isUser(alias) && isTombstone(file))) {
    135             return null;
    136         }
    137         X509Certificate cert = readCertificate(file);
    138         if (cert == null || (isSystem(alias)
    139                              && !includeDeletedSystem
    140                              && isDeletedSystemCertificate(cert))) {
    141             // skip malformed certs as well as deleted system ones
    142             return null;
    143         }
    144         return cert;
    145     }
    146 
    147     private File fileForAlias(String alias) {
    148         if (alias == null) {
    149             throw new NullPointerException("alias == null");
    150         }
    151         File file;
    152         if (isSystem(alias)) {
    153             file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
    154         } else if (isUser(alias)) {
    155             file = new File(addedDir, alias.substring(PREFIX_USER.length()));
    156         } else {
    157             return null;
    158         }
    159         if (!file.exists() || isTombstone(file)) {
    160             // silently elide tombstones
    161             return null;
    162         }
    163         return file;
    164     }
    165 
    166     private boolean isTombstone(File file) {
    167         return file.length() == 0;
    168     }
    169 
    170     private X509Certificate readCertificate(File file) {
    171         if (!file.isFile()) {
    172             return null;
    173         }
    174         InputStream is = null;
    175         try {
    176             is = new BufferedInputStream(new FileInputStream(file));
    177             return (X509Certificate) CERT_FACTORY.generateCertificate(is);
    178         } catch (IOException e) {
    179             return null;
    180         } catch (CertificateException e) {
    181             // reading a cert while its being installed can lead to this.
    182             // just pretend like its not available yet.
    183             return null;
    184         } finally {
    185             IoUtils.closeQuietly(is);
    186         }
    187     }
    188 
    189     private void writeCertificate(File file, X509Certificate cert)
    190             throws IOException, CertificateException {
    191         File dir = file.getParentFile();
    192         dir.mkdirs();
    193         dir.setReadable(true, false);
    194         dir.setExecutable(true, false);
    195         OutputStream os = null;
    196         try {
    197             os = new FileOutputStream(file);
    198             os.write(cert.getEncoded());
    199         } finally {
    200             IoUtils.closeQuietly(os);
    201         }
    202         file.setReadable(true, false);
    203     }
    204 
    205     private boolean isDeletedSystemCertificate(X509Certificate x) {
    206         return getCertificateFile(deletedDir, x).exists();
    207     }
    208 
    209     public Date getCreationDate(String alias) {
    210         // containsAlias check ensures the later fileForAlias result
    211         // was not a deleted system cert.
    212         if (!containsAlias(alias)) {
    213             return null;
    214         }
    215         File file = fileForAlias(alias);
    216         if (file == null) {
    217             return null;
    218         }
    219         long time = file.lastModified();
    220         if (time == 0) {
    221             return null;
    222         }
    223         return new Date(time);
    224     }
    225 
    226     public Set<String> aliases() {
    227         Set<String> result = new HashSet<String>();
    228         addAliases(result, PREFIX_USER, addedDir);
    229         addAliases(result, PREFIX_SYSTEM, systemDir);
    230         return result;
    231     }
    232 
    233     public Set<String> userAliases() {
    234         Set<String> result = new HashSet<String>();
    235         addAliases(result, PREFIX_USER, addedDir);
    236         return result;
    237     }
    238 
    239     private void addAliases(Set<String> result, String prefix, File dir) {
    240         String[] files = dir.list();
    241         if (files == null) {
    242             return;
    243         }
    244         for (String filename : files) {
    245             String alias = prefix + filename;
    246             if (containsAlias(alias)) {
    247                 result.add(alias);
    248             }
    249         }
    250     }
    251 
    252     public Set<String> allSystemAliases() {
    253         Set<String> result = new HashSet<String>();
    254         String[] files = systemDir.list();
    255         if (files == null) {
    256             return result;
    257         }
    258         for (String filename : files) {
    259             String alias = PREFIX_SYSTEM + filename;
    260             if (containsAlias(alias, true)) {
    261                 result.add(alias);
    262             }
    263         }
    264         return result;
    265     }
    266 
    267     public boolean containsAlias(String alias) {
    268         return containsAlias(alias, false);
    269     }
    270 
    271     private boolean containsAlias(String alias, boolean includeDeletedSystem) {
    272         return getCertificate(alias, includeDeletedSystem) != null;
    273     }
    274 
    275     public String getCertificateAlias(Certificate c) {
    276         if (c == null || !(c instanceof X509Certificate)) {
    277             return null;
    278         }
    279         X509Certificate x = (X509Certificate) c;
    280         File user = getCertificateFile(addedDir, x);
    281         if (user.exists()) {
    282             return PREFIX_USER + user.getName();
    283         }
    284         if (isDeletedSystemCertificate(x)) {
    285             return null;
    286         }
    287         File system = getCertificateFile(systemDir, x);
    288         if (system.exists()) {
    289             return PREFIX_SYSTEM + system.getName();
    290         }
    291         return null;
    292     }
    293 
    294     /**
    295      * Returns a File for where the certificate is found if it exists
    296      * or where it should be installed if it does not exist. The
    297      * caller can disambiguate these cases by calling {@code
    298      * File.exists()} on the result.
    299      */
    300     private File getCertificateFile(File dir, final X509Certificate x) {
    301         // compare X509Certificate.getEncoded values
    302         CertSelector selector = new CertSelector() {
    303             @Override public boolean match(X509Certificate cert) {
    304                 return cert.equals(x);
    305             }
    306         };
    307         return findCert(dir, x.getSubjectX500Principal(), selector, File.class);
    308     }
    309 
    310     /**
    311      * This non-{@code KeyStoreSpi} public interface is used by {@code
    312      * TrustManagerImpl} to locate a CA certificate with the same name
    313      * and public key as the provided {@code X509Certificate}. We
    314      * match on the name and public key and not the entire certificate
    315      * since a CA may be reissued with the same name and PublicKey but
    316      * with other differences (for example when switching signature
    317      * from md2WithRSAEncryption to SHA1withRSA)
    318      */
    319     public boolean isTrustAnchor(final X509Certificate c) {
    320         // compare X509Certificate.getPublicKey values
    321         CertSelector selector = new CertSelector() {
    322             @Override public boolean match(X509Certificate ca) {
    323                 return ca.getPublicKey().equals(c.getPublicKey());
    324             }
    325         };
    326         boolean user = findCert(addedDir,
    327                                 c.getSubjectX500Principal(),
    328                                 selector,
    329                                 Boolean.class);
    330         if (user) {
    331             return true;
    332         }
    333         X509Certificate system = findCert(systemDir,
    334                                           c.getSubjectX500Principal(),
    335                                           selector,
    336                                           X509Certificate.class);
    337         return system != null && !isDeletedSystemCertificate(system);
    338     }
    339 
    340     /**
    341      * This non-{@code KeyStoreSpi} public interface is used by {@code
    342      * TrustManagerImpl} to locate the CA certificate that signed the
    343      * provided {@code X509Certificate}.
    344      */
    345     public X509Certificate findIssuer(final X509Certificate c) {
    346         // match on verified issuer of Certificate
    347         CertSelector selector = new CertSelector() {
    348             @Override public boolean match(X509Certificate ca) {
    349                 try {
    350                     c.verify(ca.getPublicKey());
    351                     return true;
    352                 } catch (Exception e) {
    353                     return false;
    354                 }
    355             }
    356         };
    357         X500Principal issuer = c.getIssuerX500Principal();
    358         X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
    359         if (user != null) {
    360             return user;
    361         }
    362         X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class);
    363         if (system != null && !isDeletedSystemCertificate(system)) {
    364             return system;
    365         }
    366         return null;
    367     }
    368 
    369     // like java.security.cert.CertSelector but with X509Certificate and without cloning
    370     private static interface CertSelector {
    371         public boolean match(X509Certificate cert);
    372     }
    373 
    374     private <T> T findCert(
    375             File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) {
    376 
    377         String hash = hash(subject);
    378         for (int index = 0; true; index++) {
    379             File file = file(dir, hash, index);
    380             if (!file.isFile()) {
    381                 // could not find a match, no file exists, bail
    382                 if (desiredReturnType == Boolean.class) {
    383                     return (T) Boolean.FALSE;
    384                 }
    385                 if (desiredReturnType == File.class) {
    386                     // we return file so that caller that wants to
    387                     // write knows what the next available has
    388                     // location is
    389                     return (T) file;
    390                 }
    391                 return null;
    392             }
    393             if (isTombstone(file)) {
    394                 continue;
    395             }
    396             X509Certificate cert = readCertificate(file);
    397             if (cert == null) {
    398                 // skip problem certificates
    399                 continue;
    400             }
    401             if (selector.match(cert)) {
    402                 if (desiredReturnType == X509Certificate.class) {
    403                     return (T) cert;
    404                 }
    405                 if (desiredReturnType == Boolean.class) {
    406                     return (T) Boolean.TRUE;
    407                 }
    408                 if (desiredReturnType == File.class) {
    409                     return (T) file;
    410                 }
    411                 throw new AssertionError();
    412             }
    413         }
    414     }
    415 
    416     private String hash(X500Principal name) {
    417         int hash = NativeCrypto.X509_NAME_hash_old(name);
    418         return IntegralToString.intToHexString(hash, false, 8);
    419     }
    420 
    421     private File file(File dir, String hash, int index) {
    422         return new File(dir, hash + '.' + index);
    423     }
    424 
    425     /**
    426      * This non-{@code KeyStoreSpi} public interface is used by the
    427      * {@code KeyChainService} to install new CA certificates. It
    428      * silently ignores the certificate if it already exists in the
    429      * store.
    430      */
    431     public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
    432         if (cert == null) {
    433             throw new NullPointerException("cert == null");
    434         }
    435         File system = getCertificateFile(systemDir, cert);
    436         if (system.exists()) {
    437             File deleted = getCertificateFile(deletedDir, cert);
    438             if (deleted.exists()) {
    439                 // we have a system cert that was marked deleted.
    440                 // remove the deleted marker to expose the original
    441                 if (!deleted.delete()) {
    442                     throw new IOException("Could not remove " + deleted);
    443                 }
    444                 return;
    445             }
    446             // otherwise we just have a dup of an existing system cert.
    447             // return taking no further action.
    448             return;
    449         }
    450         File user = getCertificateFile(addedDir, cert);
    451         if (user.exists()) {
    452             // we have an already installed user cert, bail.
    453             return;
    454         }
    455         // install the user cert
    456         writeCertificate(user, cert);
    457     }
    458 
    459     /**
    460      * This could be considered the implementation of {@code
    461      * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we
    462      * consider {@code TrustedCertificateKeyStoreSpi} to be read
    463      * only. Instead, this is used by the {@code KeyChainService} to
    464      * delete CA certificates.
    465      */
    466     public void deleteCertificateEntry(String alias) throws IOException, CertificateException {
    467         if (alias == null) {
    468             return;
    469         }
    470         File file = fileForAlias(alias);
    471         if (file == null) {
    472             return;
    473         }
    474         if (isSystem(alias)) {
    475             X509Certificate cert = readCertificate(file);
    476             if (cert == null) {
    477                 // skip problem certificates
    478                 return;
    479             }
    480             File deleted = getCertificateFile(deletedDir, cert);
    481             if (deleted.exists()) {
    482                 // already deleted system certificate
    483                 return;
    484             }
    485             // write copy of system cert to marked as deleted
    486             writeCertificate(deleted, cert);
    487             return;
    488         }
    489         if (isUser(alias)) {
    490             // truncate the file to make a tombstone by opening and closing.
    491             // we need ensure that we don't leave a gap before a valid cert.
    492             new FileOutputStream(file).close();
    493             removeUnnecessaryTombstones(alias);
    494             return;
    495         }
    496         // non-existant user cert, nothing to delete
    497     }
    498 
    499     private void removeUnnecessaryTombstones(String alias) throws IOException {
    500         if (!isUser(alias)) {
    501             throw new AssertionError(alias);
    502         }
    503         int dotIndex = alias.lastIndexOf('.');
    504         if (dotIndex == -1) {
    505             throw new AssertionError(alias);
    506         }
    507 
    508         String hash = alias.substring(PREFIX_USER.length(), dotIndex);
    509         int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
    510 
    511         if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
    512             return;
    513         }
    514         while (lastTombstoneIndex >= 0) {
    515             File file = file(addedDir, hash, lastTombstoneIndex);
    516             if (!isTombstone(file)) {
    517                 break;
    518             }
    519             if (!file.delete()) {
    520                 throw new IOException("Could not remove " + file);
    521             }
    522             lastTombstoneIndex--;
    523         }
    524     }
    525 }
    526