CertificateChainValidator.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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 android.os.SystemClock; 20 21import java.io.IOException; 22 23import java.security.cert.Certificate; 24import java.security.cert.CertificateException; 25import java.security.cert.CertificateExpiredException; 26import java.security.cert.CertificateNotYetValidException; 27import java.security.cert.X509Certificate; 28import java.security.GeneralSecurityException; 29import java.security.KeyStore; 30 31import java.util.Arrays; 32import java.util.Date; 33import java.util.Enumeration; 34 35import javax.net.ssl.SSLContext; 36import javax.net.ssl.SSLHandshakeException; 37import javax.net.ssl.SSLPeerUnverifiedException; 38import javax.net.ssl.SSLSession; 39import javax.net.ssl.SSLSocket; 40import javax.net.ssl.TrustManager; 41import javax.net.ssl.TrustManagerFactory; 42import javax.net.ssl.X509TrustManager; 43 44import org.apache.http.HttpHost; 45 46import org.bouncycastle.asn1.x509.X509Name; 47 48/** 49 * Class responsible for all server certificate validation functionality 50 * 51 * {@hide} 52 */ 53class CertificateChainValidator { 54 55 private static long sTotal = 0; 56 private static long sTotalReused = 0; 57 58 /** 59 * The singleton instance of the certificate chain validator 60 */ 61 private static CertificateChainValidator sInstance; 62 63 /** 64 * Default trust manager (used to perform CA certificate validation) 65 */ 66 private X509TrustManager mDefaultTrustManager; 67 68 /** 69 * @return The singleton instance of the certificator chain validator 70 */ 71 public static CertificateChainValidator getInstance() { 72 if (sInstance == null) { 73 sInstance = new CertificateChainValidator(); 74 } 75 76 return sInstance; 77 } 78 79 /** 80 * Creates a new certificate chain validator. This is a pivate constructor. 81 * If you need a Certificate chain validator, call getInstance(). 82 */ 83 private CertificateChainValidator() { 84 try { 85 TrustManagerFactory trustManagerFactory 86 = TrustManagerFactory.getInstance("X509"); 87 trustManagerFactory.init((KeyStore)null); 88 TrustManager[] trustManagers = 89 trustManagerFactory.getTrustManagers(); 90 if (trustManagers != null && trustManagers.length > 0) { 91 for (TrustManager trustManager : trustManagers) { 92 if (trustManager instanceof X509TrustManager) { 93 mDefaultTrustManager = (X509TrustManager)(trustManager); 94 break; 95 } 96 } 97 } 98 } catch (Exception exc) { 99 if (HttpLog.LOGV) { 100 HttpLog.v("CertificateChainValidator():" + 101 " failed to initialize the trust manager"); 102 } 103 } 104 } 105 106 /** 107 * Performs the handshake and server certificates validation 108 * @param sslSocket The secure connection socket 109 * @param domain The website domain 110 * @return An SSL error object if there is an error and null otherwise 111 */ 112 public SslError doHandshakeAndValidateServerCertificates( 113 HttpsConnection connection, SSLSocket sslSocket, String domain) 114 throws SSLHandshakeException, IOException { 115 116 ++sTotal; 117 118 SSLContext sslContext = HttpsConnection.getContext(); 119 if (sslContext == null) { 120 closeSocketThrowException(sslSocket, "SSL context is null"); 121 } 122 123 X509Certificate[] serverCertificates = null; 124 125 long sessionBeforeHandshakeLastAccessedTime = 0; 126 byte[] sessionBeforeHandshakeId = null; 127 128 SSLSession sessionAfterHandshake = null; 129 130 synchronized(sslContext) { 131 // get SSL session before the handshake 132 SSLSession sessionBeforeHandshake = 133 getSSLSession(sslContext, connection.getHost()); 134 if (sessionBeforeHandshake != null) { 135 sessionBeforeHandshakeLastAccessedTime = 136 sessionBeforeHandshake.getLastAccessedTime(); 137 138 sessionBeforeHandshakeId = 139 sessionBeforeHandshake.getId(); 140 } 141 142 // start handshake, close the socket if we fail 143 try { 144 sslSocket.setUseClientMode(true); 145 sslSocket.startHandshake(); 146 } catch (IOException e) { 147 closeSocketThrowException( 148 sslSocket, e.getMessage(), 149 "failed to perform SSL handshake"); 150 } 151 152 // retrieve the chain of the server peer certificates 153 Certificate[] peerCertificates = 154 sslSocket.getSession().getPeerCertificates(); 155 156 if (peerCertificates == null || peerCertificates.length <= 0) { 157 closeSocketThrowException( 158 sslSocket, "failed to retrieve peer certificates"); 159 } else { 160 serverCertificates = 161 new X509Certificate[peerCertificates.length]; 162 for (int i = 0; i < peerCertificates.length; ++i) { 163 serverCertificates[i] = 164 (X509Certificate)(peerCertificates[i]); 165 } 166 167 // update the SSL certificate associated with the connection 168 if (connection != null) { 169 if (serverCertificates[0] != null) { 170 connection.setCertificate( 171 new SslCertificate(serverCertificates[0])); 172 } 173 } 174 } 175 176 // get SSL session after the handshake 177 sessionAfterHandshake = 178 getSSLSession(sslContext, connection.getHost()); 179 } 180 181 if (sessionBeforeHandshakeLastAccessedTime != 0 && 182 sessionAfterHandshake != null && 183 Arrays.equals( 184 sessionBeforeHandshakeId, sessionAfterHandshake.getId()) && 185 sessionBeforeHandshakeLastAccessedTime < 186 sessionAfterHandshake.getLastAccessedTime()) { 187 188 if (HttpLog.LOGV) { 189 HttpLog.v("SSL session was reused: total reused: " 190 + sTotalReused 191 + " out of total of: " + sTotal); 192 193 ++sTotalReused; 194 } 195 196 // no errors!!! 197 return null; 198 } 199 200 // check if the first certificate in the chain is for this site 201 X509Certificate currCertificate = serverCertificates[0]; 202 if (currCertificate == null) { 203 closeSocketThrowException( 204 sslSocket, "certificate for this site is null"); 205 } else { 206 if (!DomainNameChecker.match(currCertificate, domain)) { 207 String errorMessage = "certificate not for this host: " + domain; 208 209 if (HttpLog.LOGV) { 210 HttpLog.v(errorMessage); 211 } 212 213 sslSocket.getSession().invalidate(); 214 return new SslError( 215 SslError.SSL_IDMISMATCH, currCertificate); 216 } 217 } 218 219 // 220 // first, we validate the chain using the standard validation 221 // solution; if we do not find any errors, we are done; if we 222 // fail the standard validation, we re-validate again below, 223 // this time trying to retrieve any individual errors we can 224 // report back to the user. 225 // 226 try { 227 synchronized (mDefaultTrustManager) { 228 mDefaultTrustManager.checkServerTrusted( 229 serverCertificates, "RSA"); 230 231 // no errors!!! 232 return null; 233 } 234 } catch (CertificateException e) { 235 if (HttpLog.LOGV) { 236 HttpLog.v( 237 "failed to pre-validate the certificate chain, error: " + 238 e.getMessage()); 239 } 240 } 241 242 sslSocket.getSession().invalidate(); 243 244 SslError error = null; 245 246 // we check the root certificate separately from the rest of the 247 // chain; this is because we need to know what certificate in 248 // the chain resulted in an error if any 249 currCertificate = 250 serverCertificates[serverCertificates.length - 1]; 251 if (currCertificate == null) { 252 closeSocketThrowException( 253 sslSocket, "root certificate is null"); 254 } 255 256 // check if the last certificate in the chain (root) is trusted 257 X509Certificate[] rootCertificateChain = { currCertificate }; 258 try { 259 synchronized (mDefaultTrustManager) { 260 mDefaultTrustManager.checkServerTrusted( 261 rootCertificateChain, "RSA"); 262 } 263 } catch (CertificateExpiredException e) { 264 String errorMessage = e.getMessage(); 265 if (errorMessage == null) { 266 errorMessage = "root certificate has expired"; 267 } 268 269 if (HttpLog.LOGV) { 270 HttpLog.v(errorMessage); 271 } 272 273 error = new SslError( 274 SslError.SSL_EXPIRED, currCertificate); 275 } catch (CertificateNotYetValidException e) { 276 String errorMessage = e.getMessage(); 277 if (errorMessage == null) { 278 errorMessage = "root certificate not valid yet"; 279 } 280 281 if (HttpLog.LOGV) { 282 HttpLog.v(errorMessage); 283 } 284 285 error = new SslError( 286 SslError.SSL_NOTYETVALID, currCertificate); 287 } catch (CertificateException e) { 288 String errorMessage = e.getMessage(); 289 if (errorMessage == null) { 290 errorMessage = "root certificate not trusted"; 291 } 292 293 if (HttpLog.LOGV) { 294 HttpLog.v(errorMessage); 295 } 296 297 return new SslError( 298 SslError.SSL_UNTRUSTED, currCertificate); 299 } 300 301 // Then go through the certificate chain checking that each 302 // certificate trusts the next and that each certificate is 303 // within its valid date range. Walk the chain in the order 304 // from the CA to the end-user 305 X509Certificate prevCertificate = 306 serverCertificates[serverCertificates.length - 1]; 307 308 for (int i = serverCertificates.length - 2; i >= 0; --i) { 309 currCertificate = serverCertificates[i]; 310 311 // if a certificate is null, we cannot verify the chain 312 if (currCertificate == null) { 313 closeSocketThrowException( 314 sslSocket, "null certificate in the chain"); 315 } 316 317 // verify if trusted by chain 318 if (!prevCertificate.getSubjectDN().equals( 319 currCertificate.getIssuerDN())) { 320 String errorMessage = "not trusted by chain"; 321 322 if (HttpLog.LOGV) { 323 HttpLog.v(errorMessage); 324 } 325 326 return new SslError( 327 SslError.SSL_UNTRUSTED, currCertificate); 328 } 329 330 try { 331 currCertificate.verify(prevCertificate.getPublicKey()); 332 } catch (GeneralSecurityException e) { 333 String errorMessage = e.getMessage(); 334 if (errorMessage == null) { 335 errorMessage = "not trusted by chain"; 336 } 337 338 if (HttpLog.LOGV) { 339 HttpLog.v(errorMessage); 340 } 341 342 return new SslError( 343 SslError.SSL_UNTRUSTED, currCertificate); 344 } 345 346 // verify if the dates are valid 347 try { 348 currCertificate.checkValidity(); 349 } catch (CertificateExpiredException e) { 350 String errorMessage = e.getMessage(); 351 if (errorMessage == null) { 352 errorMessage = "certificate expired"; 353 } 354 355 if (HttpLog.LOGV) { 356 HttpLog.v(errorMessage); 357 } 358 359 if (error == null || 360 error.getPrimaryError() < SslError.SSL_EXPIRED) { 361 error = new SslError( 362 SslError.SSL_EXPIRED, currCertificate); 363 } 364 } catch (CertificateNotYetValidException e) { 365 String errorMessage = e.getMessage(); 366 if (errorMessage == null) { 367 errorMessage = "certificate not valid yet"; 368 } 369 370 if (HttpLog.LOGV) { 371 HttpLog.v(errorMessage); 372 } 373 374 if (error == null || 375 error.getPrimaryError() < SslError.SSL_NOTYETVALID) { 376 error = new SslError( 377 SslError.SSL_NOTYETVALID, currCertificate); 378 } 379 } 380 381 prevCertificate = currCertificate; 382 } 383 384 // if we do not have an error to report back to the user, throw 385 // an exception (a generic error will be reported instead) 386 if (error == null) { 387 closeSocketThrowException( 388 sslSocket, 389 "failed to pre-validate the certificate chain due to a non-standard error"); 390 } 391 392 return error; 393 } 394 395 private void closeSocketThrowException( 396 SSLSocket socket, String errorMessage, String defaultErrorMessage) 397 throws SSLHandshakeException, IOException { 398 closeSocketThrowException( 399 socket, errorMessage != null ? errorMessage : defaultErrorMessage); 400 } 401 402 private void closeSocketThrowException(SSLSocket socket, String errorMessage) 403 throws SSLHandshakeException, IOException { 404 if (HttpLog.LOGV) { 405 HttpLog.v("validation error: " + errorMessage); 406 } 407 408 if (socket != null) { 409 SSLSession session = socket.getSession(); 410 if (session != null) { 411 session.invalidate(); 412 } 413 414 socket.close(); 415 } 416 417 throw new SSLHandshakeException(errorMessage); 418 } 419 420 /** 421 * @param sslContext The SSL context shared accross all the SSL sessions 422 * @param host The host associated with the session 423 * @return A suitable SSL session from the SSL context 424 */ 425 private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) { 426 if (sslContext != null && host != null) { 427 Enumeration en = sslContext.getClientSessionContext().getIds(); 428 while (en.hasMoreElements()) { 429 byte[] id = (byte[]) en.nextElement(); 430 if (id != null) { 431 SSLSession session = 432 sslContext.getClientSessionContext().getSession(id); 433 if (session.isValid() && 434 host.getHostName().equals(session.getPeerHost()) && 435 host.getPort() == session.getPeerPort()) { 436 return session; 437 } 438 } 439 } 440 } 441 442 return null; 443 } 444} 445