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