Merge "Add support for debug-overrides configuration"
am: f1e813ea33

* commit 'f1e813ea334c947ae0397bfa3a8e42be4fd4ae8c':
  Add support for debug-overrides configuration
diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java
index 1c787b4..503854e 100644
--- a/core/java/android/security/net/config/NetworkSecurityConfig.java
+++ b/core/java/android/security/net/config/NetworkSecurityConfig.java
@@ -227,10 +227,14 @@
             return Collections.<CertificatesEntryRef>emptyList();
         }
 
-        public boolean hasCertificateEntryRefs() {
+        public boolean hasCertificatesEntryRefs() {
             return mCertificatesEntryRefs != null;
         }
 
+        List<CertificatesEntryRef> getCertificatesEntryRefs() {
+            return mCertificatesEntryRefs;
+        }
+
         public NetworkSecurityConfig build() {
             boolean cleartextPermitted = getEffectiveCleartextTrafficPermitted();
             boolean hstsEnforced = getEffectiveHstsEnforced();
diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java
index 6abfb66..1706e95 100644
--- a/core/java/android/security/net/config/XmlConfigSource.java
+++ b/core/java/android/security/net/config/XmlConfigSource.java
@@ -27,8 +27,13 @@
  * @hide
  */
 public class XmlConfigSource implements ConfigSource {
+    private static final int CONFIG_BASE = 0;
+    private static final int CONFIG_DOMAIN = 1;
+    private static final int CONFIG_DEBUG = 2;
+
     private final Object mLock = new Object();
     private final int mResourceId;
+    private final boolean mDebugBuild;
 
     private boolean mInitialized;
     private NetworkSecurityConfig mDefaultConfig;
@@ -36,8 +41,13 @@
     private Context mContext;
 
     public XmlConfigSource(Context context, int resourceId) {
+        this(context, resourceId, false);
+    }
+
+    public XmlConfigSource(Context context, int resourceId, boolean debugBuild) {
         mResourceId = resourceId;
         mContext = context;
+        mDebugBuild = debugBuild;
     }
 
     public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
@@ -50,6 +60,19 @@
         return mDefaultConfig;
     }
 
+    private static final String getConfigString(int configType) {
+        switch (configType) {
+            case CONFIG_BASE:
+                return "base-config";
+            case CONFIG_DOMAIN:
+                return "domain-config";
+            case CONFIG_DEBUG:
+                return "debug-overrides";
+            default:
+                throw new IllegalArgumentException("Unknown config type: " + configType);
+        }
+    }
+
     private void ensureInitialized() {
         synchronized (mLock) {
             if (mInitialized) {
@@ -147,9 +170,11 @@
         return new Domain(domain, includeSubdomains);
     }
 
-    private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser)
+    private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser,
+            boolean defaultOverridePins)
             throws IOException, XmlPullParserException, ParserException {
-        boolean overridePins = parser.getAttributeBooleanValue(null, "overridePins", false);
+        boolean overridePins =
+                parser.getAttributeBooleanValue(null, "overridePins", defaultOverridePins);
         int sourceId = parser.getAttributeResourceValue(null, "src", -1);
         String sourceString = parser.getAttributeValue(null, "src");
         CertificateSource source = null;
@@ -171,14 +196,15 @@
         return new CertificatesEntryRef(source, overridePins);
     }
 
-    private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser)
+    private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser,
+            boolean defaultOverridePins)
             throws IOException, XmlPullParserException, ParserException {
         int outerDepth = parser.getDepth();
         List<CertificatesEntryRef> anchors = new ArrayList<>();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
             String tagName = parser.getName();
             if (tagName.equals("certificates")) {
-                anchors.add(parseCertificatesEntry(parser));
+                anchors.add(parseCertificatesEntry(parser, defaultOverridePins));
             } else {
                 XmlUtils.skipCurrentTag(parser);
             }
@@ -188,7 +214,7 @@
 
     private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
             XmlResourceParser parser, Set<String> seenDomains,
-            NetworkSecurityConfig.Builder parentBuilder, boolean baseConfig)
+            NetworkSecurityConfig.Builder parentBuilder, int configType)
             throws IOException, XmlPullParserException, ParserException {
         List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
         NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
@@ -196,6 +222,7 @@
         Set<Domain> domains = new ArraySet<>();
         boolean seenPinSet = false;
         boolean seenTrustAnchors = false;
+        boolean defaultOverridePins = configType == CONFIG_DEBUG;
         String configName = parser.getName();
         int outerDepth = parser.getDepth();
         // Add this builder now so that this builder occurs before any of its children. This
@@ -219,8 +246,9 @@
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
             String tagName = parser.getName();
             if ("domain".equals(tagName)) {
-                if (baseConfig) {
-                    throw new ParserException(parser, "domain element not allowed in base-config");
+                if (configType != CONFIG_DOMAIN) {
+                    throw new ParserException(parser,
+                            "domain element not allowed in " + getConfigString(configType));
                 }
                 Domain domain = parseDomain(parser, seenDomains);
                 domains.add(domain);
@@ -229,12 +257,13 @@
                     throw new ParserException(parser,
                             "Multiple trust-anchor elements not allowed");
                 }
-                builder.addCertificatesEntryRefs(parseTrustAnchors(parser));
+                builder.addCertificatesEntryRefs(
+                        parseTrustAnchors(parser, defaultOverridePins));
                 seenTrustAnchors = true;
             } else if ("pin-set".equals(tagName)) {
-                if (baseConfig) {
+                if (configType != CONFIG_DOMAIN) {
                     throw new ParserException(parser,
-                            "pin-set element not allowed in base-config");
+                            "pin-set element not allowed in " + getConfigString(configType));
                 }
                 if (seenPinSet) {
                     throw new ParserException(parser, "Multiple pin-set elements not allowed");
@@ -242,41 +271,68 @@
                 builder.setPinSet(parsePinSet(parser));
                 seenPinSet = true;
             } else if ("domain-config".equals(tagName)) {
-                if (baseConfig) {
+                if (configType != CONFIG_DOMAIN) {
                     throw new ParserException(parser,
-                            "Nested domain-config not allowed in base-config");
+                            "Nested domain-config not allowed in " + getConfigString(configType));
                 }
-                builders.addAll(parseConfigEntry(parser, seenDomains, builder, false));
+                builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType));
             } else {
                 XmlUtils.skipCurrentTag(parser);
             }
         }
-        if (!baseConfig && domains.isEmpty()) {
+        if (configType == CONFIG_DOMAIN && domains.isEmpty()) {
             throw new ParserException(parser, "No domain elements in domain-config");
         }
         return builders;
     }
 
+    private void addDebugAnchorsIfNeeded(NetworkSecurityConfig.Builder debugConfigBuilder,
+            NetworkSecurityConfig.Builder builder) {
+        if (debugConfigBuilder == null || !debugConfigBuilder.hasCertificatesEntryRefs()) {
+            return;
+        }
+        // Don't add trust anchors if not already present, the builder will inherit the anchors
+        // from its parent, and that's where the trust anchors should be added.
+        if (!builder.hasCertificatesEntryRefs()) {
+            return;
+        }
+
+        builder.addCertificatesEntryRefs(debugConfigBuilder.getCertificatesEntryRefs());
+    }
+
     private void parseNetworkSecurityConfig(XmlResourceParser parser)
             throws IOException, XmlPullParserException, ParserException {
         Set<String> seenDomains = new ArraySet<>();
         List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
         NetworkSecurityConfig.Builder baseConfigBuilder = null;
+        NetworkSecurityConfig.Builder debugConfigBuilder = null;
         boolean seenDebugOverrides = false;
         boolean seenBaseConfig = false;
 
         XmlUtils.beginDocument(parser, "network-security-config");
         int outerDepth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-            // TODO: support debug-override.
             if ("base-config".equals(parser.getName())) {
                 if (seenBaseConfig) {
                     throw new ParserException(parser, "Only one base-config allowed");
                 }
                 seenBaseConfig = true;
-                baseConfigBuilder = parseConfigEntry(parser, seenDomains, null, true).get(0).first;
+                baseConfigBuilder =
+                        parseConfigEntry(parser, seenDomains, null, CONFIG_BASE).get(0).first;
             } else if ("domain-config".equals(parser.getName())) {
-                builders.addAll(parseConfigEntry(parser, seenDomains, baseConfigBuilder, false));
+                builders.addAll(
+                        parseConfigEntry(parser, seenDomains, baseConfigBuilder, CONFIG_DOMAIN));
+            } else if ("debug-overrides".equals(parser.getName())) {
+                if (seenDebugOverrides) {
+                    throw new ParserException(parser, "Only one debug-overrides allowed");
+                }
+                if (mDebugBuild) {
+                    debugConfigBuilder =
+                            parseConfigEntry(parser, seenDomains, null, CONFIG_DEBUG).get(0).first;
+                } else {
+                    XmlUtils.skipCurrentTag(parser);
+                }
+                seenDebugOverrides = true;
             } else {
                 XmlUtils.skipCurrentTag(parser);
             }
@@ -286,8 +342,10 @@
         // there. If there is no base config use the platform default.
         NetworkSecurityConfig.Builder platformDefaultBuilder =
                 NetworkSecurityConfig.getDefaultBuilder();
+        addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder);
         if (baseConfigBuilder != null) {
             baseConfigBuilder.setParent(platformDefaultBuilder);
+            addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder);
         } else {
             baseConfigBuilder = platformDefaultBuilder;
         }
@@ -306,6 +364,7 @@
             if (builder.getParent() == null) {
                 builder.setParent(baseConfigBuilder);
             }
+            addDebugAnchorsIfNeeded(debugConfigBuilder, builder);
             NetworkSecurityConfig config = builder.build();
             for (Domain domain : domains) {
                 configs.add(new Pair<>(domain, config));
diff --git a/tests/NetworkSecurityConfigTest/res/raw/test_debug_ca.pem b/tests/NetworkSecurityConfigTest/res/raw/test_debug_ca.pem
new file mode 100644
index 0000000..81648d9
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/raw/test_debug_ca.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAgmgAwIBAgIJAP/YiWztz/J7MA0GCSqGSIb3DQEBCwUAMCcxFjAUBgNV
+BAMMDVRlc3QgZGVidWcgQ0ExDTALBgNVBAoMBEFPU1AwHhcNMTUxMTA5MjEyNjQ2
+WhcNMTgwODI5MjEyNjQ2WjAnMRYwFAYDVQQDDA1UZXN0IGRlYnVnIENBMQ0wCwYD
+VQQKDARBT1NQMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuPFmkOJj
+ehjfvdDr2qTcBWNqNATrW1SuM88Vj00ubUFQ4tZElozj8YnQOw1FeC79c1k88b8R
+6jcqYYp/mw2JYoD6yWcFPHo5BplIpk0EhIUARH/aeoclHvsUN2GGDyTO0vf0CfJn
+9Wp6lSLjyq7V/6tYdk+0cL632t56MHp8TCO+AaveYP1T8JZqx0/50xNcsK7lIqNa
+ctWyRGFxR4ifdVsgkw9WhAB/Ow2uOwN9uLGqzsCd+yXW2weX52EIivoTGZfJo+U8
+Fi0ygnCHBv2jsJA7yWLhHmZ4ijsVtfutIKmN0w+DHkl6S25girXhy0zJp/1QvHGm
+jaF60V1gw471jQIDAQABo1AwTjAdBgNVHQ4EFgQUoq66jncy83L5eeyW1g78s/uq
+iyQwHwYDVR0jBBgwFoAUoq66jncy83L5eeyW1g78s/uqiyQwDAYDVR0TBAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAQEAohytuH4CdX0gO8EGVDRVurRH7LO69lwd/6Iw
+hJ1lIK/mzj5RM2itVGTkintyHCLu5giVkHn4FHg4X9qzZaTPOcXv9ntQNS2nacZe
+bY8nfhsAhstJT4nIOWHE3FrZkMDOK6nZHIzfscX3V/VVq5MeA+WzXwmKp6MBNr+E
+oUegXCGjd26Bl6SFz3rD7Qh+dzSTtyf/ECzXaMjpZu3k6fb4EgRz6vdBCHKKtpv6
+Mxcr0nLwdI6LnAGXvJLV4sj+l6Ngg00EeyorG8ATgtmsUrXXOR1e+yDCQv6fjQfs
+CWYztECAUE9hfCXJwb0TBrq9YeJAvcO7iE6S0Pq+X3xNtetE1A==
+-----END CERTIFICATE-----
diff --git a/tests/NetworkSecurityConfigTest/res/xml/debug_basic.xml b/tests/NetworkSecurityConfigTest/res/xml/debug_basic.xml
new file mode 100644
index 0000000..8da9317
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/debug_basic.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <debug-overrides>
+    <trust-anchors>
+      <certificates src="system" />
+    </trust-anchors>
+  </debug-overrides>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/debug_domain.xml b/tests/NetworkSecurityConfigTest/res/xml/debug_domain.xml
new file mode 100644
index 0000000..24eed7a
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/debug_domain.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="@raw/ca_certs_pem" />
+    </trust-anchors>
+  </domain-config>
+  <debug-overrides>
+    <trust-anchors>
+      <certificates src="@raw/test_debug_ca" />
+    </trust-anchors>
+  </debug-overrides>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/debug_inherit.xml b/tests/NetworkSecurityConfigTest/res/xml/debug_inherit.xml
new file mode 100644
index 0000000..ce0cbc8
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/debug_inherit.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <debug-overrides>
+    <trust-anchors>
+      <certificates src="@raw/test_debug_ca" />
+    </trust-anchors>
+  </debug-overrides>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
index f52a279..43fa830 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -26,6 +26,7 @@
 import java.net.URL;
 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;
@@ -33,6 +34,8 @@
 
 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);
@@ -274,6 +277,68 @@
         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<TrustAnchor> 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<TrustAnchor> 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<TrustAnchor> 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<TrustAnchor> 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);