Merge "Add xml source for network security configuration" am: ead46ecc84

am: 0cb8f01410

* commit '0cb8f014105b9e8e3bdb12cf091ef61e8c575333':
  Add xml source for network security configuration
This commit is contained in:
Chad Brubaker
2015-11-07 22:21:40 +00:00
committed by android-build-merger
27 changed files with 951 additions and 69 deletions

View File

@@ -30,6 +30,26 @@ public final class Pin {
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;

View File

@@ -0,0 +1,310 @@
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 Pair<NetworkSecurityConfig.Builder, Set<Domain>> parseConfigEntry(
XmlResourceParser parser, Set<String> seenDomains, boolean baseConfig)
throws IOException, XmlPullParserException, ParserException {
NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
Set<Domain> domains = new ArraySet<>();
boolean seenPinSet = false;
boolean seenTrustAnchors = false;
String configName = parser.getName();
int outerDepth = parser.getDepth();
// 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();
// TODO: Support nested domain-config entries.
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 {
XmlUtils.skipCurrentTag(parser);
}
}
if (!baseConfig && domains.isEmpty()) {
throw new ParserException(parser, "No domain elements in domain-config");
}
return new Pair<>(builder, domains);
}
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, true).first;
} else if ("domain-config".equals(parser.getName())) {
builders.add(parseConfigEntry(parser, seenDomains, 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;
// Use the base-config for inheriting any unset values in the domain-config entry.
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);
}
}
}

View File

@@ -15,7 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.security.tests"
package="android.security.net.config"
android:sharedUserId="android.uid.system">
<application>
@@ -23,7 +23,7 @@
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="android.security.tests"
android:targetPackage="android.security.net.config"
android:label="ANSC Tests">
</instrumentation>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
</network-security-config>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,49 +50,6 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
return data;
}
private void assertConnectionFails(SSLContext context, String host, int port)
throws Exception {
try {
Socket s = context.getSocketFactory().createSocket(host, port);
s.getInputStream();
fail("Expected connection to " + host + ":" + port + " to fail.");
} catch (SSLHandshakeException expected) {
}
}
private void assertConnectionSucceeds(SSLContext context, String host, int port)
throws Exception {
Socket s = context.getSocketFactory().createSocket(host, port);
s.getInputStream();
}
private void assertUrlConnectionFails(SSLContext context, String host, int port)
throws Exception {
URL url = new URL("https://" + host + ":" + port);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
try {
connection.getInputStream();
fail("Connection to " + host + ":" + port + " expected to fail");
} catch (SSLHandshakeException expected) {
// ignored.
}
}
private void assertUrlConnectionSucceeds(SSLContext context, String host, int port)
throws Exception {
URL url = new URL("https://" + host + ":" + port);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
connection.getInputStream();
}
private SSLContext getSSLContext(ConfigSource source) throws Exception {
ApplicationConfig config = new ApplicationConfig(source);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] {config.getTrustManager()}, null);
return context;
}
/**
@@ -115,8 +72,8 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
= new ArraySet<Pair<Domain, NetworkSecurityConfig>>();
ConfigSource testSource =
new TestConfigSource(domainMap, getEmptyConfig());
SSLContext context = getSSLContext(testSource);
assertConnectionFails(context, "android.com", 443);
SSLContext context = TestUtils.getSSLContext(testSource);
TestUtils.assertConnectionFails(context, "android.com", 443);
}
public void testEmptyPerNetworkSecurityConfig() throws Exception {
@@ -125,9 +82,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), getEmptyConfig()));
NetworkSecurityConfig defaultConfig = getSystemStoreConfig();
SSLContext context = getSSLContext(new TestConfigSource(domainMap, defaultConfig));
assertConnectionFails(context, "android.com", 443);
assertConnectionSucceeds(context, "google.com", 443);
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 {
@@ -143,9 +100,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), domain));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
assertConnectionFails(context, "android.com", 443);
assertConnectionSucceeds(context, "google.com", 443);
= TestUtils.getSSLContext(new TestConfigSource(domainMap, getSystemStoreConfig()));
TestUtils.assertConnectionFails(context, "android.com", 443);
TestUtils.assertConnectionSucceeds(context, "google.com", 443);
}
public void testGoodPin() throws Exception {
@@ -161,9 +118,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), domain));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertConnectionSucceeds(context, "android.com", 443);
assertConnectionSucceeds(context, "developer.android.com", 443);
= TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
TestUtils.assertConnectionSucceeds(context, "android.com", 443);
TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
}
public void testOverridePins() throws Exception {
@@ -180,8 +137,8 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), domain));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertConnectionSucceeds(context, "android.com", 443);
= TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
TestUtils.assertConnectionSucceeds(context, "android.com", 443);
}
public void testMostSpecificNetworkSecurityConfig() throws Exception {
@@ -192,9 +149,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("developer.android.com", false), getSystemStoreConfig()));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertConnectionFails(context, "android.com", 443);
assertConnectionSucceeds(context, "developer.android.com", 443);
= TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
TestUtils.assertConnectionFails(context, "android.com", 443);
TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
}
public void testSubdomainIncluded() throws Exception {
@@ -204,14 +161,14 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), getSystemStoreConfig()));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertConnectionSucceeds(context, "developer.android.com", 443);
= 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 = getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertConnectionFails(context, "developer.android.com", 443);
context = TestUtils.getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
TestUtils.assertConnectionFails(context, "developer.android.com", 443);
}
public void testConfigBuilderUsesParents() throws Exception {
@@ -246,9 +203,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
domainMap.add(new Pair<Domain, NetworkSecurityConfig>(
new Domain("android.com", true), domain));
SSLContext context
= getSSLContext(new TestConfigSource(domainMap, getEmptyConfig()));
assertUrlConnectionSucceeds(context, "android.com", 443);
assertUrlConnectionSucceeds(context, "developer.android.com", 443);
assertUrlConnectionFails(context, "google.com", 443);
= 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);
}
}

View File

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

View File

@@ -0,0 +1,284 @@
/*
* 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);
}
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);
}
}