| /* |
| * 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.internal.net.DomainNameValidator; |
| |
| import org.apache.harmony.xnet.provider.jsse.SSLParameters; |
| |
| 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.Date; |
| |
| 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} |
| */ |
| class CertificateChainValidator { |
| |
| /** |
| * The singleton instance of the certificate chain validator |
| */ |
| private static final CertificateChainValidator sInstance |
| = new CertificateChainValidator(); |
| |
| /** |
| * @return The singleton instance of the certificates chain validator |
| */ |
| public static CertificateChainValidator getInstance() { |
| return sInstance; |
| } |
| |
| /** |
| * Creates a new certificate chain validator. This is a private constructor. |
| * If you need a Certificate chain validator, call getInstance(). |
| */ |
| private CertificateChainValidator() {} |
| |
| /** |
| * 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 { |
| X509Certificate[] serverCertificates = null; |
| |
| // 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])); |
| } |
| } |
| } |
| |
| // 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 (!DomainNameValidator.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); |
| } |
| } |
| |
| // Clean up the certificates chain and build a new one. |
| // Theoretically, we shouldn't have to do this, but various web servers |
| // in practice are mis-configured to have out-of-order certificates or |
| // expired self-issued root certificate. |
| int chainLength = serverCertificates.length; |
| if (serverCertificates.length > 1) { |
| // 1. we clean the received certificates chain. |
| // We start from the end-entity certificate, tracing down by matching |
| // the "issuer" field and "subject" field until we can't continue. |
| // This helps when the certificates are out of order or |
| // some certificates are not related to the site. |
| int currIndex; |
| for (currIndex = 0; currIndex < serverCertificates.length; ++currIndex) { |
| boolean foundNext = false; |
| for (int nextIndex = currIndex + 1; |
| nextIndex < serverCertificates.length; |
| ++nextIndex) { |
| if (serverCertificates[currIndex].getIssuerDN().equals( |
| serverCertificates[nextIndex].getSubjectDN())) { |
| foundNext = true; |
| // Exchange certificates so that 0 through currIndex + 1 are in proper order |
| if (nextIndex != currIndex + 1) { |
| X509Certificate tempCertificate = serverCertificates[nextIndex]; |
| serverCertificates[nextIndex] = serverCertificates[currIndex + 1]; |
| serverCertificates[currIndex + 1] = tempCertificate; |
| } |
| break; |
| } |
| } |
| if (!foundNext) break; |
| } |
| |
| // 2. we exam if the last traced certificate is self issued and it is expired. |
| // If so, we drop it and pass the rest to checkServerTrusted(), hoping we might |
| // have a similar but unexpired trusted root. |
| chainLength = currIndex + 1; |
| X509Certificate lastCertificate = serverCertificates[chainLength - 1]; |
| Date now = new Date(); |
| if (lastCertificate.getSubjectDN().equals(lastCertificate.getIssuerDN()) |
| && now.after(lastCertificate.getNotAfter())) { |
| --chainLength; |
| } |
| } |
| |
| // 3. Now we copy the newly built chain into an appropriately sized array. |
| X509Certificate[] newServerCertificates = null; |
| newServerCertificates = new X509Certificate[chainLength]; |
| for (int i = 0; i < chainLength; ++i) { |
| newServerCertificates[i] = serverCertificates[i]; |
| } |
| |
| // first, we validate the new 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 { |
| SSLParameters.getDefaultTrustManager().checkServerTrusted( |
| newServerCertificates, "RSA"); |
| |
| // no errors!!! |
| return null; |
| } catch (CertificateException e) { |
| sslSocket.getSession().invalidate(); |
| |
| if (HttpLog.LOGV) { |
| HttpLog.v( |
| "failed to pre-validate the certificate chain, error: " + |
| e.getMessage()); |
| } |
| return new SslError( |
| SslError.SSL_UNTRUSTED, currCertificate); |
| } |
| } |
| |
| 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); |
| } |
| } |