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