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 parseCertificateTransparency(XmlResourceParser parser)174 private boolean parseCertificateTransparency(XmlResourceParser parser) 175 throws IOException, XmlPullParserException, ParserException { 176 return parser.getAttributeBooleanValue(null, "enabled", false); 177 } 178 parseCertificatesEntry(XmlResourceParser parser, boolean defaultOverridePins)179 private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser, 180 boolean defaultOverridePins) 181 throws IOException, XmlPullParserException, ParserException { 182 boolean overridePins = 183 parser.getAttributeBooleanValue(null, "overridePins", defaultOverridePins); 184 int sourceId = parser.getAttributeResourceValue(null, "src", -1); 185 boolean disableCT = false; 186 String sourceString = parser.getAttributeValue(null, "src"); 187 CertificateSource source = null; 188 if (sourceString == null) { 189 throw new ParserException(parser, "certificates element missing src attribute"); 190 } 191 if (sourceId != -1) { 192 // TODO: Cache ResourceCertificateSources by sourceId 193 source = new ResourceCertificateSource(sourceId, mContext); 194 disableCT = true; 195 } else if ("system".equals(sourceString)) { 196 source = SystemCertificateSource.getInstance(); 197 } else if ("user".equals(sourceString)) { 198 source = UserCertificateSource.getInstance(); 199 disableCT = true; 200 } else if ("wfa".equals(sourceString)) { 201 source = WfaCertificateSource.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, disableCT); 208 } 209 parseTrustAnchors(XmlResourceParser parser, boolean defaultOverridePins)210 private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser, 211 boolean defaultOverridePins) 212 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")) { 218 anchors.add(parseCertificatesEntry(parser, defaultOverridePins)); 219 } else { 220 XmlUtils.skipCurrentTag(parser); 221 } 222 } 223 return anchors; 224 } 225 parseConfigEntry( XmlResourceParser parser, Set<String> seenDomains, NetworkSecurityConfig.Builder parentBuilder, int configType)226 private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry( 227 XmlResourceParser parser, Set<String> seenDomains, 228 NetworkSecurityConfig.Builder parentBuilder, int configType) 229 throws IOException, XmlPullParserException, ParserException { 230 List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>(); 231 NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder(); 232 builder.setParent(parentBuilder); 233 Set<Domain> domains = new ArraySet<>(); 234 boolean seenPinSet = false; 235 boolean seenTrustAnchors = false; 236 boolean defaultOverridePins = configType == CONFIG_DEBUG; 237 int outerDepth = parser.getDepth(); 238 // Add this builder now so that this builder occurs before any of its children. This 239 // makes the final build pass easier. 240 builders.add(new Pair<>(builder, domains)); 241 // Parse config attributes. Only set values that are present, config inheritence will 242 // handle the rest. 243 for (int i = 0; i < parser.getAttributeCount(); i++) { 244 String name = parser.getAttributeName(i); 245 if ("hstsEnforced".equals(name)) { 246 builder.setHstsEnforced( 247 parser.getAttributeBooleanValue(i, 248 NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED)); 249 } else if ("cleartextTrafficPermitted".equals(name)) { 250 builder.setCleartextTrafficPermitted( 251 parser.getAttributeBooleanValue(i, 252 NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED)); 253 } 254 } 255 // Parse the config elements. 256 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 257 String tagName = parser.getName(); 258 if ("domain".equals(tagName)) { 259 if (configType != CONFIG_DOMAIN) { 260 throw new ParserException(parser, 261 "domain element not allowed in " + getConfigString(configType)); 262 } 263 Domain domain = parseDomain(parser, seenDomains); 264 domains.add(domain); 265 } else if ("trust-anchors".equals(tagName)) { 266 if (seenTrustAnchors) { 267 throw new ParserException(parser, 268 "Multiple trust-anchor elements not allowed"); 269 } 270 builder.addCertificatesEntryRefs( 271 parseTrustAnchors(parser, defaultOverridePins)); 272 seenTrustAnchors = true; 273 } else if ("pin-set".equals(tagName)) { 274 if (configType != CONFIG_DOMAIN) { 275 throw new ParserException(parser, 276 "pin-set element not allowed in " + getConfigString(configType)); 277 } 278 if (seenPinSet) { 279 throw new ParserException(parser, "Multiple pin-set elements not allowed"); 280 } 281 builder.setPinSet(parsePinSet(parser)); 282 seenPinSet = true; 283 } else if ("domain-config".equals(tagName)) { 284 if (configType != CONFIG_DOMAIN) { 285 throw new ParserException(parser, 286 "Nested domain-config not allowed in " + getConfigString(configType)); 287 } 288 builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType)); 289 } else if ("certificateTransparency".equals(tagName)) { 290 if (configType != CONFIG_BASE && configType != CONFIG_DOMAIN) { 291 throw new ParserException( 292 parser, 293 "certificateTransparency not allowed in " 294 + getConfigString(configType)); 295 } 296 builder.setCertificateTransparencyVerificationRequired( 297 parseCertificateTransparency(parser)); 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(mApplicationInfo); 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