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