Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5f96702f authored by Chad Brubaker's avatar Chad Brubaker
Browse files

Add xml source for network security configuration

XmlConfigSource parses an ApplicationConfig from an xml resource.
Currently this supports app-wide default configuration via the
base-config element, per domain via the domain-config element and
inheritance of unset properties at parse time.

Inheritance of unset properties is currently only:
domain-config -> base-config -> platform default configuration
Where the most specific value is used.
For example: If the base-config specifies trust anchors, all connections
will use those anchors except for connections to a domain which has a
domain-config that specifies trust anchors, in which case the
domain-config's trust anchors will be used. If the domain-config or
base-config don't set trust anchors, or don't exist, then the platform
default trust anchors will be used.

Nested domain-config entries, debug-overrides, and thorough
documentation of the xml format will follow in later commits.

Change-Id: I1232ff1e8079a81b340bc12e142f0889f6947aa0
parent 1347cb81
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -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;
+310 −0
Original line number Diff line number Diff line
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);
        }
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -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>
+1.44 KiB

File added.

No diff preview for this file type.

+35 −0
Original line number Diff line number Diff line
-----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-----
Loading