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