CertificateChainValidator.java revision 2269d1572e5fcfb725ea55f5764d8c3280d69f6d
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
17package android.net.http;
18
19
20import com.android.internal.net.DomainNameValidator;
21
22import org.apache.harmony.xnet.provider.jsse.SSLParameters;
23
24import java.io.IOException;
25
26import java.security.cert.Certificate;
27import java.security.cert.CertificateException;
28import java.security.cert.CertificateExpiredException;
29import java.security.cert.CertificateNotYetValidException;
30import java.security.cert.X509Certificate;
31import java.security.GeneralSecurityException;
32import java.security.KeyStore;
33import java.util.Date;
34
35import javax.net.ssl.SSLHandshakeException;
36import javax.net.ssl.SSLSession;
37import javax.net.ssl.SSLSocket;
38import javax.net.ssl.TrustManager;
39import javax.net.ssl.TrustManagerFactory;
40import javax.net.ssl.X509TrustManager;
41
42/**
43 * Class responsible for all server certificate validation functionality
44 *
45 * {@hide}
46 */
47class 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            SSLParameters.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