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 { 193 throw new ParserException(parser, "Unknown certificates src. " 194 + "Should be one of system|user|@resourceVal"); 195 } 196 XmlUtils.skipCurrentTag(parser); 197 return new CertificatesEntryRef(source, overridePins); 198 } 199 parseTrustAnchors(XmlResourceParser parser, boolean defaultOverridePins)200 private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser, 201 boolean defaultOverridePins) 202 throws IOException, XmlPullParserException, ParserException { 203 int outerDepth = parser.getDepth(); 204 List<CertificatesEntryRef> anchors = new ArrayList<>(); 205 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 206 String tagName = parser.getName(); 207 if (tagName.equals("certificates")) { 208 anchors.add(parseCertificatesEntry(parser, defaultOverridePins)); 209 } else { 210 XmlUtils.skipCurrentTag(parser); 211 } 212 } 213 return anchors; 214 } 215 parseConfigEntry( XmlResourceParser parser, Set<String> seenDomains, NetworkSecurityConfig.Builder parentBuilder, int configType)216 private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry( 217 XmlResourceParser parser, Set<String> seenDomains, 218 NetworkSecurityConfig.Builder parentBuilder, int configType) 219 throws IOException, XmlPullParserException, ParserException { 220 List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>(); 221 NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder(); 222 builder.setParent(parentBuilder); 223 Set<Domain> domains = new ArraySet<>(); 224 boolean seenPinSet = false; 225 boolean seenTrustAnchors = false; 226 boolean defaultOverridePins = configType == CONFIG_DEBUG; 227 String configName = parser.getName(); 228 int outerDepth = parser.getDepth(); 229 // Add this builder now so that this builder occurs before any of its children. This 230 // makes the final build pass easier. 231 builders.add(new Pair<>(builder, domains)); 232 // Parse config attributes. Only set values that are present, config inheritence will 233 // handle the rest. 234 for (int i = 0; i < parser.getAttributeCount(); i++) { 235 String name = parser.getAttributeName(i); 236 if ("hstsEnforced".equals(name)) { 237 builder.setHstsEnforced( 238 parser.getAttributeBooleanValue(i, 239 NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED)); 240 } else if ("cleartextTrafficPermitted".equals(name)) { 241 builder.setCleartextTrafficPermitted( 242 parser.getAttributeBooleanValue(i, 243 NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED)); 244 } 245 } 246 // Parse the config elements. 247 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 248 String tagName = parser.getName(); 249 if ("domain".equals(tagName)) { 250 if (configType != CONFIG_DOMAIN) { 251 throw new ParserException(parser, 252 "domain element not allowed in " + getConfigString(configType)); 253 } 254 Domain domain = parseDomain(parser, seenDomains); 255 domains.add(domain); 256 } else if ("trust-anchors".equals(tagName)) { 257 if (seenTrustAnchors) { 258 throw new ParserException(parser, 259 "Multiple trust-anchor elements not allowed"); 260 } 261 builder.addCertificatesEntryRefs( 262 parseTrustAnchors(parser, defaultOverridePins)); 263 seenTrustAnchors = true; 264 } else if ("pin-set".equals(tagName)) { 265 if (configType != CONFIG_DOMAIN) { 266 throw new ParserException(parser, 267 "pin-set element not allowed in " + getConfigString(configType)); 268 } 269 if (seenPinSet) { 270 throw new ParserException(parser, "Multiple pin-set elements not allowed"); 271 } 272 builder.setPinSet(parsePinSet(parser)); 273 seenPinSet = true; 274 } else if ("domain-config".equals(tagName)) { 275 if (configType != CONFIG_DOMAIN) { 276 throw new ParserException(parser, 277 "Nested domain-config not allowed in " + getConfigString(configType)); 278 } 279 builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType)); 280 } else { 281 XmlUtils.skipCurrentTag(parser); 282 } 283 } 284 if (configType == CONFIG_DOMAIN && domains.isEmpty()) { 285 throw new ParserException(parser, "No domain elements in domain-config"); 286 } 287 return builders; 288 } 289 addDebugAnchorsIfNeeded(NetworkSecurityConfig.Builder debugConfigBuilder, NetworkSecurityConfig.Builder builder)290 private void addDebugAnchorsIfNeeded(NetworkSecurityConfig.Builder debugConfigBuilder, 291 NetworkSecurityConfig.Builder builder) { 292 if (debugConfigBuilder == null || !debugConfigBuilder.hasCertificatesEntryRefs()) { 293 return; 294 } 295 // Don't add trust anchors if not already present, the builder will inherit the anchors 296 // from its parent, and that's where the trust anchors should be added. 297 if (!builder.hasCertificatesEntryRefs()) { 298 return; 299 } 300 301 builder.addCertificatesEntryRefs(debugConfigBuilder.getCertificatesEntryRefs()); 302 } 303 parseNetworkSecurityConfig(XmlResourceParser parser)304 private void parseNetworkSecurityConfig(XmlResourceParser parser) 305 throws IOException, XmlPullParserException, ParserException { 306 Set<String> seenDomains = new ArraySet<>(); 307 List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>(); 308 NetworkSecurityConfig.Builder baseConfigBuilder = null; 309 NetworkSecurityConfig.Builder debugConfigBuilder = null; 310 boolean seenDebugOverrides = false; 311 boolean seenBaseConfig = false; 312 313 XmlUtils.beginDocument(parser, "network-security-config"); 314 int outerDepth = parser.getDepth(); 315 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 316 if ("base-config".equals(parser.getName())) { 317 if (seenBaseConfig) { 318 throw new ParserException(parser, "Only one base-config allowed"); 319 } 320 seenBaseConfig = true; 321 baseConfigBuilder = 322 parseConfigEntry(parser, seenDomains, null, CONFIG_BASE).get(0).first; 323 } else if ("domain-config".equals(parser.getName())) { 324 builders.addAll( 325 parseConfigEntry(parser, seenDomains, baseConfigBuilder, CONFIG_DOMAIN)); 326 } else if ("debug-overrides".equals(parser.getName())) { 327 if (seenDebugOverrides) { 328 throw new ParserException(parser, "Only one debug-overrides allowed"); 329 } 330 if (mDebugBuild) { 331 debugConfigBuilder = 332 parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first; 333 } else { 334 XmlUtils.skipCurrentTag(parser); 335 } 336 seenDebugOverrides = true; 337 } else { 338 XmlUtils.skipCurrentTag(parser); 339 } 340 } 341 // If debug is true and there was no debug-overrides in the file check for an extra 342 // _debug resource. 343 if (mDebugBuild && debugConfigBuilder == null) { 344 debugConfigBuilder = parseDebugOverridesResource(); 345 } 346 347 // Use the platform default as the parent of the base config for any values not provided 348 // there. If there is no base config use the platform default. 349 NetworkSecurityConfig.Builder platformDefaultBuilder = 350 NetworkSecurityConfig.getDefaultBuilder(mApplicationInfo); 351 addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder); 352 if (baseConfigBuilder != null) { 353 baseConfigBuilder.setParent(platformDefaultBuilder); 354 addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder); 355 } else { 356 baseConfigBuilder = platformDefaultBuilder; 357 } 358 // Build the per-domain config mapping. 359 Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>(); 360 361 for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) { 362 NetworkSecurityConfig.Builder builder = entry.first; 363 Set<Domain> domains = entry.second; 364 // Set the parent of configs that do not have a parent to the base-config. This can 365 // happen if the base-config comes after a domain-config in the file. 366 // Note that this is safe with regards to children because of the order that 367 // parseConfigEntry returns builders, the parent is always before the children. The 368 // children builders will not have build called until _after_ their parents have their 369 // parent set so everything is consistent. 370 if (builder.getParent() == null) { 371 builder.setParent(baseConfigBuilder); 372 } 373 addDebugAnchorsIfNeeded(debugConfigBuilder, builder); 374 NetworkSecurityConfig config = builder.build(); 375 for (Domain domain : domains) { 376 configs.add(new Pair<>(domain, config)); 377 } 378 } 379 mDefaultConfig = baseConfigBuilder.build(); 380 mDomainMap = configs; 381 } 382 parseDebugOverridesResource()383 private NetworkSecurityConfig.Builder parseDebugOverridesResource() 384 throws IOException, XmlPullParserException, ParserException { 385 Resources resources = mContext.getResources(); 386 String packageName = resources.getResourcePackageName(mResourceId); 387 String entryName = resources.getResourceEntryName(mResourceId); 388 int resId = resources.getIdentifier(entryName + "_debug", "xml", packageName); 389 // No debug-overrides resource was found, nothing to parse. 390 if (resId == 0) { 391 return null; 392 } 393 NetworkSecurityConfig.Builder debugConfigBuilder = null; 394 // Parse debug-overrides out of the _debug resource. 395 try (XmlResourceParser parser = resources.getXml(resId)) { 396 XmlUtils.beginDocument(parser, "network-security-config"); 397 int outerDepth = parser.getDepth(); 398 boolean seenDebugOverrides = false; 399 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 400 if ("debug-overrides".equals(parser.getName())) { 401 if (seenDebugOverrides) { 402 throw new ParserException(parser, "Only one debug-overrides allowed"); 403 } 404 if (mDebugBuild) { 405 debugConfigBuilder = 406 parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first; 407 } else { 408 XmlUtils.skipCurrentTag(parser); 409 } 410 seenDebugOverrides = true; 411 } else { 412 XmlUtils.skipCurrentTag(parser); 413 } 414 } 415 } 416 417 return debugConfigBuilder; 418 } 419 420 public static class ParserException extends Exception { 421 ParserException(XmlPullParser parser, String message, Throwable cause)422 public ParserException(XmlPullParser parser, String message, Throwable cause) { 423 super(message + " at: " + parser.getPositionDescription(), cause); 424 } 425 ParserException(XmlPullParser parser, String message)426 public ParserException(XmlPullParser parser, String message) { 427 this(parser, message, null); 428 } 429 } 430 } 431