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