diff options
19 files changed, 1323 insertions, 0 deletions
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 000000000000..c67535258c98 --- /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 000000000000..386354dc4d57 --- /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 000000000000..2ba38c21c330 --- /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 000000000000..4adf265c678c --- /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 000000000000..5bb727a38033 --- /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 000000000000..915fbefb7041 --- /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 000000000000..e69082d3deec --- /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 000000000000..856743198073 --- /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 000000000000..a9ee039fd01c --- /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 000000000000..06dd9d42e364 --- /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 000000000000..1338b9ff97d4 --- /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 000000000000..640ebd9a9cf8 --- /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 000000000000..b62d85fa37ae --- /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 000000000000..77e2c88fe206 --- /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 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 @@ +<?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 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<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 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<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 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<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; + } +} |