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