CertificateChainValidator.java revision 866666071bc49bd0c4fcd1776c9d9036d4e29fec
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 java.io.IOException;
21import java.security.GeneralSecurityException;
22import java.security.KeyManagementException;
23import java.security.cert.Certificate;
24import java.security.cert.CertificateException;
25import java.security.cert.X509Certificate;
26import javax.net.ssl.DefaultHostnameVerifier;
27import javax.net.ssl.SSLHandshakeException;
28import javax.net.ssl.SSLSession;
29import javax.net.ssl.SSLSocket;
30import javax.net.ssl.X509TrustManager;
31import org.apache.harmony.security.provider.cert.X509CertImpl;
32import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
33import org.apache.harmony.xnet.provider.jsse.TrustManagerImpl;
34
35/**
36 * Class responsible for all server certificate validation functionality
37 *
38 * {@hide}
39 */
40public class CertificateChainValidator {
41
42    /**
43     * The singleton instance of the certificate chain validator
44     */
45    private static final CertificateChainValidator sInstance
46            = new CertificateChainValidator();
47
48    private static final DefaultHostnameVerifier sVerifier
49            = new DefaultHostnameVerifier();
50
51    /**
52     * @return The singleton instance of the certificates chain validator
53     */
54    public static CertificateChainValidator getInstance() {
55        return sInstance;
56    }
57
58    /**
59     * Creates a new certificate chain validator. This is a private constructor.
60     * If you need a Certificate chain validator, call getInstance().
61     */
62    private CertificateChainValidator() {}
63
64    /**
65     * Performs the handshake and server certificates validation
66     * Notice a new chain will be rebuilt by tracing the issuer and subject
67     * before calling checkServerTrusted().
68     * And if the last traced certificate is self issued and it is expired, it
69     * will be dropped.
70     * @param sslSocket The secure connection socket
71     * @param domain The website domain
72     * @return An SSL error object if there is an error and null otherwise
73     */
74    public SslError doHandshakeAndValidateServerCertificates(
75            HttpsConnection connection, SSLSocket sslSocket, String domain)
76            throws IOException {
77        // get a valid SSLSession, close the socket if we fail
78        SSLSession sslSession = sslSocket.getSession();
79        if (!sslSession.isValid()) {
80            closeSocketThrowException(sslSocket, "failed to perform SSL handshake");
81        }
82
83        // retrieve the chain of the server peer certificates
84        Certificate[] peerCertificates =
85            sslSocket.getSession().getPeerCertificates();
86
87        if (peerCertificates == null || peerCertificates.length == 0) {
88            closeSocketThrowException(
89                sslSocket, "failed to retrieve peer certificates");
90        } else {
91            // update the SSL certificate associated with the connection
92            if (connection != null) {
93                if (peerCertificates[0] != null) {
94                    connection.setCertificate(
95                        new SslCertificate((X509Certificate)peerCertificates[0]));
96                }
97            }
98        }
99
100        return verifyServerDomainAndCertificates((X509Certificate[]) peerCertificates, domain, "RSA");
101    }
102
103    /**
104     * Similar to doHandshakeAndValidateServerCertificates but exposed to JNI for use
105     * by Chromium HTTPS stack to validate the cert chain.
106     * @param certChain The bytes for certificates in ASN.1 DER encoded certificates format.
107     * @param domain The full website hostname and domain
108     * @param authType The authentication type for the cert chain
109     * @return An SSL error object if there is an error and null otherwise
110     */
111    public static SslError verifyServerCertificates(
112        byte[][] certChain, String domain, String authType)
113        throws IOException {
114
115        if (certChain == null || certChain.length == 0) {
116            throw new IllegalArgumentException("bad certificate chain");
117        }
118
119        X509Certificate[] serverCertificates = new X509Certificate[certChain.length];
120
121        for (int i = 0; i < certChain.length; ++i) {
122            serverCertificates[i] = new X509CertImpl(certChain[i]);
123        }
124
125        return verifyServerDomainAndCertificates(serverCertificates, domain, authType);
126    }
127
128    /**
129     * Handles updates to credential storage.
130     */
131    public static void handleTrustStorageUpdate() {
132
133        try {
134            X509TrustManager x509TrustManager = SSLParametersImpl.getDefaultTrustManager();
135            if( x509TrustManager instanceof TrustManagerImpl ) {
136                TrustManagerImpl trustManager = (TrustManagerImpl) x509TrustManager;
137                trustManager.handleTrustStorageUpdate();
138            }
139        } catch (KeyManagementException ignored) {
140        }
141    }
142
143    /**
144     * Common code of doHandshakeAndValidateServerCertificates and verifyServerCertificates.
145     * Calls DomainNamevalidator to verify the domain, and TrustManager to verify the certs.
146     * @param chain the cert chain in X509 cert format.
147     * @param domain The full website hostname and domain
148     * @param authType The authentication type for the cert chain
149     * @return An SSL error object if there is an error and null otherwise
150     */
151    private static SslError verifyServerDomainAndCertificates(
152            X509Certificate[] chain, String domain, String authType)
153            throws IOException {
154        // check if the first certificate in the chain is for this site
155        X509Certificate currCertificate = chain[0];
156        if (currCertificate == null) {
157            throw new IllegalArgumentException("certificate for this site is null");
158        }
159
160        boolean valid = domain != null
161                && !domain.isEmpty()
162                && sVerifier.verify(domain, currCertificate);
163        if (!valid) {
164            if (HttpLog.LOGV) {
165                HttpLog.v("certificate not for this host: " + domain);
166            }
167            return new SslError(SslError.SSL_IDMISMATCH, currCertificate);
168        }
169
170        try {
171            SSLParametersImpl.getDefaultTrustManager().checkServerTrusted(chain, authType);
172            return null;  // No errors.
173        } catch (GeneralSecurityException e) {
174            if (HttpLog.LOGV) {
175                HttpLog.v("failed to validate the certificate chain, error: " +
176                    e.getMessage());
177            }
178            return new SslError(SslError.SSL_UNTRUSTED, currCertificate);
179        }
180    }
181
182
183    private void closeSocketThrowException(
184            SSLSocket socket, String errorMessage, String defaultErrorMessage)
185            throws IOException {
186        closeSocketThrowException(
187            socket, errorMessage != null ? errorMessage : defaultErrorMessage);
188    }
189
190    private void closeSocketThrowException(SSLSocket socket,
191            String errorMessage) throws IOException {
192        if (HttpLog.LOGV) {
193            HttpLog.v("validation error: " + errorMessage);
194        }
195
196        if (socket != null) {
197            SSLSession session = socket.getSession();
198            if (session != null) {
199                session.invalidate();
200            }
201
202            socket.close();
203        }
204
205        throw new SSLHandshakeException(errorMessage);
206    }
207}
208