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