1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security.net.config;
18
19import android.content.Context;
20import android.test.AndroidTestCase;
21import android.test.MoreAsserts;
22import android.util.ArraySet;
23import android.util.Pair;
24import java.io.IOException;
25import java.net.InetAddress;
26import java.net.Socket;
27import java.net.URL;
28import java.security.KeyStore;
29import java.security.Provider;
30import java.security.Security;
31import java.security.cert.X509Certificate;
32import java.util.ArrayList;
33import java.util.Collections;
34import java.util.Set;
35import javax.net.ssl.HttpsURLConnection;
36import javax.net.ssl.SSLContext;
37import javax.net.ssl.SSLHandshakeException;
38import javax.net.ssl.SSLSocket;
39import javax.net.ssl.TrustManager;
40import javax.net.ssl.TrustManagerFactory;
41
42public class XmlConfigTests extends AndroidTestCase {
43
44    private final static String DEBUG_CA_SUBJ = "O=AOSP, CN=Test debug CA";
45
46    public void testEmptyConfigFile() throws Exception {
47        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_config);
48        ApplicationConfig appConfig = new ApplicationConfig(source);
49        assertFalse(appConfig.hasPerDomainConfigs());
50        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
51        assertNotNull(config);
52        // Check defaults.
53        assertTrue(config.isCleartextTrafficPermitted());
54        assertFalse(config.isHstsEnforced());
55        assertFalse(config.getTrustAnchors().isEmpty());
56        PinSet pinSet = config.getPins();
57        assertTrue(pinSet.pins.isEmpty());
58        // Try some connections.
59        SSLContext context = TestUtils.getSSLContext(source);
60        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
61        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
62        TestUtils.assertUrlConnectionSucceeds(context, "google.com", 443);
63    }
64
65    public void testEmptyAnchors() throws Exception {
66        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_trust);
67        ApplicationConfig appConfig = new ApplicationConfig(source);
68        assertFalse(appConfig.hasPerDomainConfigs());
69        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
70        assertNotNull(config);
71        // Check defaults.
72        assertTrue(config.isCleartextTrafficPermitted());
73        assertFalse(config.isHstsEnforced());
74        assertTrue(config.getTrustAnchors().isEmpty());
75        PinSet pinSet = config.getPins();
76        assertTrue(pinSet.pins.isEmpty());
77        SSLContext context = TestUtils.getSSLContext(source);
78        TestUtils.assertConnectionFails(context, "android.com", 443);
79        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
80        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
81    }
82
83    public void testBasicDomainConfig() throws Exception {
84        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.domain1);
85        ApplicationConfig appConfig = new ApplicationConfig(source);
86        assertTrue(appConfig.hasPerDomainConfigs());
87        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
88        assertNotNull(config);
89        // Check defaults.
90        assertTrue(config.isCleartextTrafficPermitted());
91        assertFalse(config.isHstsEnforced());
92        assertTrue(config.getTrustAnchors().isEmpty());
93        PinSet pinSet = config.getPins();
94        assertTrue(pinSet.pins.isEmpty());
95        // Check android.com.
96        config = appConfig.getConfigForHostname("android.com");
97        assertTrue(config.isCleartextTrafficPermitted());
98        assertFalse(config.isHstsEnforced());
99        assertFalse(config.getTrustAnchors().isEmpty());
100        pinSet = config.getPins();
101        assertTrue(pinSet.pins.isEmpty());
102        // Try connections.
103        SSLContext context = TestUtils.getSSLContext(source);
104        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
105        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
106        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
107        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
108        // Check that sockets created without the hostname fail with per-domain configs
109        SSLSocket socket = (SSLSocket) context.getSocketFactory()
110                .createSocket(InetAddress.getByName("android.com"), 443);
111        try {
112        socket.startHandshake();
113        socket.getInputStream();
114        fail();
115        } catch (IOException expected) {
116        }
117    }
118
119    public void testBasicPinning() throws Exception {
120        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.pins1);
121        ApplicationConfig appConfig = new ApplicationConfig(source);
122        assertTrue(appConfig.hasPerDomainConfigs());
123        // Check android.com.
124        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
125        PinSet pinSet = config.getPins();
126        assertFalse(pinSet.pins.isEmpty());
127        // Try connections.
128        SSLContext context = TestUtils.getSSLContext(source);
129        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
130        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
131        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
132    }
133
134    public void testExpiredPin() throws Exception {
135        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.expired_pin);
136        ApplicationConfig appConfig = new ApplicationConfig(source);
137        assertTrue(appConfig.hasPerDomainConfigs());
138        // Check android.com.
139        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
140        PinSet pinSet = config.getPins();
141        assertFalse(pinSet.pins.isEmpty());
142        // Try connections.
143        SSLContext context = TestUtils.getSSLContext(source);
144        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
145        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
146    }
147
148    public void testOverridesPins() throws Exception {
149        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_pins);
150        ApplicationConfig appConfig = new ApplicationConfig(source);
151        assertTrue(appConfig.hasPerDomainConfigs());
152        // Check android.com.
153        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
154        PinSet pinSet = config.getPins();
155        assertFalse(pinSet.pins.isEmpty());
156        // Try connections.
157        SSLContext context = TestUtils.getSSLContext(source);
158        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
159        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
160    }
161
162    public void testBadPin() throws Exception {
163        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin);
164        ApplicationConfig appConfig = new ApplicationConfig(source);
165        assertTrue(appConfig.hasPerDomainConfigs());
166        // Check android.com.
167        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
168        PinSet pinSet = config.getPins();
169        assertFalse(pinSet.pins.isEmpty());
170        // Try connections.
171        SSLContext context = TestUtils.getSSLContext(source);
172        TestUtils.assertConnectionFails(context, "android.com", 443);
173        TestUtils.assertUrlConnectionFails(context, "android.com", 443);
174        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
175    }
176
177    public void testMultipleDomains() throws Exception {
178        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_domains);
179        ApplicationConfig appConfig = new ApplicationConfig(source);
180        assertTrue(appConfig.hasPerDomainConfigs());
181        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
182        assertTrue(config.isCleartextTrafficPermitted());
183        assertFalse(config.isHstsEnforced());
184        assertFalse(config.getTrustAnchors().isEmpty());
185        PinSet pinSet = config.getPins();
186        assertTrue(pinSet.pins.isEmpty());
187        // Both android.com and google.com should use the same config
188        NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com");
189        assertEquals(config, other);
190        // Try connections.
191        SSLContext context = TestUtils.getSSLContext(source);
192        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
193        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
194        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
195        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
196    }
197
198    public void testMultipleDomainConfigs() throws Exception {
199        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_configs);
200        ApplicationConfig appConfig = new ApplicationConfig(source);
201        assertTrue(appConfig.hasPerDomainConfigs());
202        // Should be two different config objects
203        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
204        NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com");
205        MoreAsserts.assertNotEqual(config, other);
206        // Try connections.
207        SSLContext context = TestUtils.getSSLContext(source);
208        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
209        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
210        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
211    }
212
213    public void testIncludeSubdomains() throws Exception {
214        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.subdomains);
215        ApplicationConfig appConfig = new ApplicationConfig(source);
216        assertTrue(appConfig.hasPerDomainConfigs());
217        // Try connections.
218        SSLContext context = TestUtils.getSSLContext(source);
219        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
220        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
221        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
222        TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443);
223        TestUtils.assertConnectionFails(context, "google.com", 443);
224    }
225
226    public void testAttributes() throws Exception {
227        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.attributes);
228        ApplicationConfig appConfig = new ApplicationConfig(source);
229        assertFalse(appConfig.hasPerDomainConfigs());
230        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
231        assertTrue(config.isHstsEnforced());
232        assertFalse(config.isCleartextTrafficPermitted());
233    }
234
235    public void testResourcePemCertificateSource() throws Exception {
236        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_pem);
237        ApplicationConfig appConfig = new ApplicationConfig(source);
238        // Check android.com.
239        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
240        assertTrue(config.isCleartextTrafficPermitted());
241        assertFalse(config.isHstsEnforced());
242        assertEquals(2, config.getTrustAnchors().size());
243        // Try connections.
244        SSLContext context = TestUtils.getSSLContext(source);
245        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
246        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
247        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
248        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
249    }
250
251    public void testResourceDerCertificateSource() throws Exception {
252        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_der);
253        ApplicationConfig appConfig = new ApplicationConfig(source);
254        // Check android.com.
255        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
256        assertTrue(config.isCleartextTrafficPermitted());
257        assertFalse(config.isHstsEnforced());
258        assertEquals(2, config.getTrustAnchors().size());
259        // Try connections.
260        SSLContext context = TestUtils.getSSLContext(source);
261        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
262        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
263        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
264        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
265    }
266
267    public void testNestedDomainConfigs() throws Exception {
268        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains);
269        ApplicationConfig appConfig = new ApplicationConfig(source);
270        assertTrue(appConfig.hasPerDomainConfigs());
271        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
272        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
273        MoreAsserts.assertNotEqual(parent, child);
274        MoreAsserts.assertEmpty(parent.getPins().pins);
275        MoreAsserts.assertNotEmpty(child.getPins().pins);
276        // Check that the child inherited the cleartext value and anchors.
277        assertFalse(child.isCleartextTrafficPermitted());
278        MoreAsserts.assertNotEmpty(child.getTrustAnchors());
279        // Test connections.
280        SSLContext context = TestUtils.getSSLContext(source);
281        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
282        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
283    }
284
285    public void testNestedDomainConfigsOverride() throws Exception {
286        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains_override);
287        ApplicationConfig appConfig = new ApplicationConfig(source);
288        assertTrue(appConfig.hasPerDomainConfigs());
289        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
290        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
291        MoreAsserts.assertNotEqual(parent, child);
292        assertTrue(parent.isCleartextTrafficPermitted());
293        assertFalse(child.isCleartextTrafficPermitted());
294    }
295
296    public void testDebugOverridesDisabled() throws Exception {
297        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_basic, false);
298        ApplicationConfig appConfig = new ApplicationConfig(source);
299        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
300        Set<TrustAnchor> anchors = config.getTrustAnchors();
301        MoreAsserts.assertEmpty(anchors);
302        SSLContext context = TestUtils.getSSLContext(source);
303        TestUtils.assertConnectionFails(context, "android.com", 443);
304        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
305    }
306
307    public void testBasicDebugOverrides() throws Exception {
308        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_basic, true);
309        ApplicationConfig appConfig = new ApplicationConfig(source);
310        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
311        Set<TrustAnchor> anchors = config.getTrustAnchors();
312        MoreAsserts.assertNotEmpty(anchors);
313        for (TrustAnchor anchor : anchors) {
314            assertTrue(anchor.overridesPins);
315        }
316        SSLContext context = TestUtils.getSSLContext(source);
317        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
318        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
319    }
320
321    public void testDebugOverridesWithDomain() throws Exception {
322        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_domain, true);
323        ApplicationConfig appConfig = new ApplicationConfig(source);
324        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
325        Set<TrustAnchor> anchors = config.getTrustAnchors();
326        boolean foundDebugCA = false;
327        for (TrustAnchor anchor : anchors) {
328            if (anchor.certificate.getSubjectDN().toString().equals(DEBUG_CA_SUBJ)) {
329                foundDebugCA = true;
330                assertTrue(anchor.overridesPins);
331            }
332        }
333        assertTrue(foundDebugCA);
334        SSLContext context = TestUtils.getSSLContext(source);
335        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
336        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
337    }
338
339    public void testDebugInherit() throws Exception {
340        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_domain, true);
341        ApplicationConfig appConfig = new ApplicationConfig(source);
342        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
343        Set<TrustAnchor> anchors = config.getTrustAnchors();
344        boolean foundDebugCA = false;
345        for (TrustAnchor anchor : anchors) {
346            if (anchor.certificate.getSubjectDN().toString().equals(DEBUG_CA_SUBJ)) {
347                foundDebugCA = true;
348                assertTrue(anchor.overridesPins);
349            }
350        }
351        assertTrue(foundDebugCA);
352        assertTrue(anchors.size() > 1);
353        SSLContext context = TestUtils.getSSLContext(source);
354        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
355        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
356    }
357
358    private void testBadConfig(int configId) throws Exception {
359        try {
360            XmlConfigSource source = new XmlConfigSource(getContext(), configId);
361            ApplicationConfig appConfig = new ApplicationConfig(source);
362            appConfig.getConfigForHostname("android.com");
363            fail("Bad config " + getContext().getResources().getResourceName(configId)
364                    + " did not fail to parse");
365        } catch (RuntimeException e) {
366            MoreAsserts.assertAssignableFrom(XmlConfigSource.ParserException.class,
367                    e.getCause());
368        }
369    }
370
371    public void testBadConfig0() throws Exception {
372        testBadConfig(R.xml.bad_config0);
373    }
374
375    public void testBadConfig1() throws Exception {
376        testBadConfig(R.xml.bad_config1);
377    }
378
379    public void testBadConfig2() throws Exception {
380        testBadConfig(R.xml.bad_config2);
381    }
382
383    public void testBadConfig3() throws Exception {
384        testBadConfig(R.xml.bad_config3);
385    }
386
387    public void testBadConfig4() throws Exception {
388        testBadConfig(R.xml.bad_config4);
389    }
390
391    public void testBadConfig5() throws Exception {
392        testBadConfig(R.xml.bad_config4);
393    }
394
395    public void testTrustManagerKeystore() throws Exception {
396        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin, true);
397        ApplicationConfig appConfig = new ApplicationConfig(source);
398        Provider provider = new NetworkSecurityConfigProvider();
399        TrustManagerFactory tmf =
400                TrustManagerFactory.getInstance("PKIX", provider);
401        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
402        keystore.load(null);
403        int i = 0;
404        for (X509Certificate cert : SystemCertificateSource.getInstance().getCertificates()) {
405            keystore.setEntry(String.valueOf(i),
406                    new KeyStore.TrustedCertificateEntry(cert),
407                    null);
408            i++;
409        }
410        tmf.init(keystore);
411        TrustManager[] tms = tmf.getTrustManagers();
412        SSLContext context = SSLContext.getInstance("TLS");
413        context.init(null, tms, null);
414        TestUtils.assertConnectionSucceeds(context, "android.com" , 443);
415    }
416
417    public void testDebugDedup() throws Exception {
418        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_dedup, true);
419        ApplicationConfig appConfig = new ApplicationConfig(source);
420        assertTrue(appConfig.hasPerDomainConfigs());
421        // Check android.com.
422        NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
423        PinSet pinSet = config.getPins();
424        assertFalse(pinSet.pins.isEmpty());
425        // Check that all TrustAnchors come from the override pins debug source.
426        for (TrustAnchor anchor : config.getTrustAnchors()) {
427            assertTrue(anchor.overridesPins);
428        }
429        // Try connections.
430        SSLContext context = TestUtils.getSSLContext(source);
431        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
432        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
433    }
434
435    public void testExtraDebugResource() throws Exception {
436        XmlConfigSource source =
437                new XmlConfigSource(getContext(), R.xml.extra_debug_resource, true);
438        ApplicationConfig appConfig = new ApplicationConfig(source);
439        assertFalse(appConfig.hasPerDomainConfigs());
440        NetworkSecurityConfig config = appConfig.getConfigForHostname("");
441        MoreAsserts.assertNotEmpty(config.getTrustAnchors());
442
443        // Check that the _debug file is ignored if debug is false.
444        source = new XmlConfigSource(getContext(), R.xml.extra_debug_resource, false);
445        appConfig = new ApplicationConfig(source);
446        assertFalse(appConfig.hasPerDomainConfigs());
447        config = appConfig.getConfigForHostname("");
448        MoreAsserts.assertEmpty(config.getTrustAnchors());
449    }
450
451    public void testExtraDebugResourceIgnored() throws Exception {
452        // Verify that parsing the extra debug config resource fails only when debugging is true.
453        XmlConfigSource source =
454                new XmlConfigSource(getContext(), R.xml.bad_extra_debug_resource, false);
455        ApplicationConfig appConfig = new ApplicationConfig(source);
456        // Force parsing the config file.
457        appConfig.getConfigForHostname("");
458
459        source = new XmlConfigSource(getContext(), R.xml.bad_extra_debug_resource, true);
460        appConfig = new ApplicationConfig(source);
461        try {
462            appConfig.getConfigForHostname("");
463            fail("Bad extra debug resource did not fail to parse");
464        } catch (RuntimeException expected) {
465        }
466    }
467
468    public void testDomainWhitespaceTrimming() throws Exception {
469        XmlConfigSource source =
470                new XmlConfigSource(getContext(), R.xml.domain_whitespace, false);
471        ApplicationConfig appConfig = new ApplicationConfig(source);
472        NetworkSecurityConfig defaultConfig = appConfig.getConfigForHostname("");
473        MoreAsserts.assertNotEqual(defaultConfig, appConfig.getConfigForHostname("developer.android.com"));
474        MoreAsserts.assertNotEqual(defaultConfig, appConfig.getConfigForHostname("android.com"));
475        SSLContext context = TestUtils.getSSLContext(source);
476        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
477        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
478    }
479}
480