• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.hotspot2.osu;
2 
3 /*
4  * policy-server.r2-testbed             IN      A       10.123.107.107
5  * remediation-server.r2-testbed        IN      A       10.123.107.107
6  * subscription-server.r2-testbed       IN      A       10.123.107.107
7  * www.r2-testbed                       IN      A       10.123.107.107
8  * osu-server.r2-testbed-rks            IN      A       10.123.107.107
9  * policy-server.r2-testbed-rks         IN      A       10.123.107.107
10  * remediation-server.r2-testbed-rks    IN      A       10.123.107.107
11  * subscription-server.r2-testbed-rks   IN      A       10.123.107.107
12  */
13 
14 import android.content.Context;
15 import android.content.Intent;
16 import android.net.Network;
17 import android.util.Log;
18 
19 import com.android.hotspot2.OMADMAdapter;
20 import com.android.hotspot2.est.ESTHandler;
21 import com.android.hotspot2.flow.OSUInfo;
22 import com.android.hotspot2.flow.PlatformAdapter;
23 import com.android.hotspot2.omadm.OMAConstants;
24 import com.android.hotspot2.omadm.OMANode;
25 import com.android.hotspot2.osu.commands.BrowserURI;
26 import com.android.hotspot2.osu.commands.ClientCertInfo;
27 import com.android.hotspot2.osu.commands.GetCertData;
28 import com.android.hotspot2.osu.commands.MOData;
29 import com.android.hotspot2.osu.service.RedirectListener;
30 import com.android.hotspot2.pps.Credential;
31 import com.android.hotspot2.pps.HomeSP;
32 import com.android.hotspot2.pps.UpdateInfo;
33 
34 import java.io.IOException;
35 import java.net.MalformedURLException;
36 import java.net.URL;
37 import java.nio.charset.StandardCharsets;
38 import java.security.GeneralSecurityException;
39 import java.security.KeyStore;
40 import java.security.PrivateKey;
41 import java.security.cert.CertificateFactory;
42 import java.security.cert.X509Certificate;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collection;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.Iterator;
49 import java.util.List;
50 import java.util.Locale;
51 import java.util.Map;
52 
53 import javax.net.ssl.KeyManager;
54 
55 public class OSUClient {
56     private static final String TAG = "OSUCLT";
57 
58     private final OSUInfo mOSUInfo;
59     private final URL mURL;
60     private final KeyStore mKeyStore;
61     private final Context mContext;
62     private volatile HTTPHandler mHTTPHandler;
63     private volatile RedirectListener mRedirectListener;
64 
OSUClient(OSUInfo osuInfo, KeyStore ks, Context context)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 
OSUClient(String osu, KeyStore ks, Context context)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 
getOSUInfo()79     public OSUInfo getOSUInfo() {
80         return mOSUInfo;
81     }
82 
provision(PlatformAdapter platformAdapter, Network network, KeyManager km)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 
remediate(PlatformAdapter platformAdapter, Network network, KeyManager km, HomeSP homeSP, OSUFlowManager.FlowType flowType)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 
getOMADMAdapter()376     private OMADMAdapter getOMADMAdapter() {
377         return OMADMAdapter.getInstance(mContext);
378     }
379 
prepareUserInput(PlatformAdapter platformAdapter, String spName)380     private URL prepareUserInput(PlatformAdapter platformAdapter, String spName)
381             throws IOException {
382         mRedirectListener = new RedirectListener(platformAdapter, spName);
383         return mRedirectListener.getURL();
384     }
385 
startUserInput(URL target, Network network)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 
close(boolean abort)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 
createHandler(Network network, HomeSP homeSP, KeyManager km, OSUFlowManager.FlowType flowType)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 
checkResponse(PostDevDataResponse response)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 
retrieveCerts(OMANode ppsRoot, Map<OSUCertType, List<X509Certificate>> certs, Network network, KeyManager km, KeyStore ks)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 
getCerts(OMANode ppsRoot, String[] path, Network network, KeyManager km, KeyStore ks)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 
getCertURLs(OMANode root, Iterator<String> path, List<String> urls)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