1package 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
14import android.net.Network;
15import android.util.Log;
16
17import com.android.hotspot2.OMADMAdapter;
18import com.android.hotspot2.est.ESTHandler;
19import com.android.hotspot2.omadm.OMAConstants;
20import com.android.hotspot2.omadm.OMANode;
21import com.android.hotspot2.osu.commands.BrowserURI;
22import com.android.hotspot2.osu.commands.ClientCertInfo;
23import com.android.hotspot2.osu.commands.GetCertData;
24import com.android.hotspot2.osu.commands.MOData;
25import com.android.hotspot2.pps.Credential;
26import com.android.hotspot2.pps.HomeSP;
27import com.android.hotspot2.pps.UpdateInfo;
28
29import java.io.IOException;
30import java.net.MalformedURLException;
31import java.net.URL;
32import java.nio.charset.StandardCharsets;
33import java.security.GeneralSecurityException;
34import java.security.KeyStore;
35import java.security.PrivateKey;
36import java.security.cert.CertificateFactory;
37import java.security.cert.X509Certificate;
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.Collection;
41import java.util.HashMap;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Locale;
45import java.util.Map;
46
47import javax.net.ssl.KeyManager;
48
49public 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