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