summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorEcco Park <eccopark@google.com>2018-10-23 14:15:54 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2018-10-23 14:15:54 +0000
commit3358131b39a85aee27c3e1e0ffbc1dfc1468bd2c (patch)
treed9bd8a3e62903c3d562099040fa1c30e30147f13 /service
parent4a51c9f0e55243356c4d2e950ed0ce4661ccdf47 (diff)
parent246b0296bee15438b2e9acafbdb3117f97be88ee (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.java202
-rw-r--r--service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java284
-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.java4
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