Java tutorial
/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net.http; import android.os.SystemClock; import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Arrays; import java.util.Date; import java.util.Enumeration; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpHost; import org.bouncycastle.asn1.x509.X509Name; /** * Class responsible for all server certificate validation functionality * * {@hide} */ class CertificateChainValidator { private static long sTotal = 0; private static long sTotalReused = 0; /** * The singleton instance of the certificate chain validator */ private static CertificateChainValidator sInstance; /** * Default trust manager (used to perform CA certificate validation) */ private X509TrustManager mDefaultTrustManager; /** * @return The singleton instance of the certificator chain validator */ public static CertificateChainValidator getInstance() { if (sInstance == null) { sInstance = new CertificateChainValidator(); } return sInstance; } /** * Creates a new certificate chain validator. This is a pivate constructor. * If you need a Certificate chain validator, call getInstance(). */ private CertificateChainValidator() { try { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); trustManagerFactory.init((KeyStore) null); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers != null && trustManagers.length > 0) { for (TrustManager trustManager : trustManagers) { if (trustManager instanceof X509TrustManager) { mDefaultTrustManager = (X509TrustManager) (trustManager); break; } } } } catch (Exception exc) { if (HttpLog.LOGV) { HttpLog.v("CertificateChainValidator():" + " failed to initialize the trust manager"); } } } /** * Performs the handshake and server certificates validation * @param sslSocket The secure connection socket * @param domain The website domain * @return An SSL error object if there is an error and null otherwise */ public SslError doHandshakeAndValidateServerCertificates(HttpsConnection connection, SSLSocket sslSocket, String domain) throws SSLHandshakeException, IOException { ++sTotal; SSLContext sslContext = HttpsConnection.getContext(); if (sslContext == null) { closeSocketThrowException(sslSocket, "SSL context is null"); } X509Certificate[] serverCertificates = null; long sessionBeforeHandshakeLastAccessedTime = 0; byte[] sessionBeforeHandshakeId = null; SSLSession sessionAfterHandshake = null; synchronized (sslContext) { // get SSL session before the handshake SSLSession sessionBeforeHandshake = getSSLSession(sslContext, connection.getHost()); if (sessionBeforeHandshake != null) { sessionBeforeHandshakeLastAccessedTime = sessionBeforeHandshake.getLastAccessedTime(); sessionBeforeHandshakeId = sessionBeforeHandshake.getId(); } // start handshake, close the socket if we fail try { sslSocket.setUseClientMode(true); sslSocket.startHandshake(); } catch (IOException e) { closeSocketThrowException(sslSocket, e.getMessage(), "failed to perform SSL handshake"); } // retrieve the chain of the server peer certificates Certificate[] peerCertificates = sslSocket.getSession().getPeerCertificates(); if (peerCertificates == null || peerCertificates.length <= 0) { closeSocketThrowException(sslSocket, "failed to retrieve peer certificates"); } else { serverCertificates = new X509Certificate[peerCertificates.length]; for (int i = 0; i < peerCertificates.length; ++i) { serverCertificates[i] = (X509Certificate) (peerCertificates[i]); } // update the SSL certificate associated with the connection if (connection != null) { if (serverCertificates[0] != null) { connection.setCertificate(new SslCertificate(serverCertificates[0])); } } } // get SSL session after the handshake sessionAfterHandshake = getSSLSession(sslContext, connection.getHost()); } if (sessionBeforeHandshakeLastAccessedTime != 0 && sessionAfterHandshake != null && Arrays.equals(sessionBeforeHandshakeId, sessionAfterHandshake.getId()) && sessionBeforeHandshakeLastAccessedTime < sessionAfterHandshake.getLastAccessedTime()) { if (HttpLog.LOGV) { HttpLog.v("SSL session was reused: total reused: " + sTotalReused + " out of total of: " + sTotal); ++sTotalReused; } // no errors!!! return null; } // check if the first certificate in the chain is for this site X509Certificate currCertificate = serverCertificates[0]; if (currCertificate == null) { closeSocketThrowException(sslSocket, "certificate for this site is null"); } else { if (!DomainNameChecker.match(currCertificate, domain)) { String errorMessage = "certificate not for this host: " + domain; if (HttpLog.LOGV) { HttpLog.v(errorMessage); } sslSocket.getSession().invalidate(); return new SslError(SslError.SSL_IDMISMATCH, currCertificate); } } // // first, we validate the chain using the standard validation // solution; if we do not find any errors, we are done; if we // fail the standard validation, we re-validate again below, // this time trying to retrieve any individual errors we can // report back to the user. // try { synchronized (mDefaultTrustManager) { mDefaultTrustManager.checkServerTrusted(serverCertificates, "RSA"); // no errors!!! return null; } } catch (CertificateException e) { if (HttpLog.LOGV) { HttpLog.v("failed to pre-validate the certificate chain, error: " + e.getMessage()); } } sslSocket.getSession().invalidate(); SslError error = null; // we check the root certificate separately from the rest of the // chain; this is because we need to know what certificate in // the chain resulted in an error if any currCertificate = serverCertificates[serverCertificates.length - 1]; if (currCertificate == null) { closeSocketThrowException(sslSocket, "root certificate is null"); } // check if the last certificate in the chain (root) is trusted X509Certificate[] rootCertificateChain = { currCertificate }; try { synchronized (mDefaultTrustManager) { mDefaultTrustManager.checkServerTrusted(rootCertificateChain, "RSA"); } } catch (CertificateExpiredException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "root certificate has expired"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } error = new SslError(SslError.SSL_EXPIRED, currCertificate); } catch (CertificateNotYetValidException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "root certificate not valid yet"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } error = new SslError(SslError.SSL_NOTYETVALID, currCertificate); } catch (CertificateException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "root certificate not trusted"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } return new SslError(SslError.SSL_UNTRUSTED, currCertificate); } // Then go through the certificate chain checking that each // certificate trusts the next and that each certificate is // within its valid date range. Walk the chain in the order // from the CA to the end-user X509Certificate prevCertificate = serverCertificates[serverCertificates.length - 1]; for (int i = serverCertificates.length - 2; i >= 0; --i) { currCertificate = serverCertificates[i]; // if a certificate is null, we cannot verify the chain if (currCertificate == null) { closeSocketThrowException(sslSocket, "null certificate in the chain"); } // verify if trusted by chain if (!prevCertificate.getSubjectDN().equals(currCertificate.getIssuerDN())) { String errorMessage = "not trusted by chain"; if (HttpLog.LOGV) { HttpLog.v(errorMessage); } return new SslError(SslError.SSL_UNTRUSTED, currCertificate); } try { currCertificate.verify(prevCertificate.getPublicKey()); } catch (GeneralSecurityException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "not trusted by chain"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } return new SslError(SslError.SSL_UNTRUSTED, currCertificate); } // verify if the dates are valid try { currCertificate.checkValidity(); } catch (CertificateExpiredException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "certificate expired"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } if (error == null || error.getPrimaryError() < SslError.SSL_EXPIRED) { error = new SslError(SslError.SSL_EXPIRED, currCertificate); } } catch (CertificateNotYetValidException e) { String errorMessage = e.getMessage(); if (errorMessage == null) { errorMessage = "certificate not valid yet"; } if (HttpLog.LOGV) { HttpLog.v(errorMessage); } if (error == null || error.getPrimaryError() < SslError.SSL_NOTYETVALID) { error = new SslError(SslError.SSL_NOTYETVALID, currCertificate); } } prevCertificate = currCertificate; } // if we do not have an error to report back to the user, throw // an exception (a generic error will be reported instead) if (error == null) { closeSocketThrowException(sslSocket, "failed to pre-validate the certificate chain due to a non-standard error"); } return error; } private void closeSocketThrowException(SSLSocket socket, String errorMessage, String defaultErrorMessage) throws SSLHandshakeException, IOException { closeSocketThrowException(socket, errorMessage != null ? errorMessage : defaultErrorMessage); } private void closeSocketThrowException(SSLSocket socket, String errorMessage) throws SSLHandshakeException, IOException { if (HttpLog.LOGV) { HttpLog.v("validation error: " + errorMessage); } if (socket != null) { SSLSession session = socket.getSession(); if (session != null) { session.invalidate(); } socket.close(); } throw new SSLHandshakeException(errorMessage); } /** * @param sslContext The SSL context shared accross all the SSL sessions * @param host The host associated with the session * @return A suitable SSL session from the SSL context */ private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) { if (sslContext != null && host != null) { Enumeration en = sslContext.getClientSessionContext().getIds(); while (en.hasMoreElements()) { byte[] id = (byte[]) en.nextElement(); if (id != null) { SSLSession session = sslContext.getClientSessionContext().getSession(id); if (session.isValid() && host.getHostName().equals(session.getPeerHost()) && host.getPort() == session.getPeerPort()) { return session; } } } } return null; } }