diff options
author | Ecco Park <eccopark@google.com> | 2018-10-23 14:15:54 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-10-23 14:15:54 +0000 |
commit | 3358131b39a85aee27c3e1e0ffbc1dfc1468bd2c (patch) | |
tree | d9bd8a3e62903c3d562099040fa1c30e30147f13 /service | |
parent | 4a51c9f0e55243356c4d2e950ed0ce4661ccdf47 (diff) | |
parent | 246b0296bee15438b2e9acafbdb3117f97be88ee (diff) |
Merge "passpoint-r2: retrieve trust root certificates as final message exchange"
Diffstat (limited to 'service')
-rw-r--r-- | service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java | 202 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java | 284 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/hotspot2/ServiceProviderVerifier.java (renamed from service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java) | 55 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java | 4 |
4 files changed, 487 insertions, 58 deletions
diff --git a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java index 9952ce43b..08281cb17 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java @@ -32,19 +32,28 @@ import com.android.server.wifi.hotspot2.soap.HttpsTransport; import com.android.server.wifi.hotspot2.soap.SoapParser; import com.android.server.wifi.hotspot2.soap.SppResponseMessage; +import org.ksoap2.HeaderProperty; import org.ksoap2.serialization.AttributeInfo; import org.ksoap2.serialization.SoapObject; import org.ksoap2.serialization.SoapSerializationEnvelope; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URL; import java.security.KeyManagementException; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -76,6 +85,10 @@ public class OsuServerConnection { private boolean mVerboseLoggingEnabled = false; private Looper mLooper; + public static final int TRUST_CERT_TYPE_AAA = 1; + public static final int TRUST_CERT_TYPE_REMEDIATION = 2; + public static final int TRUST_CERT_TYPE_POLICY = 3; + @VisibleForTesting /* package */ OsuServerConnection(Looper looper) { mLooper = looper; @@ -91,9 +104,9 @@ public class OsuServerConnection { } /** - * Initialize socket factory for server connection using HTTPS + * Initializes socket factory for server connection using HTTPS * - * @param tlsContext SSLContext that will be used for HTTPS connection + * @param tlsContext SSLContext that will be used for HTTPS connection * @param trustManagerImpl TrustManagerImpl delegate to validate certs */ public void init(SSLContext tlsContext, TrustManagerImpl trustManagerImpl) { @@ -102,7 +115,7 @@ public class OsuServerConnection { } try { mTrustManager = new WFATrustManager(trustManagerImpl); - tlsContext.init(null, new TrustManager[] { mTrustManager }, null); + tlsContext.init(null, new TrustManager[]{mTrustManager}, null); mSocketFactory = tlsContext.getSocketFactory(); } catch (KeyManagementException e) { Log.w(TAG, "Initialization failed"); @@ -141,7 +154,7 @@ public class OsuServerConnection { /** * Connect to the OSU server * - * @param url Osu Server's URL + * @param url Osu Server's URL * @param network current network connection * @return boolean value, true if connection was successful * @@ -155,6 +168,8 @@ public class OsuServerConnection { try { urlConnection = (HttpsURLConnection) mNetwork.openConnection(mUrl); urlConnection.setSSLSocketFactory(mSocketFactory); + urlConnection.setConnectTimeout(HttpsServiceConnection.DEFAULT_TIMEOUT_MS); + urlConnection.setReadTimeout(HttpsServiceConnection.DEFAULT_TIMEOUT_MS); urlConnection.connect(); } catch (IOException e) { Log.e(TAG, "Unable to establish a URL connection"); @@ -183,7 +198,7 @@ public class OsuServerConnection { return false; } - for (Pair<Locale, String> identity : ASN1SubjectAltNamesParser.getProviderNames( + for (Pair<Locale, String> identity : ServiceProviderVerifier.getProviderNames( mTrustManager.getProviderCert())) { if (identity.first == null) continue; @@ -227,6 +242,36 @@ public class OsuServerConnection { return true; } + /** + * Retrieves Trust Root CA certificates for AAA, Remediation, Policy Server + * + * @param trustCertsInfo trust cert information for each type (AAA,Remediation and Policy). + * {@code Key} is the cert type. + * {@code Value} is the map that has a key for certUrl and a value for + * fingerprint of the certificate. + * @return {@code true} if {@link Network} is valid and {@code trustCertsInfo} is not null, + * {@code false} otherwise. + */ + public boolean retrieveTrustRootCerts( + @NonNull Map<Integer, Map<String, byte[]>> trustCertsInfo) { + if (mNetwork == null) { + Log.e(TAG, "Network is not established"); + return false; + } + + if (mUrlConnection == null) { + Log.e(TAG, "Server certificate is not validated"); + return false; + } + + if (trustCertsInfo == null || trustCertsInfo.isEmpty()) { + Log.e(TAG, "TrustCertsInfo is not valid"); + return false; + } + mHandler.post(() -> performRetrievingTrustRootCerts(trustCertsInfo)); + return true; + } + private void performSoapMessageExchange(@NonNull SoapSerializationEnvelope soapEnvelope) { if (mServiceConnection != null) { mServiceConnection.disconnect(); @@ -241,7 +286,7 @@ public class OsuServerConnection { return; } - SppResponseMessage sppResponse = null; + SppResponseMessage sppResponse; try { // Sending the SOAP message mHttpsTransport.call("", soapEnvelope); @@ -294,8 +339,149 @@ public class OsuServerConnection { } } + private void performRetrievingTrustRootCerts( + @NonNull Map<Integer, Map<String, byte[]>> trustCertsInfo) { + // Key: CERT_TYPE (AAA, REMEDIATION, POLICY), Value: a list of X509Certificate retrieved for + // the type. + Map<Integer, List<X509Certificate>> trustRootCertificates = new HashMap<>(); + + for (Map.Entry<Integer, Map<String, byte[]>> certInfoPerType : trustCertsInfo.entrySet()) { + List<X509Certificate> certificates = new ArrayList<>(); + + // Iterates certInfo to get a cert with a url provided in certInfo.key(). + // Key: Cert url, Value: SHA-256 hash bytes to match the fingerprint of a + // certificates retrieved from server. + for (Map.Entry<String, byte[]> certInfo : certInfoPerType.getValue().entrySet()) { + if (certInfo.getValue() == null) { + // clear all of retrieved CA certs so that PasspointProvisioner aborts + // current flow. + trustRootCertificates.clear(); + break; + } + X509Certificate certificate = getCert(certInfo.getKey()); + + if (certificate == null || !ServiceProviderVerifier.verifyCertFingerprint( + certificate, certInfo.getValue())) { + // If any failure happens, clear all of retrieved CA certs so that + // PasspointProvisioner aborts current flow. + trustRootCertificates.clear(); + break; + } + certificates.add(certificate); + } + if (!certificates.isEmpty()) { + trustRootCertificates.put(certInfoPerType.getKey(), certificates); + } + } + + if (mOsuServerCallbacks != null) { + // If it passes empty trustRootCertificates here, PasspointProvisioner will abort + // current flow because it indicates that client device doesn't get any trust root + // certificates from server. + mOsuServerCallbacks.onReceivedTrustRootCertificates(mOsuServerCallbacks.getSessionId(), + trustRootCertificates); + } + } + + /** + * Retrieves a X.509 Certificate from server. + * + * @param certUrl url to retrieve a X.509 Certificate + * @return {@link X509Certificate} in success, {@code null} otherwise. + */ + private X509Certificate getCert(@NonNull String certUrl) { + if (certUrl == null || !certUrl.toLowerCase(Locale.US).startsWith("https://")) { + Log.e(TAG, "invalid certUrl provided"); + return null; + } + + try { + URL serverUrl = new URL(certUrl); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + if (mServiceConnection != null) { + mServiceConnection.disconnect(); + } + mServiceConnection = getServiceConnection(serverUrl, mNetwork); + mServiceConnection.setRequestMethod("GET"); + mServiceConnection.setRequestProperty("Accept-Encoding", "gzip"); + + if (mServiceConnection.getResponseCode() != HttpURLConnection.HTTP_OK) { + Log.e(TAG, "The response code of the HTTPS GET to " + certUrl + + " is not OK, but " + mServiceConnection.getResponseCode()); + return null; + } + boolean bPkcs7 = false; + boolean bBase64 = false; + List<HeaderProperty> properties = mServiceConnection.getResponseProperties(); + for (HeaderProperty property : properties) { + if (property == null || property.getKey() == null || property.getValue() == null) { + continue; + } + if (property.getKey().equalsIgnoreCase("Content-Type")) { + if (property.getValue().equals("application/pkcs7-mime") + || property.getValue().equals("application/x-x509-ca-cert")) { + // application/x-x509-ca-cert : File content is a DER encoded X.509 + // certificate + if (mVerboseLoggingEnabled) { + Log.v(TAG, "a certificate found in a HTTPS response from " + certUrl); + } + + // ca cert + bPkcs7 = true; + } + } + if (property.getKey().equalsIgnoreCase("Content-Transfer-Encoding") + && property.getValue().equalsIgnoreCase("base64")) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, + "base64 encoding content in a HTTP response from " + certUrl); + } + bBase64 = true; + } + } + if (!bPkcs7) { + Log.e(TAG, "no X509Certificate found in the HTTPS response"); + return null; + } + InputStream in = mServiceConnection.openInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + while (true) { + int rd = in.read(buf, 0, 8192); + if (rd == -1) { + break; + } + bos.write(buf, 0, rd); + } + in.close(); + bos.flush(); + byte[] byteArray = bos.toByteArray(); + if (bBase64) { + String s = new String(byteArray); + byteArray = android.util.Base64.decode(s, android.util.Base64.DEFAULT); + } + + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(byteArray)); + if (mVerboseLoggingEnabled) { + Log.v(TAG, "cert : " + certificate.getSubjectDN()); + } + return certificate; + } catch (IOException e) { + Log.e(TAG, "Failed to get the data from " + certUrl + ": " + e); + } catch (CertificateException e) { + Log.e(TAG, "Failed to get instance for CertificateFactory " + e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to decode the data: " + e); + } finally { + mServiceConnection.disconnect(); + mServiceConnection = null; + } + return null; + } + /** - * Get the HTTPS service connection used for SOAP message exchange. + * Gets the HTTPS service connection used for SOAP message exchange. * * @return {@link HttpsServiceConnection} */ @@ -317,7 +503,7 @@ public class OsuServerConnection { } /** - * Clean up + * Cleans up */ public void cleanup() { if (mUrlConnection != null) { diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java index 25073506e..0dc3428cc 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java @@ -47,7 +47,11 @@ import com.android.server.wifi.hotspot2.soap.command.SppCommand; import java.net.MalformedURLException; import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; /** * Provides methods to carry out provisioning flow @@ -75,6 +79,7 @@ public class PasspointProvisioner { private int mCurrentSessionId = 0; private int mCallingUid; private boolean mVerboseLoggingEnabled = false; + private WifiManager mWifiManager; PasspointProvisioner(Context context, WifiNative wifiNative, PasspointObjectFactory objectFactory) { @@ -90,9 +95,11 @@ public class PasspointProvisioner { /** * Sets up for provisioning + * * @param looper Looper on which the Provisioning state machine will run */ public void init(Looper looper) { + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); mProvisioningStateMachine.start(new Handler(looper)); mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler()); // Offload the heavy load job to another thread @@ -106,6 +113,7 @@ public class PasspointProvisioner { /** * Enable verbose logging to help debug failures + * * @param level integer indicating verbose logging enabled if > 0 */ public void enableVerboseLogging(int level) { @@ -116,9 +124,10 @@ public class PasspointProvisioner { /** * Start provisioning flow with a given provider. + * * @param callingUid calling uid. - * @param provider {@link OsuProvider} to provision with. - * @param callback {@link IProvisioningCallback} to provide provisioning status. + * @param provider {@link OsuProvider} to provision with. + * @param callback {@link IProvisioningCallback} to provide provisioning status. * @return boolean value, true if provisioning was started, false otherwise. * * Implements HS2.0 provisioning flow with a given HS2.0 provider. @@ -154,6 +163,7 @@ public class PasspointProvisioner { static final int STATE_WAITING_FOR_REDIRECT_RESPONSE = 6; static final int STATE_WAITING_FOR_SECOND_SOAP_RESPONSE = 7; static final int STATE_WAITING_FOR_THIRD_SOAP_RESPONSE = 8; + static final int STATE_WAITING_FOR_TRUST_ROOT_CERTS = 9; private OsuProvider mOsuProvider; private IProvisioningCallback mProvisioningCallback; @@ -163,6 +173,7 @@ public class PasspointProvisioner { private Network mNetwork; private String mSessionId; private String mWebUrl; + private PasspointConfiguration mPasspointConfiguration; /** * Initializes and starts the state machine with a handler to handle incoming events @@ -195,12 +206,13 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "State Machine needs to be reset before starting provisioning"); } - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); } if (!mOsuServerConnection.canValidateServer()) { Log.w(TAG, "Provisioning is not possible"); mProvisioningCallback = callback; - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_NOT_AVAILABLE); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_PROVISIONING_NOT_AVAILABLE); return; } URL serverUrl; @@ -209,7 +221,7 @@ public class PasspointProvisioner { } catch (MalformedURLException e) { Log.e(TAG, "Invalid Server URL"); mProvisioningCallback = callback; - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_URL_INVALID); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_URL_INVALID); return; } mServerUrl = serverUrl; @@ -224,7 +236,7 @@ public class PasspointProvisioner { if (!mOsuNetworkConnection.connect(mOsuProvider.getOsuSsid(), mOsuProvider.getNetworkAccessIdentifier())) { - resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); return; } invokeProvisioningCallback(PROVISIONING_STATUS, @@ -245,7 +257,7 @@ public class PasspointProvisioner { Log.w(TAG, "Wifi Disable unhandled in state=" + mState); return; } - resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); } /** @@ -266,7 +278,7 @@ public class PasspointProvisioner { Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState); return; } - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_VALIDATION); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_VALIDATION); } /** @@ -300,7 +312,7 @@ public class PasspointProvisioner { public void handleRedirectResponse() { if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { Log.e(TAG, "Received redirect request in wrong state=" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } @@ -322,11 +334,12 @@ public class PasspointProvisioner { if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { Log.e(TAG, "Received timeout error for HTTP redirect response in wrong state=" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } mRedirectListener.stopServer(); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); } /** @@ -353,7 +366,7 @@ public class PasspointProvisioner { /** * Handles SOAP message response sent by server * - * @param sessionId indicating current session ID + * @param sessionId indicating current session ID * @param responseMessage SOAP SPP response, or {@code null} in any failure. * Note: Called on main thread (WifiService thread). */ @@ -367,7 +380,7 @@ public class PasspointProvisioner { if (responseMessage == null) { Log.e(TAG, "failed to send the sppPostDevData message"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } @@ -376,7 +389,7 @@ public class PasspointProvisioner { != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { Log.e(TAG, "Expected a PostDevDataResponse, but got " + responseMessage.getMessageType()); - resetStateMachine( + resetStateMachineForFailure( ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); return; } @@ -387,7 +400,8 @@ public class PasspointProvisioner { != SppCommand.ExecCommandId.BROWSER) { Log.e(TAG, "Expected a launchBrowser command, but got " + devDataResponse.getSppCommand().getExecCommandId()); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); return; } @@ -397,13 +411,15 @@ public class PasspointProvisioner { mWebUrl = ((BrowserUri) devDataResponse.getSppCommand().getCommandData()).getUri(); if (mWebUrl == null) { Log.e(TAG, "No Web-Url"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); return; } if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) { Log.e(TAG, "Bad or Missing session ID in webUrl"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); return; } launchOsuWebView(); @@ -412,7 +428,7 @@ public class PasspointProvisioner { != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { Log.e(TAG, "Expected a PostDevDataResponse, but got " + responseMessage.getMessageType()); - resetStateMachine( + resetStateMachineForFailure( ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); return; } @@ -424,21 +440,20 @@ public class PasspointProvisioner { Log.e(TAG, "Expected a ADD_MO command, but got " + ( (devDataResponse.getSppCommand() == null) ? "null" : devDataResponse.getSppCommand().getSppCommandId())); - resetStateMachine( + resetStateMachineForFailure( ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); return; } - PasspointConfiguration passpointConfig = buildPasspointConfiguration( - (PpsMoData) devDataResponse.getSppCommand().getCommandData()); - - thirdSoapExchange(passpointConfig == null); + mPasspointConfiguration = buildPasspointConfiguration( + (PpsMoData) devDataResponse.getSppCommand().getCommandData()); + thirdSoapExchange(mPasspointConfiguration == null); } else if (mState == STATE_WAITING_FOR_THIRD_SOAP_RESPONSE) { if (responseMessage.getMessageType() != SppResponseMessage.MessageType.EXCHANGE_COMPLETE) { Log.e(TAG, "Expected a ExchangeCompleteMessage, but got " + responseMessage.getMessageType()); - resetStateMachine( + resetStateMachineForFailure( ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); return; } @@ -449,7 +464,7 @@ public class PasspointProvisioner { != SppConstants.SppStatus.EXCHANGE_COMPLETE) { Log.e(TAG, "Expected a ExchangeCompleteMessage Status, but got " + exchangeCompleteMessage.getStatus()); - resetStateMachine( + resetStateMachineForFailure( ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_STATUS); return; } @@ -458,11 +473,16 @@ public class PasspointProvisioner { Log.e(TAG, "In the SppExchangeComplete, got error " + exchangeCompleteMessage.getError()); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } - - // TODO(b/74244324): Implement a routine to get CAs for AAA, Remediation, Policy. + if (mPasspointConfiguration == null) { + Log.e(TAG, "No PPS MO to use for retrieving TrustCerts"); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_NO_PPS_MO); + return; + } + retrieveTrustRootCerts(mPasspointConfiguration); } else { if (mVerboseLoggingEnabled) { Log.v(TAG, "Received an unexpected SOAP message in state=" + mState); @@ -471,6 +491,67 @@ public class PasspointProvisioner { } /** + * Installs the trust root CA certificates for AAA, Remediation and Policy Server + * + * @param sessionId indicating current session ID + * @param trustRootCertificates trust root CA certificates to be installed. + */ + public void installTrustRootCertificates(int sessionId, + @NonNull Map<Integer, List<X509Certificate>> trustRootCertificates) { + if (sessionId != mCurrentSessionId) { + Log.w(TAG, "Expected TrustRootCertificates callback for currentSessionId=" + + mCurrentSessionId); + return; + } + if (mState != STATE_WAITING_FOR_TRUST_ROOT_CERTS) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Received an unexpected TrustRootCertificates in state=" + mState); + } + return; + } + + if (trustRootCertificates.isEmpty()) { + Log.e(TAG, "fails to retrieve trust root certificates"); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_RETRIEVE_TRUST_ROOT_CERTIFICATES); + return; + } + + List<X509Certificate> certificates = trustRootCertificates.get( + OsuServerConnection.TRUST_CERT_TYPE_AAA); + if (certificates == null || certificates.isEmpty()) { + Log.e(TAG, "fails to retrieve trust root certificate for AAA server"); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_NO_AAA_TRUST_ROOT_CERTIFICATE); + return; + } + + // TODO(117717842) : Currently PasspointConfiguration is only allowed to save a single + // trust CA certificate for AAA server. So, add a routine in PasspointConfiguration + // to store multiple trust CA certificates for AAA server. + mPasspointConfiguration.getCredential().setCaCertificate( + certificates.get(0)); + + // TODO(b/116346527): Implement a routine to store trust CA certificates for + // remediation and policy server. + try { + mWifiManager.addOrUpdatePasspointConfiguration(mPasspointConfiguration); + } catch (IllegalArgumentException e) { + Log.e(TAG, "fails to add a new PasspointConfiguration: " + e); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_ADD_PASSPOINT_CONFIGURATION); + return; + } + + invokeProvisioningCompleteCallback(); + if (mVerboseLoggingEnabled) { + Log.i(TAG, "Provisioning is complete for " + + mPasspointConfiguration.getHomeSp().getFqdn()); + } + resetStateMachine(); + } + + /** * Disconnect event received * * Note: Called on main thread (WifiService thread). @@ -484,19 +565,24 @@ public class PasspointProvisioner { return; } mNetwork = null; - resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); } + /** + * Establishes TLS session to the server(OSU Server, Remediation or Policy Server). + * + * @param network current {@link Network} associated with the target AP. + */ private void initiateServerConnection(Network network) { if (mVerboseLoggingEnabled) { Log.v(TAG, "Initiating server connection in state=" + mState); } if (mState != STATE_OSU_AP_CONNECTED) { - Log.wtf(TAG , "Initiating server connection aborted in invalid state=" + mState); + Log.wtf(TAG, "Initiating server connection aborted in invalid state=" + mState); return; } if (!mOsuServerConnection.connect(mServerUrl, network)) { - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION); return; } mNetwork = network; @@ -523,6 +609,18 @@ public class PasspointProvisioner { } } + private void invokeProvisioningCompleteCallback() { + if (mProvisioningCallback == null) { + Log.e(TAG, "No provisioning complete callback registered"); + return; + } + try { + mProvisioningCallback.onProvisioningComplete(); + } catch (RemoteException e) { + Log.e(TAG, "Remote Exception while posting provisioning complete"); + } + } + /** * Validate the OSU Server certificate based on the procedure in 7.3.2.2 of Hotspot2.0 * rel2 spec. @@ -534,7 +632,8 @@ public class PasspointProvisioner { } if (!mOsuServerConnection.validateProvider( Locale.getDefault(), mOsuProvider.getFriendlyName())) { - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVICE_PROVIDER_VERIFICATION); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_SERVICE_PROVIDER_VERIFICATION); return; } invokeProvisioningCallback(PROVISIONING_STATUS, @@ -552,7 +651,7 @@ public class PasspointProvisioner { if (mState != STATE_OSU_SERVER_CONNECTED) { Log.e(TAG, "Initiates soap message exchange in wrong state=" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } @@ -570,11 +669,14 @@ public class PasspointProvisioner { changeState(STATE_WAITING_FOR_FIRST_SOAP_RESPONSE); } else { Log.e(TAG, "HttpsConnection is not established for soap message exchange"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } } + /** + * Launches OsuLogin Application for users to register a new subscription. + */ private void launchOsuWebView() { if (mVerboseLoggingEnabled) { Log.v(TAG, "launch Osu webview in state =" + mState); @@ -582,7 +684,7 @@ public class PasspointProvisioner { if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { Log.e(TAG, "launch Osu webview in wrong state =" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } @@ -608,7 +710,8 @@ public class PasspointProvisioner { } })) { Log.e(TAG, "fails to start redirect listener"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER); return; } @@ -628,7 +731,7 @@ public class PasspointProvisioner { changeState(STATE_WAITING_FOR_REDIRECT_RESPONSE); } else { Log.e(TAG, "can't resolve the activity for the intent"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); return; } } @@ -643,7 +746,7 @@ public class PasspointProvisioner { if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { Log.e(TAG, "Initiates the second soap message exchange in wrong state=" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } @@ -657,7 +760,7 @@ public class PasspointProvisioner { changeState(STATE_WAITING_FOR_SECOND_SOAP_RESPONSE); } else { Log.e(TAG, "HttpsConnection is not established for soap message exchange"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } } @@ -672,7 +775,7 @@ public class PasspointProvisioner { if (mState != STATE_WAITING_FOR_SECOND_SOAP_RESPONSE) { Log.e(TAG, "Initiates the third soap message exchange in wrong state=" + mState); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } @@ -684,11 +787,15 @@ public class PasspointProvisioner { changeState(STATE_WAITING_FOR_THIRD_SOAP_RESPONSE); } else { Log.e(TAG, "HttpsConnection is not established for soap message exchange"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } } + /** + * Builds {@link PasspointConfiguration} object from PPS(PerProviderSubscription) + * MO(Management Object). + */ private PasspointConfiguration buildPasspointConfiguration(@NonNull PpsMoData moData) { String moTree = moData.getPpsMoTree(); @@ -703,6 +810,72 @@ public class PasspointProvisioner { return passpointConfiguration; } + /** + * Retrieves Trust Root CA Certificates from server url defined in PPS + * (PerProviderSubscription) MO(Management Object). + */ + private void retrieveTrustRootCerts(@NonNull PasspointConfiguration passpointConfig) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Initiates retrieving trust root certs in state =" + mState); + } + + Map<String, byte[]> trustCertInfo = passpointConfig.getTrustRootCertList(); + if (trustCertInfo == null || trustCertInfo.isEmpty()) { + Log.e(TAG, "no AAATrustRoot Node found"); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_NO_AAA_SERVER_TRUST_ROOT_NODE); + return; + } + Map<Integer, Map<String, byte[]>> allTrustCerts = new HashMap<>(); + allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, trustCertInfo); + + // SubscriptionUpdate is a required node. + if (passpointConfig.getSubscriptionUpdate() != null + && passpointConfig.getSubscriptionUpdate().getTrustRootCertUrl() != null) { + trustCertInfo = new HashMap<>(); + trustCertInfo.put( + passpointConfig.getSubscriptionUpdate().getTrustRootCertUrl(), + passpointConfig.getSubscriptionUpdate() + .getTrustRootCertSha256Fingerprint()); + allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_REMEDIATION, trustCertInfo); + } else { + Log.e(TAG, "no TrustRoot Node for remediation server found"); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_NO_REMEDIATION_SERVER_TRUST_ROOT_NODE); + return; + } + + // Policy is an optional node + if (passpointConfig.getPolicy() != null) { + if (passpointConfig.getPolicy().getPolicyUpdate() != null + && passpointConfig.getPolicy().getPolicyUpdate().getTrustRootCertUrl() + != null) { + trustCertInfo = new HashMap<>(); + trustCertInfo.put( + passpointConfig.getPolicy().getPolicyUpdate() + .getTrustRootCertUrl(), + passpointConfig.getPolicy().getPolicyUpdate() + .getTrustRootCertSha256Fingerprint()); + allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_POLICY, trustCertInfo); + } else { + Log.e(TAG, "no TrustRoot Node for policy server found"); + resetStateMachineForFailure( + ProvisioningCallback.OSU_FAILURE_NO_POLICY_SERVER_TRUST_ROOT_NODE); + return; + } + } + + if (mOsuServerConnection.retrieveTrustRootCerts(allTrustCerts)) { + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS); + changeState(STATE_WAITING_FOR_TRUST_ROOT_CERTS); + } else { + Log.e(TAG, "HttpsConnection is not established for retrieving trust root certs"); + resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION); + return; + } + } + private void changeState(int nextState) { if (nextState != mState) { if (mVerboseLoggingEnabled) { @@ -712,13 +885,18 @@ public class PasspointProvisioner { } } - private void resetStateMachine(int failureCode) { + private void resetStateMachineForFailure(int failureCode) { invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode); + resetStateMachine(); + } + + private void resetStateMachine() { mRedirectListener.stopServer(); mOsuNetworkConnection.setEventCallback(null); mOsuNetworkConnection.disconnectIfNeeded(); mOsuServerConnection.setEventCallback(null); mOsuServerConnection.cleanup(); + mPasspointConfiguration = null; changeState(STATE_INIT); } } @@ -730,8 +908,6 @@ public class PasspointProvisioner { */ class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks { - OsuNetworkCallbacks() {} - @Override public void onConnected(Network network) { if (mVerboseLoggingEnabled) { @@ -821,7 +997,7 @@ public class PasspointProvisioner { /** * Callback when soap message is received from server. * - * @param sessionId indicating current session ID + * @param sessionId indicating current session ID * @param responseMessage SOAP SPP response parsed or {@code null} in any failure * Note: Called on different thread (OsuServer Thread)! */ @@ -834,6 +1010,22 @@ public class PasspointProvisioner { mProvisioningStateMachine.handleSoapMessageResponse(sessionId, responseMessage)); } + + /** + * Callback when trust root certificates are retrieved from server. + * + * @param sessionId indicating current session ID + * @param trustRootCertificates trust root CA certificates retrieved from server + * Note: Called on different thread (OsuServer Thread)! + */ + public void onReceivedTrustRootCertificates(int sessionId, + @NonNull Map<Integer, List<X509Certificate>> trustRootCertificates) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "onReceivedTrustRootCertificates with sessionId=" + sessionId); + } + mProvisioningStateMachine.getHandler().post(() -> + mProvisioningStateMachine.installTrustRootCertificates(sessionId, + trustRootCertificates)); + } } } - diff --git a/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java b/service/java/com/android/server/wifi/hotspot2/ServiceProviderVerifier.java index 33e865b78..e09ac178a 100644 --- a/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java +++ b/service/java/com/android/server/wifi/hotspot2/ServiceProviderVerifier.java @@ -16,7 +16,9 @@ package com.android.server.wifi.hotspot2; +import android.annotation.NonNull; import android.text.TextUtils; +import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; @@ -27,16 +29,21 @@ import com.android.org.bouncycastle.asn1.ASN1Sequence; import com.android.org.bouncycastle.asn1.DERTaggedObject; import com.android.org.bouncycastle.asn1.DERUTF8String; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; /** - * Utility to provide parsing of SubjectAltNames extensions from X509Certificate + * Utility class to validate a server X.509 Certificate of a service provider. */ -public class ASN1SubjectAltNamesParser { +public class ServiceProviderVerifier { + private static final String TAG = "ServiceProviderVerifier"; + private static final int OTHER_NAME = 0; private static final int ENTRY_COUNT = 2; private static final int LANGUAGE_CODE_LENGTH = 3; @@ -165,7 +172,48 @@ public class ASN1SubjectAltNamesParser { } /** - * Extract the language code and friendly Name from the alternativeName. + * Verifies a SHA-256 fingerprint of a X.509 Certificate. + * + * The SHA-256 fingerprint is calculated over the X.509 ASN.1 DER encoded certificate. + * @param x509Cert a server X.509 Certificate to verify + * @param certSHA256Fingerprint a SHA-256 hash value stored in PPS(PerProviderSubscription) + * MO(Management Object) + * SubscriptionUpdate/TrustRoot/CertSHA256Fingerprint for + * remediation server + * AAAServerTrustRoot/CertSHA256Fingerprint for AAA server + * PolicyUpdate/TrustRoot/CertSHA256Fingerprint for Policy Server + * + * @return {@code true} if the fingerprint of {@code x509Cert} is equal to {@code + * certSHA256Fingerprint}, {@code false} otherwise. + */ + public static boolean verifyCertFingerprint(@NonNull X509Certificate x509Cert, + @NonNull byte[] certSHA256Fingerprint) { + try { + byte[] fingerPrintSha256 = computeHash(x509Cert.getEncoded()); + if (fingerPrintSha256 == null) return false; + if (Arrays.equals(fingerPrintSha256, certSHA256Fingerprint)) { + return true; + } + } catch (Exception e) { + Log.e(TAG, "verifyCertFingerprint err:" + e); + } + return false; + } + + /** + * Computes a hash with SHA-256 algorithm for the input. + */ + private static byte[] computeHash(byte[] input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(input); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + /** + * Extracts the language code and friendly Name from the alternativeName. */ private static Pair<Locale, String> getFriendlyName(String alternativeName) { @@ -189,4 +237,3 @@ public class ASN1SubjectAltNamesParser { return Pair.create(locale, friendlyName); } } - diff --git a/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java b/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java index 8f22589e8..99f2f21a6 100644 --- a/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java @@ -39,10 +39,14 @@ import javax.net.ssl.SSLSocketFactory; * https connection for SOAP message. */ public class HttpsServiceConnection implements ServiceConnection { + // TODO(117906601): find an optimal value for a connection timeout + public static final int DEFAULT_TIMEOUT_MS = 5000; // 5 seconds private HttpsURLConnection mConnection; public HttpsServiceConnection(Network network, URL url) throws IOException { mConnection = (HttpsURLConnection) network.openConnection(url); + mConnection.setConnectTimeout(DEFAULT_TIMEOUT_MS); + mConnection.setReadTimeout(DEFAULT_TIMEOUT_MS); } @Override |