package com.android.hotspot2.osu; /* * policy-server.r2-testbed IN A 10.123.107.107 * remediation-server.r2-testbed IN A 10.123.107.107 * subscription-server.r2-testbed IN A 10.123.107.107 * www.r2-testbed IN A 10.123.107.107 * osu-server.r2-testbed-rks IN A 10.123.107.107 * policy-server.r2-testbed-rks IN A 10.123.107.107 * remediation-server.r2-testbed-rks IN A 10.123.107.107 * subscription-server.r2-testbed-rks IN A 10.123.107.107 */ import android.content.Context; import android.content.Intent; import android.net.Network; import android.util.Log; import com.android.hotspot2.OMADMAdapter; import com.android.hotspot2.est.ESTHandler; import com.android.hotspot2.flow.OSUInfo; import com.android.hotspot2.flow.PlatformAdapter; import com.android.hotspot2.omadm.OMAConstants; import com.android.hotspot2.omadm.OMANode; import com.android.hotspot2.osu.commands.BrowserURI; import com.android.hotspot2.osu.commands.ClientCertInfo; import com.android.hotspot2.osu.commands.GetCertData; import com.android.hotspot2.osu.commands.MOData; import com.android.hotspot2.osu.service.RedirectListener; import com.android.hotspot2.pps.Credential; import com.android.hotspot2.pps.HomeSP; import com.android.hotspot2.pps.UpdateInfo; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import javax.net.ssl.KeyManager; public class OSUClient { private static final String TAG = "OSUCLT"; private final OSUInfo mOSUInfo; private final URL mURL; private final KeyStore mKeyStore; private final Context mContext; private volatile HTTPHandler mHTTPHandler; private volatile RedirectListener mRedirectListener; public OSUClient(OSUInfo osuInfo, KeyStore ks, Context context) throws MalformedURLException { mOSUInfo = osuInfo; mURL = new URL(osuInfo.getOSUProvider().getOSUServer()); mKeyStore = ks; mContext = context; } public OSUClient(String osu, KeyStore ks, Context context) throws MalformedURLException { mOSUInfo = null; mURL = new URL(osu); mKeyStore = ks; mContext = context; } public OSUInfo getOSUInfo() { return mOSUInfo; } public void provision(PlatformAdapter platformAdapter, Network network, KeyManager km) throws IOException, GeneralSecurityException { try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, OSUSocketFactory.getSocketFactory(mKeyStore, null, OSUFlowManager.FlowType.Provisioning, network, mURL, km, true))) { mHTTPHandler = httpHandler; SPVerifier spVerifier = new SPVerifier(mOSUInfo); spVerifier.verify(httpHandler.getOSUCertificate(mURL)); URL redirectURL = prepareUserInput(platformAdapter, mOSUInfo.getName(Locale.getDefault())); OMADMAdapter omadmAdapter = getOMADMAdapter(); String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration, null, redirectURL.toString(), omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); Log.d(TAG, "Registration request: " + regRequest); OSUResponse osuResponse = httpHandler.exchangeSOAP(mURL, regRequest); Log.d(TAG, "Response: " + osuResponse); if (osuResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Expected a PostDevDataResponse"); } PostDevDataResponse regResponse = (PostDevDataResponse) osuResponse; String sessionID = regResponse.getSessionID(); if (regResponse.getExecCommand() == ExecCommand.UseClientCertTLS) { ClientCertInfo ccInfo = (ClientCertInfo) regResponse.getCommandData(); if (ccInfo.doesAcceptMfgCerts()) { throw new IOException("Mfg certs are not supported in Android"); } else if (ccInfo.doesAcceptProviderCerts()) { ((WiFiKeyManager) km).enableClientAuth(ccInfo.getIssuerNames()); httpHandler.renegotiate(null, null); } else { throw new IOException("Neither manufacturer nor provider cert specified"); } regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration, sessionID, redirectURL.toString(), omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); osuResponse = httpHandler.exchangeSOAP(mURL, regRequest); if (osuResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Expected a PostDevDataResponse"); } regResponse = (PostDevDataResponse) osuResponse; } if (regResponse.getExecCommand() != ExecCommand.Browser) { throw new IOException("Expected a launchBrowser command"); } Log.d(TAG, "Exec: " + regResponse.getExecCommand() + ", for '" + regResponse.getCommandData() + "'"); if (!osuResponse.getSessionID().equals(sessionID)) { throw new IOException("Mismatching session IDs"); } String webURL = ((BrowserURI) regResponse.getCommandData()).getURI(); if (webURL == null) { throw new IOException("No web-url"); } else if (!webURL.contains(sessionID)) { throw new IOException("Bad or missing session ID in webURL"); } if (!startUserInput(new URL(webURL), network)) { throw new IOException("User session failed"); } Log.d(TAG, " -- Sending user input complete:"); String userComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete, sessionID, null, omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); OSUResponse moResponse1 = httpHandler.exchangeSOAP(mURL, userComplete); if (moResponse1.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Bad user input complete response: " + moResponse1); } PostDevDataResponse provResponse = (PostDevDataResponse) moResponse1; GetCertData estData = checkResponse(provResponse); Map> certs = new HashMap<>(); PrivateKey clientKey = null; MOData moData; if (estData == null) { moData = (MOData) provResponse.getCommandData(); } else { try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse. getCommandData(), network, getOMADMAdapter(), km, mKeyStore, null, OSUFlowManager.FlowType.Provisioning)) { estHandler.execute(false); certs.put(OSUCertType.CA, estHandler.getCACerts()); certs.put(OSUCertType.Client, estHandler.getClientCerts()); clientKey = estHandler.getClientKey(); } Log.d(TAG, " -- Sending provisioning cert enrollment complete:"); String certComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.CertEnrollmentComplete, sessionID, null, omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); OSUResponse moResponse2 = httpHandler.exchangeSOAP(mURL, certComplete); if (moResponse2.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Bad cert enrollment complete response: " + moResponse2); } PostDevDataResponse provComplete = (PostDevDataResponse) moResponse2; if (provComplete.getStatus() != OSUStatus.ProvComplete || provComplete.getOSUCommand() != OSUCommandID.AddMO) { throw new IOException("Expected addMO: " + provComplete); } moData = (MOData) provComplete.getCommandData(); } // !!! How can an ExchangeComplete be sent w/o knowing the fate of the certs??? String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, null); Log.d(TAG, " -- Sending updateResponse:"); OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse); Log.d(TAG, "exComplete response: " + exComplete); if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) { throw new IOException("Expected ExchangeComplete: " + exComplete); } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) { throw new IOException("Bad ExchangeComplete status: " + exComplete); } retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore); platformAdapter.provisioningComplete(mOSUInfo, moData, certs, clientKey, network); } } public void remediate(PlatformAdapter platformAdapter, Network network, KeyManager km, HomeSP homeSP, OSUFlowManager.FlowType flowType) throws IOException, GeneralSecurityException { try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) { mHTTPHandler = httpHandler; URL redirectURL = prepareUserInput(platformAdapter, homeSP.getFriendlyName()); OMADMAdapter omadmAdapter = getOMADMAdapter(); String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation, null, redirectURL.toString(), omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); OSUResponse serverResponse = httpHandler.exchangeSOAP(mURL, regRequest); if (serverResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Expected a PostDevDataResponse"); } String sessionID = serverResponse.getSessionID(); PostDevDataResponse pddResponse = (PostDevDataResponse) serverResponse; Log.d(TAG, "Remediation response: " + pddResponse); Map> certs = null; PrivateKey clientKey = null; if (pddResponse.getStatus() != OSUStatus.RemediationComplete) { if (pddResponse.getExecCommand() == ExecCommand.UploadMO) { String ulMessage = SOAPBuilder.buildPostDevDataResponse(RequestReason.MOUpload, null, redirectURL.toString(), omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN), platformAdapter.getMOTree(homeSP)); Log.d(TAG, "Upload MO: " + ulMessage); OSUResponse ulResponse = httpHandler.exchangeSOAP(mURL, ulMessage); if (ulResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Expected a PostDevDataResponse to MOUpload"); } pddResponse = (PostDevDataResponse) ulResponse; } if (pddResponse.getExecCommand() == ExecCommand.Browser) { if (flowType == OSUFlowManager.FlowType.Policy) { throw new IOException("Browser launch requested in policy flow"); } String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI(); if (webURL == null) { throw new IOException("No web-url"); } else if (!webURL.contains(sessionID)) { throw new IOException("Bad or missing session ID in webURL"); } if (!startUserInput(new URL(webURL), network)) { throw new IOException("User session failed"); } Log.d(TAG, " -- Sending user input complete:"); String userComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete, sessionID, null, omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); OSUResponse udResponse = httpHandler.exchangeSOAP(mURL, userComplete); if (udResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Bad user input complete response: " + udResponse); } pddResponse = (PostDevDataResponse) udResponse; } else if (pddResponse.getExecCommand() == ExecCommand.GetCert) { certs = new HashMap<>(); try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse. getCommandData(), network, getOMADMAdapter(), km, mKeyStore, homeSP, flowType)) { estHandler.execute(true); certs.put(OSUCertType.CA, estHandler.getCACerts()); certs.put(OSUCertType.Client, estHandler.getClientCerts()); clientKey = estHandler.getClientKey(); } if (httpHandler.isHTTPAuthPerformed()) { // 8.4.3.6 httpHandler.renegotiate(certs, clientKey); } Log.d(TAG, " -- Sending remediation cert enrollment complete:"); // 8.4.3.5 in the spec actually prescribes that an update URI is sent here, // but there is no remediation flow that defines user interaction after EST // so for now a null is passed. String certComplete = SOAPBuilder .buildPostDevDataResponse(RequestReason.CertEnrollmentComplete, sessionID, null, omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN)); OSUResponse ceResponse = httpHandler.exchangeSOAP(mURL, certComplete); if (ceResponse.getMessageType() != OSUMessageType.PostDevData) { throw new IOException("Bad cert enrollment complete response: " + ceResponse); } pddResponse = (PostDevDataResponse) ceResponse; } else { throw new IOException("Unexpected command: " + pddResponse.getExecCommand()); } } if (pddResponse.getStatus() != OSUStatus.RemediationComplete) { throw new IOException("Expected a PostDevDataResponse to MOUpload"); } Log.d(TAG, "Remediation response: " + pddResponse); List mods = new ArrayList<>(); for (OSUCommand command : pddResponse.getCommands()) { if (command.getOSUCommand() == OSUCommandID.UpdateNode) { mods.add((MOData) command.getCommandData()); } else if (command.getOSUCommand() != OSUCommandID.NoMOUpdate) { throw new IOException("Unexpected OSU response: " + command); } } // 1. Machine remediation: Remediation complete + replace node // 2a. User remediation with upload: ExecCommand.UploadMO // 2b. User remediation without upload: ExecCommand.Browser // 3. User remediation only: -> sppPostDevData user input complete // // 4. Update node // 5. -> Update response // 6. Exchange complete OSUError error = null; String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, error); Log.d(TAG, " -- Sending updateResponse:"); OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse); Log.d(TAG, "exComplete response: " + exComplete); if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) { throw new IOException("Expected ExchangeComplete: " + exComplete); } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) { throw new IOException("Bad ExchangeComplete status: " + exComplete); } // There's a chicken and egg here: If the config is saved before sending update complete // the network is lost and the remediation flow fails. try { platformAdapter.remediationComplete(homeSP, mods, certs, clientKey, flowType == OSUFlowManager.FlowType.Policy); } catch (IOException | GeneralSecurityException e) { platformAdapter.provisioningFailed(homeSP.getFriendlyName(), e.getMessage()); error = OSUError.CommandFailed; } } } private OMADMAdapter getOMADMAdapter() { return OMADMAdapter.getInstance(mContext); } private URL prepareUserInput(PlatformAdapter platformAdapter, String spName) throws IOException { mRedirectListener = new RedirectListener(platformAdapter, spName); return mRedirectListener.getURL(); } private boolean startUserInput(URL target, Network network) throws IOException { mRedirectListener.startService(); Intent intent = new Intent(mContext, OSUWebView.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(OSUWebView.OSU_NETWORK, network); intent.putExtra(OSUWebView.OSU_URL, target.toString()); mContext.startActivity(intent); return mRedirectListener.waitForUser(); } public void close(boolean abort) { if (mRedirectListener != null) { mRedirectListener.abort(); mRedirectListener = null; } if (abort) { try { mHTTPHandler.close(); } catch (IOException ioe) { /**/ } } } private HTTPHandler createHandler(Network network, HomeSP homeSP, KeyManager km, OSUFlowManager.FlowType flowType) throws GeneralSecurityException, IOException { Credential credential = homeSP.getCredential(); Log.d(TAG, "Credential method " + credential.getEAPMethod().getEAPMethodID()); switch (credential.getEAPMethod().getEAPMethodID()) { case EAP_TTLS: String user; byte[] password; UpdateInfo subscriptionUpdate; if (flowType == OSUFlowManager.FlowType.Policy) { subscriptionUpdate = homeSP.getPolicy() != null ? homeSP.getPolicy().getPolicyUpdate() : null; } else { subscriptionUpdate = homeSP.getSubscriptionUpdate(); } if (subscriptionUpdate != null && subscriptionUpdate.getUsername() != null) { user = subscriptionUpdate.getUsername(); password = subscriptionUpdate.getPassword() != null ? subscriptionUpdate.getPassword().getBytes(StandardCharsets.UTF_8) : new byte[0]; } else { user = credential.getUserName(); password = credential.getPassword().getBytes(StandardCharsets.UTF_8); } return new HTTPHandler(StandardCharsets.UTF_8, OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network, mURL, km, true), user, password); case EAP_TLS: return new HTTPHandler(StandardCharsets.UTF_8, OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network, mURL, km, true)); default: throw new IOException("Cannot remediate account with " + credential.getEAPMethod().getEAPMethodID()); } } private static GetCertData checkResponse(PostDevDataResponse response) throws IOException { if (response.getStatus() == OSUStatus.ProvComplete && response.getOSUCommand() == OSUCommandID.AddMO) { return null; } if (response.getOSUCommand() == OSUCommandID.Exec && response.getExecCommand() == ExecCommand.GetCert) { return (GetCertData) response.getCommandData(); } else { throw new IOException("Unexpected command: " + response); } } private static final String[] AAACertPath = {"PerProviderSubscription", "?", "AAAServerTrustRoot", "*", "CertURL"}; private static final String[] RemdCertPath = {"PerProviderSubscription", "?", "SubscriptionUpdate", "TrustRoot", "CertURL"}; private static final String[] PolicyCertPath = {"PerProviderSubscription", "?", "Policy", "PolicyUpdate", "TrustRoot", "CertURL"}; private static void retrieveCerts(OMANode ppsRoot, Map> certs, Network network, KeyManager km, KeyStore ks) throws GeneralSecurityException, IOException { List aaaCerts = getCerts(ppsRoot, AAACertPath, network, km, ks); certs.put(OSUCertType.AAA, aaaCerts); certs.put(OSUCertType.Remediation, getCerts(ppsRoot, RemdCertPath, network, km, ks)); certs.put(OSUCertType.Policy, getCerts(ppsRoot, PolicyCertPath, network, km, ks)); } private static List getCerts(OMANode ppsRoot, String[] path, Network network, KeyManager km, KeyStore ks) throws GeneralSecurityException, IOException { List urls = new ArrayList<>(); getCertURLs(ppsRoot, Arrays.asList(path).iterator(), urls); Log.d(TAG, Arrays.toString(path) + ": " + urls); List certs = new ArrayList<>(urls.size()); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); for (String urlString : urls) { URL url = new URL(urlString); HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, OSUSocketFactory.getSocketFactory(ks, null, OSUFlowManager.FlowType.Provisioning, network, url, km, false)); certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url))); } return certs; } private static void getCertURLs(OMANode root, Iterator path, List urls) throws IOException { String name = path.next(); // Log.d(TAG, "Pulling '" + name + "' out of '" + root.getName() + "'"); Collection nodes = null; switch (name) { case "?": for (OMANode node : root.getChildren()) { if (!node.isLeaf()) { nodes = Collections.singletonList(node); break; } } break; case "*": nodes = root.getChildren(); break; default: nodes = Collections.singletonList(root.getChild(name)); break; } if (nodes == null) { throw new IllegalArgumentException("No matching node in " + root.getName() + " for " + name); } for (OMANode node : nodes) { if (path.hasNext()) { getCertURLs(node, path, urls); } else { urls.add(node.getValue()); } } } }