/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.net.config; import android.content.Context; import android.test.AndroidTestCase; import android.test.MoreAsserts; import android.util.ArraySet; import android.util.Pair; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.URL; import java.security.KeyStore; import java.security.Provider; import java.security.Security; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Set; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; public class XmlConfigTests extends AndroidTestCase { private final static String DEBUG_CA_SUBJ = "O=AOSP, CN=Test debug CA"; public void testEmptyConfigFile() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_config); ApplicationConfig appConfig = new ApplicationConfig(source); assertFalse(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); assertNotNull(config); // Check defaults. assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertFalse(config.getTrustAnchors().isEmpty()); PinSet pinSet = config.getPins(); assertTrue(pinSet.pins.isEmpty()); // Try some connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "google.com", 443); } public void testEmptyAnchors() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_trust); ApplicationConfig appConfig = new ApplicationConfig(source); assertFalse(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); assertNotNull(config); // Check defaults. assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertTrue(config.getTrustAnchors().isEmpty()); PinSet pinSet = config.getPins(); assertTrue(pinSet.pins.isEmpty()); SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionFails(context, "android.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); TestUtils.assertUrlConnectionFails(context, "google.com", 443); } public void testBasicDomainConfig() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.domain1); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); assertNotNull(config); // Check defaults. assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertTrue(config.getTrustAnchors().isEmpty()); PinSet pinSet = config.getPins(); assertTrue(pinSet.pins.isEmpty()); // Check android.com. config = appConfig.getConfigForHostname("android.com"); assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertFalse(config.getTrustAnchors().isEmpty()); pinSet = config.getPins(); assertTrue(pinSet.pins.isEmpty()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); TestUtils.assertUrlConnectionFails(context, "google.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); // Check that sockets created without the hostname fail with per-domain configs SSLSocket socket = (SSLSocket) context.getSocketFactory() .createSocket(InetAddress.getByName("android.com"), 443); try { socket.startHandshake(); socket.getInputStream(); fail(); } catch (IOException expected) { } } public void testBasicPinning() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.pins1); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); PinSet pinSet = config.getPins(); assertFalse(pinSet.pins.isEmpty()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "google.com", 443); } public void testExpiredPin() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.expired_pin); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); PinSet pinSet = config.getPins(); assertFalse(pinSet.pins.isEmpty()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testOverridesPins() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_pins); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); PinSet pinSet = config.getPins(); assertFalse(pinSet.pins.isEmpty()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testBadPin() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); PinSet pinSet = config.getPins(); assertFalse(pinSet.pins.isEmpty()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionFails(context, "android.com", 443); TestUtils.assertUrlConnectionFails(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "google.com", 443); } public void testMultipleDomains() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_domains); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertFalse(config.getTrustAnchors().isEmpty()); PinSet pinSet = config.getPins(); assertTrue(pinSet.pins.isEmpty()); // Both android.com and google.com should use the same config NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com"); assertEquals(config, other); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "google.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testMultipleDomainConfigs() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_configs); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Should be two different config objects NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com"); MoreAsserts.assertNotEqual(config, other); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "google.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testIncludeSubdomains() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.subdomains); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443); TestUtils.assertConnectionFails(context, "google.com", 443); } public void testAttributes() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.attributes); ApplicationConfig appConfig = new ApplicationConfig(source); assertFalse(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); assertTrue(config.isHstsEnforced()); assertFalse(config.isCleartextTrafficPermitted()); } public void testResourcePemCertificateSource() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_pem); ApplicationConfig appConfig = new ApplicationConfig(source); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertEquals(2, config.getTrustAnchors().size()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); TestUtils.assertUrlConnectionFails(context, "google.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testResourceDerCertificateSource() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_der); ApplicationConfig appConfig = new ApplicationConfig(source); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); assertTrue(config.isCleartextTrafficPermitted()); assertFalse(config.isHstsEnforced()); assertEquals(2, config.getTrustAnchors().size()); // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); TestUtils.assertUrlConnectionFails(context, "google.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testNestedDomainConfigs() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com"); NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com"); MoreAsserts.assertNotEqual(parent, child); MoreAsserts.assertEmpty(parent.getPins().pins); MoreAsserts.assertNotEmpty(child.getPins().pins); // Check that the child inherited the cleartext value and anchors. assertFalse(child.isCleartextTrafficPermitted()); MoreAsserts.assertNotEmpty(child.getTrustAnchors()); // Test connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); } public void testNestedDomainConfigsOverride() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains_override); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com"); NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com"); MoreAsserts.assertNotEqual(parent, child); assertTrue(parent.isCleartextTrafficPermitted()); assertFalse(child.isCleartextTrafficPermitted()); } public void testDebugOverridesDisabled() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_basic, false); ApplicationConfig appConfig = new ApplicationConfig(source); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); Set anchors = config.getTrustAnchors(); MoreAsserts.assertEmpty(anchors); SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionFails(context, "android.com", 443); TestUtils.assertConnectionFails(context, "developer.android.com", 443); } public void testBasicDebugOverrides() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_basic, true); ApplicationConfig appConfig = new ApplicationConfig(source); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); Set anchors = config.getTrustAnchors(); MoreAsserts.assertNotEmpty(anchors); for (TrustAnchor anchor : anchors) { assertTrue(anchor.overridesPins); } SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); } public void testDebugOverridesWithDomain() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_domain, true); ApplicationConfig appConfig = new ApplicationConfig(source); NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); Set anchors = config.getTrustAnchors(); boolean foundDebugCA = false; for (TrustAnchor anchor : anchors) { if (anchor.certificate.getSubjectDN().toString().equals(DEBUG_CA_SUBJ)) { foundDebugCA = true; assertTrue(anchor.overridesPins); } } assertTrue(foundDebugCA); SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); } public void testDebugInherit() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.debug_domain, true); ApplicationConfig appConfig = new ApplicationConfig(source); NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); Set anchors = config.getTrustAnchors(); boolean foundDebugCA = false; for (TrustAnchor anchor : anchors) { if (anchor.certificate.getSubjectDN().toString().equals(DEBUG_CA_SUBJ)) { foundDebugCA = true; assertTrue(anchor.overridesPins); } } assertTrue(foundDebugCA); assertTrue(anchors.size() > 1); SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); } private void testBadConfig(int configId) throws Exception { try { XmlConfigSource source = new XmlConfigSource(getContext(), configId); ApplicationConfig appConfig = new ApplicationConfig(source); appConfig.getConfigForHostname("android.com"); fail("Bad config " + getContext().getResources().getResourceName(configId) + " did not fail to parse"); } catch (RuntimeException e) { MoreAsserts.assertAssignableFrom(XmlConfigSource.ParserException.class, e.getCause()); } } public void testBadConfig0() throws Exception { testBadConfig(R.xml.bad_config0); } public void testBadConfig1() throws Exception { testBadConfig(R.xml.bad_config1); } public void testBadConfig2() throws Exception { testBadConfig(R.xml.bad_config2); } public void testBadConfig3() throws Exception { testBadConfig(R.xml.bad_config3); } public void testBadConfig4() throws Exception { testBadConfig(R.xml.bad_config4); } public void testBadConfig5() throws Exception { testBadConfig(R.xml.bad_config4); } public void testTrustManagerKeystore() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin, true); ApplicationConfig appConfig = new ApplicationConfig(source); Provider provider = new NetworkSecurityConfigProvider(); TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", provider); KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(null); int i = 0; for (X509Certificate cert : SystemCertificateSource.getInstance().getCertificates()) { keystore.setEntry(String.valueOf(i), new KeyStore.TrustedCertificateEntry(cert), null); i++; } tmf.init(keystore); TrustManager[] tms = tmf.getTrustManagers(); SSLContext context = SSLContext.getInstance("TLS"); context.init(null, tms, null); TestUtils.assertConnectionSucceeds(context, "android.com" , 443); } public void testDebugDedup() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_dedup, true); ApplicationConfig appConfig = new ApplicationConfig(source); assertTrue(appConfig.hasPerDomainConfigs()); // Check android.com. NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); PinSet pinSet = config.getPins(); assertFalse(pinSet.pins.isEmpty()); // Check that all TrustAnchors come from the override pins debug source. for (TrustAnchor anchor : config.getTrustAnchors()) { assertTrue(anchor.overridesPins); } // Try connections. SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); } public void testExtraDebugResource() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.extra_debug_resource, true); ApplicationConfig appConfig = new ApplicationConfig(source); assertFalse(appConfig.hasPerDomainConfigs()); NetworkSecurityConfig config = appConfig.getConfigForHostname(""); MoreAsserts.assertNotEmpty(config.getTrustAnchors()); // Check that the _debug file is ignored if debug is false. source = new XmlConfigSource(getContext(), R.xml.extra_debug_resource, false); appConfig = new ApplicationConfig(source); assertFalse(appConfig.hasPerDomainConfigs()); config = appConfig.getConfigForHostname(""); MoreAsserts.assertEmpty(config.getTrustAnchors()); } public void testExtraDebugResourceIgnored() throws Exception { // Verify that parsing the extra debug config resource fails only when debugging is true. XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_extra_debug_resource, false); ApplicationConfig appConfig = new ApplicationConfig(source); // Force parsing the config file. appConfig.getConfigForHostname(""); source = new XmlConfigSource(getContext(), R.xml.bad_extra_debug_resource, true); appConfig = new ApplicationConfig(source); try { appConfig.getConfigForHostname(""); fail("Bad extra debug resource did not fail to parse"); } catch (RuntimeException expected) { } } public void testDomainWhitespaceTrimming() throws Exception { XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.domain_whitespace, false); ApplicationConfig appConfig = new ApplicationConfig(source); NetworkSecurityConfig defaultConfig = appConfig.getConfigForHostname(""); MoreAsserts.assertNotEqual(defaultConfig, appConfig.getConfigForHostname("developer.android.com")); MoreAsserts.assertNotEqual(defaultConfig, appConfig.getConfigForHostname("android.com")); SSLContext context = TestUtils.getSSLContext(source); TestUtils.assertConnectionSucceeds(context, "android.com", 443); TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); } }