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