Add xml source for network security configuration

XmlConfigSource parses an ApplicationConfig from an xml resource.
Currently this supports app-wide default configuration via the
base-config element, per domain via the domain-config element and
inheritance of unset properties at parse time.

Inheritance of unset properties is currently only:
domain-config -> base-config -> platform default configuration
Where the most specific value is used.
For example: If the base-config specifies trust anchors, all connections
will use those anchors except for connections to a domain which has a
domain-config that specifies trust anchors, in which case the
domain-config's trust anchors will be used. If the domain-config or
base-config don't set trust anchors, or don't exist, then the platform
default trust anchors will be used.

Nested domain-config entries, debug-overrides, and thorough
documentation of the xml format will follow in later commits.

Change-Id: I1232ff1e8079a81b340bc12e142f0889f6947aa0
diff --git a/core/java/android/security/net/config/Pin.java b/core/java/android/security/net/config/Pin.java
index 8567431..94520e2 100644
--- a/core/java/android/security/net/config/Pin.java
+++ b/core/java/android/security/net/config/Pin.java
@@ -30,6 +30,26 @@
         this.digest = digest;
         mHashCode = Arrays.hashCode(digest) ^ digestAlgorithm.hashCode();
     }
+
+    /**
+     * @hide
+     */
+    public static boolean isSupportedDigestAlgorithm(String algorithm) {
+        // Currently only SHA-256 is supported. SHA-512 if/once Chromium networking stack
+        // supports it.
+        return "SHA-256".equalsIgnoreCase(algorithm);
+    }
+
+    /**
+     * @hide
+     */
+    public static int getDigestLength(String algorithm) {
+        if ("SHA-256".equalsIgnoreCase(algorithm)) {
+            return 32;
+        }
+        throw new IllegalArgumentException("Unsupported digest algorithm: " + algorithm);
+    }
+
     @Override
     public int hashCode() {
         return mHashCode;
diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java
new file mode 100644
index 0000000..77c9bb6
--- /dev/null
+++ b/core/java/android/security/net/config/XmlConfigSource.java
@@ -0,0 +1,310 @@
+package android.security.net.config;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.util.ArraySet;
+import android.util.Base64;
+import android.util.Pair;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * {@link ConfigSource} based on an XML configuration file.
+ *
+ * @hide
+ */
+public class XmlConfigSource implements ConfigSource {
+    private final Object mLock = new Object();
+    private final int mResourceId;
+
+    private boolean mInitialized;
+    private NetworkSecurityConfig mDefaultConfig;
+    private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap;
+    private Context mContext;
+
+    public XmlConfigSource(Context context, int resourceId) {
+        mResourceId = resourceId;
+        mContext = context;
+    }
+
+    public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
+        ensureInitialized();
+        return mDomainMap;
+    }
+
+    public NetworkSecurityConfig getDefaultConfig() {
+        ensureInitialized();
+        return mDefaultConfig;
+    }
+
+    private void ensureInitialized() {
+        synchronized (mLock) {
+            if (mInitialized) {
+                return;
+            }
+            try (XmlResourceParser parser = mContext.getResources().getXml(mResourceId)) {
+                parseNetworkSecurityConfig(parser);
+                mContext = null;
+                mInitialized = true;
+            } catch (Resources.NotFoundException | XmlPullParserException | IOException
+                    | ParserException e) {
+                throw new RuntimeException("Failed to parse XML configuration from "
+                        + mContext.getResources().getResourceEntryName(mResourceId), e);
+            }
+        }
+    }
+
+    private Pin parsePin(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        String digestAlgorithm = parser.getAttributeValue(null, "digest");
+        if (!Pin.isSupportedDigestAlgorithm(digestAlgorithm)) {
+            throw new ParserException(parser, "Unsupported pin digest algorithm: "
+                    + digestAlgorithm);
+        }
+        if (parser.next() != XmlPullParser.TEXT) {
+            throw new ParserException(parser, "Missing pin digest");
+        }
+        String digest = parser.getText();
+        byte[] decodedDigest = null;
+        try {
+            decodedDigest = Base64.decode(digest, 0);
+        } catch (IllegalArgumentException e) {
+            throw new ParserException(parser, "Invalid pin digest", e);
+        }
+        int expectedLength = Pin.getDigestLength(digestAlgorithm);
+        if (decodedDigest.length != expectedLength) {
+            throw new ParserException(parser, "digest length " + decodedDigest.length
+                    + " does not match expected length for " + digestAlgorithm + " of "
+                    + expectedLength);
+        }
+        if (parser.next() != XmlPullParser.END_TAG) {
+            throw new ParserException(parser, "pin contains additional elements");
+        }
+        return new Pin(digestAlgorithm, decodedDigest);
+    }
+
+    private PinSet parsePinSet(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        String expirationDate = parser.getAttributeValue(null, "expiration");
+        long expirationTimestampMilis = Long.MAX_VALUE;
+        if (expirationDate != null) {
+            try {
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                sdf.setLenient(false);
+                Date date = sdf.parse(expirationDate);
+                if (date == null) {
+                    throw new ParserException(parser, "Invalid expiration date in pin-set");
+                }
+                expirationTimestampMilis = date.getTime();
+            } catch (ParseException e) {
+                throw new ParserException(parser, "Invalid expiration date in pin-set", e);
+            }
+        }
+
+        int outerDepth = parser.getDepth();
+        Set<Pin> pins = new ArraySet<>();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tagName = parser.getName();
+            if (tagName.equals("pin")) {
+                pins.add(parsePin(parser));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        return new PinSet(pins, expirationTimestampMilis);
+    }
+
+    private Domain parseDomain(XmlResourceParser parser, Set<String> seenDomains)
+            throws IOException, XmlPullParserException, ParserException {
+        boolean includeSubdomains =
+                parser.getAttributeBooleanValue(null, "includeSubdomains", false);
+        if (parser.next() != XmlPullParser.TEXT) {
+            throw new ParserException(parser, "Domain name missing");
+        }
+        String domain = parser.getText().toLowerCase(Locale.US);
+        if (parser.next() != XmlPullParser.END_TAG) {
+            throw new ParserException(parser, "domain contains additional elements");
+        }
+        // Domains are matched using a most specific match, so don't allow duplicates.
+        // includeSubdomains isn't relevant here, both android.com + subdomains and android.com
+        // match for android.com equally. Do not allow any duplicates period.
+        if (!seenDomains.add(domain)) {
+            throw new ParserException(parser, domain + " has already been specified");
+        }
+        return new Domain(domain, includeSubdomains);
+    }
+
+    private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser)
+            throws IOException, XmlPullParserException, ParserException {
+        boolean overridePins = parser.getAttributeBooleanValue(null, "overridePins", false);
+        int sourceId = parser.getAttributeResourceValue(null, "src", -1);
+        String sourceString = parser.getAttributeValue(null, "src");
+        CertificateSource source = null;
+        if (sourceString == null) {
+            throw new ParserException(parser, "certificates element missing src attribute");
+        }
+        if (sourceId != -1) {
+            // TODO: Cache ResourceCertificateSources by sourceId
+            source = new ResourceCertificateSource(sourceId, mContext);
+        } else if ("system".equals(sourceString)) {
+            source = SystemCertificateSource.getInstance();
+        } else if ("user".equals(sourceString)) {
+            source = UserCertificateSource.getInstance();
+        } else {
+            throw new ParserException(parser, "Unknown certificates src. "
+                    + "Should be one of system|user|@resourceVal");
+        }
+        XmlUtils.skipCurrentTag(parser);
+        return new CertificatesEntryRef(source, overridePins);
+    }
+
+    private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser)
+            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));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        return anchors;
+    }
+
+    private Pair<NetworkSecurityConfig.Builder, Set<Domain>> parseConfigEntry(
+            XmlResourceParser parser, Set<String> seenDomains, boolean baseConfig)
+            throws IOException, XmlPullParserException, ParserException {
+        NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
+        Set<Domain> domains = new ArraySet<>();
+        boolean seenPinSet = false;
+        boolean seenTrustAnchors = false;
+        String configName = parser.getName();
+        int outerDepth = parser.getDepth();
+        // Parse config attributes. Only set values that are present, config inheritence will
+        // handle the rest.
+        for (int i = 0; i < parser.getAttributeCount(); i++) {
+            String name = parser.getAttributeName(i);
+            if ("hstsEnforced".equals(name)) {
+                builder.setHstsEnforced(
+                        parser.getAttributeBooleanValue(i,
+                                NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED));
+            } else if ("cleartextTrafficPermitted".equals(name)) {
+                builder.setCleartextTrafficPermitted(
+                        parser.getAttributeBooleanValue(i,
+                                NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED));
+            }
+        }
+        // Parse the config elements.
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            String tagName = parser.getName();
+            // TODO: Support nested domain-config entries.
+            if ("domain".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser, "domain element not allowed in base-config");
+                }
+                Domain domain = parseDomain(parser, seenDomains);
+                domains.add(domain);
+            } else if ("trust-anchors".equals(tagName)) {
+                if (seenTrustAnchors) {
+                    throw new ParserException(parser,
+                            "Multiple trust-anchor elements not allowed");
+                }
+                builder.addCertificatesEntryRefs(parseTrustAnchors(parser));
+                seenTrustAnchors = true;
+            } else if ("pin-set".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser,
+                            "pin-set element not allowed in base-config");
+                }
+                if (seenPinSet) {
+                    throw new ParserException(parser, "Multiple pin-set elements not allowed");
+                }
+                builder.setPinSet(parsePinSet(parser));
+                seenPinSet = true;
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        if (!baseConfig && domains.isEmpty()) {
+            throw new ParserException(parser, "No domain elements in domain-config");
+        }
+        return new Pair<>(builder, domains);
+    }
+
+    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;
+        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, true).first;
+            } else if ("domain-config".equals(parser.getName())) {
+                builders.add(parseConfigEntry(parser, seenDomains, false));
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        // Use the platform default as the parent of the base config for any values not provided
+        // there. If there is no base config use the platform default.
+        NetworkSecurityConfig.Builder platformDefaultBuilder =
+                NetworkSecurityConfig.getDefaultBuilder();
+        if (baseConfigBuilder != null) {
+            baseConfigBuilder.setParent(platformDefaultBuilder);
+        } else {
+            baseConfigBuilder = platformDefaultBuilder;
+        }
+        // Build the per-domain config mapping.
+        Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>();
+
+        for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
+            NetworkSecurityConfig.Builder builder = entry.first;
+            Set<Domain> domains = entry.second;
+            // Use the base-config for inheriting any unset values in the domain-config entry.
+            builder.setParent(baseConfigBuilder);
+            NetworkSecurityConfig config = builder.build();
+            for (Domain domain : domains) {
+                configs.add(new Pair<>(domain, config));
+            }
+        }
+        mDefaultConfig = baseConfigBuilder.build();
+        mDomainMap = configs;
+    }
+
+    public static class ParserException extends Exception {
+
+        public ParserException(XmlPullParser parser, String message, Throwable cause) {
+            super(message + " at: " + parser.getPositionDescription(), cause);
+        }
+
+        public ParserException(XmlPullParser parser, String message) {
+            this(parser, message, null);
+        }
+    }
+}
diff --git a/tests/NetworkSecurityConfigTest/AndroidManifest.xml b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
index 811a3f4..4c1fbd3 100644
--- a/tests/NetworkSecurityConfigTest/AndroidManifest.xml
+++ b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.security.tests"
+          package="android.security.net.config"
           android:sharedUserId="android.uid.system">
 
     <application>
@@ -23,7 +23,7 @@
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
-        android:targetPackage="android.security.tests"
+        android:targetPackage="android.security.net.config"
         android:label="ANSC Tests">
     </instrumentation>
 </manifest>
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
new file mode 100644
index 0000000..235bd47
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
Binary files differ
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
new file mode 100644
index 0000000..413e3c0
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT
+MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0
+aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw
+WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE
+AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m
+OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu
+T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c
+JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR
+Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz
+PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm
+aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM
+TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g
+LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO
+BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv
+dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB
+AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL
+NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W
+b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
+cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
+MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
+BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
+YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
+BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
+I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
+CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i
+2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ
+2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ
+-----END CERTIFICATE-----
diff --git a/tests/NetworkSecurityConfigTest/res/xml/attributes.xml b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml
new file mode 100644
index 0000000..eff13c8
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config cleartextTrafficPermitted="false" hstsEnforced="true">
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml
new file mode 100644
index 0000000..6af855d
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <!-- Bad pin digest -->
+      <pin digest="I am probably not an algorithm">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml
new file mode 100644
index 0000000..d683b74a
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+    <!-- Unknown pin digest -->
+      <pin>1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml
new file mode 100644
index 0000000..6f3f8b4
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <!-- empty digest -->
+      <pin digest="SHA-256"></pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml
new file mode 100644
index 0000000..fb2126c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+  </domain-config>
+  <domain-config>
+    <!-- Same domain name used in two configs -->
+    <domain>android.com</domain>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml
new file mode 100644
index 0000000..95972ce
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <!-- domains are not allowed in base-config -->
+    <domain>android.com</domain>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml
new file mode 100644
index 0000000..8b6b721
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <!-- pins are not allowed in base-config -->
+    <pin-set>
+    </pin-set>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml
new file mode 100644
index 0000000..62a7b88
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain1.xml b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml
new file mode 100644
index 0000000..6d8565c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml
new file mode 100644
index 0000000..1bd94b6
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml
new file mode 100644
index 0000000..8093b9d
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml
new file mode 100644
index 0000000..f9f8465
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <!-- Invalid pin that has expired -->
+    <pin-set expiration="2015-01-01">
+      <pin digest="SHA-256">aaaaaaaaaaa2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml
new file mode 100644
index 0000000..df08467
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+  <domain-config>
+    <domain>google.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml
new file mode 100644
index 0000000..9743c5f
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <domain>google.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml
new file mode 100644
index 0000000..785714a
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">aaaaaaaaIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+    <trust-anchors>
+      <certificates src="system" overridePins="true" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
new file mode 100644
index 0000000..1773d280
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <pin-set>
+      <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+    </pin-set>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml
new file mode 100644
index 0000000..dfd6fd9
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="@raw/ca_certs_der" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml
new file mode 100644
index 0000000..894f29b
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain>android.com</domain>
+    <trust-anchors>
+      <certificates src="@raw/ca_certs_pem" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml
new file mode 100644
index 0000000..482b26c
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <base-config>
+    <trust-anchors>
+    </trust-anchors>
+  </base-config>
+  <domain-config>
+    <domain includeSubdomains="true">android.com</domain>
+    <trust-anchors>
+      <certificates src="system" />
+      <certificates src="user" />
+    </trust-anchors>
+  </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
index 11d8136..9f48d56 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
@@ -50,49 +50,6 @@
         return data;
     }
 
-    private void assertConnectionFails(SSLContext context, String host, int port)
-            throws Exception {
-        try {
-            Socket s = context.getSocketFactory().createSocket(host, port);
-            s.getInputStream();
-            fail("Expected connection to " + host + ":" + port + " to fail.");
-        } catch (SSLHandshakeException expected) {
-        }
-    }
-
-    private void assertConnectionSucceeds(SSLContext context, String host, int port)
-            throws Exception {
-        Socket s = context.getSocketFactory().createSocket(host, port);
-        s.getInputStream();
-    }
-
-    private void assertUrlConnectionFails(SSLContext context, String host, int port)
-            throws Exception {
-        URL url = new URL("https://" + host + ":" + port);
-        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
-        connection.setSSLSocketFactory(context.getSocketFactory());
-        try {
-            connection.getInputStream();
-            fail("Connection to " + host + ":" + port + " expected to fail");
-        } catch (SSLHandshakeException expected) {
-            // ignored.
-        }
-    }
-
-    private void assertUrlConnectionSucceeds(SSLContext context, String host, int port)
-            throws Exception {
-        URL url = new URL("https://" + host + ":" + port);
-        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
-        connection.setSSLSocketFactory(context.getSocketFactory());
-        connection.getInputStream();
-    }
-
-    private SSLContext getSSLContext(ConfigSource source) throws Exception {
-        ApplicationConfig config = new ApplicationConfig(source);
-        SSLContext context = SSLContext.getInstance("TLS");
-        context.init(null, new TrustManager[] {config.getTrustManager()}, null);
-        return context;
-    }
 
 
     /**
@@ -115,8 +72,8 @@
                 = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         ConfigSource testSource =
                 new TestConfigSource(domainMap, getEmptyConfig());
-        SSLContext context = getSSLContext(testSource);
-        assertConnectionFails(context, "android.com", 443);
+        SSLContext context = TestUtils.getSSLContext(testSource);
+        TestUtils.assertConnectionFails(context, "android.com", 443);
     }
 
     public void testEmptyPerNetworkSecurityConfig() throws Exception {
@@ -125,9 +82,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), getEmptyConfig()));
         NetworkSecurityConfig defaultConfig = getSystemStoreConfig();
-        SSLContext context = getSSLContext(new TestConfigSource(domainMap, defaultConfig));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "google.com", 443);
+        SSLContext context = TestUtils.getSSLContext(new TestConfigSource(domainMap, defaultConfig));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
     }
 
     public void testBadPin() throws Exception {
@@ -143,9 +100,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "google.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "google.com", 443);
     }
 
     public void testGoodPin() throws Exception {
@@ -161,9 +118,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "android.com", 443);
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
     }
 
     public void testOverridePins() throws Exception {
@@ -180,8 +137,8 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
     }
 
     public void testMostSpecificNetworkSecurityConfig() throws Exception {
@@ -192,9 +149,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("developer.android.com", false), getSystemStoreConfig()));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionFails(context, "android.com", 443);
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionFails(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
     }
 
     public void testSubdomainIncluded() throws Exception {
@@ -204,14 +161,14 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), getSystemStoreConfig()));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionSucceeds(context, "developer.android.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
         // Now try without including subdomains.
         domainMap = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", false), getSystemStoreConfig()));
-        context = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertConnectionFails(context, "developer.android.com", 443);
+        context = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertConnectionFails(context, "developer.android.com", 443);
     }
 
     public void testConfigBuilderUsesParents() throws Exception {
@@ -246,9 +203,9 @@
         domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
                 new Domain("android.com", true), domain));
         SSLContext context
-                = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
-        assertUrlConnectionSucceeds(context, "android.com", 443);
-        assertUrlConnectionSucceeds(context, "developer.android.com", 443);
-        assertUrlConnectionFails(context, "google.com", 443);
+                = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+        TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443);
+        TestUtils.assertUrlConnectionFails(context, "google.com", 443);
     }
 }
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
new file mode 100644
index 0000000..43c0e57
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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 java.net.Socket;
+import java.net.URL;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManager;
+
+import junit.framework.Assert;
+
+public final class TestUtils extends Assert {
+
+    private TestUtils() {
+    }
+
+    public static void assertConnectionFails(SSLContext context, String host, int port)
+            throws Exception {
+        try {
+            Socket s = context.getSocketFactory().createSocket(host, port);
+            s.getInputStream();
+            fail("Expected connection to " + host + ":" + port + " to fail.");
+        } catch (SSLHandshakeException expected) {
+        }
+    }
+
+    public static void assertConnectionSucceeds(SSLContext context, String host, int port)
+            throws Exception {
+        Socket s = context.getSocketFactory().createSocket(host, port);
+        s.getInputStream();
+    }
+
+    public static void assertUrlConnectionFails(SSLContext context, String host, int port)
+            throws Exception {
+        URL url = new URL("https://" + host + ":" + port);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        connection.setSSLSocketFactory(context.getSocketFactory());
+        try {
+            connection.getInputStream();
+            fail("Connection to " + host + ":" + port + " expected to fail");
+        } catch (SSLHandshakeException expected) {
+            // ignored.
+        }
+    }
+
+    public static void assertUrlConnectionSucceeds(SSLContext context, String host, int port)
+            throws Exception {
+        URL url = new URL("https://" + host + ":" + port);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        connection.setSSLSocketFactory(context.getSocketFactory());
+        connection.getInputStream();
+    }
+
+    public static SSLContext getSSLContext(ConfigSource source) throws Exception {
+        ApplicationConfig config = new ApplicationConfig(source);
+        SSLContext context = SSLContext.getInstance("TLS");
+        context.init(null, new TrustManager[] {config.getTrustManager()}, null);
+        return context;
+    }
+}
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
new file mode 100644
index 0000000..4914d06
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -0,0 +1,284 @@
+/*
+ * 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.Socket;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManager;
+
+public class XmlConfigTests extends AndroidTestCase {
+
+    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);
+    }
+
+    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);
+    }
+
+    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);
+    }
+}