Merge "Add initial network security config implementation"

This commit is contained in:
Chad Brubaker
2015-11-05 18:46:24 +00:00
committed by Gerrit Code Review
19 changed files with 1323 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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];
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}