diff options
-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 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java | 152 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java | 118 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/hotspot2/ServiceProviderVerifierTest.java (renamed from tests/wifitests/src/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParserTest.java) | 78 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/hotspot2/soap/HttpsTransportTest.java | 6 |
8 files changed, 815 insertions, 84 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 diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java index f0a72efce..39ef15e5d 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java @@ -16,6 +16,7 @@ package com.android.server.wifi.hotspot2; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; @@ -41,24 +42,31 @@ import com.android.server.wifi.hotspot2.soap.SppResponseMessage; import org.junit.Before; import org.junit.Test; +import org.ksoap2.HeaderProperty; import org.ksoap2.SoapEnvelope; import org.ksoap2.serialization.SoapObject; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.Socket; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; +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; @@ -66,7 +74,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** - * Unit tests for {@link com.android.server.wifi.hotspot2.OsuServerConnection}. + * Unit tests for {@link OsuServerConnection}. */ @SmallTest public class OsuServerConnectionTest { @@ -84,6 +92,8 @@ public class OsuServerConnectionTest { private ArgumentCaptor<TrustManager[]> mTrustManagerCaptor = ArgumentCaptor.forClass(TrustManager[].class); + private Map<Integer, Map<String, byte[]>> mTrustCertsInfo = new HashMap<>(); + @Mock PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; @Mock Network mNetwork; @Mock HttpsURLConnection mUrlConnection; @@ -118,9 +128,9 @@ public class OsuServerConnectionTest { public void verifyInitAndConnect() throws Exception { // static mocking MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( - ASN1SubjectAltNamesParser.class).startMocking(); + ServiceProviderVerifier.class).startMocking(); try { - when(ASN1SubjectAltNamesParser.getProviderNames(any(X509Certificate.class))).thenReturn( + when(ServiceProviderVerifier.getProviderNames(any(X509Certificate.class))).thenReturn( mProviderIdentities); mOsuServerConnection.init(mTlsContext, mDelegate); @@ -224,9 +234,9 @@ public class OsuServerConnectionTest { public void verifyInitAndConnectInvalidProviderIdentity() throws Exception { // static mocking MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( - ASN1SubjectAltNamesParser.class).startMocking(); + ServiceProviderVerifier.class).startMocking(); try { - when(ASN1SubjectAltNamesParser.getProviderNames(any(X509Certificate.class))).thenReturn( + when(ServiceProviderVerifier.getProviderNames(any(X509Certificate.class))).thenReturn( mProviderIdentities); mOsuServerConnection.init(mTlsContext, mDelegate); @@ -263,10 +273,7 @@ public class OsuServerConnectionTest { */ @Test public void verifyExchangeSoapMessageWithInvalidArgument() { - mOsuServerConnection.init(mTlsContext, mDelegate); - mOsuServerConnection.setEventCallback(mOsuServerCallbacks); - - assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); + establishServerConnection(); assertFalse(mOsuServerConnection.exchangeSoapMessage(null)); } @@ -307,9 +314,7 @@ public class OsuServerConnectionTest { MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( HttpsTransport.class).mockStatic(SoapParser.class).startMocking(); try { - mOsuServerConnection.init(mTlsContext, mDelegate); - mOsuServerConnection.setEventCallback(mOsuServerCallbacks); - assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); + establishServerConnection(); SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER12); envelope.bodyIn = new SoapObject(); @@ -325,4 +330,127 @@ public class OsuServerConnectionTest { session.finishMocking(); } } + + /** + * Verifies {@code retrieveTrustRootCerts} should return {@code false} if there is no + * connection. + */ + @Test + public void verifyRetrieveTrustRootCertsWithoutConnection() { + assertFalse(mOsuServerConnection.retrieveTrustRootCerts(mTrustCertsInfo)); + } + + /** + * Verifies {@code retrieveTrustRootCerts} should return {@code false} if {@code + * mTrustCertsInfo} is empty. + */ + @Test + public void verifyRetrieveTrustRootCertsWithEmptyOfTrustCertsInfo() { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); + assertFalse(mOsuServerConnection.retrieveTrustRootCerts(mTrustCertsInfo)); + } + + /** + * Verifies it should return an empty collection of CA certificates if HTTPS response from + * server to get root CA certificate is not HTTP OK. + */ + @Test + public void verifyRetrieveTrustRootCertsWithErrorInHTTPSResponse() throws IOException { + // static mocking + MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( + HttpsTransport.class).startMocking(); + try { + when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( + mHttpsTransport); + when(mHttpsServiceConnection.getResponseCode()).thenReturn( + HttpURLConnection.HTTP_NO_CONTENT); + ArgumentCaptor<Map<Integer, List<X509Certificate>>> argumentCaptor = + ArgumentCaptor.forClass(Map.class); + + // Test Data + Map<String, byte[]> certInfo = new HashMap<>(); + certInfo.put("https://test.com/trustroot", "testData".getBytes()); + certInfo.put("https://test2.com/trustroot", "testData2".getBytes()); + mTrustCertsInfo.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, certInfo); + + establishServerConnection(); + + assertTrue(mOsuServerConnection.retrieveTrustRootCerts(mTrustCertsInfo)); + + mLooper.dispatchAll(); + + verify(mOsuServerCallbacks).onReceivedTrustRootCertificates(anyInt(), + argumentCaptor.capture()); + assertTrue(argumentCaptor.getValue().isEmpty()); + } finally { + session.finishMocking(); + } + } + + /** + * Verifies it should return a collection of CA certificates if there is no error while + * downloading root CA certificate from each {@code URL} provided + */ + @Test + public void verifyRetrieveTrustRootCertsWithoutError() throws IOException, + CertificateException { + // static mocking + MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( + HttpsTransport.class).mockStatic(CertificateFactory.class).mockStatic( + ServiceProviderVerifier.class).startMocking(); + try { + X509Certificate certificate = Mockito.mock(X509Certificate.class); + InputStream inputStream = Mockito.mock(InputStream.class); + + // To avoid infinite loop in OsuServerConnection.getCert. + when(inputStream.read(any(byte[].class), anyInt(), anyInt())).thenReturn(-1); + + CertificateFactory certificateFactory = Mockito.mock(CertificateFactory.class); + when(certificateFactory.generateCertificate(any(InputStream.class))).thenReturn( + certificate); + when(CertificateFactory.getInstance(anyString())).thenReturn(certificateFactory); + when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( + mHttpsTransport); + when(mHttpsServiceConnection.getResponseCode()).thenReturn( + HttpURLConnection.HTTP_OK); + when(mHttpsServiceConnection.openInputStream()).thenReturn(inputStream); + ArgumentCaptor<Map<Integer, List<X509Certificate>>> argumentCaptor = + ArgumentCaptor.forClass(Map.class); + when(ServiceProviderVerifier.verifyCertFingerprint(any(X509Certificate.class), + any(byte[].class))).thenReturn(true); + + // Test Data + Map<String, byte[]> certInfo = new HashMap<>(); + certInfo.put("https://test.com/trustroot", "testData".getBytes()); + mTrustCertsInfo.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, certInfo); + + List<HeaderProperty> properties = new ArrayList<>(); + + // Indicates that X.509 CA certificate is included. + properties.add(new HeaderProperty("Content-Type", "application/x-x509-ca-cert")); + when(mHttpsServiceConnection.getResponseProperties()).thenReturn(properties); + + establishServerConnection(); + + assertTrue(mOsuServerConnection.retrieveTrustRootCerts(mTrustCertsInfo)); + + mLooper.dispatchAll(); + + verify(mOsuServerCallbacks).onReceivedTrustRootCertificates(anyInt(), + argumentCaptor.capture()); + assertEquals(1, argumentCaptor.getValue().size()); + assertEquals(certificate, + argumentCaptor.getValue().get(OsuServerConnection.TRUST_CERT_TYPE_AAA).get(0)); + } finally { + session.finishMocking(); + } + } + + private void establishServerConnection() { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); + + assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); + } } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java index 2214d53d3..3607a0326 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java @@ -18,10 +18,14 @@ package com.android.server.wifi.hotspot2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -39,7 +43,12 @@ import android.net.wifi.WifiManager; import android.net.wifi.WifiSsid; import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.wifi.hotspot2.OsuProvider; +import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.ProvisioningCallback; +import android.net.wifi.hotspot2.omadm.PpsMoParser; +import android.net.wifi.hotspot2.pps.Credential; +import android.net.wifi.hotspot2.pps.HomeSp; +import android.net.wifi.hotspot2.pps.UpdateParameter; import android.os.Build; import android.os.Handler; import android.os.RemoteException; @@ -70,12 +79,17 @@ import org.mockito.MockitoSession; import java.net.URL; import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; import javax.net.ssl.SSLContext; /** - * Unit tests for {@link com.android.server.wifi.hotspot2.PasspointProvisioner}. + * Unit tests for {@link PasspointProvisioner}. */ @SmallTest public class PasspointProvisionerTest { @@ -86,6 +100,7 @@ public class PasspointProvisionerTest { private static final int STEP_WAIT_FOR_REDIRECT_RESPONSE = 3; private static final int STEP_WAIT_FOR_SECOND_SOAP_RESPONSE = 4; private static final int STEP_WAIT_FOR_THIRD_SOAP_RESPONSE = 5; + private static final int STEP_WAIT_FOR_TRUST_ROOT_CERTS = 6; private static final String TEST_DEV_ID = "12312341"; private static final String TEST_MANUFACTURER = Build.MANUFACTURER; @@ -141,13 +156,15 @@ public class PasspointProvisionerTest { @Mock PpsMoData mPpsMoData; @Mock RedirectListener mRedirectListener; @Mock PackageManager mPackageManager; + @Mock PasspointConfiguration mPasspointConfiguration; + @Mock X509Certificate mX509Certificate; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mTestUrl = new URL(TEST_REDIRECT_URL); mSession = ExtendedMockito.mockitoSession().mockStatic( - RedirectListener.class).startMocking(); + RedirectListener.class).mockStatic(PpsMoParser.class).startMocking(); when(RedirectListener.createInstance(mLooper.getLooper())).thenReturn( mRedirectListener); @@ -212,6 +229,21 @@ public class PasspointProvisionerTest { resolveInfo.activityInfo.applicationInfo.packageName = OSU_APP_PACKAGE; when(mPackageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + + Map<String, byte[]> trustCertInfo = new HashMap<>(); + trustCertInfo.put("https://testurl.com", "testData".getBytes()); + when(mPasspointConfiguration.getTrustRootCertList()).thenReturn(trustCertInfo); + when(mPasspointConfiguration.getCredential()).thenReturn(new Credential()); + HomeSp homeSp = new HomeSp(); + homeSp.setFqdn("test.com"); + when(mPasspointConfiguration.getHomeSp()).thenReturn(homeSp); + + UpdateParameter updateParameter = new UpdateParameter(); + updateParameter.setTrustRootCertUrl("https://testurl.com"); + updateParameter.setTrustRootCertSha256Fingerprint("testData".getBytes()); + when(mPasspointConfiguration.getSubscriptionUpdate()).thenReturn(updateParameter); + when(mOsuServerConnection.retrieveTrustRootCerts(anyMap())).thenReturn(true); + lenient().when(PpsMoParser.parseMoText(isNull())).thenReturn(mPasspointConfiguration); } @After @@ -309,6 +341,20 @@ public class PasspointProvisionerTest { mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), mExchangeCompleteMessage); mLooper.dispatchAll(); + } else if (step == STEP_WAIT_FOR_TRUST_ROOT_CERTS) { + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS); + + Map<Integer, List<X509Certificate>> trustRootCertificates = new HashMap<>(); + List<X509Certificate> certificates = new ArrayList<>(); + certificates.add(mX509Certificate); + trustRootCertificates.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, certificates); + + // Received trust root CA certificates + mOsuServerCallbacks.onReceivedTrustRootCertificates( + mOsuServerCallbacks.getSessionId(), trustRootCertificates); + mLooper.dispatchAll(); + verify(mCallback).onProvisioningComplete(); } } } @@ -646,14 +692,78 @@ public class PasspointProvisionerTest { } /** + * Verifies that the right provisioning callbacks are invoked when failing to call {@link + * OsuServerConnection#retrieveTrustRootCerts(Map)}. + */ + @Test + public void verifyHandlingErrorForCallingRetrieveTrustRootCerts() + throws RemoteException { + when(mOsuServerConnection.retrieveTrustRootCerts(anyMap())).thenReturn(false); + stopAfterStep(STEP_WAIT_FOR_THIRD_SOAP_RESPONSE); + + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION); + } + + /** + * Verifies that the right provisioning callbacks are invoked when a new {@link + * PasspointConfiguration} is failed to add. + */ + @Test + public void verifyHandlingErrorForAddingPasspointConfiguration() throws RemoteException { + doThrow(IllegalArgumentException.class).when( + mWifiManager).addOrUpdatePasspointConfiguration(any(PasspointConfiguration.class)); + stopAfterStep(STEP_WAIT_FOR_THIRD_SOAP_RESPONSE); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS); + + Map<Integer, List<X509Certificate>> trustRootCertificates = new HashMap<>(); + List<X509Certificate> certificates = new ArrayList<>(); + certificates.add(mX509Certificate); + trustRootCertificates.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, certificates); + + // Received trust root CA certificates + mOsuServerCallbacks.onReceivedTrustRootCertificates( + mOsuServerCallbacks.getSessionId(), trustRootCertificates); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_ADD_PASSPOINT_CONFIGURATION); + } + + /** + * Verifies that the right provisioning callbacks are invoked when it is failed to retrieve + * trust root certificates from the URLs provided. + */ + @Test + public void verifyHandlingEmptyTrustRootCertificateRetrieved() throws RemoteException { + doThrow(IllegalArgumentException.class).when( + mWifiManager).addOrUpdatePasspointConfiguration(any(PasspointConfiguration.class)); + stopAfterStep(STEP_WAIT_FOR_THIRD_SOAP_RESPONSE); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS); + + // Empty trust root certificates. + Map<Integer, List<X509Certificate>> trustRootCertificates = new HashMap<>(); + + // Received trust root CA certificates + mOsuServerCallbacks.onReceivedTrustRootCertificates( + mOsuServerCallbacks.getSessionId(), trustRootCertificates); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_RETRIEVE_TRUST_ROOT_CERTIFICATES); + } + + /** * Verifies that the right provisioning callbacks are invoked as the provisioner progresses * to the end as successful case. */ @Test public void verifyProvisioningFlowForSuccessfulCase() throws RemoteException { - stopAfterStep(STEP_WAIT_FOR_THIRD_SOAP_RESPONSE); + stopAfterStep(STEP_WAIT_FOR_TRUST_ROOT_CERTS); - // No further runnables posted + // No further runnable posted verifyNoMoreInteractions(mCallback); } } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParserTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/ServiceProviderVerifierTest.java index 59711fa67..ee0ac896f 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParserTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/ServiceProviderVerifierTest.java @@ -16,11 +16,12 @@ package com.android.server.wifi.hotspot2; -import static com.android.server.wifi.hotspot2.ASN1SubjectAltNamesParser +import static com.android.server.wifi.hotspot2.ServiceProviderVerifier .ID_WFA_OID_HOTSPOT_FRIENDLYNAME; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doThrow; @@ -50,10 +51,10 @@ import java.util.List; import java.util.Locale; /** - * Unit tests for {@link com.android.server.wifi.hotspot2.ASN1SubjectAltNamesParser}. + * Unit tests for {@link ServiceProviderVerifier}. */ @SmallTest -public class ASN1SubjectAltNamesParserTest { +public class ServiceProviderVerifierTest { private List<List<?>> mNewNames; private static final String LOCAL_HOST_NAME = "localhost"; private static final byte[] LOCAL_HOST_ADDRESS = {127, 0, 0, 1}; @@ -80,7 +81,7 @@ public class ASN1SubjectAltNamesParserTest { */ @Test public void testNullForProviderCertShouldReturnEmptyList() { - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(null).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(null).isEmpty()); } /** @@ -90,7 +91,7 @@ public class ASN1SubjectAltNamesParserTest { @Test public void testNullFromgetSubjectAlternativeNamesShouldReturnEmptyList() throws Exception { when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(null); - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(mX509Certificate).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty()); } /** @@ -101,7 +102,7 @@ public class ASN1SubjectAltNamesParserTest { public void testEmptyListFromGetSubjectAlternativeNamesShouldReturnEmptyList() throws Exception { when(mX509Certificate.getSubjectAlternativeNames()).thenReturn(Collections.emptySet()); - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(mX509Certificate).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty()); } /** @@ -114,7 +115,7 @@ public class ASN1SubjectAltNamesParserTest { doThrow(new CertificateParsingException()).when( mX509Certificate).getSubjectAlternativeNames(); - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(mX509Certificate).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty()); } /** @@ -131,7 +132,7 @@ public class ASN1SubjectAltNamesParserTest { when(mX509Certificate.getSubjectAlternativeNames()).thenReturn( Collections.unmodifiableCollection(mNewNames)); - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(mX509Certificate).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty()); } /** @@ -149,7 +150,7 @@ public class ASN1SubjectAltNamesParserTest { when(mX509Certificate.getSubjectAlternativeNames()).thenReturn( Collections.unmodifiableCollection(mNewNames)); - assertTrue(ASN1SubjectAltNamesParser.getProviderNames(mX509Certificate).isEmpty()); + assertTrue(ServiceProviderVerifier.getProviderNames(mX509Certificate).isEmpty()); } /** @@ -169,7 +170,7 @@ public class ASN1SubjectAltNamesParserTest { when(mX509Certificate.getSubjectAlternativeNames()).thenReturn( Collections.unmodifiableCollection(mNewNames)); - List<Pair<Locale, String>> result = ASN1SubjectAltNamesParser.getProviderNames( + List<Pair<Locale, String>> result = ServiceProviderVerifier.getProviderNames( mX509Certificate); assertThat(result.size(), is(1)); @@ -177,6 +178,36 @@ public class ASN1SubjectAltNamesParserTest { } /** + * Verify that verifyCertFingerPrint should return {@code true} when a fingerprint of {@link + * X509Certificate} is same with a value of hash provided. + */ + @Test + public void testVerifyFingerPrintOfCertificateWithSameFingerPrintValueReturnTrue() + throws Exception { + String testData = "testData"; + String testHash = "ba477a0ac57e10dd90bb5bf0289c5990fe839c619b26fde7c2aac62f526d4113"; + when(mX509Certificate.getEncoded()).thenReturn(testData.getBytes()); + + assertTrue(ServiceProviderVerifier.verifyCertFingerprint(mX509Certificate, + hexToBytes(testHash))); + } + + /** + * Verify that verifyCertFingerPrint should return {@code false} when a fingerprint of {@link + * X509Certificate} is different with a value of hash provided. + */ + @Test + public void testVerifyFingerPrintOfCertificateWithDifferentFingerPrintValueReturnFalse() + throws Exception { + String testData = "differentData"; + String testHash = "ba477a0ac57e10dd90bb5bf0289c5990fe839c619b26fde7c2aac62f526d4113"; + when(mX509Certificate.getEncoded()).thenReturn(testData.getBytes()); + + assertFalse(ServiceProviderVerifier.verifyCertFingerprint(mX509Certificate, + hexToBytes(testHash))); + } + + /** * Helper function to create an entry complying with the format returned * {@link X509Certificate#getSubjectAlternativeNames()} */ @@ -187,4 +218,31 @@ public class ASN1SubjectAltNamesParserTest { return nameEntry; } + + /** + * Converts a hex string to an array of bytes. The {@code hex} should have an even length. If + * not, the last character will be ignored. + */ + private byte[] hexToBytes(String hex) { + byte[] output = new byte[hex.length() / 2]; + for (int i = 0, j = 0; i + 1 < hex.length(); i += 2, j++) { + output[j] = (byte) (charToByte(hex.charAt(i)) << 4 | charToByte(hex.charAt(i + 1))); + } + return output; + } + + /** + * Converts a character of [0-9a-aA-F] to its hex value in a byte. If the character is not a + * hex number, 0 will be returned. + */ + private byte charToByte(char c) { + if (c >= 0x30 && c <= 0x39) { + return (byte) (c - 0x30); + } else if (c >= 0x41 && c <= 0x46) { + return (byte) (c - 0x37); + } else if (c >= 0x61 && c <= 0x66) { + return (byte) (c - 0x57); + } + return 0; + } } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/HttpsTransportTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/HttpsTransportTest.java index 7248c3823..0f189242f 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/HttpsTransportTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/HttpsTransportTest.java @@ -17,6 +17,8 @@ package com.android.server.wifi.hotspot2.soap; import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import android.net.Network; @@ -29,6 +31,8 @@ import org.mockito.Mock; import java.io.IOException; import java.net.URL; +import javax.net.ssl.HttpsURLConnection; + /** * Unit tests for {@link HttpsTransport}. */ @@ -39,6 +43,7 @@ public class HttpsTransportTest { private HttpsTransport mHttpsTransport; @Mock Network mNetwork; + @Mock HttpsURLConnection mHttpsURLConnection; /** * Sets up test. @@ -47,6 +52,7 @@ public class HttpsTransportTest { public void setUp() throws Exception { initMocks(this); mUrl = new URL(TEST_URL); + when(mNetwork.openConnection(any(URL.class))).thenReturn(mHttpsURLConnection); mHttpsTransport = HttpsTransport.createInstance(mNetwork, mUrl); } |