Home | History | Annotate | Download | only in config
      1 package android.security.net.config;
      2 
      3 import android.content.Context;
      4 import android.content.res.Resources;
      5 import android.content.res.XmlResourceParser;
      6 import android.os.Build;
      7 import android.util.ArraySet;
      8 import android.util.Base64;
      9 import android.util.Pair;
     10 import com.android.internal.annotations.VisibleForTesting;
     11 import com.android.internal.util.XmlUtils;
     12 
     13 import org.xmlpull.v1.XmlPullParser;
     14 import org.xmlpull.v1.XmlPullParserException;
     15 
     16 import java.io.IOException;
     17 import java.text.ParseException;
     18 import java.text.SimpleDateFormat;
     19 import java.util.ArrayList;
     20 import java.util.Collection;
     21 import java.util.Date;
     22 import java.util.List;
     23 import java.util.Locale;
     24 import java.util.Set;
     25 
     26 /**
     27  * {@link ConfigSource} based on an XML configuration file.
     28  *
     29  * @hide
     30  */
     31 public class XmlConfigSource implements ConfigSource {
     32     private static final int CONFIG_BASE = 0;
     33     private static final int CONFIG_DOMAIN = 1;
     34     private static final int CONFIG_DEBUG = 2;
     35 
     36     private final Object mLock = new Object();
     37     private final int mResourceId;
     38     private final boolean mDebugBuild;
     39     private final int mTargetSdkVersion;
     40 
     41     private boolean mInitialized;
     42     private NetworkSecurityConfig mDefaultConfig;
     43     private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap;
     44     private Context mContext;
     45 
     46     @VisibleForTesting
     47     public XmlConfigSource(Context context, int resourceId) {
     48         this(context, resourceId, false);
     49     }
     50 
     51     @VisibleForTesting
     52     public XmlConfigSource(Context context, int resourceId, boolean debugBuild) {
     53         this(context, resourceId, debugBuild, Build.VERSION_CODES.CUR_DEVELOPMENT);
     54     }
     55 
     56     public XmlConfigSource(Context context, int resourceId, boolean debugBuild,
     57             int targetSdkVersion) {
     58         mResourceId = resourceId;
     59         mContext = context;
     60         mDebugBuild = debugBuild;
     61         mTargetSdkVersion = targetSdkVersion;
     62     }
     63 
     64     public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
     65         ensureInitialized();
     66         return mDomainMap;
     67     }
     68 
     69     public NetworkSecurityConfig getDefaultConfig() {
     70         ensureInitialized();
     71         return mDefaultConfig;
     72     }
     73 
     74     private static final String getConfigString(int configType) {
     75         switch (configType) {
     76             case CONFIG_BASE:
     77                 return "base-config";
     78             case CONFIG_DOMAIN:
     79                 return "domain-config";
     80             case CONFIG_DEBUG:
     81                 return "debug-overrides";
     82             default:
     83                 throw new IllegalArgumentException("Unknown config type: " + configType);
     84         }
     85     }
     86 
     87     private void ensureInitialized() {
     88         synchronized (mLock) {
     89             if (mInitialized) {
     90                 return;
     91             }
     92             try (XmlResourceParser parser = mContext.getResources().getXml(mResourceId)) {
     93                 parseNetworkSecurityConfig(parser);
     94                 mContext = null;
     95                 mInitialized = true;
     96             } catch (Resources.NotFoundException | XmlPullParserException | IOException
     97                     | ParserException e) {
     98                 throw new RuntimeException("Failed to parse XML configuration from "
     99                         + mContext.getResources().getResourceEntryName(mResourceId), e);
    100             }
    101         }
    102     }
    103 
    104     private Pin parsePin(XmlResourceParser parser)
    105             throws IOException, XmlPullParserException, ParserException {
    106         String digestAlgorithm = parser.getAttributeValue(null, "digest");
    107         if (!Pin.isSupportedDigestAlgorithm(digestAlgorithm)) {
    108             throw new ParserException(parser, "Unsupported pin digest algorithm: "
    109                     + digestAlgorithm);
    110         }
    111         if (parser.next() != XmlPullParser.TEXT) {
    112             throw new ParserException(parser, "Missing pin digest");
    113         }
    114         String digest = parser.getText().trim();
    115         byte[] decodedDigest = null;
    116         try {
    117             decodedDigest = Base64.decode(digest, 0);
    118         } catch (IllegalArgumentException e) {
    119             throw new ParserException(parser, "Invalid pin digest", e);
    120         }
    121         int expectedLength = Pin.getDigestLength(digestAlgorithm);
    122         if (decodedDigest.length != expectedLength) {
    123             throw new ParserException(parser, "digest length " + decodedDigest.length
    124                     + " does not match expected length for " + digestAlgorithm + " of "
    125                     + expectedLength);
    126         }
    127         if (parser.next() != XmlPullParser.END_TAG) {
    128             throw new ParserException(parser, "pin contains additional elements");
    129         }
    130         return new Pin(digestAlgorithm, decodedDigest);
    131     }
    132 
    133     private PinSet parsePinSet(XmlResourceParser parser)
    134             throws IOException, XmlPullParserException, ParserException {
    135         String expirationDate = parser.getAttributeValue(null, "expiration");
    136         long expirationTimestampMilis = Long.MAX_VALUE;
    137         if (expirationDate != null) {
    138             try {
    139                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    140                 sdf.setLenient(false);
    141                 Date date = sdf.parse(expirationDate);
    142                 if (date == null) {
    143                     throw new ParserException(parser, "Invalid expiration date in pin-set");
    144                 }
    145                 expirationTimestampMilis = date.getTime();
    146             } catch (ParseException e) {
    147                 throw new ParserException(parser, "Invalid expiration date in pin-set", e);
    148             }
    149         }
    150 
    151         int outerDepth = parser.getDepth();
    152         Set<Pin> pins = new ArraySet<>();
    153         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    154             String tagName = parser.getName();
    155             if (tagName.equals("pin")) {
    156                 pins.add(parsePin(parser));
    157             } else {
    158                 XmlUtils.skipCurrentTag(parser);
    159             }
    160         }
    161         return new PinSet(pins, expirationTimestampMilis);
    162     }
    163 
    164     private Domain parseDomain(XmlResourceParser parser, Set<String> seenDomains)
    165             throws IOException, XmlPullParserException, ParserException {
    166         boolean includeSubdomains =
    167                 parser.getAttributeBooleanValue(null, "includeSubdomains", false);
    168         if (parser.next() != XmlPullParser.TEXT) {
    169             throw new ParserException(parser, "Domain name missing");
    170         }
    171         String domain = parser.getText().trim().toLowerCase(Locale.US);
    172         if (parser.next() != XmlPullParser.END_TAG) {
    173             throw new ParserException(parser, "domain contains additional elements");
    174         }
    175         // Domains are matched using a most specific match, so don't allow duplicates.
    176         // includeSubdomains isn't relevant here, both android.com + subdomains and android.com
    177         // match for android.com equally. Do not allow any duplicates period.
    178         if (!seenDomains.add(domain)) {
    179             throw new ParserException(parser, domain + " has already been specified");
    180         }
    181         return new Domain(domain, includeSubdomains);
    182     }
    183 
    184     private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser,
    185             boolean defaultOverridePins)
    186             throws IOException, XmlPullParserException, ParserException {
    187         boolean overridePins =
    188                 parser.getAttributeBooleanValue(null, "overridePins", defaultOverridePins);
    189         int sourceId = parser.getAttributeResourceValue(null, "src", -1);
    190         String sourceString = parser.getAttributeValue(null, "src");
    191         CertificateSource source = null;
    192         if (sourceString == null) {
    193             throw new ParserException(parser, "certificates element missing src attribute");
    194         }
    195         if (sourceId != -1) {
    196             // TODO: Cache ResourceCertificateSources by sourceId
    197             source = new ResourceCertificateSource(sourceId, mContext);
    198         } else if ("system".equals(sourceString)) {
    199             source = SystemCertificateSource.getInstance();
    200         } else if ("user".equals(sourceString)) {
    201             source = UserCertificateSource.getInstance();
    202         } else {
    203             throw new ParserException(parser, "Unknown certificates src. "
    204                     + "Should be one of system|user|@resourceVal");
    205         }
    206         XmlUtils.skipCurrentTag(parser);
    207         return new CertificatesEntryRef(source, overridePins);
    208     }
    209 
    210     private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser,
    211             boolean defaultOverridePins)
    212             throws IOException, XmlPullParserException, ParserException {
    213         int outerDepth = parser.getDepth();
    214         List<CertificatesEntryRef> anchors = new ArrayList<>();
    215         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    216             String tagName = parser.getName();
    217             if (tagName.equals("certificates")) {
    218                 anchors.add(parseCertificatesEntry(parser, defaultOverridePins));
    219             } else {
    220                 XmlUtils.skipCurrentTag(parser);
    221             }
    222         }
    223         return anchors;
    224     }
    225 
    226     private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
    227             XmlResourceParser parser, Set<String> seenDomains,
    228             NetworkSecurityConfig.Builder parentBuilder, int configType)
    229             throws IOException, XmlPullParserException, ParserException {
    230         List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
    231         NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
    232         builder.setParent(parentBuilder);
    233         Set<Domain> domains = new ArraySet<>();
    234         boolean seenPinSet = false;
    235         boolean seenTrustAnchors = false;
    236         boolean defaultOverridePins = configType == CONFIG_DEBUG;
    237         String configName = parser.getName();
    238         int outerDepth = parser.getDepth();
    239         // Add this builder now so that this builder occurs before any of its children. This
    240         // makes the final build pass easier.
    241         builders.add(new Pair<>(builder, domains));
    242         // Parse config attributes. Only set values that are present, config inheritence will
    243         // handle the rest.
    244         for (int i = 0; i < parser.getAttributeCount(); i++) {
    245             String name = parser.getAttributeName(i);
    246             if ("hstsEnforced".equals(name)) {
    247                 builder.setHstsEnforced(
    248                         parser.getAttributeBooleanValue(i,
    249                                 NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED));
    250             } else if ("cleartextTrafficPermitted".equals(name)) {
    251                 builder.setCleartextTrafficPermitted(
    252                         parser.getAttributeBooleanValue(i,
    253                                 NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED));
    254             }
    255         }
    256         // Parse the config elements.
    257         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    258             String tagName = parser.getName();
    259             if ("domain".equals(tagName)) {
    260                 if (configType != CONFIG_DOMAIN) {
    261                     throw new ParserException(parser,
    262                             "domain element not allowed in " + getConfigString(configType));
    263                 }
    264                 Domain domain = parseDomain(parser, seenDomains);
    265                 domains.add(domain);
    266             } else if ("trust-anchors".equals(tagName)) {
    267                 if (seenTrustAnchors) {
    268                     throw new ParserException(parser,
    269                             "Multiple trust-anchor elements not allowed");
    270                 }
    271                 builder.addCertificatesEntryRefs(
    272                         parseTrustAnchors(parser, defaultOverridePins));
    273                 seenTrustAnchors = true;
    274             } else if ("pin-set".equals(tagName)) {
    275                 if (configType != CONFIG_DOMAIN) {
    276                     throw new ParserException(parser,
    277                             "pin-set element not allowed in " + getConfigString(configType));
    278                 }
    279                 if (seenPinSet) {
    280                     throw new ParserException(parser, "Multiple pin-set elements not allowed");
    281                 }
    282                 builder.setPinSet(parsePinSet(parser));
    283                 seenPinSet = true;
    284             } else if ("domain-config".equals(tagName)) {
    285                 if (configType != CONFIG_DOMAIN) {
    286                     throw new ParserException(parser,
    287                             "Nested domain-config not allowed in " + getConfigString(configType));
    288                 }
    289                 builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType));
    290             } else {
    291                 XmlUtils.skipCurrentTag(parser);
    292             }
    293         }
    294         if (configType == CONFIG_DOMAIN && domains.isEmpty()) {
    295             throw new ParserException(parser, "No domain elements in domain-config");
    296         }
    297         return builders;
    298     }
    299 
    300     private void addDebugAnchorsIfNeeded(NetworkSecurityConfig.Builder debugConfigBuilder,
    301             NetworkSecurityConfig.Builder builder) {
    302         if (debugConfigBuilder == null || !debugConfigBuilder.hasCertificatesEntryRefs()) {
    303             return;
    304         }
    305         // Don't add trust anchors if not already present, the builder will inherit the anchors
    306         // from its parent, and that's where the trust anchors should be added.
    307         if (!builder.hasCertificatesEntryRefs()) {
    308             return;
    309         }
    310 
    311         builder.addCertificatesEntryRefs(debugConfigBuilder.getCertificatesEntryRefs());
    312     }
    313 
    314     private void parseNetworkSecurityConfig(XmlResourceParser parser)
    315             throws IOException, XmlPullParserException, ParserException {
    316         Set<String> seenDomains = new ArraySet<>();
    317         List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
    318         NetworkSecurityConfig.Builder baseConfigBuilder = null;
    319         NetworkSecurityConfig.Builder debugConfigBuilder = null;
    320         boolean seenDebugOverrides = false;
    321         boolean seenBaseConfig = false;
    322 
    323         XmlUtils.beginDocument(parser, "network-security-config");
    324         int outerDepth = parser.getDepth();
    325         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    326             if ("base-config".equals(parser.getName())) {
    327                 if (seenBaseConfig) {
    328                     throw new ParserException(parser, "Only one base-config allowed");
    329                 }
    330                 seenBaseConfig = true;
    331                 baseConfigBuilder =
    332                         parseConfigEntry(parser, seenDomains, null, CONFIG_BASE).get(0).first;
    333             } else if ("domain-config".equals(parser.getName())) {
    334                 builders.addAll(
    335                         parseConfigEntry(parser, seenDomains, baseConfigBuilder, CONFIG_DOMAIN));
    336             } else if ("debug-overrides".equals(parser.getName())) {
    337                 if (seenDebugOverrides) {
    338                     throw new ParserException(parser, "Only one debug-overrides allowed");
    339                 }
    340                 if (mDebugBuild) {
    341                     debugConfigBuilder =
    342                             parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first;
    343                 } else {
    344                     XmlUtils.skipCurrentTag(parser);
    345                 }
    346                 seenDebugOverrides = true;
    347             } else {
    348                 XmlUtils.skipCurrentTag(parser);
    349             }
    350         }
    351         // If debug is true and there was no debug-overrides in the file check for an extra
    352         // _debug resource.
    353         if (mDebugBuild && debugConfigBuilder == null) {
    354             debugConfigBuilder = parseDebugOverridesResource();
    355         }
    356 
    357         // Use the platform default as the parent of the base config for any values not provided
    358         // there. If there is no base config use the platform default.
    359         NetworkSecurityConfig.Builder platformDefaultBuilder =
    360                 NetworkSecurityConfig.getDefaultBuilder(mTargetSdkVersion);
    361         addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder);
    362         if (baseConfigBuilder != null) {
    363             baseConfigBuilder.setParent(platformDefaultBuilder);
    364             addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder);
    365         } else {
    366             baseConfigBuilder = platformDefaultBuilder;
    367         }
    368         // Build the per-domain config mapping.
    369         Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>();
    370 
    371         for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
    372             NetworkSecurityConfig.Builder builder = entry.first;
    373             Set<Domain> domains = entry.second;
    374             // Set the parent of configs that do not have a parent to the base-config. This can
    375             // happen if the base-config comes after a domain-config in the file.
    376             // Note that this is safe with regards to children because of the order that
    377             // parseConfigEntry returns builders, the parent is always before the children. The
    378             // children builders will not have build called until _after_ their parents have their
    379             // parent set so everything is consistent.
    380             if (builder.getParent() == null) {
    381                 builder.setParent(baseConfigBuilder);
    382             }
    383             addDebugAnchorsIfNeeded(debugConfigBuilder, builder);
    384             NetworkSecurityConfig config = builder.build();
    385             for (Domain domain : domains) {
    386                 configs.add(new Pair<>(domain, config));
    387             }
    388         }
    389         mDefaultConfig = baseConfigBuilder.build();
    390         mDomainMap = configs;
    391     }
    392 
    393     private NetworkSecurityConfig.Builder parseDebugOverridesResource()
    394             throws IOException, XmlPullParserException, ParserException {
    395         Resources resources = mContext.getResources();
    396         String packageName = resources.getResourcePackageName(mResourceId);
    397         String entryName = resources.getResourceEntryName(mResourceId);
    398         int resId = resources.getIdentifier(entryName + "_debug", "xml", packageName);
    399         // No debug-overrides resource was found, nothing to parse.
    400         if (resId == 0) {
    401             return null;
    402         }
    403         NetworkSecurityConfig.Builder debugConfigBuilder = null;
    404         // Parse debug-overrides out of the _debug resource.
    405         try (XmlResourceParser parser = resources.getXml(resId)) {
    406             XmlUtils.beginDocument(parser, "network-security-config");
    407             int outerDepth = parser.getDepth();
    408             boolean seenDebugOverrides = false;
    409             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
    410                 if ("debug-overrides".equals(parser.getName())) {
    411                     if (seenDebugOverrides) {
    412                         throw new ParserException(parser, "Only one debug-overrides allowed");
    413                     }
    414                     if (mDebugBuild) {
    415                         debugConfigBuilder =
    416                                 parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first;
    417                     } else {
    418                         XmlUtils.skipCurrentTag(parser);
    419                     }
    420                     seenDebugOverrides = true;
    421                 } else {
    422                     XmlUtils.skipCurrentTag(parser);
    423                 }
    424             }
    425         }
    426 
    427         return debugConfigBuilder;
    428     }
    429 
    430     public static class ParserException extends Exception {
    431 
    432         public ParserException(XmlPullParser parser, String message, Throwable cause) {
    433             super(message + " at: " + parser.getPositionDescription(), cause);
    434         }
    435 
    436         public ParserException(XmlPullParser parser, String message) {
    437             this(parser, message, null);
    438         }
    439     }
    440 }
    441