From 6bc1e3966c4890ee3d47b5e527b800f2700ed627 Mon Sep 17 00:00:00 2001 From: Chad Brubaker Date: Fri, 23 Oct 2015 15:33:56 -0700 Subject: [PATCH] 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 --- .../net/config/ApplicationConfig.java | 132 ++++++++++ .../net/config/CertificateSource.java | 25 ++ .../net/config/CertificatesEntryRef.java | 41 +++ .../security/net/config/ConfigSource.java | 26 ++ .../android/security/net/config/Domain.java | 57 +++++ .../net/config/NetworkSecurityConfig.java | 86 +++++++ .../config/NetworkSecurityTrustManager.java | 135 ++++++++++ .../java/android/security/net/config/Pin.java | 58 +++++ .../android/security/net/config/PinSet.java | 43 ++++ .../net/config/ResourceCertificateSource.java | 72 ++++++ .../security/net/config/RootTrustManager.java | 74 ++++++ .../net/config/SystemCertificateSource.java | 96 +++++++ .../security/net/config/TrustAnchor.java | 33 +++ .../net/config/UserCertificateSource.java | 88 +++++++ tests/NetworkSecurityConfigTest/Android.mk | 15 ++ .../AndroidManifest.xml | 29 +++ .../config/NetworkSecurityConfigTests.java | 241 ++++++++++++++++++ .../net/config/TestCertificateSource.java | 33 +++ .../security/net/config/TestConfigSource.java | 39 +++ 19 files changed, 1323 insertions(+) create mode 100644 core/java/android/security/net/config/ApplicationConfig.java create mode 100644 core/java/android/security/net/config/CertificateSource.java create mode 100644 core/java/android/security/net/config/CertificatesEntryRef.java create mode 100644 core/java/android/security/net/config/ConfigSource.java create mode 100644 core/java/android/security/net/config/Domain.java create mode 100644 core/java/android/security/net/config/NetworkSecurityConfig.java create mode 100644 core/java/android/security/net/config/NetworkSecurityTrustManager.java create mode 100644 core/java/android/security/net/config/Pin.java create mode 100644 core/java/android/security/net/config/PinSet.java create mode 100644 core/java/android/security/net/config/ResourceCertificateSource.java create mode 100644 core/java/android/security/net/config/RootTrustManager.java create mode 100644 core/java/android/security/net/config/SystemCertificateSource.java create mode 100644 core/java/android/security/net/config/TrustAnchor.java create mode 100644 core/java/android/security/net/config/UserCertificateSource.java 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 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 0000000000000..c67535258c987 --- /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. + * + *

{@link #getConfigForHostname(String)} provides a means to obtain network security + * configuration to be used for communicating with a specific hostname.

+ * + * @hide + */ +public final class ApplicationConfig { + private Set> 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 bestMatch = null; + for (Pair 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 0000000000000..386354dc4d57e --- /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 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 0000000000000..2ba38c21c330f --- /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 getTrustAnchors() { + // TODO: cache this [but handle mutable sources] + Set anchors = new ArraySet(); + 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 0000000000000..4adf265c678cc --- /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> 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 0000000000000..5bb727a38033a --- /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 0000000000000..915fbefb70410 --- /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 mCertificatesEntryRefs; + private Set 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 certificatesEntryRefs) { + mCleartextTrafficPermitted = cleartextTrafficPermitted; + mHstsEnforced = hstsEnforced; + mPins = pins; + mCertificatesEntryRefs = certificatesEntryRefs; + } + + public Set getTrustAnchors() { + synchronized (mAnchorsLock) { + if (mAnchors != null) { + return mAnchors; + } + Set anchors = new ArraySet(); + 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 0000000000000..e69082d3deeca --- /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 trustedChain = + mDelegate.checkServerTrusted(certs, authType, (String) null); + checkPins(trustedChain); + } + + private void checkPins(List chain) throws CertificateException { + PinSet pinSet = mNetworkSecurityConfig.getPins(); + if (pinSet.pins.isEmpty() + || System.currentTimeMillis() > pinSet.expirationTime + || !isPinningEnforced(chain)) { + return; + } + Set pinAlgorithms = pinSet.getPinAlgorithms(); + Map digestMap = new ArrayMap( + 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 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 0000000000000..856743198073e --- /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 0000000000000..a9ee039fd01c3 --- /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 pins; + + public PinSet(Set pins, long expirationTime) { + if (pins == null) { + throw new NullPointerException("pins must not be null"); + } + this.pins = pins; + this.expirationTime = expirationTime; + } + + Set getPinAlgorithms() { + // TODO: Cache this. + Set algorithms = new ArraySet(); + 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 0000000000000..06dd9d42e364f --- /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 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 getCertificates() { + synchronized (mLock) { + if (mCertificates != null) { + return mCertificates; + } + Set certificates = new ArraySet(); + Collection 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 0000000000000..1338b9ff97d42 --- /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}. + * + *

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}).

+ * + * 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 0000000000000..640ebd9a9cf84 --- /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 sSystemCerts = null; + private static final Object sLock = new Object(); + + public SystemCertificateSource() { + } + + @Override + public Set 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 systemCerts = new ArraySet(); + 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 0000000000000..b62d85fa37ae0 --- /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 0000000000000..77e2c88fe2067 --- /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 sUserCerts = null; + private static final Object sLock = new Object(); + + public UserCertificateSource() { + } + + @Override + public Set 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 userCerts = new ArraySet(); + 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 0000000000000..a63162d9ba09e --- /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 0000000000000..811a3f4f4f800 --- /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 0000000000000..9a1fe151a2dc8 --- /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 0000000000000..92eadc06cd497 --- /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 0000000000000..609f481a312cf --- /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; + } +}