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