CertificateChainValidator.java revision 8f028a94fc533e75077485a7d11a04e4de820335
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 19import com.android.common.DomainNameValidator; 20 21import org.apache.harmony.xnet.provider.jsse.SSLParameters; 22 23import java.io.IOException; 24 25import java.security.cert.Certificate; 26import java.security.cert.CertificateException; 27import java.security.cert.CertificateExpiredException; 28import java.security.cert.CertificateNotYetValidException; 29import java.security.cert.X509Certificate; 30import java.security.GeneralSecurityException; 31import java.security.KeyStore; 32 33import javax.net.ssl.SSLHandshakeException; 34import javax.net.ssl.SSLSession; 35import javax.net.ssl.SSLSocket; 36import javax.net.ssl.TrustManager; 37import javax.net.ssl.TrustManagerFactory; 38import javax.net.ssl.X509TrustManager; 39 40/** 41 * Class responsible for all server certificate validation functionality 42 * 43 * {@hide} 44 */ 45class CertificateChainValidator { 46 47 /** 48 * The singleton instance of the certificate chain validator 49 */ 50 private static final CertificateChainValidator sInstance 51 = new CertificateChainValidator(); 52 53 /** 54 * @return The singleton instance of the certificator chain validator 55 */ 56 public static CertificateChainValidator getInstance() { 57 return sInstance; 58 } 59 60 /** 61 * Creates a new certificate chain validator. This is a pivate constructor. 62 * If you need a Certificate chain validator, call getInstance(). 63 */ 64 private CertificateChainValidator() {} 65 66 /** 67 * Performs the handshake and server certificates validation 68 * @param sslSocket The secure connection socket 69 * @param domain The website domain 70 * @return An SSL error object if there is an error and null otherwise 71 */ 72 public SslError doHandshakeAndValidateServerCertificates( 73 HttpsConnection connection, SSLSocket sslSocket, String domain) 74 throws IOException { 75 X509Certificate[] serverCertificates = null; 76 77 // start handshake, close the socket if we fail 78 try { 79 sslSocket.setUseClientMode(true); 80 sslSocket.startHandshake(); 81 } catch (IOException e) { 82 closeSocketThrowException( 83 sslSocket, e.getMessage(), 84 "failed to perform SSL handshake"); 85 } 86 87 // retrieve the chain of the server peer certificates 88 Certificate[] peerCertificates = 89 sslSocket.getSession().getPeerCertificates(); 90 91 if (peerCertificates == null || peerCertificates.length <= 0) { 92 closeSocketThrowException( 93 sslSocket, "failed to retrieve peer certificates"); 94 } else { 95 serverCertificates = 96 new X509Certificate[peerCertificates.length]; 97 for (int i = 0; i < peerCertificates.length; ++i) { 98 serverCertificates[i] = 99 (X509Certificate)(peerCertificates[i]); 100 } 101 102 // update the SSL certificate associated with the connection 103 if (connection != null) { 104 if (serverCertificates[0] != null) { 105 connection.setCertificate( 106 new SslCertificate(serverCertificates[0])); 107 } 108 } 109 } 110 111 // check if the first certificate in the chain is for this site 112 X509Certificate currCertificate = serverCertificates[0]; 113 if (currCertificate == null) { 114 closeSocketThrowException( 115 sslSocket, "certificate for this site is null"); 116 } else { 117 if (!DomainNameValidator.match(currCertificate, domain)) { 118 String errorMessage = "certificate not for this host: " + domain; 119 120 if (HttpLog.LOGV) { 121 HttpLog.v(errorMessage); 122 } 123 124 sslSocket.getSession().invalidate(); 125 return new SslError( 126 SslError.SSL_IDMISMATCH, currCertificate); 127 } 128 } 129 130 // first, we validate the chain using the standard validation 131 // solution; if we do not find any errors, we are done; if we 132 // fail the standard validation, we re-validate again below, 133 // this time trying to retrieve any individual errors we can 134 // report back to the user. 135 // 136 try { 137 SSLParameters.getDefaultTrustManager().checkServerTrusted( 138 serverCertificates, "RSA"); 139 140 // no errors!!! 141 return null; 142 } catch (CertificateException e) { 143 if (HttpLog.LOGV) { 144 HttpLog.v( 145 "failed to pre-validate the certificate chain, error: " + 146 e.getMessage()); 147 } 148 } 149 150 sslSocket.getSession().invalidate(); 151 152 SslError error = null; 153 154 // we check the root certificate separately from the rest of the 155 // chain; this is because we need to know what certificate in 156 // the chain resulted in an error if any 157 currCertificate = 158 serverCertificates[serverCertificates.length - 1]; 159 if (currCertificate == null) { 160 closeSocketThrowException( 161 sslSocket, "root certificate is null"); 162 } 163 164 // check if the last certificate in the chain (root) is trusted 165 X509Certificate[] rootCertificateChain = { currCertificate }; 166 try { 167 SSLParameters.getDefaultTrustManager().checkServerTrusted( 168 rootCertificateChain, "RSA"); 169 } catch (CertificateExpiredException e) { 170 String errorMessage = e.getMessage(); 171 if (errorMessage == null) { 172 errorMessage = "root certificate has expired"; 173 } 174 175 if (HttpLog.LOGV) { 176 HttpLog.v(errorMessage); 177 } 178 179 error = new SslError( 180 SslError.SSL_EXPIRED, currCertificate); 181 } catch (CertificateNotYetValidException e) { 182 String errorMessage = e.getMessage(); 183 if (errorMessage == null) { 184 errorMessage = "root certificate not valid yet"; 185 } 186 187 if (HttpLog.LOGV) { 188 HttpLog.v(errorMessage); 189 } 190 191 error = new SslError( 192 SslError.SSL_NOTYETVALID, currCertificate); 193 } catch (CertificateException e) { 194 String errorMessage = e.getMessage(); 195 if (errorMessage == null) { 196 errorMessage = "root certificate not trusted"; 197 } 198 199 if (HttpLog.LOGV) { 200 HttpLog.v(errorMessage); 201 } 202 203 return new SslError( 204 SslError.SSL_UNTRUSTED, currCertificate); 205 } 206 207 // Then go through the certificate chain checking that each 208 // certificate trusts the next and that each certificate is 209 // within its valid date range. Walk the chain in the order 210 // from the CA to the end-user 211 X509Certificate prevCertificate = 212 serverCertificates[serverCertificates.length - 1]; 213 214 for (int i = serverCertificates.length - 2; i >= 0; --i) { 215 currCertificate = serverCertificates[i]; 216 217 // if a certificate is null, we cannot verify the chain 218 if (currCertificate == null) { 219 closeSocketThrowException( 220 sslSocket, "null certificate in the chain"); 221 } 222 223 // verify if trusted by chain 224 if (!prevCertificate.getSubjectDN().equals( 225 currCertificate.getIssuerDN())) { 226 String errorMessage = "not trusted by chain"; 227 228 if (HttpLog.LOGV) { 229 HttpLog.v(errorMessage); 230 } 231 232 return new SslError( 233 SslError.SSL_UNTRUSTED, currCertificate); 234 } 235 236 try { 237 currCertificate.verify(prevCertificate.getPublicKey()); 238 } catch (GeneralSecurityException e) { 239 String errorMessage = e.getMessage(); 240 if (errorMessage == null) { 241 errorMessage = "not trusted by chain"; 242 } 243 244 if (HttpLog.LOGV) { 245 HttpLog.v(errorMessage); 246 } 247 248 return new SslError( 249 SslError.SSL_UNTRUSTED, currCertificate); 250 } 251 252 // verify if the dates are valid 253 try { 254 currCertificate.checkValidity(); 255 } catch (CertificateExpiredException e) { 256 String errorMessage = e.getMessage(); 257 if (errorMessage == null) { 258 errorMessage = "certificate expired"; 259 } 260 261 if (HttpLog.LOGV) { 262 HttpLog.v(errorMessage); 263 } 264 265 if (error == null || 266 error.getPrimaryError() < SslError.SSL_EXPIRED) { 267 error = new SslError( 268 SslError.SSL_EXPIRED, currCertificate); 269 } 270 } catch (CertificateNotYetValidException e) { 271 String errorMessage = e.getMessage(); 272 if (errorMessage == null) { 273 errorMessage = "certificate not valid yet"; 274 } 275 276 if (HttpLog.LOGV) { 277 HttpLog.v(errorMessage); 278 } 279 280 if (error == null || 281 error.getPrimaryError() < SslError.SSL_NOTYETVALID) { 282 error = new SslError( 283 SslError.SSL_NOTYETVALID, currCertificate); 284 } 285 } 286 287 prevCertificate = currCertificate; 288 } 289 290 // if we do not have an error to report back to the user, throw 291 // an exception (a generic error will be reported instead) 292 if (error == null) { 293 closeSocketThrowException( 294 sslSocket, 295 "failed to pre-validate the certificate chain due to a non-standard error"); 296 } 297 298 return error; 299 } 300 301 private void closeSocketThrowException( 302 SSLSocket socket, String errorMessage, String defaultErrorMessage) 303 throws IOException { 304 closeSocketThrowException( 305 socket, errorMessage != null ? errorMessage : defaultErrorMessage); 306 } 307 308 private void closeSocketThrowException(SSLSocket socket, 309 String errorMessage) throws IOException { 310 if (HttpLog.LOGV) { 311 HttpLog.v("validation error: " + errorMessage); 312 } 313 314 if (socket != null) { 315 SSLSession session = socket.getSession(); 316 if (session != null) { 317 session.invalidate(); 318 } 319 320 socket.close(); 321 } 322 323 throw new SSLHandshakeException(errorMessage); 324 } 325} 326