diff options
53 files changed, 2508 insertions, 59 deletions
diff --git a/cmds/idmap/create.cpp b/cmds/idmap/create.cpp index 6b2f46099b80..c13d318f7449 100644 --- a/cmds/idmap/create.cpp +++ b/cmds/idmap/create.cpp @@ -1,6 +1,6 @@ #include "idmap.h" -#include <UniquePtr.h> +#include <memory> #include <androidfw/AssetManager.h> #include <androidfw/ResourceTypes.h> #include <androidfw/ZipFileRO.h> @@ -15,7 +15,7 @@ using namespace android; namespace { int get_zip_entry_crc(const char *zip_path, const char *entry_name, uint32_t *crc) { - UniquePtr<ZipFileRO> zip(ZipFileRO::open(zip_path)); + std::unique_ptr<ZipFileRO> zip(ZipFileRO::open(zip_path)); if (zip.get() == NULL) { return -1; } diff --git a/cmds/idmap/scan.cpp b/cmds/idmap/scan.cpp index f1b2f9e72ca0..6d30f0d0ce34 100644 --- a/cmds/idmap/scan.cpp +++ b/cmds/idmap/scan.cpp @@ -4,7 +4,7 @@ #include "idmap.h" -#include <UniquePtr.h> +#include <memory> #include <androidfw/ResourceTypes.h> #include <androidfw/StreamingZipInflater.h> #include <androidfw/ZipFileRO.h> @@ -120,7 +120,7 @@ namespace { int parse_apk(const char *path, const char *target_package_name) { - UniquePtr<ZipFileRO> zip(ZipFileRO::open(path)); + std::unique_ptr<ZipFileRO> zip(ZipFileRO::open(path)); if (zip.get() == NULL) { ALOGW("%s: failed to open zip %s\n", __FUNCTION__, path); return -1; diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index 4a0a49b4e777..005153178298 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -764,8 +764,7 @@ public final class Pm { String optionData = nextOptionData(); if (optionData == null || !isNumber(optionData)) { System.err.println("Error: no USER_ID specified"); - showUsage(); - return 1; + return showUsage(); } else { userId = Integer.parseInt(optionData); } @@ -868,8 +867,7 @@ public final class Pm { } } else { System.err.println("Error: unknown option: " + opt); - showUsage(); - return 1; + return showUsage(); } } @@ -877,16 +875,14 @@ public final class Pm { final String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package specified."); - showUsage(); - return 1; + return showUsage(); } // State to apply; {always|ask|never|undefined}, required final String modeString = nextArg(); if (modeString == null) { System.err.println("Error: no app link state specified."); - showUsage(); - return 1; + return showUsage(); } final int newMode; @@ -955,8 +951,7 @@ public final class Pm { } } else { System.err.println("Error: unknown option: " + opt); - showUsage(); - return 1; + return showUsage(); } } @@ -964,8 +959,7 @@ public final class Pm { final String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package specified."); - showUsage(); - return 1; + return showUsage(); } try { @@ -1370,8 +1364,7 @@ public final class Pm { String optionData = nextOptionData(); if (optionData == null || !isNumber(optionData)) { System.err.println("Error: no USER_ID specified"); - showUsage(); - return 1; + return showUsage(); } else { userId = Integer.parseInt(optionData); } @@ -1379,8 +1372,7 @@ public final class Pm { flags |= UserInfo.FLAG_MANAGED_PROFILE; } else { System.err.println("Error: unknown option " + opt); - showUsage(); - return 1; + return showUsage(); } } String arg = nextArg(); @@ -1557,8 +1549,7 @@ public final class Pm { String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package specified"); - showUsage(); - return 1; + return showUsage(); } if (userId == UserHandle.USER_ALL) { @@ -1626,8 +1617,7 @@ public final class Pm { String optionData = nextOptionData(); if (optionData == null || !isNumber(optionData)) { System.err.println("Error: no USER_ID specified"); - showUsage(); - return 1; + return showUsage(); } else { userId = Integer.parseInt(optionData); } @@ -1636,8 +1626,7 @@ public final class Pm { String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package specified"); - showUsage(); - return 1; + return showUsage(); } ClearDataObserver obs = new ClearDataObserver(); @@ -1698,8 +1687,7 @@ public final class Pm { String optionData = nextOptionData(); if (optionData == null || !isNumber(optionData)) { System.err.println("Error: no USER_ID specified"); - showUsage(); - return 1; + return showUsage(); } else { userId = Integer.parseInt(optionData); } @@ -1708,8 +1696,7 @@ public final class Pm { String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package or component specified"); - showUsage(); - return 1; + return showUsage(); } ComponentName cn = ComponentName.unflattenFromString(pkg); if (cn == null) { @@ -1747,8 +1734,7 @@ public final class Pm { String optionData = nextOptionData(); if (optionData == null || !isNumber(optionData)) { System.err.println("Error: no USER_ID specified"); - showUsage(); - return 1; + return showUsage(); } else { userId = Integer.parseInt(optionData); } @@ -1757,8 +1743,7 @@ public final class Pm { String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package or component specified"); - showUsage(); - return 1; + return showUsage(); } try { mPm.setApplicationHiddenSettingAsUser(pkg, state, userId); @@ -1785,14 +1770,12 @@ public final class Pm { String pkg = nextArg(); if (pkg == null) { System.err.println("Error: no package specified"); - showUsage(); - return 1; + return showUsage(); } String perm = nextArg(); if (perm == null) { System.err.println("Error: no permission specified"); - showUsage(); - return 1; + return showUsage(); } try { @@ -1808,8 +1791,7 @@ public final class Pm { return 1; } catch (IllegalArgumentException e) { System.err.println("Bad argument: " + e.toString()); - showUsage(); - return 1; + return showUsage(); } catch (SecurityException e) { System.err.println("Operation not allowed: " + e.toString()); return 1; @@ -1826,8 +1808,7 @@ public final class Pm { return 1; } catch (IllegalArgumentException e) { System.err.println("Bad argument: " + e.toString()); - showUsage(); - return 1; + return showUsage(); } catch (SecurityException e) { System.err.println("Operation not allowed: " + e.toString()); return 1; @@ -1838,14 +1819,12 @@ public final class Pm { final String permission = nextArg(); if (permission == null) { System.err.println("Error: no permission specified"); - showUsage(); - return 1; + return showUsage(); } final String enforcedRaw = nextArg(); if (enforcedRaw == null) { System.err.println("Error: no enforcement specified"); - showUsage(); - return 1; + return showUsage(); } final boolean enforced = Boolean.parseBoolean(enforcedRaw); try { @@ -1857,8 +1836,7 @@ public final class Pm { return 1; } catch (IllegalArgumentException e) { System.err.println("Bad argument: " + e.toString()); - showUsage(); - return 1; + return showUsage(); } catch (SecurityException e) { System.err.println("Operation not allowed: " + e.toString()); return 1; @@ -1884,8 +1862,7 @@ public final class Pm { String size = nextArg(); if (size == null) { System.err.println("Error: no size specified"); - showUsage(); - return 1; + return showUsage(); } int len = size.length(); long multiplier = 1; @@ -1899,8 +1876,7 @@ public final class Pm { multiplier = 1024L*1024L*1024L; } else { System.err.println("Invalid suffix: " + c); - showUsage(); - return 1; + return showUsage(); } size = size.substring(0, len-1); } @@ -1909,8 +1885,7 @@ public final class Pm { sizeVal = Long.parseLong(size) * multiplier; } catch (NumberFormatException e) { System.err.println("Error: expected number at: " + size); - showUsage(); - return 1; + return showUsage(); } String volumeUuid = nextArg(); if ("internal".equals(volumeUuid)) { @@ -1934,8 +1909,7 @@ public final class Pm { return 1; } catch (IllegalArgumentException e) { System.err.println("Bad argument: " + e.toString()); - showUsage(); - return 1; + return showUsage(); } catch (SecurityException e) { System.err.println("Operation not allowed: " + e.toString()); return 1; diff --git a/cmds/sm/src/com/android/commands/sm/Sm.java b/cmds/sm/src/com/android/commands/sm/Sm.java index 0a1ba4d19c5c..1ee60b068694 100644 --- a/cmds/sm/src/com/android/commands/sm/Sm.java +++ b/cmds/sm/src/com/android/commands/sm/Sm.java @@ -42,6 +42,7 @@ public final class Sm { } catch (Exception e) { if (e instanceof IllegalArgumentException) { showUsage(); + System.exit(1); } Log.e(TAG, "Error", e); System.err.println("Error: " + e); 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..9bf344a0e2bc --- /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..1c787b407086 --- /dev/null +++ b/core/java/android/security/net/config/NetworkSecurityConfig.java @@ -0,0 +1,242 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.X509TrustManager; + +/** + * @hide + */ +public final class NetworkSecurityConfig { + /** @hide */ + public static final boolean DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED = true; + /** @hide */ + public static final boolean DEFAULT_HSTS_ENFORCED = false; + public static final NetworkSecurityConfig DEFAULT = getDefaultBuilder().build(); + + 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(); + + private 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; + } + } + + /** + * Return a {@link Builder} for the default {@code NetworkSecurityConfig}. + * + * <p> + * The default configuration has the following properties: + * <ol> + * <li>Cleartext traffic is permitted.</li> + * <li>HSTS is not enforced.</li> + * <li>No certificate pinning is used.</li> + * <li>The system and user added trusted certificate stores are trusted for connections.</li> + * </ol> + * + * @hide + */ + public static final Builder getDefaultBuilder() { + return new Builder() + .setCleartextTrafficPermitted(DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED) + .setHstsEnforced(DEFAULT_HSTS_ENFORCED) + // System certificate store, does not bypass static pins. + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)) + // User certificate store, does not bypass static pins. + .addCertificatesEntryRef( + new CertificatesEntryRef(UserCertificateSource.getInstance(), false)); + } + + /** + * Builder for creating {@code NetworkSecurityConfig} objects. + * @hide + */ + public static final class Builder { + private List<CertificatesEntryRef> mCertificatesEntryRefs; + private PinSet mPinSet; + private boolean mCleartextTrafficPermitted = DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED; + private boolean mHstsEnforced = DEFAULT_HSTS_ENFORCED; + private boolean mCleartextTrafficPermittedSet = false; + private boolean mHstsEnforcedSet = false; + private Builder mParentBuilder; + + /** + * Sets the parent {@code Builder} for this {@code Builder}. + * The parent will be used to determine values not configured in this {@code Builder} + * in {@link Builder#build()}, recursively if needed. + */ + public Builder setParent(Builder parent) { + // Sanity check to avoid adding loops. + Builder current = parent; + while (current != null) { + if (current == this) { + throw new IllegalArgumentException("Loops are not allowed in Builder parents"); + } + current = current.getParent(); + } + mParentBuilder = parent; + return this; + } + + public Builder getParent() { + return mParentBuilder; + } + + public Builder setPinSet(PinSet pinSet) { + mPinSet = pinSet; + return this; + } + + private PinSet getEffectivePinSet() { + if (mPinSet != null) { + return mPinSet; + } + if (mParentBuilder != null) { + return mParentBuilder.getEffectivePinSet(); + } + return PinSet.EMPTY_PINSET; + } + + public Builder setCleartextTrafficPermitted(boolean cleartextTrafficPermitted) { + mCleartextTrafficPermitted = cleartextTrafficPermitted; + mCleartextTrafficPermittedSet = true; + return this; + } + + private boolean getEffectiveCleartextTrafficPermitted() { + if (mCleartextTrafficPermittedSet) { + return mCleartextTrafficPermitted; + } + if (mParentBuilder != null) { + return mParentBuilder.getEffectiveCleartextTrafficPermitted(); + } + return DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED; + } + + public Builder setHstsEnforced(boolean hstsEnforced) { + mHstsEnforced = hstsEnforced; + mHstsEnforcedSet = true; + return this; + } + + private boolean getEffectiveHstsEnforced() { + if (mHstsEnforcedSet) { + return mHstsEnforced; + } + if (mParentBuilder != null) { + return mParentBuilder.getEffectiveHstsEnforced(); + } + return DEFAULT_HSTS_ENFORCED; + } + + public Builder addCertificatesEntryRef(CertificatesEntryRef ref) { + if (mCertificatesEntryRefs == null) { + mCertificatesEntryRefs = new ArrayList<CertificatesEntryRef>(); + } + mCertificatesEntryRefs.add(ref); + return this; + } + + public Builder addCertificatesEntryRefs(Collection<? extends CertificatesEntryRef> refs) { + if (mCertificatesEntryRefs == null) { + mCertificatesEntryRefs = new ArrayList<CertificatesEntryRef>(); + } + mCertificatesEntryRefs.addAll(refs); + return this; + } + + private List<CertificatesEntryRef> getEffectiveCertificatesEntryRefs() { + if (mCertificatesEntryRefs != null) { + return mCertificatesEntryRefs; + } + if (mParentBuilder != null) { + return mParentBuilder.getEffectiveCertificatesEntryRefs(); + } + return Collections.<CertificatesEntryRef>emptyList(); + } + + public boolean hasCertificateEntryRefs() { + return mCertificatesEntryRefs != null; + } + + public NetworkSecurityConfig build() { + boolean cleartextPermitted = getEffectiveCleartextTrafficPermitted(); + boolean hstsEnforced = getEffectiveHstsEnforced(); + PinSet pinSet = getEffectivePinSet(); + List<CertificatesEntryRef> entryRefs = getEffectiveCertificatesEntryRefs(); + return new NetworkSecurityConfig(cleartextPermitted, hstsEnforced, pinSet, entryRefs); + } + } +} 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..94520e22bd02 --- /dev/null +++ b/core/java/android/security/net/config/Pin.java @@ -0,0 +1,78 @@ +/* + * 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(); + } + + /** + * @hide + */ + public static boolean isSupportedDigestAlgorithm(String algorithm) { + // Currently only SHA-256 is supported. SHA-512 if/once Chromium networking stack + // supports it. + return "SHA-256".equalsIgnoreCase(algorithm); + } + + /** + * @hide + */ + public static int getDigestLength(String algorithm) { + if ("SHA-256".equalsIgnoreCase(algorithm)) { + return 32; + } + throw new IllegalArgumentException("Unsupported digest algorithm: " + algorithm); + } + + @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..d3c975eb3101 --- /dev/null +++ b/core/java/android/security/net/config/PinSet.java @@ -0,0 +1,46 @@ +/* + * 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.Collections; +import java.util.Set; + +/** @hide */ +public final class PinSet { + public static final PinSet EMPTY_PINSET = + new PinSet(Collections.<Pin>emptySet(), Long.MAX_VALUE); + 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..7649a977ff5c --- /dev/null +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -0,0 +1,101 @@ +/* + * 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 final SystemCertificateSource INSTANCE = new SystemCertificateSource(); + private Set<X509Certificate> mSystemCerts = null; + private final Object mLock = new Object(); + + private SystemCertificateSource() { + } + + public static SystemCertificateSource getInstance() { + return INSTANCE; + } + + @Override + public Set<X509Certificate> getCertificates() { + // TODO: loading all of these is wasteful, we should instead use a keystore style API. + synchronized (mLock) { + if (mSystemCerts != null) { + return mSystemCerts; + } + 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); + } + } + mSystemCerts = systemCerts; + return mSystemCerts; + } + } + + public void onCertificateStorageChange() { + synchronized (mLock) { + mSystemCerts = 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..e9d5aa1e2f34 --- /dev/null +++ b/core/java/android/security/net/config/UserCertificateSource.java @@ -0,0 +1,92 @@ +/* + * 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 final UserCertificateSource INSTANCE = new UserCertificateSource(); + private Set<X509Certificate> mUserCerts = null; + private final Object mLock = new Object(); + + private UserCertificateSource() { + } + + public static UserCertificateSource getInstance() { + return INSTANCE; + } + + @Override + public Set<X509Certificate> getCertificates() { + // TODO: loading all of these is wasteful, we should instead use a keystore style API. + synchronized (mLock) { + if (mUserCerts != null) { + return mUserCerts; + } + 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"); + Set<X509Certificate> userCerts = new ArraySet<X509Certificate>(); + // If the user hasn't added any certificates the directory may not exist. + if (userCaDir.isDirectory()) { + 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); + } + } + } + mUserCerts = userCerts; + return mUserCerts; + } + } + + public void onCertificateStorageChange() { + synchronized (mLock) { + mUserCerts = null; + } + } +} diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java new file mode 100644 index 000000000000..6abfb66f9450 --- /dev/null +++ b/core/java/android/security/net/config/XmlConfigSource.java @@ -0,0 +1,328 @@ +package android.security.net.config; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.util.ArraySet; +import android.util.Base64; +import android.util.Pair; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * {@link ConfigSource} based on an XML configuration file. + * + * @hide + */ +public class XmlConfigSource implements ConfigSource { + private final Object mLock = new Object(); + private final int mResourceId; + + private boolean mInitialized; + private NetworkSecurityConfig mDefaultConfig; + private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap; + private Context mContext; + + public XmlConfigSource(Context context, int resourceId) { + mResourceId = resourceId; + mContext = context; + } + + public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() { + ensureInitialized(); + return mDomainMap; + } + + public NetworkSecurityConfig getDefaultConfig() { + ensureInitialized(); + return mDefaultConfig; + } + + private void ensureInitialized() { + synchronized (mLock) { + if (mInitialized) { + return; + } + try (XmlResourceParser parser = mContext.getResources().getXml(mResourceId)) { + parseNetworkSecurityConfig(parser); + mContext = null; + mInitialized = true; + } catch (Resources.NotFoundException | XmlPullParserException | IOException + | ParserException e) { + throw new RuntimeException("Failed to parse XML configuration from " + + mContext.getResources().getResourceEntryName(mResourceId), e); + } + } + } + + private Pin parsePin(XmlResourceParser parser) + throws IOException, XmlPullParserException, ParserException { + String digestAlgorithm = parser.getAttributeValue(null, "digest"); + if (!Pin.isSupportedDigestAlgorithm(digestAlgorithm)) { + throw new ParserException(parser, "Unsupported pin digest algorithm: " + + digestAlgorithm); + } + if (parser.next() != XmlPullParser.TEXT) { + throw new ParserException(parser, "Missing pin digest"); + } + String digest = parser.getText(); + byte[] decodedDigest = null; + try { + decodedDigest = Base64.decode(digest, 0); + } catch (IllegalArgumentException e) { + throw new ParserException(parser, "Invalid pin digest", e); + } + int expectedLength = Pin.getDigestLength(digestAlgorithm); + if (decodedDigest.length != expectedLength) { + throw new ParserException(parser, "digest length " + decodedDigest.length + + " does not match expected length for " + digestAlgorithm + " of " + + expectedLength); + } + if (parser.next() != XmlPullParser.END_TAG) { + throw new ParserException(parser, "pin contains additional elements"); + } + return new Pin(digestAlgorithm, decodedDigest); + } + + private PinSet parsePinSet(XmlResourceParser parser) + throws IOException, XmlPullParserException, ParserException { + String expirationDate = parser.getAttributeValue(null, "expiration"); + long expirationTimestampMilis = Long.MAX_VALUE; + if (expirationDate != null) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setLenient(false); + Date date = sdf.parse(expirationDate); + if (date == null) { + throw new ParserException(parser, "Invalid expiration date in pin-set"); + } + expirationTimestampMilis = date.getTime(); + } catch (ParseException e) { + throw new ParserException(parser, "Invalid expiration date in pin-set", e); + } + } + + int outerDepth = parser.getDepth(); + Set<Pin> pins = new ArraySet<>(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + String tagName = parser.getName(); + if (tagName.equals("pin")) { + pins.add(parsePin(parser)); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + return new PinSet(pins, expirationTimestampMilis); + } + + private Domain parseDomain(XmlResourceParser parser, Set<String> seenDomains) + throws IOException, XmlPullParserException, ParserException { + boolean includeSubdomains = + parser.getAttributeBooleanValue(null, "includeSubdomains", false); + if (parser.next() != XmlPullParser.TEXT) { + throw new ParserException(parser, "Domain name missing"); + } + String domain = parser.getText().toLowerCase(Locale.US); + if (parser.next() != XmlPullParser.END_TAG) { + throw new ParserException(parser, "domain contains additional elements"); + } + // Domains are matched using a most specific match, so don't allow duplicates. + // includeSubdomains isn't relevant here, both android.com + subdomains and android.com + // match for android.com equally. Do not allow any duplicates period. + if (!seenDomains.add(domain)) { + throw new ParserException(parser, domain + " has already been specified"); + } + return new Domain(domain, includeSubdomains); + } + + private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser) + throws IOException, XmlPullParserException, ParserException { + boolean overridePins = parser.getAttributeBooleanValue(null, "overridePins", false); + int sourceId = parser.getAttributeResourceValue(null, "src", -1); + String sourceString = parser.getAttributeValue(null, "src"); + CertificateSource source = null; + if (sourceString == null) { + throw new ParserException(parser, "certificates element missing src attribute"); + } + if (sourceId != -1) { + // TODO: Cache ResourceCertificateSources by sourceId + source = new ResourceCertificateSource(sourceId, mContext); + } else if ("system".equals(sourceString)) { + source = SystemCertificateSource.getInstance(); + } else if ("user".equals(sourceString)) { + source = UserCertificateSource.getInstance(); + } else { + throw new ParserException(parser, "Unknown certificates src. " + + "Should be one of system|user|@resourceVal"); + } + XmlUtils.skipCurrentTag(parser); + return new CertificatesEntryRef(source, overridePins); + } + + private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser) + throws IOException, XmlPullParserException, ParserException { + int outerDepth = parser.getDepth(); + List<CertificatesEntryRef> anchors = new ArrayList<>(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + String tagName = parser.getName(); + if (tagName.equals("certificates")) { + anchors.add(parseCertificatesEntry(parser)); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + return anchors; + } + + private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry( + XmlResourceParser parser, Set<String> seenDomains, + NetworkSecurityConfig.Builder parentBuilder, boolean baseConfig) + throws IOException, XmlPullParserException, ParserException { + List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>(); + NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder(); + builder.setParent(parentBuilder); + Set<Domain> domains = new ArraySet<>(); + boolean seenPinSet = false; + boolean seenTrustAnchors = false; + String configName = parser.getName(); + int outerDepth = parser.getDepth(); + // Add this builder now so that this builder occurs before any of its children. This + // makes the final build pass easier. + builders.add(new Pair<>(builder, domains)); + // Parse config attributes. Only set values that are present, config inheritence will + // handle the rest. + for (int i = 0; i < parser.getAttributeCount(); i++) { + String name = parser.getAttributeName(i); + if ("hstsEnforced".equals(name)) { + builder.setHstsEnforced( + parser.getAttributeBooleanValue(i, + NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED)); + } else if ("cleartextTrafficPermitted".equals(name)) { + builder.setCleartextTrafficPermitted( + parser.getAttributeBooleanValue(i, + NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED)); + } + } + // Parse the config elements. + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + String tagName = parser.getName(); + if ("domain".equals(tagName)) { + if (baseConfig) { + throw new ParserException(parser, "domain element not allowed in base-config"); + } + Domain domain = parseDomain(parser, seenDomains); + domains.add(domain); + } else if ("trust-anchors".equals(tagName)) { + if (seenTrustAnchors) { + throw new ParserException(parser, + "Multiple trust-anchor elements not allowed"); + } + builder.addCertificatesEntryRefs(parseTrustAnchors(parser)); + seenTrustAnchors = true; + } else if ("pin-set".equals(tagName)) { + if (baseConfig) { + throw new ParserException(parser, + "pin-set element not allowed in base-config"); + } + if (seenPinSet) { + throw new ParserException(parser, "Multiple pin-set elements not allowed"); + } + builder.setPinSet(parsePinSet(parser)); + seenPinSet = true; + } else if ("domain-config".equals(tagName)) { + if (baseConfig) { + throw new ParserException(parser, + "Nested domain-config not allowed in base-config"); + } + builders.addAll(parseConfigEntry(parser, seenDomains, builder, false)); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + if (!baseConfig && domains.isEmpty()) { + throw new ParserException(parser, "No domain elements in domain-config"); + } + return builders; + } + + private void parseNetworkSecurityConfig(XmlResourceParser parser) + throws IOException, XmlPullParserException, ParserException { + Set<String> seenDomains = new ArraySet<>(); + List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>(); + NetworkSecurityConfig.Builder baseConfigBuilder = null; + boolean seenDebugOverrides = false; + boolean seenBaseConfig = false; + + XmlUtils.beginDocument(parser, "network-security-config"); + int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + // TODO: support debug-override. + if ("base-config".equals(parser.getName())) { + if (seenBaseConfig) { + throw new ParserException(parser, "Only one base-config allowed"); + } + seenBaseConfig = true; + baseConfigBuilder = parseConfigEntry(parser, seenDomains, null, true).get(0).first; + } else if ("domain-config".equals(parser.getName())) { + builders.addAll(parseConfigEntry(parser, seenDomains, baseConfigBuilder, false)); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + // Use the platform default as the parent of the base config for any values not provided + // there. If there is no base config use the platform default. + NetworkSecurityConfig.Builder platformDefaultBuilder = + NetworkSecurityConfig.getDefaultBuilder(); + if (baseConfigBuilder != null) { + baseConfigBuilder.setParent(platformDefaultBuilder); + } else { + baseConfigBuilder = platformDefaultBuilder; + } + // Build the per-domain config mapping. + Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>(); + + for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) { + NetworkSecurityConfig.Builder builder = entry.first; + Set<Domain> domains = entry.second; + // Set the parent of configs that do not have a parent to the base-config. This can + // happen if the base-config comes after a domain-config in the file. + // Note that this is safe with regards to children because of the order that + // parseConfigEntry returns builders, the parent is always before the children. The + // children builders will not have build called until _after_ their parents have their + // parent set so everything is consistent. + if (builder.getParent() == null) { + builder.setParent(baseConfigBuilder); + } + NetworkSecurityConfig config = builder.build(); + for (Domain domain : domains) { + configs.add(new Pair<>(domain, config)); + } + } + mDefaultConfig = baseConfigBuilder.build(); + mDomainMap = configs; + } + + public static class ParserException extends Exception { + + public ParserException(XmlPullParser parser, String message, Throwable cause) { + super(message + " at: " + parser.getPositionDescription(), cause); + } + + public ParserException(XmlPullParser parser, String message) { + this(parser, message, null); + } + } +} diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 77f1efaefafe..4bcfa4caf158 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -519,7 +519,7 @@ public class ZygoteInit { String args[] = { "--setuid=1000", "--setgid=1000", - "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007", + "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007,3009", "--capabilities=" + capabilities + "," + capabilities, "--nice-name=system_server", "--runtime-args", diff --git a/core/jni/Android.mk b/core/jni/Android.mk index 47b2f3b0f6ab..4c68e015e531 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -273,7 +273,7 @@ LOCAL_CFLAGS += -Wall -Werror -Wno-error=deprecated-declarations -Wunused -Wunre # -Wno-c++11-extensions: Clang warns about Skia using the C++11 override keyword, but this project # is not being compiled with that level. Remove once this has changed. -LOCAL_CFLAGS += -Wno-c++11-extensions +LOCAL_CLANG_CFLAGS += -Wno-c++11-extensions # b/22414716: thread_local (android/graphics/Paint.cpp) and Clang don't like each other at the # moment. diff --git a/libs/hwui/ShadowTessellator.cpp b/libs/hwui/ShadowTessellator.cpp index 09d125839a68..595c10c88554 100644 --- a/libs/hwui/ShadowTessellator.cpp +++ b/libs/hwui/ShadowTessellator.cpp @@ -80,6 +80,11 @@ void ShadowTessellator::tessellateSpotShadow(bool isCasterOpaque, ALOGD("light center %f %f %f", adjustedLightCenter.x, adjustedLightCenter.y, adjustedLightCenter.z); #endif + if (isnan(adjustedLightCenter.x) + || isnan(adjustedLightCenter.y) + || isnan(adjustedLightCenter.z)) { + return; + } // light position (because it's in local space) needs to compensate for receiver transform // TODO: should apply to light orientation, not just position diff --git a/services/core/java/com/android/server/pm/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/DefaultPermissionGrantPolicy.java index 6fbe8e448476..a7682dc06d18 100644 --- a/services/core/java/com/android/server/pm/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/DefaultPermissionGrantPolicy.java @@ -20,6 +20,7 @@ import android.Manifest; import android.app.DownloadManager; import android.app.admin.DevicePolicyManager; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal.PackagesProvider; @@ -673,7 +674,12 @@ final class DefaultPermissionGrantPolicy { Intent intent, int userId) { ResolveInfo handler = mService.resolveIntent(intent, intent.resolveType(mService.mContext.getContentResolver()), 0, userId); - if (handler == null) { + if (handler == null || handler.activityInfo == null) { + return null; + } + ActivityInfo activityInfo = handler.activityInfo; + if (activityInfo.packageName.equals(mService.mResolveActivity.packageName) + && activityInfo.name.equals(mService.mResolveActivity.name)) { return null; } return getSystemPackageLPr(handler.activityInfo.packageName); 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..4c1fbd375e1e --- /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.net.config" + android:sharedUserId="android.uid.system"> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="android.security.net.config" + android:label="ANSC Tests"> + </instrumentation> +</manifest> diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der Binary files differnew file mode 100644 index 000000000000..235bd4797b78 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem new file mode 100644 index 000000000000..413e3c07d2ab --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT +MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 +aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw +WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE +AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m +OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu +T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c +JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR +Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz +PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm +aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM +TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g +LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO +BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv +dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB +AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL +NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W +b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- diff --git a/tests/NetworkSecurityConfigTest/res/xml/attributes.xml b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml new file mode 100644 index 000000000000..eff13c80c343 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/attributes.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config cleartextTrafficPermitted="false" hstsEnforced="true"> + </base-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml new file mode 100644 index 000000000000..6af855d12d5f --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config0.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <!-- Bad pin digest --> + <pin digest="I am probably not an algorithm">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml new file mode 100644 index 000000000000..d683b74ae197 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config1.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <!-- Unknown pin digest --> + <pin>1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml new file mode 100644 index 000000000000..6f3f8b43d653 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config2.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <!-- empty digest --> + <pin digest="SHA-256"></pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml new file mode 100644 index 000000000000..fb2126c0f778 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config3.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + </domain-config> + <domain-config> + <!-- Same domain name used in two configs --> + <domain>android.com</domain> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml new file mode 100644 index 000000000000..95972cedfc90 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config4.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <!-- domains are not allowed in base-config --> + <domain>android.com</domain> + </base-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml new file mode 100644 index 000000000000..8b6b72151269 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_config5.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <!-- pins are not allowed in base-config --> + <pin-set> + </pin-set> + </base-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml new file mode 100644 index 000000000000..62a7b8819d87 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/bad_pin.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <pin digest="SHA-256">1HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain1.xml b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml new file mode 100644 index 000000000000..6d8565c35717 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/domain1.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain>android.com</domain> + <trust-anchors> + <certificates src="system" /> + <certificates src="user" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml new file mode 100644 index 000000000000..1bd94b648d22 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/empty_config.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml new file mode 100644 index 000000000000..8093b9d05153 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/empty_trust.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml new file mode 100644 index 000000000000..f9f846526a92 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/expired_pin.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <!-- Invalid pin that has expired --> + <pin-set expiration="2015-01-01"> + <pin digest="SHA-256">aaaaaaaaaaa2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml new file mode 100644 index 000000000000..df08467af744 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_configs.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain>android.com</domain> + <trust-anchors> + <certificates src="system" /> + <certificates src="user" /> + </trust-anchors> + </domain-config> + <domain-config> + <domain>google.com</domain> + <trust-anchors> + <certificates src="system" /> + <certificates src="user" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml new file mode 100644 index 000000000000..9743c5f0c4a0 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/multiple_domains.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain>android.com</domain> + <domain>google.com</domain> + <trust-anchors> + <certificates src="system" /> + <certificates src="user" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml new file mode 100644 index 000000000000..d45fd77a5f0f --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain includeSubdomains="true">android.com</domain> + <trust-anchors> + <certificates src="system" /> + </trust-anchors> + <!-- nested config that adds pins --> + <domain-config> + <domain>developer.android.com</domain> + <pin-set> + <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> + </domain-config> + <base-config cleartextTrafficPermitted="false"> + </base-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml new file mode 100644 index 000000000000..84e06e324513 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config cleartextTrafficPermitted="false"> + </base-config> + <!-- Nested config that overrides parent --> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">android.com</domain> + <domain-config cleartextTrafficPermitted="false"> + <domain>developer.android.com</domain> + </domain-config> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml new file mode 100644 index 000000000000..785714a8f19e --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/override_pins.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <pin digest="SHA-256">aaaaaaaaIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + <trust-anchors> + <certificates src="system" overridePins="true" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml new file mode 100644 index 000000000000..1773d28094a3 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config> + <domain>android.com</domain> + <pin-set> + <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml new file mode 100644 index 000000000000..dfd6fd9cc373 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_der.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain>android.com</domain> + <trust-anchors> + <certificates src="@raw/ca_certs_der" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml new file mode 100644 index 000000000000..894f29b4027c --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/resource_anchors_pem.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain>android.com</domain> + <trust-anchors> + <certificates src="@raw/ca_certs_pem" /> + </trust-anchors> + </domain-config> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml new file mode 100644 index 000000000000..482b26c18716 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/subdomains.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <base-config> + <trust-anchors> + </trust-anchors> + </base-config> + <domain-config> + <domain includeSubdomains="true">android.com</domain> + <trust-anchors> + <certificates src="system" /> + <certificates src="user" /> + </trust-anchors> + </domain-config> +</network-security-config> 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..9f48d56cb56b --- /dev/null +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java @@ -0,0 +1,211 @@ +/* + * 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; + } + + + + /** + * 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.Builder().build(); + } + + private NetworkSecurityConfig getSystemStoreConfig() { + return new NetworkSecurityConfig.Builder() + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)) + .build(); + } + + public void testEmptyConfig() throws Exception { + ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap + = new ArraySet<Pair<Domain, NetworkSecurityConfig>>(); + ConfigSource testSource = + new TestConfigSource(domainMap, getEmptyConfig()); + SSLContext context = TestUtils.getSSLContext(testSource); + TestUtils.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())); + NetworkSecurityConfig defaultConfig = getSystemStoreConfig(); + SSLContext context = TestUtils.getSSLContext(new TestConfigSource(domainMap, defaultConfig)); + TestUtils.assertConnectionFails(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + } + + public void testBadPin() throws Exception { + ArraySet<Pin> pins = new ArraySet<Pin>(); + pins.add(new Pin("SHA-256", new byte[0])); + NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder() + .setPinSet(new PinSet(pins, Long.MAX_VALUE)) + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)) + .build(); + ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap + = new ArraySet<Pair<Domain, NetworkSecurityConfig>>(); + domainMap.add(new Pair<Domain, NetworkSecurityConfig>( + new Domain("android.com", true), domain)); + SSLContext context + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig())); + TestUtils.assertConnectionFails(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + } + + public void testGoodPin() throws Exception { + ArraySet<Pin> pins = new ArraySet<Pin>(); + pins.add(new Pin("SHA-256", G2_SPKI_SHA256)); + NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder() + .setPinSet(new PinSet(pins, Long.MAX_VALUE)) + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)) + .build(); + ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap + = new ArraySet<Pair<Domain, NetworkSecurityConfig>>(); + domainMap.add(new Pair<Domain, NetworkSecurityConfig>( + new Domain("android.com", true), domain)); + SSLContext context + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.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. + ArraySet<Pin> pins = new ArraySet<Pin>(); + pins.add(new Pin("SHA-256", new byte[0])); + NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder() + .setPinSet(new PinSet(pins, Long.MAX_VALUE)) + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), true)) + .build(); + ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap + = new ArraySet<Pair<Domain, NetworkSecurityConfig>>(); + domainMap.add(new Pair<Domain, NetworkSecurityConfig>( + new Domain("android.com", true), domain)); + SSLContext context + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.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 + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.assertConnectionFails(context, "android.com", 443); + TestUtils.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 + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.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 = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + } + + public void testConfigBuilderUsesParents() throws Exception { + // Check that a builder with a parent uses the parent's values when non is set. + NetworkSecurityConfig config = new NetworkSecurityConfig.Builder() + .setParent(NetworkSecurityConfig.getDefaultBuilder()) + .build(); + assert(!config.getTrustAnchors().isEmpty()); + } + + public void testConfigBuilderParentLoop() throws Exception { + NetworkSecurityConfig.Builder config1 = new NetworkSecurityConfig.Builder(); + NetworkSecurityConfig.Builder config2 = new NetworkSecurityConfig.Builder(); + config1.setParent(config2); + try { + config2.setParent(config1); + fail("Loop in NetworkSecurityConfig parents"); + } catch (IllegalArgumentException expected) { + } + } + + public void testWithUrlConnection() throws Exception { + ArraySet<Pin> pins = new ArraySet<Pin>(); + pins.add(new Pin("SHA-256", G2_SPKI_SHA256)); + NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder() + .setPinSet(new PinSet(pins, Long.MAX_VALUE)) + .addCertificatesEntryRef( + new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)) + .build(); + ArraySet<Pair<Domain, NetworkSecurityConfig>> domainMap + = new ArraySet<Pair<Domain, NetworkSecurityConfig>>(); + domainMap.add(new Pair<Domain, NetworkSecurityConfig>( + new Domain("android.com", true), domain)); + SSLContext context + = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig())); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443); + TestUtils.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; + } +} diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java new file mode 100644 index 000000000000..43c0e5708233 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java @@ -0,0 +1,76 @@ +/* + * 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.net.Socket; +import java.net.URL; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; + +import junit.framework.Assert; + +public final class TestUtils extends Assert { + + private TestUtils() { + } + + public static 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) { + } + } + + public static void assertConnectionSucceeds(SSLContext context, String host, int port) + throws Exception { + Socket s = context.getSocketFactory().createSocket(host, port); + s.getInputStream(); + } + + public static 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. + } + } + + public static 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(); + } + + public static 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; + } +} diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java new file mode 100644 index 000000000000..f52a27995854 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java @@ -0,0 +1,313 @@ +/* + * 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.test.AndroidTestCase; +import android.test.MoreAsserts; +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 XmlConfigTests extends AndroidTestCase { + + public void testEmptyConfigFile() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_config); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertFalse(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig config = appConfig.getConfigForHostname(""); + assertNotNull(config); + // Check defaults. + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertFalse(config.getTrustAnchors().isEmpty()); + PinSet pinSet = config.getPins(); + assertTrue(pinSet.pins.isEmpty()); + // Try some connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "google.com", 443); + } + + public void testEmptyAnchors() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.empty_trust); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertFalse(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig config = appConfig.getConfigForHostname(""); + assertNotNull(config); + // Check defaults. + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertTrue(config.getTrustAnchors().isEmpty()); + PinSet pinSet = config.getPins(); + assertTrue(pinSet.pins.isEmpty()); + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionFails(context, "android.com", 443); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionFails(context, "google.com", 443); + } + + public void testBasicDomainConfig() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.domain1); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig config = appConfig.getConfigForHostname(""); + assertNotNull(config); + // Check defaults. + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertTrue(config.getTrustAnchors().isEmpty()); + PinSet pinSet = config.getPins(); + assertTrue(pinSet.pins.isEmpty()); + // Check android.com. + config = appConfig.getConfigForHostname("android.com"); + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertFalse(config.getTrustAnchors().isEmpty()); + pinSet = config.getPins(); + assertTrue(pinSet.pins.isEmpty()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionFails(context, "google.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testBasicPinning() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.pins1); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + PinSet pinSet = config.getPins(); + assertFalse(pinSet.pins.isEmpty()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + } + + public void testExpiredPin() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.expired_pin); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + PinSet pinSet = config.getPins(); + assertFalse(pinSet.pins.isEmpty()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testOverridesPins() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_pins); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + PinSet pinSet = config.getPins(); + assertFalse(pinSet.pins.isEmpty()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testBadPin() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.bad_pin); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + PinSet pinSet = config.getPins(); + assertFalse(pinSet.pins.isEmpty()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionFails(context, "android.com", 443); + TestUtils.assertUrlConnectionFails(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + } + + public void testMultipleDomains() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_domains); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertFalse(config.getTrustAnchors().isEmpty()); + PinSet pinSet = config.getPins(); + assertTrue(pinSet.pins.isEmpty()); + // Both android.com and google.com should use the same config + NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com"); + assertEquals(config, other); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testMultipleDomainConfigs() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.multiple_configs); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Should be two different config objects + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + NetworkSecurityConfig other = appConfig.getConfigForHostname("google.com"); + MoreAsserts.assertNotEqual(config, other); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "google.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testIncludeSubdomains() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.subdomains); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "developer.android.com", 443); + TestUtils.assertConnectionFails(context, "google.com", 443); + } + + public void testAttributes() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.attributes); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertFalse(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig config = appConfig.getConfigForHostname(""); + assertTrue(config.isHstsEnforced()); + assertFalse(config.isCleartextTrafficPermitted()); + } + + public void testResourcePemCertificateSource() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_pem); + ApplicationConfig appConfig = new ApplicationConfig(source); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertEquals(2, config.getTrustAnchors().size()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionFails(context, "google.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testResourceDerCertificateSource() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.resource_anchors_der); + ApplicationConfig appConfig = new ApplicationConfig(source); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + assertTrue(config.isCleartextTrafficPermitted()); + assertFalse(config.isHstsEnforced()); + assertEquals(2, config.getTrustAnchors().size()); + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionFails(context, "developer.android.com", 443); + TestUtils.assertUrlConnectionFails(context, "google.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } + + public void testNestedDomainConfigs() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com"); + NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com"); + MoreAsserts.assertNotEqual(parent, child); + MoreAsserts.assertEmpty(parent.getPins().pins); + MoreAsserts.assertNotEmpty(child.getPins().pins); + // Check that the child inherited the cleartext value and anchors. + assertFalse(child.isCleartextTrafficPermitted()); + MoreAsserts.assertNotEmpty(child.getTrustAnchors()); + // Test connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443); + } + + public void testNestedDomainConfigsOverride() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains_override); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com"); + NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com"); + MoreAsserts.assertNotEqual(parent, child); + assertTrue(parent.isCleartextTrafficPermitted()); + assertFalse(child.isCleartextTrafficPermitted()); + } + + private void testBadConfig(int configId) throws Exception { + try { + XmlConfigSource source = new XmlConfigSource(getContext(), configId); + ApplicationConfig appConfig = new ApplicationConfig(source); + appConfig.getConfigForHostname("android.com"); + fail("Bad config " + getContext().getResources().getResourceName(configId) + + " did not fail to parse"); + } catch (RuntimeException e) { + MoreAsserts.assertAssignableFrom(XmlConfigSource.ParserException.class, + e.getCause()); + } + } + + public void testBadConfig0() throws Exception { + testBadConfig(R.xml.bad_config0); + } + + public void testBadConfig1() throws Exception { + testBadConfig(R.xml.bad_config1); + } + + public void testBadConfig2() throws Exception { + testBadConfig(R.xml.bad_config2); + } + + public void testBadConfig3() throws Exception { + testBadConfig(R.xml.bad_config3); + } + + public void testBadConfig4() throws Exception { + testBadConfig(R.xml.bad_config4); + } + + public void testBadConfig5() throws Exception { + testBadConfig(R.xml.bad_config4); + } +} |