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