SELinuxMMAC.java revision 096d304ae3d85c1bfcda1a1d9cd4eb13d0815500
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 /** 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.isInstantApp()) 291 pkg.applicationInfo.seinfo += EPHEMERAL_APP_STR; 292 293 if (pkg.applicationInfo.targetSandboxVersion == 2) 294 pkg.applicationInfo.seinfo += SANDBOX_V2_STR; 295 296 if (pkg.applicationInfo.isPrivilegedApp()) 297 pkg.applicationInfo.seinfo += PRIVILEGED_APP_STR; 298 299 if (DEBUG_POLICY_INSTALL) { 300 Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " + 301 "seinfo=" + pkg.applicationInfo.seinfo); 302 } 303 } 304} 305 306/** 307 * Holds valid policy representations of individual stanzas from a mac_permissions.xml 308 * file. Each instance can further be used to assign seinfo values to apks using the 309 * {@link Policy#getMatchedSeinfo} method. To create an instance of this use the 310 * {@link PolicyBuilder} pattern class, where each instance is validated against a set 311 * of invariants before being built and returned. Each instance can be guaranteed to 312 * hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml 313 * file. 314 * <p> 315 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 316 * signer based Policy instance with only inner package name refinements. 317 * </p> 318 * <pre> 319 * {@code 320 * Policy policy = new Policy.PolicyBuilder() 321 * .addSignature("308204a8...") 322 * .addSignature("483538c8...") 323 * .addInnerPackageMapOrThrow("com.foo.", "bar") 324 * .addInnerPackageMapOrThrow("com.foo.other", "bar") 325 * .build(); 326 * } 327 * </pre> 328 * <p> 329 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 330 * signer based Policy instance with only a global seinfo tag. 331 * </p> 332 * <pre> 333 * {@code 334 * Policy policy = new Policy.PolicyBuilder() 335 * .addSignature("308204a8...") 336 * .addSignature("483538c8...") 337 * .setGlobalSeinfoOrThrow("paltform") 338 * .build(); 339 * } 340 * </pre> 341 */ 342final class Policy { 343 344 private final String mSeinfo; 345 private final Set<Signature> mCerts; 346 private final Map<String, String> mPkgMap; 347 348 // Use the PolicyBuilder pattern to instantiate 349 private Policy(PolicyBuilder builder) { 350 mSeinfo = builder.mSeinfo; 351 mCerts = Collections.unmodifiableSet(builder.mCerts); 352 mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); 353 } 354 355 /** 356 * Return all the certs stored with this policy stanza. 357 * 358 * @return A set of Signature objects representing all the certs stored 359 * with the policy. 360 */ 361 public Set<Signature> getSignatures() { 362 return mCerts; 363 } 364 365 /** 366 * Return whether this policy object contains package name mapping refinements. 367 * 368 * @return A boolean indicating if this object has inner package name mappings. 369 */ 370 public boolean hasInnerPackages() { 371 return !mPkgMap.isEmpty(); 372 } 373 374 /** 375 * Return the mapping of all package name refinements. 376 * 377 * @return A Map object whose keys are the package names and whose values are 378 * the seinfo assignments. 379 */ 380 public Map<String, String> getInnerPackages() { 381 return mPkgMap; 382 } 383 384 /** 385 * Return whether the policy object has a global seinfo tag attached. 386 * 387 * @return A boolean indicating if this stanza has a global seinfo tag. 388 */ 389 public boolean hasGlobalSeinfo() { 390 return mSeinfo != null; 391 } 392 393 @Override 394 public String toString() { 395 StringBuilder sb = new StringBuilder(); 396 for (Signature cert : mCerts) { 397 sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... "); 398 } 399 400 if (mSeinfo != null) { 401 sb.append("seinfo=" + mSeinfo); 402 } 403 404 for (String name : mPkgMap.keySet()) { 405 sb.append(" " + name + "=" + mPkgMap.get(name)); 406 } 407 408 return sb.toString(); 409 } 410 411 /** 412 * <p> 413 * Determine the seinfo value to assign to an apk. The appropriate seinfo value 414 * is determined using the following steps: 415 * </p> 416 * <ul> 417 * <li> All certs used to sign the apk and all certs stored with this policy 418 * instance are tested for set equality. If this fails then null is returned. 419 * </li> 420 * <li> If all certs match then an appropriate inner package stanza is 421 * searched based on package name alone. If matched, the stored seinfo 422 * value for that mapping is returned. 423 * </li> 424 * <li> If all certs matched and no inner package stanza matches then return 425 * the global seinfo value. The returned value can be null in this case. 426 * </li> 427 * </ul> 428 * <p> 429 * In all cases, a return value of null should be interpreted as the apk failing 430 * to match this Policy instance; i.e. failing this policy stanza. 431 * </p> 432 * @param pkg the apk to check given as a PackageParser.Package object 433 * @return A string representing the seinfo matched during policy lookup. 434 * A value of null can also be returned if no match occured. 435 */ 436 public String getMatchedSeinfo(PackageParser.Package pkg) { 437 // Check for exact signature matches across all certs. 438 Signature[] certs = mCerts.toArray(new Signature[0]); 439 if (!Signature.areExactMatch(certs, pkg.mSignatures)) { 440 return null; 441 } 442 443 // Check for inner package name matches given that the 444 // signature checks already passed. 445 String seinfoValue = mPkgMap.get(pkg.packageName); 446 if (seinfoValue != null) { 447 return seinfoValue; 448 } 449 450 // Return the global seinfo value. 451 return mSeinfo; 452 } 453 454 /** 455 * A nested builder class to create {@link Policy} instances. A {@link Policy} 456 * class instance represents one valid policy stanza found in a mac_permissions.xml 457 * file. A valid policy stanza is defined to be a signer stanza which obeys the rules 458 * outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method 459 * ensures a set of invariants are upheld enforcing the correct stanza structure 460 * before returning a valid Policy object. 461 */ 462 public static final class PolicyBuilder { 463 464 private String mSeinfo; 465 private final Set<Signature> mCerts; 466 private final Map<String, String> mPkgMap; 467 468 public PolicyBuilder() { 469 mCerts = new HashSet<Signature>(2); 470 mPkgMap = new HashMap<String, String>(2); 471 } 472 473 /** 474 * Adds a signature to the set of certs used for validation checks. The purpose 475 * being that all contained certs will need to be matched against all certs 476 * contained with an apk. 477 * 478 * @param cert the signature to add given as a String. 479 * @return The reference to this PolicyBuilder. 480 * @throws IllegalArgumentException if the cert value fails validation; 481 * null or is an invalid hex-encoded ASCII string. 482 */ 483 public PolicyBuilder addSignature(String cert) { 484 if (cert == null) { 485 String err = "Invalid signature value " + cert; 486 throw new IllegalArgumentException(err); 487 } 488 489 mCerts.add(new Signature(cert)); 490 return this; 491 } 492 493 /** 494 * Set the global seinfo tag for this policy stanza. The global seinfo tag 495 * when attached to a signer tag represents the assignment when there isn't a 496 * further inner package refinement in policy. 497 * 498 * @param seinfo the seinfo value given as a String. 499 * @return The reference to this PolicyBuilder. 500 * @throws IllegalArgumentException if the seinfo value fails validation; 501 * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. 502 * @throws IllegalStateException if an seinfo value has already been found 503 */ 504 public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { 505 if (!validateValue(seinfo)) { 506 String err = "Invalid seinfo value " + seinfo; 507 throw new IllegalArgumentException(err); 508 } 509 510 if (mSeinfo != null && !mSeinfo.equals(seinfo)) { 511 String err = "Duplicate seinfo tag found"; 512 throw new IllegalStateException(err); 513 } 514 515 mSeinfo = seinfo; 516 return this; 517 } 518 519 /** 520 * Create a package name to seinfo value mapping. Each mapping represents 521 * the seinfo value that will be assigned to the described package name. 522 * These localized mappings allow the global seinfo to be overriden. 523 * 524 * @param pkgName the android package name given to the app 525 * @param seinfo the seinfo value that will be assigned to the passed pkgName 526 * @return The reference to this PolicyBuilder. 527 * @throws IllegalArgumentException if the seinfo value fails validation; 528 * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. 529 * Or, if the package name isn't a valid android package name. 530 * @throws IllegalStateException if trying to reset a package mapping with a 531 * different seinfo value. 532 */ 533 public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { 534 if (!validateValue(pkgName)) { 535 String err = "Invalid package name " + pkgName; 536 throw new IllegalArgumentException(err); 537 } 538 if (!validateValue(seinfo)) { 539 String err = "Invalid seinfo value " + seinfo; 540 throw new IllegalArgumentException(err); 541 } 542 543 String pkgValue = mPkgMap.get(pkgName); 544 if (pkgValue != null && !pkgValue.equals(seinfo)) { 545 String err = "Conflicting seinfo value found"; 546 throw new IllegalStateException(err); 547 } 548 549 mPkgMap.put(pkgName, seinfo); 550 return this; 551 } 552 553 /** 554 * General validation routine for the attribute strings of an element. Checks 555 * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. 556 * 557 * @param name the string to validate. 558 * @return boolean indicating if the string was valid. 559 */ 560 private boolean validateValue(String name) { 561 if (name == null) 562 return false; 563 564 // Want to match on [0-9a-zA-Z_.] 565 if (!name.matches("\\A[\\.\\w]+\\z")) { 566 return false; 567 } 568 569 return true; 570 } 571 572 /** 573 * <p> 574 * Create a {@link Policy} instance based on the current configuration. This 575 * method checks for certain policy invariants used to enforce certain guarantees 576 * about the expected structure of a policy stanza. 577 * Those invariants are: 578 * </p> 579 * <ul> 580 * <li> at least one cert must be found </li> 581 * <li> either a global seinfo value is present OR at least one 582 * inner package mapping must be present BUT not both. </li> 583 * </ul> 584 * @return an instance of {@link Policy} with the options set from this builder 585 * @throws IllegalStateException if an invariant is violated. 586 */ 587 public Policy build() { 588 Policy p = new Policy(this); 589 590 if (p.mCerts.isEmpty()) { 591 String err = "Missing certs with signer tag. Expecting at least one."; 592 throw new IllegalStateException(err); 593 } 594 if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) { 595 String err = "Only seinfo tag XOR package tags are allowed within " + 596 "a signer stanza."; 597 throw new IllegalStateException(err); 598 } 599 600 return p; 601 } 602 } 603} 604 605/** 606 * Comparision imposing an ordering on Policy objects. It is understood that Policy 607 * objects can only take one of three forms and ordered according to the following 608 * set of rules most specific to least. 609 * <ul> 610 * <li> signer stanzas with inner package mappings </li> 611 * <li> signer stanzas with global seinfo tags </li> 612 * </ul> 613 * This comparison also checks for duplicate entries on the input selectors. Any 614 * found duplicates will be flagged and can be checked with {@link #foundDuplicate}. 615 */ 616 617final class PolicyComparator implements Comparator<Policy> { 618 619 private boolean duplicateFound = false; 620 621 public boolean foundDuplicate() { 622 return duplicateFound; 623 } 624 625 @Override 626 public int compare(Policy p1, Policy p2) { 627 628 // Give precedence to stanzas with inner package mappings 629 if (p1.hasInnerPackages() != p2.hasInnerPackages()) { 630 return p1.hasInnerPackages() ? -1 : 1; 631 } 632 633 // Check for duplicate entries 634 if (p1.getSignatures().equals(p2.getSignatures())) { 635 // Checks if signer w/o inner package names 636 if (p1.hasGlobalSeinfo()) { 637 duplicateFound = true; 638 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 639 } 640 641 // Look for common inner package name mappings 642 final Map<String, String> p1Packages = p1.getInnerPackages(); 643 final Map<String, String> p2Packages = p2.getInnerPackages(); 644 if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) { 645 duplicateFound = true; 646 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 647 } 648 } 649 650 return 0; 651 } 652} 653