CertificateChainValidator.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
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 java.io.IOException;
20
21import java.security.cert.Certificate;
22import java.security.cert.CertificateException;
23import java.security.cert.CertificateExpiredException;
24import java.security.cert.CertificateNotYetValidException;
25import java.security.cert.X509Certificate;
26import java.security.GeneralSecurityException;
27import java.security.KeyStore;
28
29import javax.net.ssl.SSLHandshakeException;
30import javax.net.ssl.SSLSession;
31import javax.net.ssl.SSLSocket;
32import javax.net.ssl.TrustManager;
33import javax.net.ssl.TrustManagerFactory;
34import javax.net.ssl.X509TrustManager;
35
36/**
37 * Class responsible for all server certificate validation functionality
38 *
39 * {@hide}
40 */
41class CertificateChainValidator {
42
43    /**
44     * The singleton instance of the certificate chain validator
45     */
46    private static CertificateChainValidator sInstance;
47
48    /**
49     * Default trust manager (used to perform CA certificate validation)
50     */
51    private X509TrustManager mDefaultTrustManager;
52
53    /**
54     * @return The singleton instance of the certificator chain validator
55     */
56    public static CertificateChainValidator getInstance() {
57        if (sInstance == null) {
58            sInstance = new CertificateChainValidator();
59        }
60
61        return sInstance;
62    }
63
64    /**
65     * Creates a new certificate chain validator. This is a pivate constructor.
66     * If you need a Certificate chain validator, call getInstance().
67     */
68    private CertificateChainValidator() {
69        try {
70            TrustManagerFactory trustManagerFactory
71                = TrustManagerFactory.getInstance("X509");
72            trustManagerFactory.init((KeyStore)null);
73            TrustManager[] trustManagers =
74                trustManagerFactory.getTrustManagers();
75            if (trustManagers != null && trustManagers.length > 0) {
76                for (TrustManager trustManager : trustManagers) {
77                    if (trustManager instanceof X509TrustManager) {
78                        mDefaultTrustManager = (X509TrustManager)(trustManager);
79                        break;
80                    }
81                }
82            }
83        } catch (Exception exc) {
84            if (HttpLog.LOGV) {
85                HttpLog.v("CertificateChainValidator():" +
86                          " failed to initialize the trust manager");
87            }
88        }
89    }
90
91    /**
92     * Performs the handshake and server certificates validation
93     * @param sslSocket The secure connection socket
94     * @param domain The website domain
95     * @return An SSL error object if there is an error and null otherwise
96     */
97    public SslError doHandshakeAndValidateServerCertificates(
98            HttpsConnection connection, SSLSocket sslSocket, String domain)
99            throws IOException {
100        X509Certificate[] serverCertificates = null;
101
102        // start handshake, close the socket if we fail
103        try {
104            sslSocket.setUseClientMode(true);
105            sslSocket.startHandshake();
106        } catch (IOException e) {
107            closeSocketThrowException(
108                sslSocket, e.getMessage(),
109                "failed to perform SSL handshake");
110        }
111
112        // retrieve the chain of the server peer certificates
113        Certificate[] peerCertificates =
114            sslSocket.getSession().getPeerCertificates();
115
116        if (peerCertificates == null || peerCertificates.length <= 0) {
117            closeSocketThrowException(
118                sslSocket, "failed to retrieve peer certificates");
119        } else {
120            serverCertificates =
121                new X509Certificate[peerCertificates.length];
122            for (int i = 0; i < peerCertificates.length; ++i) {
123                serverCertificates[i] =
124                    (X509Certificate)(peerCertificates[i]);
125            }
126
127            // update the SSL certificate associated with the connection
128            if (connection != null) {
129                if (serverCertificates[0] != null) {
130                    connection.setCertificate(
131                        new SslCertificate(serverCertificates[0]));
132                }
133            }
134        }
135
136        // check if the first certificate in the chain is for this site
137        X509Certificate currCertificate = serverCertificates[0];
138        if (currCertificate == null) {
139            closeSocketThrowException(
140                sslSocket, "certificate for this site is null");
141        } else {
142            if (!DomainNameChecker.match(currCertificate, domain)) {
143                String errorMessage = "certificate not for this host: " + domain;
144
145                if (HttpLog.LOGV) {
146                    HttpLog.v(errorMessage);
147                }
148
149                sslSocket.getSession().invalidate();
150                return new SslError(
151                    SslError.SSL_IDMISMATCH, currCertificate);
152            }
153        }
154
155        // first, we validate the chain using the standard validation
156        // solution; if we do not find any errors, we are done; if we
157        // fail the standard validation, we re-validate again below,
158        // this time trying to retrieve any individual errors we can
159        // report back to the user.
160        //
161        try {
162            synchronized (mDefaultTrustManager) {
163                mDefaultTrustManager.checkServerTrusted(
164                    serverCertificates, "RSA");
165
166                // no errors!!!
167                return null;
168            }
169        } catch (CertificateException e) {
170            if (HttpLog.LOGV) {
171                HttpLog.v(
172                    "failed to pre-validate the certificate chain, error: " +
173                    e.getMessage());
174            }
175        }
176
177        sslSocket.getSession().invalidate();
178
179        SslError error = null;
180
181        // we check the root certificate separately from the rest of the
182        // chain; this is because we need to know what certificate in
183        // the chain resulted in an error if any
184        currCertificate =
185            serverCertificates[serverCertificates.length - 1];
186        if (currCertificate == null) {
187            closeSocketThrowException(
188                sslSocket, "root certificate is null");
189        }
190
191        // check if the last certificate in the chain (root) is trusted
192        X509Certificate[] rootCertificateChain = { currCertificate };
193        try {
194            synchronized (mDefaultTrustManager) {
195                mDefaultTrustManager.checkServerTrusted(
196                    rootCertificateChain, "RSA");
197            }
198        } catch (CertificateExpiredException e) {
199            String errorMessage = e.getMessage();
200            if (errorMessage == null) {
201                errorMessage = "root certificate has expired";
202            }
203
204            if (HttpLog.LOGV) {
205                HttpLog.v(errorMessage);
206            }
207
208            error = new SslError(
209                SslError.SSL_EXPIRED, currCertificate);
210        } catch (CertificateNotYetValidException e) {
211            String errorMessage = e.getMessage();
212            if (errorMessage == null) {
213                errorMessage = "root certificate not valid yet";
214            }
215
216            if (HttpLog.LOGV) {
217                HttpLog.v(errorMessage);
218            }
219
220            error = new SslError(
221                SslError.SSL_NOTYETVALID, currCertificate);
222        } catch (CertificateException e) {
223            String errorMessage = e.getMessage();
224            if (errorMessage == null) {
225                errorMessage = "root certificate not trusted";
226            }
227
228            if (HttpLog.LOGV) {
229                HttpLog.v(errorMessage);
230            }
231
232            return new SslError(
233                SslError.SSL_UNTRUSTED, currCertificate);
234        }
235
236        // Then go through the certificate chain checking that each
237        // certificate trusts the next and that each certificate is
238        // within its valid date range. Walk the chain in the order
239        // from the CA to the end-user
240        X509Certificate prevCertificate =
241            serverCertificates[serverCertificates.length - 1];
242
243        for (int i = serverCertificates.length - 2; i >= 0; --i) {
244            currCertificate = serverCertificates[i];
245
246            // if a certificate is null, we cannot verify the chain
247            if (currCertificate == null) {
248                closeSocketThrowException(
249                    sslSocket, "null certificate in the chain");
250            }
251
252            // verify if trusted by chain
253            if (!prevCertificate.getSubjectDN().equals(
254                    currCertificate.getIssuerDN())) {
255                String errorMessage = "not trusted by chain";
256
257                if (HttpLog.LOGV) {
258                    HttpLog.v(errorMessage);
259                }
260
261                return new SslError(
262                    SslError.SSL_UNTRUSTED, currCertificate);
263            }
264
265            try {
266                currCertificate.verify(prevCertificate.getPublicKey());
267            } catch (GeneralSecurityException e) {
268                String errorMessage = e.getMessage();
269                if (errorMessage == null) {
270                    errorMessage = "not trusted by chain";
271                }
272
273                if (HttpLog.LOGV) {
274                    HttpLog.v(errorMessage);
275                }
276
277                return new SslError(
278                    SslError.SSL_UNTRUSTED, currCertificate);
279            }
280
281            // verify if the dates are valid
282            try {
283              currCertificate.checkValidity();
284            } catch (CertificateExpiredException e) {
285                String errorMessage = e.getMessage();
286                if (errorMessage == null) {
287                    errorMessage = "certificate expired";
288                }
289
290                if (HttpLog.LOGV) {
291                    HttpLog.v(errorMessage);
292                }
293
294                if (error == null ||
295                    error.getPrimaryError() < SslError.SSL_EXPIRED) {
296                    error = new SslError(
297                        SslError.SSL_EXPIRED, currCertificate);
298                }
299            } catch (CertificateNotYetValidException e) {
300                String errorMessage = e.getMessage();
301                if (errorMessage == null) {
302                    errorMessage = "certificate not valid yet";
303                }
304
305                if (HttpLog.LOGV) {
306                    HttpLog.v(errorMessage);
307                }
308
309                if (error == null ||
310                    error.getPrimaryError() < SslError.SSL_NOTYETVALID) {
311                    error = new SslError(
312                        SslError.SSL_NOTYETVALID, currCertificate);
313                }
314            }
315
316            prevCertificate = currCertificate;
317        }
318
319        // if we do not have an error to report back to the user, throw
320        // an exception (a generic error will be reported instead)
321        if (error == null) {
322            closeSocketThrowException(
323                sslSocket,
324                "failed to pre-validate the certificate chain due to a non-standard error");
325        }
326
327        return error;
328    }
329
330    private void closeSocketThrowException(
331            SSLSocket socket, String errorMessage, String defaultErrorMessage)
332            throws IOException {
333        closeSocketThrowException(
334            socket, errorMessage != null ? errorMessage : defaultErrorMessage);
335    }
336
337    private void closeSocketThrowException(SSLSocket socket,
338            String errorMessage) throws IOException {
339        if (HttpLog.LOGV) {
340            HttpLog.v("validation error: " + errorMessage);
341        }
342
343        if (socket != null) {
344            SSLSession session = socket.getSession();
345            if (session != null) {
346                session.invalidate();
347            }
348
349            socket.close();
350        }
351
352        throw new SSLHandshakeException(errorMessage);
353    }
354}
355