From 6bc1e3966c4890ee3d47b5e527b800f2700ed627 Mon Sep 17 00:00:00 2001 From: Chad Brubaker Date: Fri, 23 Oct 2015 15:33:56 -0700 Subject: 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 --- tests/NetworkSecurityConfigTest/Android.mk | 15 ++ .../NetworkSecurityConfigTest/AndroidManifest.xml | 29 +++ .../net/config/NetworkSecurityConfigTests.java | 241 +++++++++++++++++++++ .../security/net/config/TestCertificateSource.java | 33 +++ .../security/net/config/TestConfigSource.java | 39 ++++ 5 files changed, 357 insertions(+) create mode 100644 tests/NetworkSecurityConfigTest/Android.mk create mode 100644 tests/NetworkSecurityConfigTest/AndroidManifest.xml create mode 100644 tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java create mode 100644 tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java create mode 100644 tests/NetworkSecurityConfigTest/src/android/security/net/config/TestConfigSource.java (limited to 'tests') diff --git a/tests/NetworkSecurityConfigTest/Android.mk b/tests/NetworkSecurityConfigTest/Android.mk new file mode 100644 index 000000000000..a63162d9ba09 --- /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 000000000000..811a3f4f4f80 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + 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 000000000000..9a1fe151a2dc --- /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 { + + 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(), -1), + new ArrayList()); + } + + private NetworkSecurityConfig getSystemStoreConfig() { + ArrayList defaultSource = new ArrayList(); + defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false)); + return new NetworkSecurityConfig(true, false, new PinSet(new ArraySet(), + -1), defaultSource); + } + + public void testEmptyConfig() throws Exception { + ArraySet> domainMap + = new ArraySet>(); + ConfigSource testSource = + new TestConfigSource(domainMap, getEmptyConfig()); + SSLContext context = getSSLContext(testSource); + assertConnectionFails(context, "android.com", 443); + } + + public void testEmptyPerNetworkSecurityConfig() throws Exception { + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + new Domain("android.com", true), getEmptyConfig())); + ArrayList defaultSource = new ArrayList(); + defaultSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false)); + NetworkSecurityConfig defaultConfig = new NetworkSecurityConfig(true, false, + new PinSet(new ArraySet(), -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 systemSource = new ArrayList(); + systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false)); + ArraySet pins = new ArraySet(); + pins.add(new Pin("SHA-256", new byte[0])); + NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false, + new PinSet(pins, Long.MAX_VALUE), + systemSource); + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + 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 systemSource = new ArrayList(); + systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false)); + ArraySet pins = new ArraySet(); + pins.add(new Pin("SHA-256", G2_SPKI_SHA256)); + NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false, + new PinSet(pins, Long.MAX_VALUE), + systemSource); + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + 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 systemSource = new ArrayList(); + systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), true)); + ArraySet pins = new ArraySet(); + pins.add(new Pin("SHA-256", new byte[0])); + NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false, + new PinSet(pins, Long.MAX_VALUE), + systemSource); + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + new Domain("android.com", true), domain)); + SSLContext context + = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + assertConnectionSucceeds(context, "android.com", 443); + } + + public void testMostSpecificNetworkSecurityConfig() throws Exception { + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + new Domain("android.com", true), getEmptyConfig())); + domainMap.add(new Pair( + 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> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + 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>(); + domainMap.add(new Pair( + new Domain("android.com", false), getSystemStoreConfig())); + context = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + assertConnectionFails(context, "developer.android.com", 443); + } + + public void testWithUrlConnection() throws Exception { + ArrayList systemSource = new ArrayList(); + systemSource.add(new CertificatesEntryRef(new SystemCertificateSource(), false)); + ArraySet pins = new ArraySet(); + pins.add(new Pin("SHA-256", G2_SPKI_SHA256)); + NetworkSecurityConfig domain = new NetworkSecurityConfig(true, false, + new PinSet(pins, Long.MAX_VALUE), + systemSource); + ArraySet> domainMap + = new ArraySet>(); + domainMap.add(new Pair( + 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 000000000000..92eadc06cd49 --- /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 mCertificates; + public TestCertificateSource(Set certificates) { + mCertificates = certificates; + } + + public Set 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 000000000000..609f481a312c --- /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> mConfigs; + private final NetworkSecurityConfig mDefaultConfig; + public TestConfigSource(Set> configs, + NetworkSecurityConfig defaultConfig) { + mConfigs = configs; + mDefaultConfig = defaultConfig; + } + + public Set> getPerDomainConfigs() { + return mConfigs; + } + + public NetworkSecurityConfig getDefaultConfig() { + return mDefaultConfig; + } +} -- cgit v1.2.3-59-g8ed1b