1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.ChangeId; 22 import android.compat.annotation.Disabled; 23 import android.compat.annotation.EnabledAfter; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.Signature; 26 import android.content.pm.SigningDetails; 27 import android.os.Environment; 28 import android.util.Slog; 29 import android.util.Xml; 30 31 import com.android.server.compat.PlatformCompat; 32 import com.android.server.pm.parsing.pkg.AndroidPackageUtils; 33 import com.android.server.pm.pkg.AndroidPackage; 34 import com.android.server.pm.pkg.PackageState; 35 import com.android.server.pm.pkg.SharedUserApi; 36 37 import libcore.io.IoUtils; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 42 import java.io.File; 43 import java.io.FileReader; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.HashMap; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * Centralized access to SELinux MMAC (middleware MAC) implementation. This 56 * class is responsible for loading the appropriate mac_permissions.xml file 57 * as well as providing an interface for assigning seinfo values to apks. 58 * 59 * {@hide} 60 */ 61 public final class SELinuxMMAC { 62 63 static final String TAG = "SELinuxMMAC"; 64 65 private static final boolean DEBUG_POLICY = false; 66 private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; 67 private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false; 68 69 // All policy stanzas read from mac_permissions.xml. This is also the lock 70 // to synchronize access during policy load and access attempts. 71 private static final List<Policy> sPolicies = new ArrayList<>(); 72 /** Whether or not the policy files have been read */ 73 private static boolean sPolicyRead; 74 75 /** Required MAC permissions files */ 76 private static final List<File> sMacPermissions = new ArrayList<>(); 77 78 private static final String DEFAULT_SEINFO = "default"; 79 80 // Append privapp to existing seinfo label 81 private static final String PRIVILEGED_APP_STR = ":privapp"; 82 83 // Append targetSdkVersion=n to existing seinfo label where n is the app's targetSdkVersion 84 private static final String TARGETSDKVERSION_STR = ":targetSdkVersion="; 85 86 private static final String PARTITION_STR = ":partition="; 87 88 /** 89 * Allows opt-in to the latest targetSdkVersion enforced changes without changing target SDK. 90 * Turning this change on for an app targeting the latest SDK or higher is a no-op. 91 * 92 * <p>Has no effect for apps using shared user id. 93 * 94 * TODO(b/143539591): Update description with relevant SELINUX changes this opts in to. 95 */ 96 @Disabled 97 @ChangeId 98 static final long SELINUX_LATEST_CHANGES = 143539591L; 99 100 /** 101 * This change gates apps access to untrusted_app_R-targetSDK SELinux domain. Allows opt-in 102 * to R targetSdkVersion enforced changes without changing target SDK. Turning this change 103 * off for an app targeting {@code >= android.os.Build.VERSION_CODES.R} is a no-op. 104 * 105 * <p>Has no effect for apps using shared user id. 106 * 107 * TODO(b/143539591): Update description with relevant SELINUX changes this opts in to. 108 */ 109 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q) 110 @ChangeId 111 static final long SELINUX_R_CHANGES = 168782947L; 112 113 // Only initialize sMacPermissions once. 114 static { 115 // Platform mac permissions. sMacPermissions.add(new File( Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml"))116 sMacPermissions.add(new File( 117 Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml")); 118 119 // SystemExt mac permissions (optional). 120 final File systemExtMacPermission = new File( 121 Environment.getSystemExtDirectory(), "/etc/selinux/system_ext_mac_permissions.xml"); 122 if (systemExtMacPermission.exists()) { 123 sMacPermissions.add(systemExtMacPermission); 124 } 125 126 // Product mac permissions (optional). 127 final File productMacPermission = new File( 128 Environment.getProductDirectory(), "/etc/selinux/product_mac_permissions.xml"); 129 if (productMacPermission.exists()) { 130 sMacPermissions.add(productMacPermission); 131 } 132 133 // Vendor mac permissions. 134 final File vendorMacPermission = new File( 135 Environment.getVendorDirectory(), "/etc/selinux/vendor_mac_permissions.xml"); 136 if (vendorMacPermission.exists()) { 137 sMacPermissions.add(vendorMacPermission); 138 } 139 140 // ODM mac permissions (optional). 141 final File odmMacPermission = new File( 142 Environment.getOdmDirectory(), "/etc/selinux/odm_mac_permissions.xml"); 143 if (odmMacPermission.exists()) { 144 sMacPermissions.add(odmMacPermission); 145 } 146 } 147 148 /** 149 * Load the mac_permissions.xml file containing all seinfo assignments used to 150 * label apps. The loaded mac_permissions.xml files are plat_mac_permissions.xml and 151 * vendor_mac_permissions.xml, on /system and /vendor partitions, respectively. 152 * odm_mac_permissions.xml on /odm partition is optional. For further guidance on 153 * the proper structure of a mac_permissions.xml file consult the source code 154 * located at system/sepolicy/private/mac_permissions.xml. 155 * 156 * @return boolean indicating if policy was correctly loaded. A value of false 157 * typically indicates a structural problem with the xml or incorrectly 158 * constructed policy stanzas. A value of true means that all stanzas 159 * were loaded successfully; no partial loading is possible. 160 */ readInstallPolicy()161 public static boolean readInstallPolicy() { 162 synchronized (sPolicies) { 163 if (sPolicyRead) { 164 return true; 165 } 166 } 167 168 // Temp structure to hold the rules while we parse the xml file 169 List<Policy> policies = new ArrayList<>(); 170 171 FileReader policyFile = null; 172 XmlPullParser parser = Xml.newPullParser(); 173 174 final int count = sMacPermissions.size(); 175 for (int i = 0; i < count; ++i) { 176 final File macPermission = sMacPermissions.get(i); 177 try { 178 policyFile = new FileReader(macPermission); 179 Slog.d(TAG, "Using policy file " + macPermission); 180 181 parser.setInput(policyFile); 182 parser.nextTag(); 183 parser.require(XmlPullParser.START_TAG, null, "policy"); 184 185 while (parser.next() != XmlPullParser.END_TAG) { 186 if (parser.getEventType() != XmlPullParser.START_TAG) { 187 continue; 188 } 189 190 switch (parser.getName()) { 191 case "signer": 192 policies.add(readSignerOrThrow(parser)); 193 break; 194 default: 195 skip(parser); 196 } 197 } 198 } catch (IllegalStateException | IllegalArgumentException | 199 XmlPullParserException ex) { 200 StringBuilder sb = new StringBuilder("Exception @"); 201 sb.append(parser.getPositionDescription()); 202 sb.append(" while parsing "); 203 sb.append(macPermission); 204 sb.append(":"); 205 sb.append(ex); 206 Slog.w(TAG, sb.toString()); 207 return false; 208 } catch (IOException ioe) { 209 Slog.w(TAG, "Exception parsing " + macPermission, ioe); 210 return false; 211 } finally { 212 IoUtils.closeQuietly(policyFile); 213 } 214 } 215 216 // Now sort the policy stanzas 217 PolicyComparator policySort = new PolicyComparator(); 218 Collections.sort(policies, policySort); 219 if (policySort.foundDuplicate()) { 220 Slog.w(TAG, "ERROR! Duplicate entries found parsing mac_permissions.xml files"); 221 return false; 222 } 223 224 synchronized (sPolicies) { 225 sPolicies.clear(); 226 sPolicies.addAll(policies); 227 sPolicyRead = true; 228 229 if (DEBUG_POLICY_ORDER) { 230 for (Policy policy : sPolicies) { 231 Slog.d(TAG, "Policy: " + policy.toString()); 232 } 233 } 234 } 235 236 return true; 237 } 238 239 /** 240 * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy} 241 * instance will be created and returned in the process. During the pass all other 242 * tag elements will be skipped. 243 * 244 * @param parser an XmlPullParser object representing a signer element. 245 * @return the constructed {@link Policy} instance 246 * @throws IOException 247 * @throws XmlPullParserException 248 * @throws IllegalArgumentException if any of the validation checks fail while 249 * parsing tag values. 250 * @throws IllegalStateException if any of the invariants fail when constructing 251 * the {@link Policy} instance. 252 */ readSignerOrThrow(XmlPullParser parser)253 private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException, 254 XmlPullParserException { 255 256 parser.require(XmlPullParser.START_TAG, null, "signer"); 257 Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); 258 259 // Check for a cert attached to the signer tag. We allow a signature 260 // to appear as an attribute as well as those attached to cert tags. 261 String cert = parser.getAttributeValue(null, "signature"); 262 if (cert != null) { 263 pb.addSignature(cert); 264 } 265 266 while (parser.next() != XmlPullParser.END_TAG) { 267 if (parser.getEventType() != XmlPullParser.START_TAG) { 268 continue; 269 } 270 271 String tagName = parser.getName(); 272 if ("seinfo".equals(tagName)) { 273 String seinfo = parser.getAttributeValue(null, "value"); 274 pb.setGlobalSeinfoOrThrow(seinfo); 275 readSeinfo(parser); 276 } else if ("package".equals(tagName)) { 277 readPackageOrThrow(parser, pb); 278 } else if ("cert".equals(tagName)) { 279 String sig = parser.getAttributeValue(null, "signature"); 280 pb.addSignature(sig); 281 readCert(parser); 282 } else { 283 skip(parser); 284 } 285 } 286 287 return pb.build(); 288 } 289 290 /** 291 * Loop over a package element looking for seinfo child tags. If found return the 292 * value attribute of the seinfo tag, otherwise return null. All other tags encountered 293 * will be skipped. 294 * 295 * @param parser an XmlPullParser object representing a package element. 296 * @param pb a Policy.PolicyBuilder instance to build 297 * @throws IOException 298 * @throws XmlPullParserException 299 * @throws IllegalArgumentException if any of the validation checks fail while 300 * parsing tag values. 301 * @throws IllegalStateException if there is a duplicate seinfo tag for the current 302 * package tag. 303 */ readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb)304 private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws 305 IOException, XmlPullParserException { 306 parser.require(XmlPullParser.START_TAG, null, "package"); 307 String pkgName = parser.getAttributeValue(null, "name"); 308 309 while (parser.next() != XmlPullParser.END_TAG) { 310 if (parser.getEventType() != XmlPullParser.START_TAG) { 311 continue; 312 } 313 314 String tagName = parser.getName(); 315 if ("seinfo".equals(tagName)) { 316 String seinfo = parser.getAttributeValue(null, "value"); 317 pb.addInnerPackageMapOrThrow(pkgName, seinfo); 318 readSeinfo(parser); 319 } else { 320 skip(parser); 321 } 322 } 323 } 324 readCert(XmlPullParser parser)325 private static void readCert(XmlPullParser parser) throws IOException, 326 XmlPullParserException { 327 parser.require(XmlPullParser.START_TAG, null, "cert"); 328 parser.nextTag(); 329 } 330 readSeinfo(XmlPullParser parser)331 private static void readSeinfo(XmlPullParser parser) throws IOException, 332 XmlPullParserException { 333 parser.require(XmlPullParser.START_TAG, null, "seinfo"); 334 parser.nextTag(); 335 } 336 skip(XmlPullParser p)337 private static void skip(XmlPullParser p) throws IOException, XmlPullParserException { 338 if (p.getEventType() != XmlPullParser.START_TAG) { 339 throw new IllegalStateException(); 340 } 341 int depth = 1; 342 while (depth != 0) { 343 switch (p.next()) { 344 case XmlPullParser.END_TAG: 345 depth--; 346 break; 347 case XmlPullParser.START_TAG: 348 depth++; 349 break; 350 } 351 } 352 } 353 getTargetSdkVersionForSeInfo(AndroidPackage pkg, SharedUserApi sharedUser, PlatformCompat compatibility)354 private static int getTargetSdkVersionForSeInfo(AndroidPackage pkg, 355 SharedUserApi sharedUser, PlatformCompat compatibility) { 356 // Apps which share a sharedUserId must be placed in the same selinux domain. If this 357 // package is the first app installed as this shared user, set seInfoTargetSdkVersion to its 358 // targetSdkVersion. These are later adjusted in PackageManagerService's constructor to be 359 // the lowest targetSdkVersion of all apps within the shared user, which corresponds to the 360 // least restrictive selinux domain. 361 // NOTE: As new packages are installed / updated, the shared user's seinfoTargetSdkVersion 362 // will NOT be modified until next boot, even if a lower targetSdkVersion is used. This 363 // ensures that all packages continue to run in the same selinux domain. 364 if ((sharedUser != null) && (sharedUser.getPackages().size() != 0)) { 365 return sharedUser.getSeInfoTargetSdkVersion(); 366 } 367 final ApplicationInfo appInfo = AndroidPackageUtils.generateAppInfoWithoutState(pkg); 368 if (compatibility.isChangeEnabledInternal(SELINUX_LATEST_CHANGES, appInfo)) { 369 return Math.max( 370 android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, pkg.getTargetSdkVersion()); 371 } else if (compatibility.isChangeEnabledInternal(SELINUX_R_CHANGES, appInfo)) { 372 return Math.max(android.os.Build.VERSION_CODES.R, pkg.getTargetSdkVersion()); 373 } 374 375 return pkg.getTargetSdkVersion(); 376 } 377 getPartition(PackageState state)378 private static String getPartition(PackageState state) { 379 if (state.isSystemExt()) { 380 return "system_ext"; 381 } else if (state.isProduct()) { 382 return "product"; 383 } else if (state.isVendor()) { 384 return "vendor"; 385 } else if (state.isOem()) { 386 return "oem"; 387 } else if (state.isOdm()) { 388 return "odm"; 389 } else if (state.isSystem()) { 390 return "system"; 391 } 392 return ""; 393 } 394 395 /** 396 * Selects a security label to a package based on input parameters and the seinfo tag taken 397 * from a matched policy. All signature based policy stanzas are consulted and, if no match 398 * is found, the default seinfo label of 'default' is used. The security label is attached to 399 * the ApplicationInfo instance of the package. 400 * 401 * @param packageState {@link PackageState} object representing the package to be labeled. 402 * @param pkg {@link AndroidPackage} object representing the package to be labeled. 403 * @param sharedUser if the app shares a sharedUserId, then this has the shared setting. 404 * @param compatibility the PlatformCompat service to ask about state of compat changes. 405 * @return String representing the resulting seinfo. 406 */ getSeInfo(@onNull PackageState packageState, @NonNull AndroidPackage pkg, @Nullable SharedUserApi sharedUser, @NonNull PlatformCompat compatibility)407 public static String getSeInfo(@NonNull PackageState packageState, @NonNull AndroidPackage pkg, 408 @Nullable SharedUserApi sharedUser, @NonNull PlatformCompat compatibility) { 409 final int targetSdkVersion = getTargetSdkVersionForSeInfo(pkg, sharedUser, 410 compatibility); 411 // TODO(b/71593002): isPrivileged for sharedUser and appInfo should never be out of sync. 412 // They currently can be if the sharedUser apps are signed with the platform key. 413 final boolean isPrivileged = 414 (sharedUser != null) ? sharedUser.isPrivileged() | packageState.isPrivileged() 415 : packageState.isPrivileged(); 416 return getSeInfo(packageState, pkg, isPrivileged, targetSdkVersion); 417 } 418 419 /** 420 * Selects a security label to a package based on input parameters and the seinfo tag taken 421 * from a matched policy. All signature based policy stanzas are consulted and, if no match 422 * is found, the default seinfo label of 'default' is used. The security label is attached to 423 * the ApplicationInfo instance of the package. 424 * 425 * @param packageState {@link PackageState} object representing the package to be labeled. 426 * @param pkg {@link AndroidPackage} object representing the package to be labeled. 427 * @param isPrivileged boolean. 428 * @param targetSdkVersion int. If this pkg runs as a sharedUser, targetSdkVersion is the 429 * greater of: lowest targetSdk for all pkgs in the sharedUser, or 430 * MINIMUM_TARGETSDKVERSION. 431 * @return String representing the resulting seinfo. 432 */ getSeInfo(PackageState packageState, AndroidPackage pkg, boolean isPrivileged, int targetSdkVersion)433 public static String getSeInfo(PackageState packageState, AndroidPackage pkg, 434 boolean isPrivileged, int targetSdkVersion) { 435 String seInfo = null; 436 synchronized (sPolicies) { 437 if (!sPolicyRead) { 438 if (DEBUG_POLICY) { 439 Slog.d(TAG, "Policy not read"); 440 } 441 } else { 442 for (Policy policy : sPolicies) { 443 seInfo = policy.getMatchedSeInfo(pkg); 444 if (seInfo != null) { 445 break; 446 } 447 } 448 } 449 } 450 451 if (seInfo == null) { 452 seInfo = DEFAULT_SEINFO; 453 } 454 455 if (isPrivileged) { 456 seInfo += PRIVILEGED_APP_STR; 457 } 458 459 seInfo += TARGETSDKVERSION_STR + targetSdkVersion; 460 461 String partition = getPartition(packageState); 462 if (!partition.isEmpty()) { 463 seInfo += PARTITION_STR + partition; 464 } 465 466 if (DEBUG_POLICY_INSTALL) { 467 Slog.i(TAG, "package (" + packageState.getPackageName() + ") labeled with " 468 + "seinfo=" + seInfo); 469 } 470 return seInfo; 471 } 472 } 473 474 /** 475 * Holds valid policy representations of individual stanzas from a mac_permissions.xml 476 * file. Each instance can further be used to assign seinfo values to apks using the 477 * {@link Policy#getMatchedSeInfo(AndroidPackage)} method. To create an instance of this use the 478 * {@link PolicyBuilder} pattern class, where each instance is validated against a set 479 * of invariants before being built and returned. Each instance can be guaranteed to 480 * hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml 481 * file. 482 * <p> 483 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 484 * signer based Policy instance with only inner package name refinements. 485 * </p> 486 * <pre> 487 * {@code 488 * Policy policy = new Policy.PolicyBuilder() 489 * .addSignature("308204a8...") 490 * .addSignature("483538c8...") 491 * .addInnerPackageMapOrThrow("com.foo.", "bar") 492 * .addInnerPackageMapOrThrow("com.foo.other", "bar") 493 * .build(); 494 * } 495 * </pre> 496 * <p> 497 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 498 * signer based Policy instance with only a global seinfo tag. 499 * </p> 500 * <pre> 501 * {@code 502 * Policy policy = new Policy.PolicyBuilder() 503 * .addSignature("308204a8...") 504 * .addSignature("483538c8...") 505 * .setGlobalSeinfoOrThrow("paltform") 506 * .build(); 507 * } 508 * </pre> 509 */ 510 final class Policy { 511 512 private final String mSeinfo; 513 private final Set<Signature> mCerts; 514 private final Map<String, String> mPkgMap; 515 516 // Use the PolicyBuilder pattern to instantiate Policy(PolicyBuilder builder)517 private Policy(PolicyBuilder builder) { 518 mSeinfo = builder.mSeinfo; 519 mCerts = Collections.unmodifiableSet(builder.mCerts); 520 mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); 521 } 522 523 /** 524 * Return all the certs stored with this policy stanza. 525 * 526 * @return A set of Signature objects representing all the certs stored 527 * with the policy. 528 */ getSignatures()529 public Set<Signature> getSignatures() { 530 return mCerts; 531 } 532 533 /** 534 * Return whether this policy object contains package name mapping refinements. 535 * 536 * @return A boolean indicating if this object has inner package name mappings. 537 */ hasInnerPackages()538 public boolean hasInnerPackages() { 539 return !mPkgMap.isEmpty(); 540 } 541 542 /** 543 * Return the mapping of all package name refinements. 544 * 545 * @return A Map object whose keys are the package names and whose values are 546 * the seinfo assignments. 547 */ getInnerPackages()548 public Map<String, String> getInnerPackages() { 549 return mPkgMap; 550 } 551 552 /** 553 * Return whether the policy object has a global seinfo tag attached. 554 * 555 * @return A boolean indicating if this stanza has a global seinfo tag. 556 */ hasGlobalSeinfo()557 public boolean hasGlobalSeinfo() { 558 return mSeinfo != null; 559 } 560 561 @Override toString()562 public String toString() { 563 StringBuilder sb = new StringBuilder(); 564 for (Signature cert : mCerts) { 565 sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... "); 566 } 567 568 if (mSeinfo != null) { 569 sb.append("seinfo=" + mSeinfo); 570 } 571 572 for (String name : mPkgMap.keySet()) { 573 sb.append(" " + name + "=" + mPkgMap.get(name)); 574 } 575 576 return sb.toString(); 577 } 578 579 /** 580 * <p> 581 * Determine the seinfo value to assign to an apk. The appropriate seinfo value 582 * is determined using the following steps: 583 * </p> 584 * <ul> 585 * <li> All certs used to sign the apk and all certs stored with this policy 586 * instance are tested for set equality. If this fails then null is returned. 587 * </li> 588 * <li> If all certs match then an appropriate inner package stanza is 589 * searched based on package name alone. If matched, the stored seinfo 590 * value for that mapping is returned. 591 * </li> 592 * <li> If all certs matched and no inner package stanza matches then return 593 * the global seinfo value. The returned value can be null in this case. 594 * </li> 595 * </ul> 596 * <p> 597 * In all cases, a return value of null should be interpreted as the apk failing 598 * to match this Policy instance; i.e. failing this policy stanza. 599 * </p> 600 * @param pkg the apk to check given as a AndroidPackage object 601 * @return A string representing the seinfo matched during policy lookup. 602 * A value of null can also be returned if no match occured. 603 */ getMatchedSeInfo(AndroidPackage pkg)604 public String getMatchedSeInfo(AndroidPackage pkg) { 605 // Check for exact signature matches across all certs. 606 Signature[] certs = mCerts.toArray(new Signature[0]); 607 if (pkg.getSigningDetails() != SigningDetails.UNKNOWN 608 && !Signature.areExactMatch(pkg.getSigningDetails(), certs)) { 609 610 // certs aren't exact match, but the package may have rotated from the known system cert 611 if (certs.length > 1 || !pkg.getSigningDetails().hasCertificate(certs[0])) { 612 return null; 613 } 614 } 615 616 // Check for inner package name matches given that the 617 // signature checks already passed. 618 String seinfoValue = mPkgMap.get(pkg.getPackageName()); 619 if (seinfoValue != null) { 620 return seinfoValue; 621 } 622 623 // Return the global seinfo value. 624 return mSeinfo; 625 } 626 627 /** 628 * A nested builder class to create {@link Policy} instances. A {@link Policy} 629 * class instance represents one valid policy stanza found in a mac_permissions.xml 630 * file. A valid policy stanza is defined to be a signer stanza which obeys the rules 631 * outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method 632 * ensures a set of invariants are upheld enforcing the correct stanza structure 633 * before returning a valid Policy object. 634 */ 635 public static final class PolicyBuilder { 636 637 private String mSeinfo; 638 private final Set<Signature> mCerts; 639 private final Map<String, String> mPkgMap; 640 PolicyBuilder()641 public PolicyBuilder() { 642 mCerts = new HashSet<Signature>(2); 643 mPkgMap = new HashMap<String, String>(2); 644 } 645 646 /** 647 * Adds a signature to the set of certs used for validation checks. The purpose 648 * being that all contained certs will need to be matched against all certs 649 * contained with an apk. 650 * 651 * @param cert the signature to add given as a String. 652 * @return The reference to this PolicyBuilder. 653 * @throws IllegalArgumentException if the cert value fails validation; 654 * null or is an invalid hex-encoded ASCII string. 655 */ addSignature(String cert)656 public PolicyBuilder addSignature(String cert) { 657 if (cert == null) { 658 String err = "Invalid signature value " + cert; 659 throw new IllegalArgumentException(err); 660 } 661 662 mCerts.add(new Signature(cert)); 663 return this; 664 } 665 666 /** 667 * Set the global seinfo tag for this policy stanza. The global seinfo tag 668 * when attached to a signer tag represents the assignment when there isn't a 669 * further inner package refinement in policy. 670 * 671 * @param seinfo the seinfo value given as a String. 672 * @return The reference to this PolicyBuilder. 673 * @throws IllegalArgumentException if the seinfo value fails validation; 674 * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. 675 * @throws IllegalStateException if an seinfo value has already been found 676 */ setGlobalSeinfoOrThrow(String seinfo)677 public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { 678 if (!validateValue(seinfo)) { 679 String err = "Invalid seinfo value " + seinfo; 680 throw new IllegalArgumentException(err); 681 } 682 683 if (mSeinfo != null && !mSeinfo.equals(seinfo)) { 684 String err = "Duplicate seinfo tag found"; 685 throw new IllegalStateException(err); 686 } 687 688 mSeinfo = seinfo; 689 return this; 690 } 691 692 /** 693 * Create a package name to seinfo value mapping. Each mapping represents 694 * the seinfo value that will be assigned to the described package name. 695 * These localized mappings allow the global seinfo to be overriden. 696 * 697 * @param pkgName the android package name given to the app 698 * @param seinfo the seinfo value that will be assigned to the passed pkgName 699 * @return The reference to this PolicyBuilder. 700 * @throws IllegalArgumentException if the seinfo value fails validation; 701 * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. 702 * Or, if the package name isn't a valid android package name. 703 * @throws IllegalStateException if trying to reset a package mapping with a 704 * different seinfo value. 705 */ addInnerPackageMapOrThrow(String pkgName, String seinfo)706 public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { 707 if (!validateValue(pkgName)) { 708 String err = "Invalid package name " + pkgName; 709 throw new IllegalArgumentException(err); 710 } 711 if (!validateValue(seinfo)) { 712 String err = "Invalid seinfo value " + seinfo; 713 throw new IllegalArgumentException(err); 714 } 715 716 String pkgValue = mPkgMap.get(pkgName); 717 if (pkgValue != null && !pkgValue.equals(seinfo)) { 718 String err = "Conflicting seinfo value found"; 719 throw new IllegalStateException(err); 720 } 721 722 mPkgMap.put(pkgName, seinfo); 723 return this; 724 } 725 726 /** 727 * General validation routine for the attribute strings of an element. Checks 728 * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. 729 * 730 * @param name the string to validate. 731 * @return boolean indicating if the string was valid. 732 */ validateValue(String name)733 private boolean validateValue(String name) { 734 if (name == null) 735 return false; 736 737 // Want to match on [0-9a-zA-Z_.] 738 if (!name.matches("\\A[\\.\\w]+\\z")) { 739 return false; 740 } 741 742 return true; 743 } 744 745 /** 746 * <p> 747 * Create a {@link Policy} instance based on the current configuration. This 748 * method checks for certain policy invariants used to enforce certain guarantees 749 * about the expected structure of a policy stanza. 750 * Those invariants are: 751 * </p> 752 * <ul> 753 * <li> at least one cert must be found </li> 754 * <li> either a global seinfo value is present OR at least one 755 * inner package mapping must be present BUT not both. </li> 756 * </ul> 757 * @return an instance of {@link Policy} with the options set from this builder 758 * @throws IllegalStateException if an invariant is violated. 759 */ build()760 public Policy build() { 761 Policy p = new Policy(this); 762 763 if (p.mCerts.isEmpty()) { 764 String err = "Missing certs with signer tag. Expecting at least one."; 765 throw new IllegalStateException(err); 766 } 767 if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) { 768 String err = "Only seinfo tag XOR package tags are allowed within " + 769 "a signer stanza."; 770 throw new IllegalStateException(err); 771 } 772 773 return p; 774 } 775 } 776 } 777 778 /** 779 * Comparision imposing an ordering on Policy objects. It is understood that Policy 780 * objects can only take one of three forms and ordered according to the following 781 * set of rules most specific to least. 782 * <ul> 783 * <li> signer stanzas with inner package mappings </li> 784 * <li> signer stanzas with global seinfo tags </li> 785 * </ul> 786 * This comparison also checks for duplicate entries on the input selectors. Any 787 * found duplicates will be flagged and can be checked with {@link #foundDuplicate}. 788 */ 789 790 final class PolicyComparator implements Comparator<Policy> { 791 792 private boolean duplicateFound = false; 793 foundDuplicate()794 public boolean foundDuplicate() { 795 return duplicateFound; 796 } 797 798 @Override compare(Policy p1, Policy p2)799 public int compare(Policy p1, Policy p2) { 800 801 // Give precedence to stanzas with inner package mappings 802 if (p1.hasInnerPackages() != p2.hasInnerPackages()) { 803 return p1.hasInnerPackages() ? -1 : 1; 804 } 805 806 // Check for duplicate entries 807 if (p1.getSignatures().equals(p2.getSignatures())) { 808 // Checks if signer w/o inner package names 809 if (p1.hasGlobalSeinfo()) { 810 duplicateFound = true; 811 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 812 } 813 814 // Look for common inner package name mappings 815 final Map<String, String> p1Packages = p1.getInnerPackages(); 816 final Map<String, String> p2Packages = p2.getInnerPackages(); 817 if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) { 818 duplicateFound = true; 819 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 820 } 821 } 822 823 return 0; 824 } 825 } 826