CertificateChainValidator.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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
19import android.os.SystemClock;
20
21import java.io.IOException;
22
23import java.security.cert.Certificate;
24import java.security.cert.CertificateException;
25import java.security.cert.CertificateExpiredException;
26import java.security.cert.CertificateNotYetValidException;
27import java.security.cert.X509Certificate;
28import java.security.GeneralSecurityException;
29import java.security.KeyStore;
30
31import java.util.Arrays;
32import java.util.Date;
33import java.util.Enumeration;
34
35import javax.net.ssl.SSLContext;
36import javax.net.ssl.SSLHandshakeException;
37import javax.net.ssl.SSLPeerUnverifiedException;
38import javax.net.ssl.SSLSession;
39import javax.net.ssl.SSLSocket;
40import javax.net.ssl.TrustManager;
41import javax.net.ssl.TrustManagerFactory;
42import javax.net.ssl.X509TrustManager;
43
44import org.apache.http.HttpHost;
45
46import org.bouncycastle.asn1.x509.X509Name;
47
48/**
49 * Class responsible for all server certificate validation functionality
50 *
51 * {@hide}
52 */
53class CertificateChainValidator {
54
55    private static long sTotal = 0;
56    private static long sTotalReused = 0;
57
58    /**
59     * The singleton instance of the certificate chain validator
60     */
61    private static CertificateChainValidator sInstance;
62
63    /**
64     * Default trust manager (used to perform CA certificate validation)
65     */
66    private X509TrustManager mDefaultTrustManager;
67
68    /**
69     * @return The singleton instance of the certificator chain validator
70     */
71    public static CertificateChainValidator getInstance() {
72        if (sInstance == null) {
73            sInstance = new CertificateChainValidator();
74        }
75
76        return sInstance;
77    }
78
79    /**
80     * Creates a new certificate chain validator. This is a pivate constructor.
81     * If you need a Certificate chain validator, call getInstance().
82     */
83    private CertificateChainValidator() {
84        try {
85            TrustManagerFactory trustManagerFactory
86                = TrustManagerFactory.getInstance("X509");
87            trustManagerFactory.init((KeyStore)null);
88            TrustManager[] trustManagers =
89                trustManagerFactory.getTrustManagers();
90            if (trustManagers != null && trustManagers.length > 0) {
91                for (TrustManager trustManager : trustManagers) {
92                    if (trustManager instanceof X509TrustManager) {
93                        mDefaultTrustManager = (X509TrustManager)(trustManager);
94                        break;
95                    }
96                }
97            }
98        } catch (Exception exc) {
99            if (HttpLog.LOGV) {
100                HttpLog.v("CertificateChainValidator():" +
101                          " failed to initialize the trust manager");
102            }
103        }
104    }
105
106    /**
107     * Performs the handshake and server certificates validation
108     * @param sslSocket The secure connection socket
109     * @param domain The website domain
110     * @return An SSL error object if there is an error and null otherwise
111     */
112    public SslError doHandshakeAndValidateServerCertificates(
113        HttpsConnection connection, SSLSocket sslSocket, String domain)
114        throws SSLHandshakeException, IOException {
115
116        ++sTotal;
117
118        SSLContext sslContext = HttpsConnection.getContext();
119        if (sslContext == null) {
120            closeSocketThrowException(sslSocket, "SSL context is null");
121        }
122
123        X509Certificate[] serverCertificates = null;
124
125        long sessionBeforeHandshakeLastAccessedTime = 0;
126        byte[] sessionBeforeHandshakeId = null;
127
128        SSLSession sessionAfterHandshake = null;
129
130        synchronized(sslContext) {
131            // get SSL session before the handshake
132            SSLSession sessionBeforeHandshake =
133                getSSLSession(sslContext, connection.getHost());
134            if (sessionBeforeHandshake != null) {
135                sessionBeforeHandshakeLastAccessedTime =
136                    sessionBeforeHandshake.getLastAccessedTime();
137
138                sessionBeforeHandshakeId =
139                    sessionBeforeHandshake.getId();
140            }
141
142            // start handshake, close the socket if we fail
143            try {
144                sslSocket.setUseClientMode(true);
145                sslSocket.startHandshake();
146            } catch (IOException e) {
147                closeSocketThrowException(
148                    sslSocket, e.getMessage(),
149                    "failed to perform SSL handshake");
150            }
151
152            // retrieve the chain of the server peer certificates
153            Certificate[] peerCertificates =
154                sslSocket.getSession().getPeerCertificates();
155
156            if (peerCertificates == null || peerCertificates.length <= 0) {
157                closeSocketThrowException(
158                    sslSocket, "failed to retrieve peer certificates");
159            } else {
160                serverCertificates =
161                    new X509Certificate[peerCertificates.length];
162                for (int i = 0; i < peerCertificates.length; ++i) {
163                    serverCertificates[i] =
164                        (X509Certificate)(peerCertificates[i]);
165                }
166
167                // update the SSL certificate associated with the connection
168                if (connection != null) {
169                    if (serverCertificates[0] != null) {
170                        connection.setCertificate(
171                            new SslCertificate(serverCertificates[0]));
172                    }
173                }
174            }
175
176            // get SSL session after the handshake
177            sessionAfterHandshake =
178                getSSLSession(sslContext, connection.getHost());
179        }
180
181        if (sessionBeforeHandshakeLastAccessedTime != 0 &&
182            sessionAfterHandshake != null &&
183            Arrays.equals(
184                sessionBeforeHandshakeId, sessionAfterHandshake.getId()) &&
185            sessionBeforeHandshakeLastAccessedTime <
186            sessionAfterHandshake.getLastAccessedTime()) {
187
188            if (HttpLog.LOGV) {
189                HttpLog.v("SSL session was reused: total reused: "
190                          + sTotalReused
191                          + " out of total of: " + sTotal);
192
193                ++sTotalReused;
194            }
195
196            // no errors!!!
197            return null;
198        }
199
200        // check if the first certificate in the chain is for this site
201        X509Certificate currCertificate = serverCertificates[0];
202        if (currCertificate == null) {
203            closeSocketThrowException(
204                sslSocket, "certificate for this site is null");
205        } else {
206            if (!DomainNameChecker.match(currCertificate, domain)) {
207                String errorMessage = "certificate not for this host: " + domain;
208
209                if (HttpLog.LOGV) {
210                    HttpLog.v(errorMessage);
211                }
212
213                sslSocket.getSession().invalidate();
214                return new SslError(
215                    SslError.SSL_IDMISMATCH, currCertificate);
216            }
217        }
218
219        //
220        // first, we validate the chain using the standard validation
221        // solution; if we do not find any errors, we are done; if we
222        // fail the standard validation, we re-validate again below,
223        // this time trying to retrieve any individual errors we can
224        // report back to the user.
225        //
226        try {
227            synchronized (mDefaultTrustManager) {
228                mDefaultTrustManager.checkServerTrusted(
229                    serverCertificates, "RSA");
230
231                // no errors!!!
232                return null;
233            }
234        } catch (CertificateException e) {
235            if (HttpLog.LOGV) {
236                HttpLog.v(
237                    "failed to pre-validate the certificate chain, error: " +
238                    e.getMessage());
239            }
240        }
241
242        sslSocket.getSession().invalidate();
243
244        SslError error = null;
245
246        // we check the root certificate separately from the rest of the
247        // chain; this is because we need to know what certificate in
248        // the chain resulted in an error if any
249        currCertificate =
250            serverCertificates[serverCertificates.length - 1];
251        if (currCertificate == null) {
252            closeSocketThrowException(
253                sslSocket, "root certificate is null");
254        }
255
256        // check if the last certificate in the chain (root) is trusted
257        X509Certificate[] rootCertificateChain = { currCertificate };
258        try {
259            synchronized (mDefaultTrustManager) {
260                mDefaultTrustManager.checkServerTrusted(
261                    rootCertificateChain, "RSA");
262            }
263        } catch (CertificateExpiredException e) {
264            String errorMessage = e.getMessage();
265            if (errorMessage == null) {
266                errorMessage = "root certificate has expired";
267            }
268
269            if (HttpLog.LOGV) {
270                HttpLog.v(errorMessage);
271            }
272
273            error = new SslError(
274                SslError.SSL_EXPIRED, currCertificate);
275        } catch (CertificateNotYetValidException e) {
276            String errorMessage = e.getMessage();
277            if (errorMessage == null) {
278                errorMessage = "root certificate not valid yet";
279            }
280
281            if (HttpLog.LOGV) {
282                HttpLog.v(errorMessage);
283            }
284
285            error = new SslError(
286                SslError.SSL_NOTYETVALID, currCertificate);
287        } catch (CertificateException e) {
288            String errorMessage = e.getMessage();
289            if (errorMessage == null) {
290                errorMessage = "root certificate not trusted";
291            }
292
293            if (HttpLog.LOGV) {
294                HttpLog.v(errorMessage);
295            }
296
297            return new SslError(
298                SslError.SSL_UNTRUSTED, currCertificate);
299        }
300
301        // Then go through the certificate chain checking that each
302        // certificate trusts the next and that each certificate is
303        // within its valid date range. Walk the chain in the order
304        // from the CA to the end-user
305        X509Certificate prevCertificate =
306            serverCertificates[serverCertificates.length - 1];
307
308        for (int i = serverCertificates.length - 2; i >= 0; --i) {
309            currCertificate = serverCertificates[i];
310
311            // if a certificate is null, we cannot verify the chain
312            if (currCertificate == null) {
313                closeSocketThrowException(
314                    sslSocket, "null certificate in the chain");
315            }
316
317            // verify if trusted by chain
318            if (!prevCertificate.getSubjectDN().equals(
319                    currCertificate.getIssuerDN())) {
320                String errorMessage = "not trusted by chain";
321
322                if (HttpLog.LOGV) {
323                    HttpLog.v(errorMessage);
324                }
325
326                return new SslError(
327                    SslError.SSL_UNTRUSTED, currCertificate);
328            }
329
330            try {
331                currCertificate.verify(prevCertificate.getPublicKey());
332            } catch (GeneralSecurityException e) {
333                String errorMessage = e.getMessage();
334                if (errorMessage == null) {
335                    errorMessage = "not trusted by chain";
336                }
337
338                if (HttpLog.LOGV) {
339                    HttpLog.v(errorMessage);
340                }
341
342                return new SslError(
343                    SslError.SSL_UNTRUSTED, currCertificate);
344            }
345
346            // verify if the dates are valid
347            try {
348              currCertificate.checkValidity();
349            } catch (CertificateExpiredException e) {
350                String errorMessage = e.getMessage();
351                if (errorMessage == null) {
352                    errorMessage = "certificate expired";
353                }
354
355                if (HttpLog.LOGV) {
356                    HttpLog.v(errorMessage);
357                }
358
359                if (error == null ||
360                    error.getPrimaryError() < SslError.SSL_EXPIRED) {
361                    error = new SslError(
362                        SslError.SSL_EXPIRED, currCertificate);
363                }
364            } catch (CertificateNotYetValidException e) {
365                String errorMessage = e.getMessage();
366                if (errorMessage == null) {
367                    errorMessage = "certificate not valid yet";
368                }
369
370                if (HttpLog.LOGV) {
371                    HttpLog.v(errorMessage);
372                }
373
374                if (error == null ||
375                    error.getPrimaryError() < SslError.SSL_NOTYETVALID) {
376                    error = new SslError(
377                        SslError.SSL_NOTYETVALID, currCertificate);
378                }
379            }
380
381            prevCertificate = currCertificate;
382        }
383
384        // if we do not have an error to report back to the user, throw
385        // an exception (a generic error will be reported instead)
386        if (error == null) {
387            closeSocketThrowException(
388                sslSocket,
389                "failed to pre-validate the certificate chain due to a non-standard error");
390        }
391
392        return error;
393    }
394
395    private void closeSocketThrowException(
396        SSLSocket socket, String errorMessage, String defaultErrorMessage)
397        throws SSLHandshakeException, IOException {
398        closeSocketThrowException(
399            socket, errorMessage != null ? errorMessage : defaultErrorMessage);
400    }
401
402    private void closeSocketThrowException(SSLSocket socket, String errorMessage)
403        throws SSLHandshakeException, IOException {
404        if (HttpLog.LOGV) {
405            HttpLog.v("validation error: " + errorMessage);
406        }
407
408        if (socket != null) {
409            SSLSession session = socket.getSession();
410            if (session != null) {
411                session.invalidate();
412            }
413
414            socket.close();
415        }
416
417        throw new SSLHandshakeException(errorMessage);
418    }
419
420    /**
421     * @param sslContext The SSL context shared accross all the SSL sessions
422     * @param host The host associated with the session
423     * @return A suitable SSL session from the SSL context
424     */
425    private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) {
426        if (sslContext != null && host != null) {
427            Enumeration en = sslContext.getClientSessionContext().getIds();
428            while (en.hasMoreElements()) {
429                byte[] id = (byte[]) en.nextElement();
430                if (id != null) {
431                    SSLSession session =
432                        sslContext.getClientSessionContext().getSession(id);
433                    if (session.isValid() &&
434                        host.getHostName().equals(session.getPeerHost()) &&
435                        host.getPort() == session.getPeerPort()) {
436                        return session;
437                    }
438                }
439            }
440        }
441
442        return null;
443    }
444}
445