Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2008 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 android.net.http;
     18 
     19 
     20 import com.android.internal.net.DomainNameValidator;
     21 
     22 import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
     23 
     24 import java.io.IOException;
     25 
     26 import java.security.cert.Certificate;
     27 import java.security.cert.CertificateException;
     28 import java.security.cert.CertificateExpiredException;
     29 import java.security.cert.CertificateNotYetValidException;
     30 import java.security.cert.X509Certificate;
     31 import java.security.GeneralSecurityException;
     32 import java.security.KeyStore;
     33 import java.util.Date;
     34 
     35 import javax.net.ssl.SSLHandshakeException;
     36 import javax.net.ssl.SSLSession;
     37 import javax.net.ssl.SSLSocket;
     38 import javax.net.ssl.TrustManager;
     39 import javax.net.ssl.TrustManagerFactory;
     40 import javax.net.ssl.X509TrustManager;
     41 
     42 /**
     43  * Class responsible for all server certificate validation functionality
     44  *
     45  * {@hide}
     46  */
     47 class CertificateChainValidator {
     48 
     49     /**
     50      * The singleton instance of the certificate chain validator
     51      */
     52     private static final CertificateChainValidator sInstance
     53             = new CertificateChainValidator();
     54 
     55     /**
     56      * @return The singleton instance of the certificates chain validator
     57      */
     58     public static CertificateChainValidator getInstance() {
     59         return sInstance;
     60     }
     61 
     62     /**
     63      * Creates a new certificate chain validator. This is a private constructor.
     64      * If you need a Certificate chain validator, call getInstance().
     65      */
     66     private CertificateChainValidator() {}
     67 
     68     /**
     69      * Performs the handshake and server certificates validation
     70      * Notice a new chain will be rebuilt by tracing the issuer and subject
     71      * before calling checkServerTrusted().
     72      * And if the last traced certificate is self issued and it is expired, it
     73      * will be dropped.
     74      * @param sslSocket The secure connection socket
     75      * @param domain The website domain
     76      * @return An SSL error object if there is an error and null otherwise
     77      */
     78     public SslError doHandshakeAndValidateServerCertificates(
     79             HttpsConnection connection, SSLSocket sslSocket, String domain)
     80             throws IOException {
     81         X509Certificate[] serverCertificates = null;
     82 
     83         // start handshake, close the socket if we fail
     84         try {
     85             sslSocket.setUseClientMode(true);
     86             sslSocket.startHandshake();
     87         } catch (IOException e) {
     88             closeSocketThrowException(
     89                 sslSocket, e.getMessage(),
     90                 "failed to perform SSL handshake");
     91         }
     92 
     93         // retrieve the chain of the server peer certificates
     94         Certificate[] peerCertificates =
     95             sslSocket.getSession().getPeerCertificates();
     96 
     97         if (peerCertificates == null || peerCertificates.length <= 0) {
     98             closeSocketThrowException(
     99                 sslSocket, "failed to retrieve peer certificates");
    100         } else {
    101             serverCertificates =
    102                 new X509Certificate[peerCertificates.length];
    103             for (int i = 0; i < peerCertificates.length; ++i) {
    104                 serverCertificates[i] =
    105                     (X509Certificate)(peerCertificates[i]);
    106             }
    107 
    108             // update the SSL certificate associated with the connection
    109             if (connection != null) {
    110                 if (serverCertificates[0] != null) {
    111                     connection.setCertificate(
    112                         new SslCertificate(serverCertificates[0]));
    113                 }
    114             }
    115         }
    116 
    117         // check if the first certificate in the chain is for this site
    118         X509Certificate currCertificate = serverCertificates[0];
    119         if (currCertificate == null) {
    120             closeSocketThrowException(
    121                 sslSocket, "certificate for this site is null");
    122         } else {
    123             if (!DomainNameValidator.match(currCertificate, domain)) {
    124                 String errorMessage = "certificate not for this host: " + domain;
    125 
    126                 if (HttpLog.LOGV) {
    127                     HttpLog.v(errorMessage);
    128                 }
    129 
    130                 sslSocket.getSession().invalidate();
    131                 return new SslError(
    132                     SslError.SSL_IDMISMATCH, currCertificate);
    133             }
    134         }
    135 
    136         // Clean up the certificates chain and build a new one.
    137         // Theoretically, we shouldn't have to do this, but various web servers
    138         // in practice are mis-configured to have out-of-order certificates or
    139         // expired self-issued root certificate.
    140         int chainLength = serverCertificates.length;
    141         if (serverCertificates.length > 1) {
    142           // 1. we clean the received certificates chain.
    143           // We start from the end-entity certificate, tracing down by matching
    144           // the "issuer" field and "subject" field until we can't continue.
    145           // This helps when the certificates are out of order or
    146           // some certificates are not related to the site.
    147           int currIndex;
    148           for (currIndex = 0; currIndex < serverCertificates.length; ++currIndex) {
    149             boolean foundNext = false;
    150             for (int nextIndex = currIndex + 1;
    151                  nextIndex < serverCertificates.length;
    152                  ++nextIndex) {
    153               if (serverCertificates[currIndex].getIssuerDN().equals(
    154                   serverCertificates[nextIndex].getSubjectDN())) {
    155                 foundNext = true;
    156                 // Exchange certificates so that 0 through currIndex + 1 are in proper order
    157                 if (nextIndex != currIndex + 1) {
    158                   X509Certificate tempCertificate = serverCertificates[nextIndex];
    159                   serverCertificates[nextIndex] = serverCertificates[currIndex + 1];
    160                   serverCertificates[currIndex + 1] = tempCertificate;
    161                 }
    162                 break;
    163               }
    164             }
    165             if (!foundNext) break;
    166           }
    167 
    168           // 2. we exam if the last traced certificate is self issued and it is expired.
    169           // If so, we drop it and pass the rest to checkServerTrusted(), hoping we might
    170           // have a similar but unexpired trusted root.
    171           chainLength = currIndex + 1;
    172           X509Certificate lastCertificate = serverCertificates[chainLength - 1];
    173           Date now = new Date();
    174           if (lastCertificate.getSubjectDN().equals(lastCertificate.getIssuerDN())
    175               && now.after(lastCertificate.getNotAfter())) {
    176             --chainLength;
    177           }
    178         }
    179 
    180         // 3. Now we copy the newly built chain into an appropriately sized array.
    181         X509Certificate[] newServerCertificates = null;
    182         newServerCertificates = new X509Certificate[chainLength];
    183         for (int i = 0; i < chainLength; ++i) {
    184           newServerCertificates[i] = serverCertificates[i];
    185         }
    186 
    187         // first, we validate the new chain using the standard validation
    188         // solution; if we do not find any errors, we are done; if we
    189         // fail the standard validation, we re-validate again below,
    190         // this time trying to retrieve any individual errors we can
    191         // report back to the user.
    192         //
    193         try {
    194             SSLParametersImpl.getDefaultTrustManager().checkServerTrusted(
    195                 newServerCertificates, "RSA");
    196 
    197             // no errors!!!
    198             return null;
    199         } catch (CertificateException e) {
    200             sslSocket.getSession().invalidate();
    201 
    202             if (HttpLog.LOGV) {
    203                 HttpLog.v(
    204                     "failed to pre-validate the certificate chain, error: " +
    205                     e.getMessage());
    206             }
    207             return new SslError(
    208                 SslError.SSL_UNTRUSTED, currCertificate);
    209         }
    210     }
    211 
    212     private void closeSocketThrowException(
    213             SSLSocket socket, String errorMessage, String defaultErrorMessage)
    214             throws IOException {
    215         closeSocketThrowException(
    216             socket, errorMessage != null ? errorMessage : defaultErrorMessage);
    217     }
    218 
    219     private void closeSocketThrowException(SSLSocket socket,
    220             String errorMessage) throws IOException {
    221         if (HttpLog.LOGV) {
    222             HttpLog.v("validation error: " + errorMessage);
    223         }
    224 
    225         if (socket != null) {
    226             SSLSession session = socket.getSession();
    227             if (session != null) {
    228                 session.invalidate();
    229             }
    230 
    231             socket.close();
    232         }
    233 
    234         throw new SSLHandshakeException(errorMessage);
    235     }
    236 }
    237