Add initial network security config implementation
Initial implementation of a unified application wide static
network security configuration.
This currently encompases:
* Trust decisions such as what trust anchors to use as well as static
certificate pinning.
* Policy on what to do with cleartext traffic.
In order to prevent issues due to interplay of various components in an
application and their potentially different security requirements
configuration can be specified at a per-domain granularity in addition
to application wide defaults.
This change contains the internal data structures and trust management
code, hooking these up in application startup will come in a future
commit.
Change-Id: I53ce5ba510a4221d58839e61713262a8f4c6699c
diff --git a/core/java/android/security/net/config/ApplicationConfig.java b/core/java/android/security/net/config/ApplicationConfig.java
new file mode 100644
index 0000000..c675352
--- /dev/null
+++ b/core/java/android/security/net/config/ApplicationConfig.java
@@ -0,0 +1,132 @@
+/*
+ * 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.util.Pair;
+import java.util.Locale;
+import java.util.Set;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * An application's network security configuration.
+ *
+ * <p>{@link #getConfigForHostname(String)} provides a means to obtain network security
+ * configuration to be used for communicating with a specific hostname.</p>
+ *
+ * @hide
+ */
+public final class ApplicationConfig {
+ private Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
+ private NetworkSecurityConfig mDefaultConfig;
+ private X509TrustManager mTrustManager;
+
+ private ConfigSource mConfigSource;
+ private boolean mInitialized;
+ private final Object mLock = new Object();
+
+ public ApplicationConfig(ConfigSource configSource) {
+ mConfigSource = configSource;
+ mInitialized = false;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean hasPerDomainConfigs() {
+ ensureInitialized();
+ return mConfigs == null || !mConfigs.isEmpty();
+ }
+
+ /**
+ * Get the {@link NetworkSecurityConfig} corresponding to the provided hostname.
+ * When matching the most specific matching domain rule will be used, if no match exists
+ * then the default configuration will be returned.
+ *
+ * {@code NetworkSecurityConfig} objects returned by this method can be safely cached for
+ * {@code hostname}. Subsequent calls with the same hostname will always return the same
+ * {@code NetworkSecurityConfig}.
+ *
+ * @return {@link NetworkSecurityConfig} to be used to determine
+ * the network security configuration for connections to {@code hostname}.
+ */
+ public NetworkSecurityConfig getConfigForHostname(String hostname) {
+ ensureInitialized();
+ if (hostname.isEmpty() || mConfigs == null) {
+ return mDefaultConfig;
+ }
+ if (hostname.charAt(0) == '.') {
+ throw new IllegalArgumentException("hostname must not begin with a .");
+ }
+ // Domains are case insensitive.
+ hostname = hostname.toLowerCase(Locale.US);
+ // Normalize hostname by removing trailing . if present, all Domain hostnames are
+ // absolute.
+ if (hostname.charAt(hostname.length() - 1) == '.') {
+ hostname = hostname.substring(0, hostname.length() - 1);
+ }
+ // Find the Domain -> NetworkSecurityConfig entry with the most specific matching
+ // Domain entry for hostname.
+ // TODO: Use a smarter data structure for the lookup.
+ Pair<Domain, NetworkSecurityConfig> bestMatch = null;
+ for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
+ Domain domain = entry.first;
+ NetworkSecurityConfig config = entry.second;
+ // Check for an exact match.
+ if (domain.hostname.equals(hostname)) {
+ return config;
+ }
+ // Otherwise check if the Domain includes sub-domains and that the hostname is a
+ // sub-domain of the Domain.
+ if (domain.subdomainsIncluded
+ && hostname.endsWith(domain.hostname)
+ && hostname.charAt(hostname.length() - domain.hostname.length() - 1) == '.') {
+ if (bestMatch == null) {
+ bestMatch = entry;
+ } else if (domain.hostname.length() > bestMatch.first.hostname.length()) {
+ bestMatch = entry;
+ }
+ }
+ }
+ if (bestMatch != null) {
+ return bestMatch.second;
+ }
+ // If no match was found use the default configuration.
+ return mDefaultConfig;
+ }
+
+ /**
+ * Returns the {@link X509TrustManager} that implements the checking of trust anchors and
+ * certificate pinning based on this configuration.
+ */
+ public X509TrustManager getTrustManager() {
+ ensureInitialized();
+ return mTrustManager;
+ }
+
+ private void ensureInitialized() {
+ synchronized(mLock) {
+ if (mInitialized) {
+ return;
+ }
+ mConfigs = mConfigSource.getPerDomainConfigs();
+ mDefaultConfig = mConfigSource.getDefaultConfig();
+ mConfigSource = null;
+ mTrustManager = new RootTrustManager(this);
+ mInitialized = true;
+ }
+ }
+}
diff --git a/core/java/android/security/net/config/CertificateSource.java b/core/java/android/security/net/config/CertificateSource.java
new file mode 100644
index 0000000..386354d
--- /dev/null
+++ b/core/java/android/security/net/config/CertificateSource.java
@@ -0,0 +1,25 @@
+/*
+ * 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.util.Set;
+import java.security.cert.X509Certificate;
+
+/** @hide */
+public interface CertificateSource {
+ Set<X509Certificate> getCertificates();
+}
diff --git a/core/java/android/security/net/config/CertificatesEntryRef.java b/core/java/android/security/net/config/CertificatesEntryRef.java
new file mode 100644
index 0000000..2ba38c21
--- /dev/null
+++ b/core/java/android/security/net/config/CertificatesEntryRef.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util.ArraySet;
+import java.util.Set;
+import java.security.cert.X509Certificate;
+
+/** @hide */
+public final class CertificatesEntryRef {
+ private final CertificateSource mSource;
+ private final boolean mOverridesPins;
+
+ public CertificatesEntryRef(CertificateSource source, boolean overridesPins) {
+ mSource = source;
+ mOverridesPins = overridesPins;
+ }
+
+ public Set<TrustAnchor> getTrustAnchors() {
+ // TODO: cache this [but handle mutable sources]
+ Set<TrustAnchor> anchors = new ArraySet<TrustAnchor>();
+ for (X509Certificate cert : mSource.getCertificates()) {
+ anchors.add(new TrustAnchor(cert, mOverridesPins));
+ }
+ return anchors;
+ }
+}
diff --git a/core/java/android/security/net/config/ConfigSource.java b/core/java/android/security/net/config/ConfigSource.java
new file mode 100644
index 0000000..4adf265
--- /dev/null
+++ b/core/java/android/security/net/config/ConfigSource.java
@@ -0,0 +1,26 @@
+/*
+ * 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.util.Pair;
+import java.util.Set;
+
+/** @hide */
+public interface ConfigSource {
+ Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs();
+ NetworkSecurityConfig getDefaultConfig();
+}
diff --git a/core/java/android/security/net/config/Domain.java b/core/java/android/security/net/config/Domain.java
new file mode 100644
index 0000000..5bb727a3
--- /dev/null
+++ b/core/java/android/security/net/config/Domain.java
@@ -0,0 +1,57 @@
+/*
+ * 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.util.Locale;
+/** @hide */
+public final class Domain {
+ /**
+ * Lower case hostname for this domain rule.
+ */
+ public final String hostname;
+
+ /**
+ * Whether this domain includes subdomains.
+ */
+ public final boolean subdomainsIncluded;
+
+ public Domain(String hostname, boolean subdomainsIncluded) {
+ if (hostname == null) {
+ throw new NullPointerException("Hostname must not be null");
+ }
+ this.hostname = hostname.toLowerCase(Locale.US);
+ this.subdomainsIncluded = subdomainsIncluded;
+ }
+
+ @Override
+ public int hashCode() {
+ return hostname.hashCode() ^ (subdomainsIncluded ? 1231 : 1237);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof Domain)) {
+ return false;
+ }
+ Domain otherDomain = (Domain) other;
+ return otherDomain.subdomainsIncluded == this.subdomainsIncluded &&
+ otherDomain.hostname.equals(this.hostname);
+ }
+}
diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java
new file mode 100644
index 0000000..915fbef
--- /dev/null
+++ b/core/java/android/security/net/config/NetworkSecurityConfig.java
@@ -0,0 +1,86 @@
+/*
+ * 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.util.ArraySet;
+import java.util.List;
+import java.util.Set;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * @hide
+ */
+public final class NetworkSecurityConfig {
+ private final boolean mCleartextTrafficPermitted;
+ private final boolean mHstsEnforced;
+ private final PinSet mPins;
+ private final List<CertificatesEntryRef> mCertificatesEntryRefs;
+ private Set<TrustAnchor> mAnchors;
+ private final Object mAnchorsLock = new Object();
+ private X509TrustManager mTrustManager;
+ private final Object mTrustManagerLock = new Object();
+
+ public NetworkSecurityConfig(boolean cleartextTrafficPermitted, boolean hstsEnforced,
+ PinSet pins, List<CertificatesEntryRef> certificatesEntryRefs) {
+ mCleartextTrafficPermitted = cleartextTrafficPermitted;
+ mHstsEnforced = hstsEnforced;
+ mPins = pins;
+ mCertificatesEntryRefs = certificatesEntryRefs;
+ }
+
+ public Set<TrustAnchor> getTrustAnchors() {
+ synchronized (mAnchorsLock) {
+ if (mAnchors != null) {
+ return mAnchors;
+ }
+ Set<TrustAnchor> anchors = new ArraySet<TrustAnchor>();
+ for (CertificatesEntryRef ref : mCertificatesEntryRefs) {
+ anchors.addAll(ref.getTrustAnchors());
+ }
+ mAnchors = anchors;
+ return anchors;
+ }
+ }
+
+ public boolean isCleartextTrafficPermitted() {
+ return mCleartextTrafficPermitted;
+ }
+
+ public boolean isHstsEnforced() {
+ return mHstsEnforced;
+ }
+
+ public PinSet getPins() {
+ return mPins;
+ }
+
+ public X509TrustManager getTrustManager() {
+ synchronized(mTrustManagerLock) {
+ if (mTrustManager == null) {
+ mTrustManager = new NetworkSecurityTrustManager(this);
+ }
+ return mTrustManager;
+ }
+ }
+
+ void onTrustStoreChange() {
+ synchronized (mAnchorsLock) {
+ mAnchors = null;
+ }
+ }
+}
diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
new file mode 100644
index 0000000..e69082d
--- /dev/null
+++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
@@ -0,0 +1,135 @@
+/*
+ * 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 com.android.org.conscrypt.TrustManagerImpl;
+
+import android.util.ArrayMap;
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * {@link X509TrustManager} that implements the trust anchor and pinning for a
+ * given {@link NetworkSecurityConfig}.
+ * @hide
+ */
+public class NetworkSecurityTrustManager implements X509TrustManager {
+ // TODO: Replace this with a general X509TrustManager and use duck-typing.
+ private final TrustManagerImpl mDelegate;
+ private final NetworkSecurityConfig mNetworkSecurityConfig;
+
+ public NetworkSecurityTrustManager(NetworkSecurityConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config must not be null");
+ }
+ mNetworkSecurityConfig = config;
+ // TODO: Create our own better KeyStoreImpl
+ try {
+ KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
+ store.load(null);
+ int certNum = 0;
+ for (TrustAnchor anchor : mNetworkSecurityConfig.getTrustAnchors()) {
+ store.setEntry(String.valueOf(certNum++),
+ new KeyStore.TrustedCertificateEntry(anchor.certificate),
+ null);
+ }
+ mDelegate = new TrustManagerImpl(store);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ throw new CertificateException("Client authentication not supported");
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ List<X509Certificate> trustedChain =
+ mDelegate.checkServerTrusted(certs, authType, (String) null);
+ checkPins(trustedChain);
+ }
+
+ private void checkPins(List<X509Certificate> chain) throws CertificateException {
+ PinSet pinSet = mNetworkSecurityConfig.getPins();
+ if (pinSet.pins.isEmpty()
+ || System.currentTimeMillis() > pinSet.expirationTime
+ || !isPinningEnforced(chain)) {
+ return;
+ }
+ Set<String> pinAlgorithms = pinSet.getPinAlgorithms();
+ Map<String, MessageDigest> digestMap = new ArrayMap<String, MessageDigest>(
+ pinAlgorithms.size());
+ for (int i = chain.size() - 1; i >= 0 ; i--) {
+ X509Certificate cert = chain.get(i);
+ byte[] encodedSPKI = cert.getPublicKey().getEncoded();
+ for (String algorithm : pinAlgorithms) {
+ MessageDigest md = digestMap.get(algorithm);
+ if (md == null) {
+ try {
+ md = MessageDigest.getInstance(algorithm);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ digestMap.put(algorithm, md);
+ }
+ if (pinSet.pins.contains(new Pin(algorithm, md.digest(encodedSPKI)))) {
+ return;
+ }
+ }
+ }
+
+ // TODO: Throw a subclass of CertificateException which indicates a pinning failure.
+ throw new CertificateException("Pin verification failed");
+ }
+
+ private boolean isPinningEnforced(List<X509Certificate> chain) throws CertificateException {
+ if (chain.isEmpty()) {
+ return false;
+ }
+ X509Certificate anchorCert = chain.get(chain.size() - 1);
+ TrustAnchor chainAnchor = null;
+ // TODO: faster lookup
+ for (TrustAnchor anchor : mNetworkSecurityConfig.getTrustAnchors()) {
+ if (anchor.certificate.equals(anchorCert)) {
+ chainAnchor = anchor;
+ break;
+ }
+ }
+ if (chainAnchor == null) {
+ throw new CertificateException("Trusted chain does not end in a TrustAnchor");
+ }
+ return !chainAnchor.overridesPins;
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+}
diff --git a/core/java/android/security/net/config/Pin.java b/core/java/android/security/net/config/Pin.java
new file mode 100644
index 0000000..8567431
--- /dev/null
+++ b/core/java/android/security/net/config/Pin.java
@@ -0,0 +1,58 @@
+/*
+ * 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.util.Arrays;
+
+/** @hide */
+public final class Pin {
+ public final String digestAlgorithm;
+ public final byte[] digest;
+
+ private final int mHashCode;
+
+ public Pin(String digestAlgorithm, byte[] digest) {
+ this.digestAlgorithm = digestAlgorithm;
+ this.digest = digest;
+ mHashCode = Arrays.hashCode(digest) ^ digestAlgorithm.hashCode();
+ }
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Pin)) {
+ return false;
+ }
+ Pin other = (Pin) obj;
+ if (other.hashCode() != mHashCode) {
+ return false;
+ }
+ if (!Arrays.equals(digest, other.digest)) {
+ return false;
+ }
+ if (!digestAlgorithm.equals(other.digestAlgorithm)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/core/java/android/security/net/config/PinSet.java b/core/java/android/security/net/config/PinSet.java
new file mode 100644
index 0000000..a9ee039
--- /dev/null
+++ b/core/java/android/security/net/config/PinSet.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util.ArraySet;
+import java.util.Set;
+
+/** @hide */
+public final class PinSet {
+ public final long expirationTime;
+ public final Set<Pin> pins;
+
+ public PinSet(Set<Pin> pins, long expirationTime) {
+ if (pins == null) {
+ throw new NullPointerException("pins must not be null");
+ }
+ this.pins = pins;
+ this.expirationTime = expirationTime;
+ }
+
+ Set<String> getPinAlgorithms() {
+ // TODO: Cache this.
+ Set<String> algorithms = new ArraySet<String>();
+ for (Pin pin : pins) {
+ algorithms.add(pin.digestAlgorithm);
+ }
+ return algorithms;
+ }
+}
diff --git a/core/java/android/security/net/config/ResourceCertificateSource.java b/core/java/android/security/net/config/ResourceCertificateSource.java
new file mode 100644
index 0000000..06dd9d4
--- /dev/null
+++ b/core/java/android/security/net/config/ResourceCertificateSource.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util.ArraySet;
+import libcore.io.IoUtils;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * {@link CertificateSource} based on certificates contained in an application resource file.
+ * @hide
+ */
+public class ResourceCertificateSource implements CertificateSource {
+ private Set<X509Certificate> mCertificates;
+ private final int mResourceId;
+ private Context mContext;
+ private final Object mLock = new Object();
+
+ public ResourceCertificateSource(int resourceId, Context context) {
+ mResourceId = resourceId;
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public Set<X509Certificate> getCertificates() {
+ synchronized (mLock) {
+ if (mCertificates != null) {
+ return mCertificates;
+ }
+ Set<X509Certificate> certificates = new ArraySet<X509Certificate>();
+ Collection<? extends Certificate> certs;
+ InputStream in = null;
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ in = mContext.getResources().openRawResource(mResourceId);
+ certs = factory.generateCertificates(in);
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to load trust anchors from id " + mResourceId,
+ e);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ for (Certificate cert : certs) {
+ certificates.add((X509Certificate) cert);
+ }
+ mCertificates = certificates;
+ mContext = null;
+ return mCertificates;
+ }
+ }
+}
diff --git a/core/java/android/security/net/config/RootTrustManager.java b/core/java/android/security/net/config/RootTrustManager.java
new file mode 100644
index 0000000..1338b9f
--- /dev/null
+++ b/core/java/android/security/net/config/RootTrustManager.java
@@ -0,0 +1,74 @@
+/*
+ * 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.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * {@link X509TrustManager} based on an {@link ApplicationConfig}.
+ *
+ * <p>This {@code X509TrustManager} delegates to the specific trust manager for the hostname
+ * being used for the connection (See {@link ApplicationConfig#getConfigForHostname(String)} and
+ * {@link NetworkSecurityTrustManager}).</p>
+ *
+ * Note that if the {@code ApplicationConfig} has per-domain configurations the hostname aware
+ * {@link #checkServerTrusted(X509Certificate[], String String)} must be used instead of the normal
+ * non-aware call.
+ * @hide */
+public class RootTrustManager implements X509TrustManager {
+ private final ApplicationConfig mConfig;
+ private static final X509Certificate[] EMPTY_ISSUERS = new X509Certificate[0];
+
+ public RootTrustManager(ApplicationConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config must not be null");
+ }
+ mConfig = config;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ throw new CertificateException("Client authentication not supported");
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ if (mConfig.hasPerDomainConfigs()) {
+ throw new CertificateException(
+ "Domain specific configurations require that hostname aware"
+ + " checkServerTrusted(X509Certificate[], String, String) is used");
+ }
+ NetworkSecurityConfig config = mConfig.getConfigForHostname("");
+ config.getTrustManager().checkServerTrusted(certs, authType);
+ }
+
+ public void checkServerTrusted(X509Certificate[] certs, String authType, String hostname)
+ throws CertificateException {
+ NetworkSecurityConfig config = mConfig.getConfigForHostname(hostname);
+ config.getTrustManager().checkServerTrusted(certs, authType);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return EMPTY_ISSUERS;
+ }
+}
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
new file mode 100644
index 0000000..640ebd9
--- /dev/null
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -0,0 +1,96 @@
+/*
+ * 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.os.Environment;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Set;
+import libcore.io.IoUtils;
+
+/**
+ * {@link CertificateSource} based on the system trusted CA store.
+ * @hide
+ */
+public class SystemCertificateSource implements CertificateSource {
+ private static Set<X509Certificate> sSystemCerts = null;
+ private static final Object sLock = new Object();
+
+ public SystemCertificateSource() {
+ }
+
+ @Override
+ public Set<X509Certificate> getCertificates() {
+ // TODO: loading all of these is wasteful, we should instead use a keystore style API.
+ synchronized (sLock) {
+ if (sSystemCerts != null) {
+ return sSystemCerts;
+ }
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+
+ final String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
+ final File systemCaDir = new File(ANDROID_ROOT + "/etc/security/cacerts");
+ final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
+ final File userRemovedCaDir = new File(configDir, "cacerts-removed");
+ // Sanity check
+ if (!systemCaDir.isDirectory()) {
+ throw new AssertionError(systemCaDir + " is not a directory");
+ }
+
+ Set<X509Certificate> systemCerts = new ArraySet<X509Certificate>();
+ for (String caFile : systemCaDir.list()) {
+ // Skip any CAs in the user's deleted directory.
+ if (new File(userRemovedCaDir, caFile).exists()) {
+ continue;
+ }
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(
+ new FileInputStream(new File(systemCaDir, caFile)));
+ systemCerts.add((X509Certificate) certFactory.generateCertificate(is));
+ } catch (CertificateException | IOException e) {
+ // Don't rethrow to be consistent with conscrypt's cert loading code.
+ continue;
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
+ sSystemCerts = systemCerts;
+ return sSystemCerts;
+ }
+ }
+
+ public void onCertificateStorageChange() {
+ synchronized (sLock) {
+ sSystemCerts = null;
+ }
+ }
+}
diff --git a/core/java/android/security/net/config/TrustAnchor.java b/core/java/android/security/net/config/TrustAnchor.java
new file mode 100644
index 0000000..b62d85f
--- /dev/null
+++ b/core/java/android/security/net/config/TrustAnchor.java
@@ -0,0 +1,33 @@
+/*
+ * 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.security.cert.X509Certificate;
+
+/** @hide */
+public final class TrustAnchor {
+ public final X509Certificate certificate;
+ public final boolean overridesPins;
+
+ public TrustAnchor(X509Certificate certificate, boolean overridesPins) {
+ if (certificate == null) {
+ throw new NullPointerException("certificate");
+ }
+ this.certificate = certificate;
+ this.overridesPins = overridesPins;
+ }
+}
diff --git a/core/java/android/security/net/config/UserCertificateSource.java b/core/java/android/security/net/config/UserCertificateSource.java
new file mode 100644
index 0000000..77e2c88
--- /dev/null
+++ b/core/java/android/security/net/config/UserCertificateSource.java
@@ -0,0 +1,88 @@
+/*
+ * 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.os.Environment;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Set;
+import libcore.io.IoUtils;
+
+/**
+ * {@link CertificateSource} based on the user-installed trusted CA store.
+ * @hide
+ */
+public class UserCertificateSource implements CertificateSource {
+ private static Set<X509Certificate> sUserCerts = null;
+ private static final Object sLock = new Object();
+
+ public UserCertificateSource() {
+ }
+
+ @Override
+ public Set<X509Certificate> getCertificates() {
+ // TODO: loading all of these is wasteful, we should instead use a keystore style API.
+ synchronized (sLock) {
+ if (sUserCerts != null) {
+ return sUserCerts;
+ }
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
+ final File userCaDir = new File(configDir, "cacerts-added");
+ if (!userCaDir.isDirectory()) {
+ throw new AssertionError(userCaDir + " is not a directory");
+ }
+
+ Set<X509Certificate> userCerts = new ArraySet<X509Certificate>();
+ for (String caFile : userCaDir.list()) {
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(
+ new FileInputStream(new File(userCaDir, caFile)));
+ userCerts.add((X509Certificate) certFactory.generateCertificate(is));
+ } catch (CertificateException | IOException e) {
+ // Don't rethrow to be consistent with conscrypt's cert loading code.
+ continue;
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
+ sUserCerts = userCerts;
+ return sUserCerts;
+ }
+ }
+
+ public void onCertificateStorageChange() {
+ synchronized (sLock) {
+ sUserCerts = null;
+ }
+ }
+}
diff --git a/tests/NetworkSecurityConfigTest/Android.mk b/tests/NetworkSecurityConfigTest/Android.mk
new file mode 100644
index 0000000..a63162d
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+
+LOCAL_JAVA_LIBRARIES := android.test.runner bouncycastle conscrypt
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := NetworkSecurityConfigTests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/NetworkSecurityConfigTest/AndroidManifest.xml b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
new file mode 100644
index 0000000..811a3f4
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.tests"
+ android:sharedUserId="android.uid.system">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="android.security.tests"
+ android:label="ANSC Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
new file mode 100644
index 0000000..9a1fe15
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
@@ -0,0 +1,241 @@
+/*
+ * 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.app.Activity;
+import android.test.ActivityUnitTestCase;
+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 NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
+
+ public NetworkSecurityConfigTests() {
+ super(Activity.class);
+ }
+
+ // SHA-256 of the G2 intermediate CA for android.com (as of 10/2015).
+ private static final byte[] G2_SPKI_SHA256
+ = hexToBytes("ec722969cb64200ab6638f68ac538e40abab5b19a6485661042a1061c4612776");
+
+ private static byte[] hexToBytes(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(
+ s.charAt(i + 1), 16));
+ }
+ 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;
+ }
+
+
+ /**
+ * Return a NetworkSecurityConfig that has an empty TrustAnchor set. This should always cause a
+ * SSLHandshakeException when used for a connection.
+ */
+ private NetworkSecurityConfig getEmptyConfig() {
+ return new NetworkSecurityConfig(true, false,
+ new PinSet(new ArraySet<Pin>(), -1),
+ new ArrayList<CertificatesEntryRef>());
+ }
+
+ private NetworkSecurityConfig getSystemStoreConfig() {
+ ArrayList<CertificatesEntryRef> defaultSource = new ArrayList<CertificatesEntryRef>();
+ defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
+ return new NetworkSecurityConfig(true, false, new PinSet(new ArraySet<Pin>(),
+ -1), defaultSource);
+ }
+
+ public void testEmptyConfig() throws Exception {
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ ConfigSource testSource =
+ new TestConfigSource(domainMap, getEmptyConfig());
+ SSLContext context = getSSLContext(testSource);
+ assertConnectionFails(context, "android.com", 443);
+ }
+
+ public void testEmptyPerNetworkSecurityConfig() throws Exception {
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
+ new Domain("android.com", true), getEmptyConfig()));
+ ArrayList<CertificatesEntryRef> defaultSource = new ArrayList<CertificatesEntryRef>();
+ defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
+ NetworkSecurityConfig defaultConfig = new NetworkSecurityConfig(true, false,
+ new PinSet(new ArraySet<Pin>(), -1),
+ defaultSource);
+ SSLContext context = getSSLContext(new TestConfigSource(domainMap, defaultConfig));
+ assertConnectionFails(context, "android.com", 443);
+ assertConnectionSucceeds(context, "google.com", 443);
+ }
+
+ public void testBadPin() throws Exception {
+ ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
+ systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
+ ArraySet<Pin> pins = new ArraySet<Pin>();
+ pins.add(new Pin("SHA-256", new byte[0]));
+ NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
+ new PinSet(pins, Long.MAX_VALUE),
+ systemSource);
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ 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);
+ }
+
+ public void testGoodPin() throws Exception {
+ ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
+ systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
+ ArraySet<Pin> pins = new ArraySet<Pin>();
+ pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+ NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
+ new PinSet(pins, Long.MAX_VALUE),
+ systemSource);
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ 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);
+ }
+
+ public void testOverridePins() throws Exception {
+ // Use a bad pin + granting the system CA store the ability to override pins.
+ ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
+ systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), true));
+ ArraySet<Pin> pins = new ArraySet<Pin>();
+ pins.add(new Pin("SHA-256", new byte[0]));
+ NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
+ new PinSet(pins, Long.MAX_VALUE),
+ systemSource);
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
+ new Domain("android.com", true), domain));
+ SSLContext context
+ = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
+ assertConnectionSucceeds(context, "android.com", 443);
+ }
+
+ public void testMostSpecificNetworkSecurityConfig() throws Exception {
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
+ new Domain("android.com", true), getEmptyConfig()));
+ 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);
+ }
+
+ public void testSubdomainIncluded() throws Exception {
+ // First try connecting to a subdomain of a domain entry that includes subdomains.
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ 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);
+ // 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);
+ }
+
+ public void testWithUrlConnection() throws Exception {
+ ArrayList<CertificatesEntryRef> systemSource = new ArrayList<CertificatesEntryRef>();
+ systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false));
+ ArraySet<Pin> pins = new ArraySet<Pin>();
+ pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+ NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false,
+ new PinSet(pins, Long.MAX_VALUE),
+ systemSource);
+ ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap
+ = new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
+ 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);
+ }
+}
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java
new file mode 100644
index 0000000..92eadc0
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java
@@ -0,0 +1,33 @@
+/*
+ * 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.util.Set;
+import java.security.cert.X509Certificate;
+
+/** @hide */
+public class TestCertificateSource implements CertificateSource {
+
+ private final Set<X509Certificate> mCertificates;
+ public TestCertificateSource(Set<X509Certificate> certificates) {
+ mCertificates = certificates;
+ }
+
+ public Set<X509Certificate> getCertificates() {
+ return mCertificates;
+ }
+}
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestConfigSource.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestConfigSource.java
new file mode 100644
index 0000000..609f481
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestConfigSource.java
@@ -0,0 +1,39 @@
+/*
+ * 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.util.Pair;
+import java.util.Set;
+
+/** @hide */
+public class TestConfigSource implements ConfigSource {
+ private final Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
+ private final NetworkSecurityConfig mDefaultConfig;
+ public TestConfigSource(Set<Pair<Domain, NetworkSecurityConfig>> configs,
+ NetworkSecurityConfig defaultConfig) {
+ mConfigs = configs;
+ mDefaultConfig = defaultConfig;
+ }
+
+ public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
+ return mConfigs;
+ }
+
+ public NetworkSecurityConfig getDefaultConfig() {
+ return mDefaultConfig;
+ }
+}