1 package com.android.hotspot2.osu; 2 3 /* 4 * policy-server.r2-testbed IN A 10.123.107.107 5 * remediation-server.r2-testbed IN A 10.123.107.107 6 * subscription-server.r2-testbed IN A 10.123.107.107 7 * www.r2-testbed IN A 10.123.107.107 8 * osu-server.r2-testbed-rks IN A 10.123.107.107 9 * policy-server.r2-testbed-rks IN A 10.123.107.107 10 * remediation-server.r2-testbed-rks IN A 10.123.107.107 11 * subscription-server.r2-testbed-rks IN A 10.123.107.107 12 */ 13 14 import android.content.Context; 15 import android.content.Intent; 16 import android.net.Network; 17 import android.util.Log; 18 19 import com.android.hotspot2.OMADMAdapter; 20 import com.android.hotspot2.est.ESTHandler; 21 import com.android.hotspot2.flow.OSUInfo; 22 import com.android.hotspot2.flow.PlatformAdapter; 23 import com.android.hotspot2.omadm.OMAConstants; 24 import com.android.hotspot2.omadm.OMANode; 25 import com.android.hotspot2.osu.commands.BrowserURI; 26 import com.android.hotspot2.osu.commands.ClientCertInfo; 27 import com.android.hotspot2.osu.commands.GetCertData; 28 import com.android.hotspot2.osu.commands.MOData; 29 import com.android.hotspot2.osu.service.RedirectListener; 30 import com.android.hotspot2.pps.Credential; 31 import com.android.hotspot2.pps.HomeSP; 32 import com.android.hotspot2.pps.UpdateInfo; 33 34 import java.io.IOException; 35 import java.net.MalformedURLException; 36 import java.net.URL; 37 import java.nio.charset.StandardCharsets; 38 import java.security.GeneralSecurityException; 39 import java.security.KeyStore; 40 import java.security.PrivateKey; 41 import java.security.cert.CertificateFactory; 42 import java.security.cert.X509Certificate; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.Iterator; 49 import java.util.List; 50 import java.util.Locale; 51 import java.util.Map; 52 53 import javax.net.ssl.KeyManager; 54 55 public class OSUClient { 56 private static final String TAG = "OSUCLT"; 57 58 private final OSUInfo mOSUInfo; 59 private final URL mURL; 60 private final KeyStore mKeyStore; 61 private final Context mContext; 62 private volatile HTTPHandler mHTTPHandler; 63 private volatile RedirectListener mRedirectListener; 64 65 public OSUClient(OSUInfo osuInfo, KeyStore ks, Context context) throws MalformedURLException { 66 mOSUInfo = osuInfo; 67 mURL = new URL(osuInfo.getOSUProvider().getOSUServer()); 68 mKeyStore = ks; 69 mContext = context; 70 } 71 72 public OSUClient(String osu, KeyStore ks, Context context) throws MalformedURLException { 73 mOSUInfo = null; 74 mURL = new URL(osu); 75 mKeyStore = ks; 76 mContext = context; 77 } 78 79 public OSUInfo getOSUInfo() { 80 return mOSUInfo; 81 } 82 83 public void provision(PlatformAdapter platformAdapter, Network network, KeyManager km) 84 throws IOException, GeneralSecurityException { 85 try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, 86 OSUSocketFactory.getSocketFactory(mKeyStore, null, 87 OSUFlowManager.FlowType.Provisioning, network, mURL, km, true))) { 88 89 mHTTPHandler = httpHandler; 90 91 SPVerifier spVerifier = new SPVerifier(mOSUInfo); 92 spVerifier.verify(httpHandler.getOSUCertificate(mURL)); 93 94 URL redirectURL = prepareUserInput(platformAdapter, 95 mOSUInfo.getName(Locale.getDefault())); 96 OMADMAdapter omadmAdapter = getOMADMAdapter(); 97 98 String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration, 99 null, 100 redirectURL.toString(), 101 omadmAdapter.getMO(OMAConstants.DevInfoURN), 102 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 103 Log.d(TAG, "Registration request: " + regRequest); 104 OSUResponse osuResponse = httpHandler.exchangeSOAP(mURL, regRequest); 105 106 Log.d(TAG, "Response: " + osuResponse); 107 if (osuResponse.getMessageType() != OSUMessageType.PostDevData) { 108 throw new IOException("Expected a PostDevDataResponse"); 109 } 110 PostDevDataResponse regResponse = (PostDevDataResponse) osuResponse; 111 String sessionID = regResponse.getSessionID(); 112 if (regResponse.getExecCommand() == ExecCommand.UseClientCertTLS) { 113 ClientCertInfo ccInfo = (ClientCertInfo) regResponse.getCommandData(); 114 if (ccInfo.doesAcceptMfgCerts()) { 115 throw new IOException("Mfg certs are not supported in Android"); 116 } else if (ccInfo.doesAcceptProviderCerts()) { 117 ((WiFiKeyManager) km).enableClientAuth(ccInfo.getIssuerNames()); 118 httpHandler.renegotiate(null, null); 119 } else { 120 throw new IOException("Neither manufacturer nor provider cert specified"); 121 } 122 regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration, 123 sessionID, 124 redirectURL.toString(), 125 omadmAdapter.getMO(OMAConstants.DevInfoURN), 126 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 127 128 osuResponse = httpHandler.exchangeSOAP(mURL, regRequest); 129 if (osuResponse.getMessageType() != OSUMessageType.PostDevData) { 130 throw new IOException("Expected a PostDevDataResponse"); 131 } 132 regResponse = (PostDevDataResponse) osuResponse; 133 } 134 135 if (regResponse.getExecCommand() != ExecCommand.Browser) { 136 throw new IOException("Expected a launchBrowser command"); 137 } 138 Log.d(TAG, "Exec: " + regResponse.getExecCommand() + ", for '" + 139 regResponse.getCommandData() + "'"); 140 141 if (!osuResponse.getSessionID().equals(sessionID)) { 142 throw new IOException("Mismatching session IDs"); 143 } 144 String webURL = ((BrowserURI) regResponse.getCommandData()).getURI(); 145 146 if (webURL == null) { 147 throw new IOException("No web-url"); 148 } else if (!webURL.contains(sessionID)) { 149 throw new IOException("Bad or missing session ID in webURL"); 150 } 151 152 if (!startUserInput(new URL(webURL), network)) { 153 throw new IOException("User session failed"); 154 } 155 156 Log.d(TAG, " -- Sending user input complete:"); 157 String userComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete, 158 sessionID, null, 159 omadmAdapter.getMO(OMAConstants.DevInfoURN), 160 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 161 OSUResponse moResponse1 = httpHandler.exchangeSOAP(mURL, userComplete); 162 if (moResponse1.getMessageType() != OSUMessageType.PostDevData) { 163 throw new IOException("Bad user input complete response: " + moResponse1); 164 } 165 PostDevDataResponse provResponse = (PostDevDataResponse) moResponse1; 166 GetCertData estData = checkResponse(provResponse); 167 168 Map<OSUCertType, List<X509Certificate>> certs = new HashMap<>(); 169 PrivateKey clientKey = null; 170 171 MOData moData; 172 if (estData == null) { 173 moData = (MOData) provResponse.getCommandData(); 174 } else { 175 try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse. 176 getCommandData(), network, getOMADMAdapter(), 177 km, mKeyStore, null, OSUFlowManager.FlowType.Provisioning)) { 178 estHandler.execute(false); 179 certs.put(OSUCertType.CA, estHandler.getCACerts()); 180 certs.put(OSUCertType.Client, estHandler.getClientCerts()); 181 clientKey = estHandler.getClientKey(); 182 } 183 184 Log.d(TAG, " -- Sending provisioning cert enrollment complete:"); 185 String certComplete = 186 SOAPBuilder.buildPostDevDataResponse(RequestReason.CertEnrollmentComplete, 187 sessionID, null, 188 omadmAdapter.getMO(OMAConstants.DevInfoURN), 189 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 190 OSUResponse moResponse2 = httpHandler.exchangeSOAP(mURL, certComplete); 191 if (moResponse2.getMessageType() != OSUMessageType.PostDevData) { 192 throw new IOException("Bad cert enrollment complete response: " + moResponse2); 193 } 194 PostDevDataResponse provComplete = (PostDevDataResponse) moResponse2; 195 if (provComplete.getStatus() != OSUStatus.ProvComplete || 196 provComplete.getOSUCommand() != OSUCommandID.AddMO) { 197 throw new IOException("Expected addMO: " + provComplete); 198 } 199 moData = (MOData) provComplete.getCommandData(); 200 } 201 202 // !!! How can an ExchangeComplete be sent w/o knowing the fate of the certs??? 203 String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, null); 204 Log.d(TAG, " -- Sending updateResponse:"); 205 OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse); 206 Log.d(TAG, "exComplete response: " + exComplete); 207 if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) { 208 throw new IOException("Expected ExchangeComplete: " + exComplete); 209 } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) { 210 throw new IOException("Bad ExchangeComplete status: " + exComplete); 211 } 212 213 retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore); 214 platformAdapter.provisioningComplete(mOSUInfo, moData, certs, clientKey, network); 215 } 216 } 217 218 public void remediate(PlatformAdapter platformAdapter, Network network, KeyManager km, 219 HomeSP homeSP, OSUFlowManager.FlowType flowType) 220 throws IOException, GeneralSecurityException { 221 try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) { 222 223 mHTTPHandler = httpHandler; 224 225 URL redirectURL = prepareUserInput(platformAdapter, homeSP.getFriendlyName()); 226 OMADMAdapter omadmAdapter = getOMADMAdapter(); 227 228 String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation, 229 null, 230 redirectURL.toString(), 231 omadmAdapter.getMO(OMAConstants.DevInfoURN), 232 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 233 234 OSUResponse serverResponse = httpHandler.exchangeSOAP(mURL, regRequest); 235 if (serverResponse.getMessageType() != OSUMessageType.PostDevData) { 236 throw new IOException("Expected a PostDevDataResponse"); 237 } 238 String sessionID = serverResponse.getSessionID(); 239 240 PostDevDataResponse pddResponse = (PostDevDataResponse) serverResponse; 241 Log.d(TAG, "Remediation response: " + pddResponse); 242 243 Map<OSUCertType, List<X509Certificate>> certs = null; 244 PrivateKey clientKey = null; 245 246 if (pddResponse.getStatus() != OSUStatus.RemediationComplete) { 247 if (pddResponse.getExecCommand() == ExecCommand.UploadMO) { 248 String ulMessage = SOAPBuilder.buildPostDevDataResponse(RequestReason.MOUpload, 249 null, 250 redirectURL.toString(), 251 omadmAdapter.getMO(OMAConstants.DevInfoURN), 252 omadmAdapter.getMO(OMAConstants.DevDetailURN), 253 platformAdapter.getMOTree(homeSP)); 254 255 Log.d(TAG, "Upload MO: " + ulMessage); 256 257 OSUResponse ulResponse = httpHandler.exchangeSOAP(mURL, ulMessage); 258 if (ulResponse.getMessageType() != OSUMessageType.PostDevData) { 259 throw new IOException("Expected a PostDevDataResponse to MOUpload"); 260 } 261 pddResponse = (PostDevDataResponse) ulResponse; 262 } 263 264 if (pddResponse.getExecCommand() == ExecCommand.Browser) { 265 if (flowType == OSUFlowManager.FlowType.Policy) { 266 throw new IOException("Browser launch requested in policy flow"); 267 } 268 String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI(); 269 270 if (webURL == null) { 271 throw new IOException("No web-url"); 272 } else if (!webURL.contains(sessionID)) { 273 throw new IOException("Bad or missing session ID in webURL"); 274 } 275 276 if (!startUserInput(new URL(webURL), network)) { 277 throw new IOException("User session failed"); 278 } 279 280 Log.d(TAG, " -- Sending user input complete:"); 281 String userComplete = 282 SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete, 283 sessionID, null, 284 omadmAdapter.getMO(OMAConstants.DevInfoURN), 285 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 286 287 OSUResponse udResponse = httpHandler.exchangeSOAP(mURL, userComplete); 288 if (udResponse.getMessageType() != OSUMessageType.PostDevData) { 289 throw new IOException("Bad user input complete response: " + udResponse); 290 } 291 pddResponse = (PostDevDataResponse) udResponse; 292 } else if (pddResponse.getExecCommand() == ExecCommand.GetCert) { 293 certs = new HashMap<>(); 294 try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse. 295 getCommandData(), network, getOMADMAdapter(), 296 km, mKeyStore, homeSP, flowType)) { 297 estHandler.execute(true); 298 certs.put(OSUCertType.CA, estHandler.getCACerts()); 299 certs.put(OSUCertType.Client, estHandler.getClientCerts()); 300 clientKey = estHandler.getClientKey(); 301 } 302 303 if (httpHandler.isHTTPAuthPerformed()) { // 8.4.3.6 304 httpHandler.renegotiate(certs, clientKey); 305 } 306 307 Log.d(TAG, " -- Sending remediation cert enrollment complete:"); 308 // 8.4.3.5 in the spec actually prescribes that an update URI is sent here, 309 // but there is no remediation flow that defines user interaction after EST 310 // so for now a null is passed. 311 String certComplete = 312 SOAPBuilder 313 .buildPostDevDataResponse(RequestReason.CertEnrollmentComplete, 314 sessionID, null, 315 omadmAdapter.getMO(OMAConstants.DevInfoURN), 316 omadmAdapter.getMO(OMAConstants.DevDetailURN)); 317 OSUResponse ceResponse = httpHandler.exchangeSOAP(mURL, certComplete); 318 if (ceResponse.getMessageType() != OSUMessageType.PostDevData) { 319 throw new IOException("Bad cert enrollment complete response: " 320 + ceResponse); 321 } 322 pddResponse = (PostDevDataResponse) ceResponse; 323 } else { 324 throw new IOException("Unexpected command: " + pddResponse.getExecCommand()); 325 } 326 } 327 328 if (pddResponse.getStatus() != OSUStatus.RemediationComplete) { 329 throw new IOException("Expected a PostDevDataResponse to MOUpload"); 330 } 331 332 Log.d(TAG, "Remediation response: " + pddResponse); 333 334 List<MOData> mods = new ArrayList<>(); 335 for (OSUCommand command : pddResponse.getCommands()) { 336 if (command.getOSUCommand() == OSUCommandID.UpdateNode) { 337 mods.add((MOData) command.getCommandData()); 338 } else if (command.getOSUCommand() != OSUCommandID.NoMOUpdate) { 339 throw new IOException("Unexpected OSU response: " + command); 340 } 341 } 342 343 // 1. Machine remediation: Remediation complete + replace node 344 // 2a. User remediation with upload: ExecCommand.UploadMO 345 // 2b. User remediation without upload: ExecCommand.Browser 346 // 3. User remediation only: -> sppPostDevData user input complete 347 // 348 // 4. Update node 349 // 5. -> Update response 350 // 6. Exchange complete 351 352 OSUError error = null; 353 354 String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, error); 355 Log.d(TAG, " -- Sending updateResponse:"); 356 OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse); 357 Log.d(TAG, "exComplete response: " + exComplete); 358 if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) { 359 throw new IOException("Expected ExchangeComplete: " + exComplete); 360 } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) { 361 throw new IOException("Bad ExchangeComplete status: " + exComplete); 362 } 363 364 // There's a chicken and egg here: If the config is saved before sending update complete 365 // the network is lost and the remediation flow fails. 366 try { 367 platformAdapter.remediationComplete(homeSP, mods, certs, clientKey, 368 flowType == OSUFlowManager.FlowType.Policy); 369 } catch (IOException | GeneralSecurityException e) { 370 platformAdapter.provisioningFailed(homeSP.getFriendlyName(), e.getMessage()); 371 error = OSUError.CommandFailed; 372 } 373 } 374 } 375 376 private OMADMAdapter getOMADMAdapter() { 377 return OMADMAdapter.getInstance(mContext); 378 } 379 380 private URL prepareUserInput(PlatformAdapter platformAdapter, String spName) 381 throws IOException { 382 mRedirectListener = new RedirectListener(platformAdapter, spName); 383 return mRedirectListener.getURL(); 384 } 385 386 private boolean startUserInput(URL target, Network network) 387 throws IOException { 388 mRedirectListener.startService(); 389 390 Intent intent = new Intent(mContext, OSUWebView.class); 391 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 392 intent.putExtra(OSUWebView.OSU_NETWORK, network); 393 intent.putExtra(OSUWebView.OSU_URL, target.toString()); 394 mContext.startActivity(intent); 395 396 return mRedirectListener.waitForUser(); 397 } 398 399 public void close(boolean abort) { 400 if (mRedirectListener != null) { 401 mRedirectListener.abort(); 402 mRedirectListener = null; 403 } 404 if (abort) { 405 try { 406 mHTTPHandler.close(); 407 } catch (IOException ioe) { 408 /**/ 409 } 410 } 411 } 412 413 private HTTPHandler createHandler(Network network, HomeSP homeSP, KeyManager km, 414 OSUFlowManager.FlowType flowType) 415 throws GeneralSecurityException, IOException { 416 Credential credential = homeSP.getCredential(); 417 418 Log.d(TAG, "Credential method " + credential.getEAPMethod().getEAPMethodID()); 419 switch (credential.getEAPMethod().getEAPMethodID()) { 420 case EAP_TTLS: 421 String user; 422 byte[] password; 423 UpdateInfo subscriptionUpdate; 424 if (flowType == OSUFlowManager.FlowType.Policy) { 425 subscriptionUpdate = homeSP.getPolicy() != null ? 426 homeSP.getPolicy().getPolicyUpdate() : null; 427 } else { 428 subscriptionUpdate = homeSP.getSubscriptionUpdate(); 429 } 430 if (subscriptionUpdate != null && subscriptionUpdate.getUsername() != null) { 431 user = subscriptionUpdate.getUsername(); 432 password = subscriptionUpdate.getPassword() != null ? 433 subscriptionUpdate.getPassword().getBytes(StandardCharsets.UTF_8) : 434 new byte[0]; 435 } else { 436 user = credential.getUserName(); 437 password = credential.getPassword().getBytes(StandardCharsets.UTF_8); 438 } 439 return new HTTPHandler(StandardCharsets.UTF_8, 440 OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network, 441 mURL, km, true), user, password); 442 case EAP_TLS: 443 return new HTTPHandler(StandardCharsets.UTF_8, 444 OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network, 445 mURL, km, true)); 446 default: 447 throw new IOException("Cannot remediate account with " + 448 credential.getEAPMethod().getEAPMethodID()); 449 } 450 } 451 452 private static GetCertData checkResponse(PostDevDataResponse response) throws IOException { 453 if (response.getStatus() == OSUStatus.ProvComplete && 454 response.getOSUCommand() == OSUCommandID.AddMO) { 455 return null; 456 } 457 458 if (response.getOSUCommand() == OSUCommandID.Exec && 459 response.getExecCommand() == ExecCommand.GetCert) { 460 return (GetCertData) response.getCommandData(); 461 } else { 462 throw new IOException("Unexpected command: " + response); 463 } 464 } 465 466 private static final String[] AAACertPath = 467 {"PerProviderSubscription", "?", "AAAServerTrustRoot", "*", "CertURL"}; 468 private static final String[] RemdCertPath = 469 {"PerProviderSubscription", "?", "SubscriptionUpdate", "TrustRoot", "CertURL"}; 470 private static final String[] PolicyCertPath = 471 {"PerProviderSubscription", "?", "Policy", "PolicyUpdate", "TrustRoot", "CertURL"}; 472 473 private static void retrieveCerts(OMANode ppsRoot, 474 Map<OSUCertType, List<X509Certificate>> certs, 475 Network network, KeyManager km, KeyStore ks) 476 throws GeneralSecurityException, IOException { 477 478 List<X509Certificate> aaaCerts = getCerts(ppsRoot, AAACertPath, network, km, ks); 479 certs.put(OSUCertType.AAA, aaaCerts); 480 certs.put(OSUCertType.Remediation, getCerts(ppsRoot, RemdCertPath, network, km, ks)); 481 certs.put(OSUCertType.Policy, getCerts(ppsRoot, PolicyCertPath, network, km, ks)); 482 } 483 484 private static List<X509Certificate> getCerts(OMANode ppsRoot, String[] path, Network network, 485 KeyManager km, KeyStore ks) 486 throws GeneralSecurityException, IOException { 487 List<String> urls = new ArrayList<>(); 488 getCertURLs(ppsRoot, Arrays.asList(path).iterator(), urls); 489 Log.d(TAG, Arrays.toString(path) + ": " + urls); 490 491 List<X509Certificate> certs = new ArrayList<>(urls.size()); 492 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 493 for (String urlString : urls) { 494 URL url = new URL(urlString); 495 HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, 496 OSUSocketFactory.getSocketFactory(ks, null, 497 OSUFlowManager.FlowType.Provisioning, network, url, km, false)); 498 499 certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url))); 500 } 501 return certs; 502 } 503 504 private static void getCertURLs(OMANode root, Iterator<String> path, List<String> urls) 505 throws IOException { 506 507 String name = path.next(); 508 // Log.d(TAG, "Pulling '" + name + "' out of '" + root.getName() + "'"); 509 Collection<OMANode> nodes = null; 510 switch (name) { 511 case "?": 512 for (OMANode node : root.getChildren()) { 513 if (!node.isLeaf()) { 514 nodes = Collections.singletonList(node); 515 break; 516 } 517 } 518 break; 519 case "*": 520 nodes = root.getChildren(); 521 break; 522 default: 523 nodes = Collections.singletonList(root.getChild(name)); 524 break; 525 } 526 527 if (nodes == null) { 528 throw new IllegalArgumentException("No matching node in " + root.getName() 529 + " for " + name); 530 } 531 532 for (OMANode node : nodes) { 533 if (path.hasNext()) { 534 getCertURLs(node, path, urls); 535 } else { 536 urls.add(node.getValue()); 537 } 538 } 539 } 540 } 541