diff options
Diffstat (limited to 'android/src/android/net/http/CertificateChainValidator.java')
-rw-r--r-- | android/src/android/net/http/CertificateChainValidator.java | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/android/src/android/net/http/CertificateChainValidator.java b/android/src/android/net/http/CertificateChainValidator.java new file mode 100644 index 0000000..bf3fe02 --- /dev/null +++ b/android/src/android/net/http/CertificateChainValidator.java @@ -0,0 +1,279 @@ +/* + * 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 com.android.org.conscrypt.SSLParametersImpl; +import com.android.org.conscrypt.TrustManagerImpl; + +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; +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; + +/** + * Class responsible for all server certificate validation functionality + * + * {@hide} + */ +public class CertificateChainValidator { + private static final String TAG = "CertificateChainValidator"; + + private static class NoPreloadHolder { + /** + * The singleton instance of the certificate chain validator. + */ + private static final CertificateChainValidator sInstance = new CertificateChainValidator(); + + /** + * The singleton instance of the hostname verifier. + */ + private static final HostnameVerifier sVerifier = HttpsURLConnection + .getDefaultHostnameVerifier(); + } + + private X509TrustManager mTrustManager; + + /** + * @return The singleton instance of the certificates chain validator + */ + public static CertificateChainValidator getInstance() { + return NoPreloadHolder.sInstance; + } + + /** + * Creates a new certificate chain validator. This is a private constructor. + * If you need a Certificate chain validator, call getInstance(). + */ + private CertificateChainValidator() { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X.509"); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + mTrustManager = (X509TrustManager) tm; + } + } + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("X.509 TrustManagerFactory must be available", e); + } catch (KeyStoreException e) { + throw new RuntimeException("X.509 TrustManagerFactory cannot be initialized", e); + } + + if (mTrustManager == null) { + throw new RuntimeException( + "None of the X.509 TrustManagers are X509TrustManager"); + } + } + + /** + * Performs the handshake and server certificates validation + * Notice a new chain will be rebuilt by tracing the issuer and subject + * before calling checkServerTrusted(). + * And if the last traced certificate is self issued and it is expired, it + * will be dropped. + * @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 IOException { + // get a valid SSLSession, close the socket if we fail + SSLSession sslSession = sslSocket.getSession(); + if (!sslSession.isValid()) { + closeSocketThrowException(sslSocket, "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 { + // update the SSL certificate associated with the connection + if (connection != null) { + if (peerCertificates[0] != null) { + connection.setCertificate( + new SslCertificate((X509Certificate)peerCertificates[0])); + } + } + } + + return verifyServerDomainAndCertificates((X509Certificate[]) peerCertificates, domain, "RSA"); + } + + /** + * Similar to doHandshakeAndValidateServerCertificates but exposed to JNI for use + * by Chromium HTTPS stack to validate the cert chain. + * @param certChain The bytes for certificates in ASN.1 DER encoded certificates format. + * @param domain The full website hostname and domain + * @param authType The authentication type for the cert chain + * @return An SSL error object if there is an error and null otherwise + */ + public static SslError verifyServerCertificates( + byte[][] certChain, String domain, String authType) + throws IOException { + + if (certChain == null || certChain.length == 0) { + throw new IllegalArgumentException("bad certificate chain"); + } + + X509Certificate[] serverCertificates = new X509Certificate[certChain.length]; + + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (int i = 0; i < certChain.length; ++i) { + serverCertificates[i] = (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(certChain[i])); + } + } catch (CertificateException e) { + throw new IOException("can't read certificate", e); + } + + return verifyServerDomainAndCertificates(serverCertificates, domain, authType); + } + + /** + * Handles updates to credential storage. + */ + public static void handleTrustStorageUpdate() { + TrustManagerFactory tmf; + try { + tmf = TrustManagerFactory.getInstance("X.509"); + tmf.init((KeyStore) null); + } catch (NoSuchAlgorithmException e) { + Slog.w(TAG, "Couldn't find default X.509 TrustManagerFactory"); + return; + } catch (KeyStoreException e) { + Slog.w(TAG, "Couldn't initialize default X.509 TrustManagerFactory", e); + return; + } + + TrustManager[] tms = tmf.getTrustManagers(); + boolean sentUpdate = false; + for (TrustManager tm : tms) { + try { + Method updateMethod = tm.getClass().getDeclaredMethod("handleTrustStorageUpdate"); + updateMethod.setAccessible(true); + updateMethod.invoke(tm); + sentUpdate = true; + } catch (Exception e) { + } + } + if (!sentUpdate) { + Slog.w(TAG, "Didn't find a TrustManager to handle CA list update"); + } + } + + /** + * Common code of doHandshakeAndValidateServerCertificates and verifyServerCertificates. + * Calls DomainNamevalidator to verify the domain, and TrustManager to verify the certs. + * @param chain the cert chain in X509 cert format. + * @param domain The full website hostname and domain + * @param authType The authentication type for the cert chain + * @return An SSL error object if there is an error and null otherwise + */ + private static SslError verifyServerDomainAndCertificates( + X509Certificate[] chain, String domain, String authType) + throws IOException { + // check if the first certificate in the chain is for this site + X509Certificate currCertificate = chain[0]; + if (currCertificate == null) { + throw new IllegalArgumentException("certificate for this site is null"); + } + + boolean valid = domain != null + && !domain.isEmpty() + && NoPreloadHolder.sVerifier.verify(domain, + new DelegatingSSLSession.CertificateWrap(currCertificate)); + if (!valid) { + if (HttpLog.LOGV) { + HttpLog.v("certificate not for this host: " + domain); + } + return new SslError(SslError.SSL_IDMISMATCH, currCertificate); + } + + try { + X509TrustManager x509TrustManager = SSLParametersImpl.getDefaultX509TrustManager(); + if (x509TrustManager instanceof TrustManagerImpl) { + TrustManagerImpl trustManager = (TrustManagerImpl) x509TrustManager; + trustManager.checkServerTrusted(chain, authType, domain); + } else { + x509TrustManager.checkServerTrusted(chain, authType); + } + return null; // No errors. + } catch (GeneralSecurityException e) { + if (HttpLog.LOGV) { + HttpLog.v("failed to validate the certificate chain, error: " + + e.getMessage()); + } + return new SslError(SslError.SSL_UNTRUSTED, currCertificate); + } + } + + /** + * Returns the platform default {@link X509TrustManager}. + */ + private X509TrustManager getTrustManager() { + return mTrustManager; + } + + private void closeSocketThrowException( + SSLSocket socket, String errorMessage, String defaultErrorMessage) + throws IOException { + closeSocketThrowException( + socket, errorMessage != null ? errorMessage : defaultErrorMessage); + } + + private void closeSocketThrowException(SSLSocket socket, + String errorMessage) throws 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); + } +} |