blob: 4ca59031270ef2abd4ab54e1c04c88fa3036f7d4 [file] [log] [blame]
/*
* 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.SSLParametersImpl;
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 {
SSLParametersImpl.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);
}
}