1 /* 2 * Copyright 2017 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 com.android.server.wifi.hotspot2; 18 19 import android.annotation.NonNull; 20 import android.net.Network; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.os.Looper; 24 import android.text.TextUtils; 25 import android.util.Log; 26 import android.util.Pair; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.org.conscrypt.TrustManagerImpl; 30 import com.android.server.wifi.hotspot2.soap.HttpsServiceConnection; 31 import com.android.server.wifi.hotspot2.soap.HttpsTransport; 32 import com.android.server.wifi.hotspot2.soap.SoapParser; 33 import com.android.server.wifi.hotspot2.soap.SppResponseMessage; 34 35 import org.ksoap2.HeaderProperty; 36 import org.ksoap2.serialization.AttributeInfo; 37 import org.ksoap2.serialization.SoapObject; 38 import org.ksoap2.serialization.SoapSerializationEnvelope; 39 40 import java.io.ByteArrayInputStream; 41 import java.io.ByteArrayOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.net.HttpURLConnection; 45 import java.net.URL; 46 import java.security.KeyManagementException; 47 import java.security.cert.CertificateException; 48 import java.security.cert.CertificateFactory; 49 import java.security.cert.CertificateParsingException; 50 import java.security.cert.X509Certificate; 51 import java.util.ArrayList; 52 import java.util.Collection; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 58 import javax.net.ssl.HttpsURLConnection; 59 import javax.net.ssl.SSLContext; 60 import javax.net.ssl.SSLHandshakeException; 61 import javax.net.ssl.SSLSocket; 62 import javax.net.ssl.SSLSocketFactory; 63 import javax.net.ssl.TrustManager; 64 import javax.net.ssl.X509TrustManager; 65 66 /** 67 * Provides methods to interface with the OSU server 68 */ 69 public class OsuServerConnection { 70 private static final String TAG = "PasspointOsuServerConnection"; 71 72 private static final int DNS_NAME = 2; 73 74 private SSLSocketFactory mSocketFactory; 75 private URL mUrl; 76 private Network mNetwork; 77 private WFATrustManager mTrustManager; 78 private HttpsTransport mHttpsTransport; 79 private HttpsServiceConnection mServiceConnection = null; 80 private HttpsURLConnection mUrlConnection = null; 81 private HandlerThread mOsuServerHandlerThread; 82 private Handler mHandler; 83 private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; 84 private boolean mSetupComplete = false; 85 private boolean mVerboseLoggingEnabled = false; 86 private Looper mLooper; 87 88 public static final int TRUST_CERT_TYPE_AAA = 1; 89 public static final int TRUST_CERT_TYPE_REMEDIATION = 2; 90 public static final int TRUST_CERT_TYPE_POLICY = 3; 91 92 @VisibleForTesting OsuServerConnection(Looper looper)93 /* package */ OsuServerConnection(Looper looper) { 94 mLooper = looper; 95 } 96 97 /** 98 * Sets up callback for event 99 * 100 * @param callbacks OsuServerCallbacks to be invoked for server related events 101 */ setEventCallback(PasspointProvisioner.OsuServerCallbacks callbacks)102 public void setEventCallback(PasspointProvisioner.OsuServerCallbacks callbacks) { 103 mOsuServerCallbacks = callbacks; 104 } 105 106 /** 107 * Initializes socket factory for server connection using HTTPS 108 * 109 * @param tlsContext SSLContext that will be used for HTTPS connection 110 * @param trustManagerImpl TrustManagerImpl delegate to validate certs 111 */ init(SSLContext tlsContext, TrustManagerImpl trustManagerImpl)112 public void init(SSLContext tlsContext, TrustManagerImpl trustManagerImpl) { 113 if (tlsContext == null) { 114 return; 115 } 116 try { 117 mTrustManager = new WFATrustManager(trustManagerImpl); 118 tlsContext.init(null, new TrustManager[]{mTrustManager}, null); 119 mSocketFactory = tlsContext.getSocketFactory(); 120 } catch (KeyManagementException e) { 121 Log.w(TAG, "Initialization failed"); 122 e.printStackTrace(); 123 return; 124 } 125 mSetupComplete = true; 126 127 // If mLooper is already set by unit test, don't overwrite it. 128 if (mLooper == null) { 129 mOsuServerHandlerThread = new HandlerThread("OsuServerHandler"); 130 mOsuServerHandlerThread.start(); 131 mLooper = mOsuServerHandlerThread.getLooper(); 132 } 133 mHandler = new Handler(mLooper); 134 } 135 136 /** 137 * Provides the capability to run OSU server validation 138 * 139 * @return boolean true if capability available 140 */ canValidateServer()141 public boolean canValidateServer() { 142 return mSetupComplete; 143 } 144 145 /** 146 * Enables verbose logging 147 * 148 * @param verbose a value greater than zero enables verbose logging 149 */ enableVerboseLogging(int verbose)150 public void enableVerboseLogging(int verbose) { 151 mVerboseLoggingEnabled = verbose > 0 ? true : false; 152 } 153 154 /** 155 * Connects to the OSU server 156 * 157 * @param url Osu Server's URL 158 * @param network current network connection 159 * @return {@code true} if {@code url} and {@code network} are not null 160 * 161 * Note: Relies on the caller to ensure that the capability to validate the OSU 162 * Server is available. 163 */ connect(@onNull URL url, @NonNull Network network)164 public boolean connect(@NonNull URL url, @NonNull Network network) { 165 if (url == null) { 166 Log.e(TAG, "url is null"); 167 return false; 168 } 169 if (network == null) { 170 Log.e(TAG, "network is null"); 171 return false; 172 } 173 174 mHandler.post(() -> performTlsConnection(url, network)); 175 return true; 176 } 177 178 /** 179 * Validates the service provider by comparing its identities found in OSU Server cert 180 * to the friendlyName obtained from ANQP exchange that is displayed to the user. 181 * 182 * @param locale a {@link Locale} object used for matching the friendly name in 183 * subjectAltName section of the certificate along with 184 * {@param friendlyName}. 185 * @param friendlyName a string of the friendly name used for finding the same name in 186 * subjectAltName section of the certificate. 187 * @return boolean true if friendlyName shows up as one of the identities in the cert 188 */ validateProvider(Locale locale, String friendlyName)189 public boolean validateProvider(Locale locale, 190 String friendlyName) { 191 192 if (locale == null || TextUtils.isEmpty(friendlyName)) { 193 return false; 194 } 195 196 for (Pair<Locale, String> identity : ServiceProviderVerifier.getProviderNames( 197 mTrustManager.getProviderCert())) { 198 if (identity.first == null) continue; 199 200 // Compare the language code for ISO-639. 201 if (identity.first.getISO3Language().equals(locale.getISO3Language()) && 202 TextUtils.equals(identity.second, friendlyName)) { 203 if (mVerboseLoggingEnabled) { 204 Log.v(TAG, "OSU certificate is valid for " 205 + identity.first.getISO3Language() + "/" + identity.second); 206 } 207 return true; 208 } 209 } 210 return false; 211 } 212 213 /** 214 * The helper method to exchange a SOAP message. 215 * 216 * @param soapEnvelope the soap message to be sent. 217 * @return {@code true} if {@link Network} is valid and {@code soapEnvelope} is not {@code 218 * null}, {@code false} otherwise. 219 */ exchangeSoapMessage(@onNull SoapSerializationEnvelope soapEnvelope)220 public boolean exchangeSoapMessage(@NonNull SoapSerializationEnvelope soapEnvelope) { 221 if (mNetwork == null) { 222 Log.e(TAG, "Network is not established"); 223 return false; 224 } 225 226 if (mUrlConnection == null) { 227 Log.e(TAG, "Server certificate is not validated"); 228 return false; 229 } 230 231 if (soapEnvelope == null) { 232 Log.e(TAG, "soapEnvelope is null"); 233 return false; 234 } 235 236 mHandler.post(() -> performSoapMessageExchange(soapEnvelope)); 237 return true; 238 } 239 240 /** 241 * Retrieves Trust Root CA certificates for AAA, Remediation, Policy Server 242 * 243 * @param trustCertsInfo trust cert information for each type (AAA,Remediation and Policy). 244 * {@code Key} is the cert type. 245 * {@code Value} is the map that has a key for certUrl and a value for 246 * fingerprint of the certificate. 247 * @return {@code true} if {@link Network} is valid and {@code trustCertsInfo} is not {@code 248 * null}, {@code false} otherwise. 249 */ retrieveTrustRootCerts( @onNull Map<Integer, Map<String, byte[]>> trustCertsInfo)250 public boolean retrieveTrustRootCerts( 251 @NonNull Map<Integer, Map<String, byte[]>> trustCertsInfo) { 252 if (mNetwork == null) { 253 Log.e(TAG, "Network is not established"); 254 return false; 255 } 256 257 if (mUrlConnection == null) { 258 Log.e(TAG, "Server certificate is not validated"); 259 return false; 260 } 261 262 if (trustCertsInfo == null || trustCertsInfo.isEmpty()) { 263 Log.e(TAG, "TrustCertsInfo is not valid"); 264 return false; 265 } 266 mHandler.post(() -> performRetrievingTrustRootCerts(trustCertsInfo)); 267 return true; 268 } 269 performTlsConnection(URL url, Network network)270 private void performTlsConnection(URL url, Network network) { 271 mNetwork = network; 272 mUrl = url; 273 274 HttpsURLConnection urlConnection; 275 try { 276 urlConnection = (HttpsURLConnection) mNetwork.openConnection(mUrl); 277 urlConnection.setSSLSocketFactory(mSocketFactory); 278 urlConnection.setConnectTimeout(HttpsServiceConnection.DEFAULT_TIMEOUT_MS); 279 urlConnection.setReadTimeout(HttpsServiceConnection.DEFAULT_TIMEOUT_MS); 280 urlConnection.connect(); 281 } catch (IOException e) { 282 Log.e(TAG, "Unable to establish a URL connection: " + e); 283 if (mOsuServerCallbacks != null) { 284 mOsuServerCallbacks.onServerConnectionStatus(mOsuServerCallbacks.getSessionId(), 285 false); 286 } 287 return; 288 } 289 mUrlConnection = urlConnection; 290 if (mOsuServerCallbacks != null) { 291 mOsuServerCallbacks.onServerConnectionStatus(mOsuServerCallbacks.getSessionId(), true); 292 } 293 } 294 performSoapMessageExchange(@onNull SoapSerializationEnvelope soapEnvelope)295 private void performSoapMessageExchange(@NonNull SoapSerializationEnvelope soapEnvelope) { 296 if (mServiceConnection != null) { 297 mServiceConnection.disconnect(); 298 } 299 300 mServiceConnection = getServiceConnection(mUrl, mNetwork); 301 if (mServiceConnection == null) { 302 Log.e(TAG, "ServiceConnection for https is null"); 303 if (mOsuServerCallbacks != null) { 304 mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); 305 } 306 return; 307 } 308 309 SppResponseMessage sppResponse; 310 try { 311 // Sending the SOAP message 312 mHttpsTransport.call("", soapEnvelope); 313 Object response = soapEnvelope.bodyIn; 314 if (response == null) { 315 Log.e(TAG, "SoapObject is null"); 316 if (mOsuServerCallbacks != null) { 317 mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), 318 null); 319 } 320 return; 321 } 322 if (!(response instanceof SoapObject)) { 323 Log.e(TAG, "Not a SoapObject instance"); 324 if (mOsuServerCallbacks != null) { 325 mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), 326 null); 327 } 328 return; 329 } 330 SoapObject soapResponse = (SoapObject) response; 331 if (mVerboseLoggingEnabled) { 332 for (int i = 0; i < soapResponse.getAttributeCount(); i++) { 333 AttributeInfo attributeInfo = new AttributeInfo(); 334 soapResponse.getAttributeInfo(i, attributeInfo); 335 Log.v(TAG, "Attribute : " + attributeInfo.toString()); 336 } 337 Log.v(TAG, "response : " + soapResponse.toString()); 338 } 339 340 // Get the parsed SOAP SPP Response message 341 sppResponse = SoapParser.getResponse(soapResponse); 342 } catch (Exception e) { 343 if (e instanceof SSLHandshakeException) { 344 Log.e(TAG, "Failed to make TLS connection: " + e); 345 } else { 346 Log.e(TAG, "Failed to exchange the SOAP message: " + e); 347 } 348 if (mOsuServerCallbacks != null) { 349 mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); 350 } 351 return; 352 } finally { 353 mServiceConnection.disconnect(); 354 mServiceConnection = null; 355 } 356 if (mOsuServerCallbacks != null) { 357 mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), 358 sppResponse); 359 } 360 } 361 performRetrievingTrustRootCerts( @onNull Map<Integer, Map<String, byte[]>> trustCertsInfo)362 private void performRetrievingTrustRootCerts( 363 @NonNull Map<Integer, Map<String, byte[]>> trustCertsInfo) { 364 // Key: CERT_TYPE (AAA, REMEDIATION, POLICY), Value: a list of X509Certificate retrieved for 365 // the type. 366 Map<Integer, List<X509Certificate>> trustRootCertificates = new HashMap<>(); 367 368 for (Map.Entry<Integer, Map<String, byte[]>> certInfoPerType : trustCertsInfo.entrySet()) { 369 List<X509Certificate> certificates = new ArrayList<>(); 370 371 // Iterates certInfo to get a cert with a url provided in certInfo.key(). 372 // Key: Cert url, Value: SHA-256 hash bytes to match the fingerprint of a 373 // certificates retrieved from server. 374 for (Map.Entry<String, byte[]> certInfo : certInfoPerType.getValue().entrySet()) { 375 if (certInfo.getValue() == null) { 376 // clear all of retrieved CA certs so that PasspointProvisioner aborts 377 // current flow. 378 trustRootCertificates.clear(); 379 break; 380 } 381 X509Certificate certificate = getCert(certInfo.getKey()); 382 383 if (certificate == null || !ServiceProviderVerifier.verifyCertFingerprint( 384 certificate, certInfo.getValue())) { 385 // If any failure happens, clear all of retrieved CA certs so that 386 // PasspointProvisioner aborts current flow. 387 trustRootCertificates.clear(); 388 break; 389 } 390 certificates.add(certificate); 391 } 392 if (!certificates.isEmpty()) { 393 trustRootCertificates.put(certInfoPerType.getKey(), certificates); 394 } 395 } 396 397 if (mOsuServerCallbacks != null) { 398 // If it passes empty trustRootCertificates here, PasspointProvisioner will abort 399 // current flow because it indicates that client device doesn't get any trust root 400 // certificates from server. 401 mOsuServerCallbacks.onReceivedTrustRootCertificates(mOsuServerCallbacks.getSessionId(), 402 trustRootCertificates); 403 } 404 } 405 406 /** 407 * Retrieves a X.509 Certificate from server. 408 * 409 * @param certUrl url to retrieve a X.509 Certificate 410 * @return {@link X509Certificate} in success, {@code null} otherwise. 411 */ getCert(@onNull String certUrl)412 private X509Certificate getCert(@NonNull String certUrl) { 413 if (certUrl == null || !certUrl.toLowerCase(Locale.US).startsWith("https://")) { 414 Log.e(TAG, "invalid certUrl provided"); 415 return null; 416 } 417 418 try { 419 URL serverUrl = new URL(certUrl); 420 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 421 if (mServiceConnection != null) { 422 mServiceConnection.disconnect(); 423 } 424 mServiceConnection = getServiceConnection(serverUrl, mNetwork); 425 if (mServiceConnection == null) { 426 return null; 427 } 428 mServiceConnection.setRequestMethod("GET"); 429 mServiceConnection.setRequestProperty("Accept-Encoding", "gzip"); 430 431 if (mServiceConnection.getResponseCode() != HttpURLConnection.HTTP_OK) { 432 Log.e(TAG, "The response code of the HTTPS GET to " + certUrl 433 + " is not OK, but " + mServiceConnection.getResponseCode()); 434 return null; 435 } 436 boolean bPkcs7 = false; 437 boolean bBase64 = false; 438 List<HeaderProperty> properties = mServiceConnection.getResponseProperties(); 439 for (HeaderProperty property : properties) { 440 if (property == null || property.getKey() == null || property.getValue() == null) { 441 continue; 442 } 443 if (property.getKey().equalsIgnoreCase("Content-Type")) { 444 if (property.getValue().equals("application/pkcs7-mime") 445 || property.getValue().equals("application/x-x509-ca-cert")) { 446 // application/x-x509-ca-cert : File content is a DER encoded X.509 447 // certificate 448 if (mVerboseLoggingEnabled) { 449 Log.v(TAG, "a certificate found in a HTTPS response from " + certUrl); 450 } 451 452 // ca cert 453 bPkcs7 = true; 454 } 455 } 456 if (property.getKey().equalsIgnoreCase("Content-Transfer-Encoding") 457 && property.getValue().equalsIgnoreCase("base64")) { 458 if (mVerboseLoggingEnabled) { 459 Log.v(TAG, 460 "base64 encoding content in a HTTP response from " + certUrl); 461 } 462 bBase64 = true; 463 } 464 } 465 if (!bPkcs7) { 466 Log.e(TAG, "no X509Certificate found in the HTTPS response"); 467 return null; 468 } 469 InputStream in = mServiceConnection.openInputStream(); 470 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 471 byte[] buf = new byte[8192]; 472 while (true) { 473 int rd = in.read(buf, 0, 8192); 474 if (rd == -1) { 475 break; 476 } 477 bos.write(buf, 0, rd); 478 } 479 in.close(); 480 bos.flush(); 481 byte[] byteArray = bos.toByteArray(); 482 if (bBase64) { 483 String s = new String(byteArray); 484 byteArray = android.util.Base64.decode(s, android.util.Base64.DEFAULT); 485 } 486 487 X509Certificate certificate = (X509Certificate) certFactory.generateCertificate( 488 new ByteArrayInputStream(byteArray)); 489 if (mVerboseLoggingEnabled) { 490 Log.v(TAG, "cert : " + certificate.getSubjectDN()); 491 } 492 return certificate; 493 } catch (IOException e) { 494 Log.e(TAG, "Failed to get the data from " + certUrl + ": " + e); 495 } catch (CertificateException e) { 496 Log.e(TAG, "Failed to get instance for CertificateFactory " + e); 497 } catch (IllegalArgumentException e) { 498 Log.e(TAG, "Failed to decode the data: " + e); 499 } finally { 500 mServiceConnection.disconnect(); 501 mServiceConnection = null; 502 } 503 return null; 504 } 505 506 /** 507 * Gets the HTTPS service connection used for SOAP message exchange. 508 * 509 * @return {@link HttpsServiceConnection} 510 */ getServiceConnection(@onNull URL url, @NonNull Network network)511 private HttpsServiceConnection getServiceConnection(@NonNull URL url, 512 @NonNull Network network) { 513 HttpsServiceConnection serviceConnection; 514 try { 515 // Creates new HTTPS connection. 516 mHttpsTransport = HttpsTransport.createInstance(network, url); 517 serviceConnection = (HttpsServiceConnection) mHttpsTransport.getServiceConnection(); 518 if (serviceConnection != null) { 519 serviceConnection.setSSLSocketFactory(mSocketFactory); 520 } 521 } catch (IOException e) { 522 Log.e(TAG, "Unable to establish a URL connection"); 523 return null; 524 } 525 return serviceConnection; 526 } 527 cleanupConnection()528 private void cleanupConnection() { 529 if (mUrlConnection != null) { 530 mUrlConnection.disconnect(); 531 mUrlConnection = null; 532 } 533 if (mServiceConnection != null) { 534 mServiceConnection.disconnect(); 535 mServiceConnection = null; 536 } 537 } 538 539 /** 540 * Cleans up 541 */ cleanup()542 public void cleanup() { 543 mHandler.post(() -> cleanupConnection()); 544 } 545 546 private class WFATrustManager implements X509TrustManager { 547 private TrustManagerImpl mDelegate; 548 private List<X509Certificate> mServerCerts; 549 WFATrustManager(TrustManagerImpl trustManagerImpl)550 WFATrustManager(TrustManagerImpl trustManagerImpl) { 551 mDelegate = trustManagerImpl; 552 } 553 554 @Override checkClientTrusted(X509Certificate[] chain, String authType)555 public void checkClientTrusted(X509Certificate[] chain, String authType) 556 throws CertificateException { 557 if (mVerboseLoggingEnabled) { 558 Log.v(TAG, "checkClientTrusted " + authType); 559 } 560 } 561 562 @Override checkServerTrusted(X509Certificate[] chain, String authType)563 public void checkServerTrusted(X509Certificate[] chain, String authType) 564 throws CertificateException { 565 if (mVerboseLoggingEnabled) { 566 Log.v(TAG, "checkServerTrusted " + authType); 567 } 568 boolean certsValid = false; 569 try { 570 // Perform certificate path validation and get validated certs 571 mServerCerts = mDelegate.getTrustedChainForServer(chain, authType, 572 (SSLSocket) null); 573 certsValid = true; 574 } catch (CertificateException e) { 575 Log.e(TAG, "Unable to validate certs " + e); 576 if (mVerboseLoggingEnabled) { 577 e.printStackTrace(); 578 } 579 } 580 if (mOsuServerCallbacks != null) { 581 mOsuServerCallbacks.onServerValidationStatus(mOsuServerCallbacks.getSessionId(), 582 certsValid); 583 } 584 } 585 586 @Override getAcceptedIssuers()587 public X509Certificate[] getAcceptedIssuers() { 588 if (mVerboseLoggingEnabled) { 589 Log.v(TAG, "getAcceptedIssuers "); 590 } 591 return null; 592 } 593 594 /** 595 * Returns the OSU certificate matching the FQDN of the OSU server 596 * 597 * @return {@link X509Certificate} OSU certificate matching FQDN of OSU server 598 */ getProviderCert()599 public X509Certificate getProviderCert() { 600 if (mServerCerts == null || mServerCerts.size() <= 0) { 601 return null; 602 } 603 X509Certificate providerCert = null; 604 String fqdn = mUrl.getHost(); 605 try { 606 for (X509Certificate certificate : mServerCerts) { 607 Collection<List<?>> col = certificate.getSubjectAlternativeNames(); 608 if (col == null) { 609 continue; 610 } 611 for (List<?> name : col) { 612 if (name == null) { 613 continue; 614 } 615 if (name.size() >= DNS_NAME 616 && name.get(0).getClass() == Integer.class 617 && name.get(1).toString().equals(fqdn)) { 618 providerCert = certificate; 619 if (mVerboseLoggingEnabled) { 620 Log.v(TAG, "OsuCert found"); 621 } 622 break; 623 } 624 } 625 } 626 } catch (CertificateParsingException e) { 627 Log.e(TAG, "Unable to match certificate to " + fqdn); 628 if (mVerboseLoggingEnabled) { 629 e.printStackTrace(); 630 } 631 } 632 return providerCert; 633 } 634 } 635 } 636 637