CertificateChainValidator.java revision 2269d1572e5fcfb725ea55f5764d8c3280d69f6d
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 19 20import com.android.internal.net.DomainNameValidator; 21 22import org.apache.harmony.xnet.provider.jsse.SSLParameters; 23 24import java.io.IOException; 25 26import java.security.cert.Certificate; 27import java.security.cert.CertificateException; 28import java.security.cert.CertificateExpiredException; 29import java.security.cert.CertificateNotYetValidException; 30import java.security.cert.X509Certificate; 31import java.security.GeneralSecurityException; 32import java.security.KeyStore; 33import java.util.Date; 34 35import javax.net.ssl.SSLHandshakeException; 36import javax.net.ssl.SSLSession; 37import javax.net.ssl.SSLSocket; 38import javax.net.ssl.TrustManager; 39import javax.net.ssl.TrustManagerFactory; 40import javax.net.ssl.X509TrustManager; 41 42/** 43 * Class responsible for all server certificate validation functionality 44 * 45 * {@hide} 46 */ 47class CertificateChainValidator { 48 49 /** 50 * The singleton instance of the certificate chain validator 51 */ 52 private static final CertificateChainValidator sInstance 53 = new CertificateChainValidator(); 54 55 /** 56 * @return The singleton instance of the certificates chain validator 57 */ 58 public static CertificateChainValidator getInstance() { 59 return sInstance; 60 } 61 62 /** 63 * Creates a new certificate chain validator. This is a private constructor. 64 * If you need a Certificate chain validator, call getInstance(). 65 */ 66 private CertificateChainValidator() {} 67 68 /** 69 * Performs the handshake and server certificates validation 70 * Notice a new chain will be rebuilt by tracing the issuer and subject 71 * before calling checkServerTrusted(). 72 * And if the last traced certificate is self issued and it is expired, it 73 * will be dropped. 74 * @param sslSocket The secure connection socket 75 * @param domain The website domain 76 * @return An SSL error object if there is an error and null otherwise 77 */ 78 public SslError doHandshakeAndValidateServerCertificates( 79 HttpsConnection connection, SSLSocket sslSocket, String domain) 80 throws IOException { 81 X509Certificate[] serverCertificates = null; 82 83 // start handshake, close the socket if we fail 84 try { 85 sslSocket.setUseClientMode(true); 86 sslSocket.startHandshake(); 87 } catch (IOException e) { 88 closeSocketThrowException( 89 sslSocket, e.getMessage(), 90 "failed to perform SSL handshake"); 91 } 92 93 // retrieve the chain of the server peer certificates 94 Certificate[] peerCertificates = 95 sslSocket.getSession().getPeerCertificates(); 96 97 if (peerCertificates == null || peerCertificates.length <= 0) { 98 closeSocketThrowException( 99 sslSocket, "failed to retrieve peer certificates"); 100 } else { 101 serverCertificates = 102 new X509Certificate[peerCertificates.length]; 103 for (int i = 0; i < peerCertificates.length; ++i) { 104 serverCertificates[i] = 105 (X509Certificate)(peerCertificates[i]); 106 } 107 108 // update the SSL certificate associated with the connection 109 if (connection != null) { 110 if (serverCertificates[0] != null) { 111 connection.setCertificate( 112 new SslCertificate(serverCertificates[0])); 113 } 114 } 115 } 116 117 // check if the first certificate in the chain is for this site 118 X509Certificate currCertificate = serverCertificates[0]; 119 if (currCertificate == null) { 120 closeSocketThrowException( 121 sslSocket, "certificate for this site is null"); 122 } else { 123 if (!DomainNameValidator.match(currCertificate, domain)) { 124 String errorMessage = "certificate not for this host: " + domain; 125 126 if (HttpLog.LOGV) { 127 HttpLog.v(errorMessage); 128 } 129 130 sslSocket.getSession().invalidate(); 131 return new SslError( 132 SslError.SSL_IDMISMATCH, currCertificate); 133 } 134 } 135 136 // Clean up the certificates chain and build a new one. 137 // Theoretically, we shouldn't have to do this, but various web servers 138 // in practice are mis-configured to have out-of-order certificates or 139 // expired self-issued root certificate. 140 int chainLength = serverCertificates.length; 141 if (serverCertificates.length > 1) { 142 // 1. we clean the received certificates chain. 143 // We start from the end-entity certificate, tracing down by matching 144 // the "issuer" field and "subject" field until we can't continue. 145 // This helps when the certificates are out of order or 146 // some certificates are not related to the site. 147 int currIndex; 148 for (currIndex = 0; currIndex < serverCertificates.length; ++currIndex) { 149 boolean foundNext = false; 150 for (int nextIndex = currIndex + 1; 151 nextIndex < serverCertificates.length; 152 ++nextIndex) { 153 if (serverCertificates[currIndex].getIssuerDN().equals( 154 serverCertificates[nextIndex].getSubjectDN())) { 155 foundNext = true; 156 // Exchange certificates so that 0 through currIndex + 1 are in proper order 157 if (nextIndex != currIndex + 1) { 158 X509Certificate tempCertificate = serverCertificates[nextIndex]; 159 serverCertificates[nextIndex] = serverCertificates[currIndex + 1]; 160 serverCertificates[currIndex + 1] = tempCertificate; 161 } 162 break; 163 } 164 } 165 if (!foundNext) break; 166 } 167 168 // 2. we exam if the last traced certificate is self issued and it is expired. 169 // If so, we drop it and pass the rest to checkServerTrusted(), hoping we might 170 // have a similar but unexpired trusted root. 171 chainLength = currIndex + 1; 172 X509Certificate lastCertificate = serverCertificates[chainLength - 1]; 173 Date now = new Date(); 174 if (lastCertificate.getSubjectDN().equals(lastCertificate.getIssuerDN()) 175 && now.after(lastCertificate.getNotAfter())) { 176 --chainLength; 177 } 178 } 179 180 // 3. Now we copy the newly built chain into an appropriately sized array. 181 X509Certificate[] newServerCertificates = null; 182 newServerCertificates = new X509Certificate[chainLength]; 183 for (int i = 0; i < chainLength; ++i) { 184 newServerCertificates[i] = serverCertificates[i]; 185 } 186 187 // first, we validate the new chain using the standard validation 188 // solution; if we do not find any errors, we are done; if we 189 // fail the standard validation, we re-validate again below, 190 // this time trying to retrieve any individual errors we can 191 // report back to the user. 192 // 193 try { 194 SSLParameters.getDefaultTrustManager().checkServerTrusted( 195 newServerCertificates, "RSA"); 196 197 // no errors!!! 198 return null; 199 } catch (CertificateException e) { 200 sslSocket.getSession().invalidate(); 201 202 if (HttpLog.LOGV) { 203 HttpLog.v( 204 "failed to pre-validate the certificate chain, error: " + 205 e.getMessage()); 206 } 207 return new SslError( 208 SslError.SSL_UNTRUSTED, currCertificate); 209 } 210 } 211 212 private void closeSocketThrowException( 213 SSLSocket socket, String errorMessage, String defaultErrorMessage) 214 throws IOException { 215 closeSocketThrowException( 216 socket, errorMessage != null ? errorMessage : defaultErrorMessage); 217 } 218 219 private void closeSocketThrowException(SSLSocket socket, 220 String errorMessage) throws IOException { 221 if (HttpLog.LOGV) { 222 HttpLog.v("validation error: " + errorMessage); 223 } 224 225 if (socket != null) { 226 SSLSession session = socket.getSession(); 227 if (session != null) { 228 session.invalidate(); 229 } 230 231 socket.close(); 232 } 233 234 throw new SSLHandshakeException(errorMessage); 235 } 236} 237