Home | History | Annotate | Download | only in osu
      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