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