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