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