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