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