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