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