blob: 2a8773cd3d8649859824e692f0bb0c804a80b1e5 [file] [log] [blame]
Chad Brubaker5f967022015-11-04 23:55:29 -08001package android.security.net.config;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.content.res.XmlResourceParser;
Chad Brubaker32d2a102016-02-23 16:01:55 -08006import android.os.Build;
Chad Brubaker5f967022015-11-04 23:55:29 -08007import android.util.ArraySet;
8import android.util.Base64;
9import android.util.Pair;
Chad Brubaker32d2a102016-02-23 16:01:55 -080010import com.android.internal.annotations.VisibleForTesting;
Chad Brubaker5f967022015-11-04 23:55:29 -080011import 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 {
Chad Brubaker08d36202015-11-09 13:38:51 -080032 private static final int CONFIG_BASE = 0;
33 private static final int CONFIG_DOMAIN = 1;
34 private static final int CONFIG_DEBUG = 2;
35
Chad Brubaker5f967022015-11-04 23:55:29 -080036 private final Object mLock = new Object();
37 private final int mResourceId;
Chad Brubaker08d36202015-11-09 13:38:51 -080038 private final boolean mDebugBuild;
Chad Brubaker32d2a102016-02-23 16:01:55 -080039 private final int mTargetSdkVersion;
Chad Brubaker5f967022015-11-04 23:55:29 -080040
41 private boolean mInitialized;
42 private NetworkSecurityConfig mDefaultConfig;
43 private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap;
44 private Context mContext;
45
Chad Brubaker32d2a102016-02-23 16:01:55 -080046 @VisibleForTesting
Chad Brubaker5f967022015-11-04 23:55:29 -080047 public XmlConfigSource(Context context, int resourceId) {
Chad Brubaker08d36202015-11-09 13:38:51 -080048 this(context, resourceId, false);
49 }
50
Chad Brubaker32d2a102016-02-23 16:01:55 -080051 @VisibleForTesting
Chad Brubaker08d36202015-11-09 13:38:51 -080052 public XmlConfigSource(Context context, int resourceId, boolean debugBuild) {
Chad Brubaker32d2a102016-02-23 16:01:55 -080053 this(context, resourceId, debugBuild, Build.VERSION_CODES.CUR_DEVELOPMENT);
54 }
55
56 public XmlConfigSource(Context context, int resourceId, boolean debugBuild,
57 int targetSdkVersion) {
Chad Brubaker5f967022015-11-04 23:55:29 -080058 mResourceId = resourceId;
59 mContext = context;
Chad Brubaker08d36202015-11-09 13:38:51 -080060 mDebugBuild = debugBuild;
Chad Brubaker32d2a102016-02-23 16:01:55 -080061 mTargetSdkVersion = targetSdkVersion;
Chad Brubaker5f967022015-11-04 23:55:29 -080062 }
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
Chad Brubaker08d36202015-11-09 13:38:51 -080074 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
Chad Brubaker5f967022015-11-04 23:55:29 -080087 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
Chad Brubaker08d36202015-11-09 13:38:51 -0800184 private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser,
185 boolean defaultOverridePins)
Chad Brubaker5f967022015-11-04 23:55:29 -0800186 throws IOException, XmlPullParserException, ParserException {
Chad Brubaker08d36202015-11-09 13:38:51 -0800187 boolean overridePins =
188 parser.getAttributeBooleanValue(null, "overridePins", defaultOverridePins);
Chad Brubaker5f967022015-11-04 23:55:29 -0800189 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
Chad Brubaker08d36202015-11-09 13:38:51 -0800210 private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser,
211 boolean defaultOverridePins)
Chad Brubaker5f967022015-11-04 23:55:29 -0800212 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")) {
Chad Brubaker08d36202015-11-09 13:38:51 -0800218 anchors.add(parseCertificatesEntry(parser, defaultOverridePins));
Chad Brubaker5f967022015-11-04 23:55:29 -0800219 } else {
220 XmlUtils.skipCurrentTag(parser);
221 }
222 }
223 return anchors;
224 }
225
Chad Brubakerbd173c22015-11-06 23:02:37 -0800226 private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
227 XmlResourceParser parser, Set<String> seenDomains,
Chad Brubaker08d36202015-11-09 13:38:51 -0800228 NetworkSecurityConfig.Builder parentBuilder, int configType)
Chad Brubaker5f967022015-11-04 23:55:29 -0800229 throws IOException, XmlPullParserException, ParserException {
Chad Brubakerbd173c22015-11-06 23:02:37 -0800230 List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
Chad Brubaker5f967022015-11-04 23:55:29 -0800231 NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
Chad Brubakerbd173c22015-11-06 23:02:37 -0800232 builder.setParent(parentBuilder);
Chad Brubaker5f967022015-11-04 23:55:29 -0800233 Set<Domain> domains = new ArraySet<>();
234 boolean seenPinSet = false;
235 boolean seenTrustAnchors = false;
Chad Brubaker08d36202015-11-09 13:38:51 -0800236 boolean defaultOverridePins = configType == CONFIG_DEBUG;
Chad Brubaker5f967022015-11-04 23:55:29 -0800237 String configName = parser.getName();
238 int outerDepth = parser.getDepth();
Chad Brubakerbd173c22015-11-06 23:02:37 -0800239 // 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));
Chad Brubaker5f967022015-11-04 23:55:29 -0800242 // 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();
Chad Brubaker5f967022015-11-04 23:55:29 -0800259 if ("domain".equals(tagName)) {
Chad Brubaker08d36202015-11-09 13:38:51 -0800260 if (configType != CONFIG_DOMAIN) {
261 throw new ParserException(parser,
262 "domain element not allowed in " + getConfigString(configType));
Chad Brubaker5f967022015-11-04 23:55:29 -0800263 }
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 }
Chad Brubaker08d36202015-11-09 13:38:51 -0800271 builder.addCertificatesEntryRefs(
272 parseTrustAnchors(parser, defaultOverridePins));
Chad Brubaker5f967022015-11-04 23:55:29 -0800273 seenTrustAnchors = true;
274 } else if ("pin-set".equals(tagName)) {
Chad Brubaker08d36202015-11-09 13:38:51 -0800275 if (configType != CONFIG_DOMAIN) {
Chad Brubaker5f967022015-11-04 23:55:29 -0800276 throw new ParserException(parser,
Chad Brubaker08d36202015-11-09 13:38:51 -0800277 "pin-set element not allowed in " + getConfigString(configType));
Chad Brubaker5f967022015-11-04 23:55:29 -0800278 }
279 if (seenPinSet) {
280 throw new ParserException(parser, "Multiple pin-set elements not allowed");
281 }
282 builder.setPinSet(parsePinSet(parser));
283 seenPinSet = true;
Chad Brubakerbd173c22015-11-06 23:02:37 -0800284 } else if ("domain-config".equals(tagName)) {
Chad Brubaker08d36202015-11-09 13:38:51 -0800285 if (configType != CONFIG_DOMAIN) {
Chad Brubakerbd173c22015-11-06 23:02:37 -0800286 throw new ParserException(parser,
Chad Brubaker08d36202015-11-09 13:38:51 -0800287 "Nested domain-config not allowed in " + getConfigString(configType));
Chad Brubakerbd173c22015-11-06 23:02:37 -0800288 }
Chad Brubaker08d36202015-11-09 13:38:51 -0800289 builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType));
Chad Brubaker5f967022015-11-04 23:55:29 -0800290 } else {
291 XmlUtils.skipCurrentTag(parser);
292 }
293 }
Chad Brubaker08d36202015-11-09 13:38:51 -0800294 if (configType == CONFIG_DOMAIN && domains.isEmpty()) {
Chad Brubaker5f967022015-11-04 23:55:29 -0800295 throw new ParserException(parser, "No domain elements in domain-config");
296 }
Chad Brubakerbd173c22015-11-06 23:02:37 -0800297 return builders;
Chad Brubaker5f967022015-11-04 23:55:29 -0800298 }
299
Chad Brubaker08d36202015-11-09 13:38:51 -0800300 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
Chad Brubaker5f967022015-11-04 23:55:29 -0800314 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;
Chad Brubaker08d36202015-11-09 13:38:51 -0800319 NetworkSecurityConfig.Builder debugConfigBuilder = null;
Chad Brubaker5f967022015-11-04 23:55:29 -0800320 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)) {
Chad Brubaker5f967022015-11-04 23:55:29 -0800326 if ("base-config".equals(parser.getName())) {
327 if (seenBaseConfig) {
328 throw new ParserException(parser, "Only one base-config allowed");
329 }
330 seenBaseConfig = true;
Chad Brubaker08d36202015-11-09 13:38:51 -0800331 baseConfigBuilder =
332 parseConfigEntry(parser, seenDomains, null, CONFIG_BASE).get(0).first;
Chad Brubaker5f967022015-11-04 23:55:29 -0800333 } else if ("domain-config".equals(parser.getName())) {
Chad Brubaker08d36202015-11-09 13:38:51 -0800334 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, seenDomains, null, CONFIG_DEBUG).get(0).first;
343 } else {
344 XmlUtils.skipCurrentTag(parser);
345 }
346 seenDebugOverrides = true;
Chad Brubaker5f967022015-11-04 23:55:29 -0800347 } else {
348 XmlUtils.skipCurrentTag(parser);
349 }
350 }
351
352 // Use the platform default as the parent of the base config for any values not provided
353 // there. If there is no base config use the platform default.
354 NetworkSecurityConfig.Builder platformDefaultBuilder =
Chad Brubaker32d2a102016-02-23 16:01:55 -0800355 NetworkSecurityConfig.getDefaultBuilder(mTargetSdkVersion);
Chad Brubaker08d36202015-11-09 13:38:51 -0800356 addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder);
Chad Brubaker5f967022015-11-04 23:55:29 -0800357 if (baseConfigBuilder != null) {
358 baseConfigBuilder.setParent(platformDefaultBuilder);
Chad Brubaker08d36202015-11-09 13:38:51 -0800359 addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder);
Chad Brubaker5f967022015-11-04 23:55:29 -0800360 } else {
361 baseConfigBuilder = platformDefaultBuilder;
362 }
363 // Build the per-domain config mapping.
364 Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>();
365
366 for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
367 NetworkSecurityConfig.Builder builder = entry.first;
368 Set<Domain> domains = entry.second;
Chad Brubakerbd173c22015-11-06 23:02:37 -0800369 // Set the parent of configs that do not have a parent to the base-config. This can
370 // happen if the base-config comes after a domain-config in the file.
371 // Note that this is safe with regards to children because of the order that
372 // parseConfigEntry returns builders, the parent is always before the children. The
373 // children builders will not have build called until _after_ their parents have their
374 // parent set so everything is consistent.
375 if (builder.getParent() == null) {
376 builder.setParent(baseConfigBuilder);
377 }
Chad Brubaker08d36202015-11-09 13:38:51 -0800378 addDebugAnchorsIfNeeded(debugConfigBuilder, builder);
Chad Brubaker5f967022015-11-04 23:55:29 -0800379 NetworkSecurityConfig config = builder.build();
380 for (Domain domain : domains) {
381 configs.add(new Pair<>(domain, config));
382 }
383 }
384 mDefaultConfig = baseConfigBuilder.build();
385 mDomainMap = configs;
386 }
387
388 public static class ParserException extends Exception {
389
390 public ParserException(XmlPullParser parser, String message, Throwable cause) {
391 super(message + " at: " + parser.getPositionDescription(), cause);
392 }
393
394 public ParserException(XmlPullParser parser, String message) {
395 this(parser, message, null);
396 }
397 }
398}