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.org.conscrypt.SSLParametersImpl;
20import com.android.org.conscrypt.TrustManagerImpl;
21
22import android.util.Log;
23
24import java.io.ByteArrayInputStream;
25import java.io.IOException;
26import java.lang.reflect.InvocationTargetException;
27import java.lang.reflect.Method;
28import java.security.GeneralSecurityException;
29import java.security.KeyStore;
30import java.security.KeyStoreException;
31import java.security.NoSuchAlgorithmException;
32import java.security.cert.Certificate;
33import java.security.cert.CertificateException;
34import java.security.cert.CertificateFactory;
35import java.security.cert.X509Certificate;
36
37import javax.net.ssl.HostnameVerifier;
38import javax.net.ssl.HttpsURLConnection;
39import javax.net.ssl.SSLHandshakeException;
40import javax.net.ssl.SSLSession;
41import javax.net.ssl.SSLSocket;
42import javax.net.ssl.TrustManager;
43import javax.net.ssl.TrustManagerFactory;
44import javax.net.ssl.X509TrustManager;
45
46/**
47 * Class responsible for all server certificate validation functionality
48 */
49public class CertificateChainValidator {
50    private static final String TAG = "CertificateChainValidator";
51
52    private static class NoPreloadHolder {
53        /**
54         * The singleton instance of the certificate chain validator.
55         */
56        private static final CertificateChainValidator sInstance = new CertificateChainValidator();
57
58        /**
59         * The singleton instance of the hostname verifier.
60         */
61        private static final HostnameVerifier sVerifier = HttpsURLConnection
62                .getDefaultHostnameVerifier();
63    }
64
65    private X509TrustManager mTrustManager;
66
67    /**
68     * @return The singleton instance of the certificates chain validator
69     */
70    public static CertificateChainValidator getInstance() {
71        return NoPreloadHolder.sInstance;
72    }
73
74    /**
75     * Creates a new certificate chain validator. This is a private constructor.
76     * If you need a Certificate chain validator, call getInstance().
77     */
78    private CertificateChainValidator() {
79        try {
80            TrustManagerFactory tmf = TrustManagerFactory.getInstance("X.509");
81            tmf.init((KeyStore) null);
82            for (TrustManager tm : tmf.getTrustManagers()) {
83                if (tm instanceof X509TrustManager) {
84                    mTrustManager = (X509TrustManager) tm;
85                }
86            }
87        } catch (NoSuchAlgorithmException e) {
88            throw new RuntimeException("X.509 TrustManagerFactory must be available", e);
89        } catch (KeyStoreException e) {
90            throw new RuntimeException("X.509 TrustManagerFactory cannot be initialized", e);
91        }
92
93        if (mTrustManager == null) {
94            throw new RuntimeException(
95                    "None of the X.509 TrustManagers are X509TrustManager");
96        }
97    }
98
99    /**
100     * Performs the handshake and server certificates validation
101     * Notice a new chain will be rebuilt by tracing the issuer and subject
102     * before calling checkServerTrusted().
103     * And if the last traced certificate is self issued and it is expired, it
104     * will be dropped.
105     * @param sslSocket The secure connection socket
106     * @param domain The website domain
107     * @return An SSL error object if there is an error and null otherwise
108     */
109    public SslError doHandshakeAndValidateServerCertificates(
110            HttpsConnection connection, SSLSocket sslSocket, String domain)
111            throws IOException {
112        // get a valid SSLSession, close the socket if we fail
113        SSLSession sslSession = sslSocket.getSession();
114        if (!sslSession.isValid()) {
115            closeSocketThrowException(sslSocket, "failed to perform SSL handshake");
116        }
117
118        // retrieve the chain of the server peer certificates
119        Certificate[] peerCertificates =
120            sslSocket.getSession().getPeerCertificates();
121
122        if (peerCertificates == null || peerCertificates.length == 0) {
123            closeSocketThrowException(
124                sslSocket, "failed to retrieve peer certificates");
125        } else {
126            // update the SSL certificate associated with the connection
127            if (connection != null) {
128                if (peerCertificates[0] != null) {
129                    connection.setCertificate(
130                        new SslCertificate((X509Certificate)peerCertificates[0]));
131                }
132            }
133        }
134
135        return verifyServerDomainAndCertificates((X509Certificate[]) peerCertificates, domain, "RSA");
136    }
137
138    /**
139     * Similar to doHandshakeAndValidateServerCertificates but exposed to JNI for use
140     * by Chromium HTTPS stack to validate the cert chain.
141     * @param certChain The bytes for certificates in ASN.1 DER encoded certificates format.
142     * @param domain The full website hostname and domain
143     * @param authType The authentication type for the cert chain
144     * @return An SSL error object if there is an error and null otherwise
145     */
146    public static SslError verifyServerCertificates(
147        byte[][] certChain, String domain, String authType)
148        throws IOException {
149
150        if (certChain == null || certChain.length == 0) {
151            throw new IllegalArgumentException("bad certificate chain");
152        }
153
154        X509Certificate[] serverCertificates = new X509Certificate[certChain.length];
155
156        try {
157            CertificateFactory cf = CertificateFactory.getInstance("X.509");
158            for (int i = 0; i < certChain.length; ++i) {
159                serverCertificates[i] = (X509Certificate) cf.generateCertificate(
160                        new ByteArrayInputStream(certChain[i]));
161            }
162        } catch (CertificateException e) {
163            throw new IOException("can't read certificate", e);
164        }
165
166        return verifyServerDomainAndCertificates(serverCertificates, domain, authType);
167    }
168
169    /**
170     * Handles updates to credential storage.
171     */
172    public static void handleTrustStorageUpdate() {
173        TrustManagerFactory tmf;
174        try {
175            tmf = TrustManagerFactory.getInstance("X.509");
176            tmf.init((KeyStore) null);
177        } catch (NoSuchAlgorithmException e) {
178            Log.w(TAG, "Couldn't find default X.509 TrustManagerFactory");
179            return;
180        } catch (KeyStoreException e) {
181            Log.w(TAG, "Couldn't initialize default X.509 TrustManagerFactory", e);
182            return;
183        }
184
185        TrustManager[] tms = tmf.getTrustManagers();
186        boolean sentUpdate = false;
187        for (TrustManager tm : tms) {
188            try {
189                Method updateMethod = tm.getClass().getDeclaredMethod("handleTrustStorageUpdate");
190                updateMethod.setAccessible(true);
191                updateMethod.invoke(tm);
192                sentUpdate = true;
193            } catch (Exception e) {
194            }
195        }
196        if (!sentUpdate) {
197            Log.w(TAG, "Didn't find a TrustManager to handle CA list update");
198        }
199    }
200
201    /**
202     * Common code of doHandshakeAndValidateServerCertificates and verifyServerCertificates.
203     * Calls DomainNamevalidator to verify the domain, and TrustManager to verify the certs.
204     * @param chain the cert chain in X509 cert format.
205     * @param domain The full website hostname and domain
206     * @param authType The authentication type for the cert chain
207     * @return An SSL error object if there is an error and null otherwise
208     */
209    private static SslError verifyServerDomainAndCertificates(
210            X509Certificate[] chain, String domain, String authType)
211            throws IOException {
212        // check if the first certificate in the chain is for this site
213        X509Certificate currCertificate = chain[0];
214        if (currCertificate == null) {
215            throw new IllegalArgumentException("certificate for this site is null");
216        }
217
218        boolean valid = domain != null
219                && !domain.isEmpty()
220                && NoPreloadHolder.sVerifier.verify(domain,
221                        new DelegatingSSLSession.CertificateWrap(currCertificate));
222        if (!valid) {
223            if (HttpLog.LOGV) {
224                HttpLog.v("certificate not for this host: " + domain);
225            }
226            return new SslError(SslError.SSL_IDMISMATCH, currCertificate);
227        }
228
229        try {
230            X509TrustManager x509TrustManager = SSLParametersImpl.getDefaultX509TrustManager();
231            // Use duck-typing to try and call the hostname aware checkServerTrusted if
232            // available.
233            try {
234                Method method = x509TrustManager.getClass().getMethod("checkServerTrusted",
235                        X509Certificate[].class,
236                        String.class,
237                        String.class);
238                method.invoke(x509TrustManager, chain, authType, domain);
239            } catch (NoSuchMethodException | IllegalAccessException e) {
240                x509TrustManager.checkServerTrusted(chain, authType);
241            } catch (InvocationTargetException e) {
242                if (e.getCause() instanceof CertificateException) {
243                    throw (CertificateException) e.getCause();
244                }
245                throw new RuntimeException(e.getCause());
246            }
247            return null;  // No errors.
248        } catch (GeneralSecurityException e) {
249            if (HttpLog.LOGV) {
250                HttpLog.v("failed to validate the certificate chain, error: " +
251                    e.getMessage());
252            }
253            return new SslError(SslError.SSL_UNTRUSTED, currCertificate);
254        }
255    }
256
257    /**
258     * Returns the platform default {@link X509TrustManager}.
259     */
260    private X509TrustManager getTrustManager() {
261        return mTrustManager;
262    }
263
264    private void closeSocketThrowException(
265            SSLSocket socket, String errorMessage, String defaultErrorMessage)
266            throws IOException {
267        closeSocketThrowException(
268            socket, errorMessage != null ? errorMessage : defaultErrorMessage);
269    }
270
271    private void closeSocketThrowException(SSLSocket socket,
272            String errorMessage) throws IOException {
273        if (HttpLog.LOGV) {
274            HttpLog.v("validation error: " + errorMessage);
275        }
276
277        if (socket != null) {
278            SSLSession session = socket.getSession();
279            if (session != null) {
280                session.invalidate();
281            }
282
283            socket.close();
284        }
285
286        throw new SSLHandshakeException(errorMessage);
287    }
288}
289