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