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