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