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 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 48 public XmlConfigSource(Context context, int resourceId) { 49 this(context, resourceId, false); 50 } 51 52 @VisibleForTesting 53 public XmlConfigSource(Context context, int resourceId, boolean debugBuild) { 54 this(context, resourceId, debugBuild, Build.VERSION_CODES.CUR_DEVELOPMENT); 55 } 56 57 @VisibleForTesting 58 public XmlConfigSource(Context context, int resourceId, boolean debugBuild, 59 int targetSdkVersion) { 60 this(context, resourceId, debugBuild, targetSdkVersion, 1 /*targetSandboxVersion*/); 61 } 62 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 72 public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() { 73 ensureInitialized(); 74 return mDomainMap; 75 } 76 77 public NetworkSecurityConfig getDefaultConfig() { 78 ensureInitialized(); 79 return mDefaultConfig; 80 } 81 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 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 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 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 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 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 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 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 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 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 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 440 public ParserException(XmlPullParser parser, String message, Throwable cause) { 441 super(message + " at: " + parser.getPositionDescription(), cause); 442 } 443 444 public ParserException(XmlPullParser parser, String message) { 445 this(parser, message, null); 446 } 447 } 448} 449