XmlConfigSource.java revision 567f6f24747c80b4ab362a22985576c4f8a418fd
1package android.security.net.config;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.content.res.XmlResourceParser;
6import android.os.Build;
7import android.util.ArraySet;
8import android.util.Base64;
9import android.util.Pair;
10import com.android.internal.annotations.VisibleForTesting;
11import com.android.internal.util.XmlUtils;
12
13import org.xmlpull.v1.XmlPullParser;
14import org.xmlpull.v1.XmlPullParserException;
15
16import java.io.IOException;
17import java.text.ParseException;
18import java.text.SimpleDateFormat;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Date;
22import java.util.List;
23import java.util.Locale;
24import java.util.Set;
25
26/**
27 * {@link ConfigSource} based on an XML configuration file.
28 *
29 * @hide
30 */
31public 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();
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().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