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 java.io.File; 28 import java.io.FileNotFoundException; 29 import java.io.FileOutputStream; 30 import java.io.FileReader; 31 import java.io.IOException; 32 import java.security.MessageDigest; 33 import java.security.NoSuchAlgorithmException; 34 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 44 import org.xmlpull.v1.XmlPullParser; 45 import org.xmlpull.v1.XmlPullParserException; 46 47 /** 48 * Centralized access to SELinux MMAC (middleware MAC) implementation. This 49 * class is responsible for loading the appropriate mac_permissions.xml file 50 * as well as providing an interface for assigning seinfo values to apks. 51 * 52 * {@hide} 53 */ 54 public final class SELinuxMMAC { 55 56 static final String TAG = "SELinuxMMAC"; 57 58 private static final boolean DEBUG_POLICY = false; 59 private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; 60 private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false; 61 62 // All policy stanzas read from mac_permissions.xml. This is also the lock 63 // to synchronize access during policy load and access attempts. 64 private static List<Policy> sPolicies = new ArrayList<>(); 65 66 // Data policy override version file. 67 private static final String DATA_VERSION_FILE = 68 Environment.getDataDirectory() + "/security/current/selinux_version"; 69 70 // Base policy version file. 71 private static final String BASE_VERSION_FILE = "/selinux_version"; 72 73 // Whether override security policies should be loaded. 74 private static final boolean USE_OVERRIDE_POLICY = useOverridePolicy(); 75 76 // Data override mac_permissions.xml policy file. 77 private static final String DATA_MAC_PERMISSIONS = 78 Environment.getDataDirectory() + "/security/current/mac_permissions.xml"; 79 80 // Base mac_permissions.xml policy file. 81 private static final String BASE_MAC_PERMISSIONS = 82 Environment.getRootDirectory() + "/etc/security/mac_permissions.xml"; 83 84 // Determine which mac_permissions.xml file to use. 85 private static final String MAC_PERMISSIONS = USE_OVERRIDE_POLICY ? 86 DATA_MAC_PERMISSIONS : BASE_MAC_PERMISSIONS; 87 88 // Data override seapp_contexts policy file. 89 private static final String DATA_SEAPP_CONTEXTS = 90 Environment.getDataDirectory() + "/security/current/seapp_contexts"; 91 92 // Base seapp_contexts policy file. 93 private static final String BASE_SEAPP_CONTEXTS = "/seapp_contexts"; 94 95 // Determine which seapp_contexts file to use. 96 private static final String SEAPP_CONTEXTS = USE_OVERRIDE_POLICY ? 97 DATA_SEAPP_CONTEXTS : BASE_SEAPP_CONTEXTS; 98 99 // Stores the hash of the last used seapp_contexts file. 100 private static final String SEAPP_HASH_FILE = 101 Environment.getDataDirectory().toString() + "/system/seapp_hash"; 102 103 /** 104 * Load the mac_permissions.xml file containing all seinfo assignments used to 105 * label apps. The loaded mac_permissions.xml file is determined by the 106 * MAC_PERMISSIONS class variable which is set at class load time which itself 107 * is based on the USE_OVERRIDE_POLICY class variable. For further guidance on 108 * the proper structure of a mac_permissions.xml file consult the source code 109 * located at external/sepolicy/mac_permissions.xml. 110 * 111 * @return boolean indicating if policy was correctly loaded. A value of false 112 * typically indicates a structural problem with the xml or incorrectly 113 * constructed policy stanzas. A value of true means that all stanzas 114 * were loaded successfully; no partial loading is possible. 115 */ 116 public static boolean readInstallPolicy() { 117 // Temp structure to hold the rules while we parse the xml file 118 List<Policy> policies = new ArrayList<>(); 119 120 FileReader policyFile = null; 121 XmlPullParser parser = Xml.newPullParser(); 122 try { 123 policyFile = new FileReader(MAC_PERMISSIONS); 124 Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS); 125 126 parser.setInput(policyFile); 127 parser.nextTag(); 128 parser.require(XmlPullParser.START_TAG, null, "policy"); 129 130 while (parser.next() != XmlPullParser.END_TAG) { 131 if (parser.getEventType() != XmlPullParser.START_TAG) { 132 continue; 133 } 134 135 switch (parser.getName()) { 136 case "signer": 137 policies.add(readSignerOrThrow(parser)); 138 break; 139 case "default": 140 policies.add(readDefaultOrThrow(parser)); 141 break; 142 default: 143 skip(parser); 144 } 145 } 146 } catch (IllegalStateException | IllegalArgumentException | 147 XmlPullParserException ex) { 148 StringBuilder sb = new StringBuilder("Exception @"); 149 sb.append(parser.getPositionDescription()); 150 sb.append(" while parsing "); 151 sb.append(MAC_PERMISSIONS); 152 sb.append(":"); 153 sb.append(ex); 154 Slog.w(TAG, sb.toString()); 155 return false; 156 } catch (IOException ioe) { 157 Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe); 158 return false; 159 } finally { 160 IoUtils.closeQuietly(policyFile); 161 } 162 163 // Now sort the policy stanzas 164 PolicyComparator policySort = new PolicyComparator(); 165 Collections.sort(policies, policySort); 166 if (policySort.foundDuplicate()) { 167 Slog.w(TAG, "ERROR! Duplicate entries found parsing " + MAC_PERMISSIONS); 168 return false; 169 } 170 171 synchronized (sPolicies) { 172 sPolicies = policies; 173 174 if (DEBUG_POLICY_ORDER) { 175 for (Policy policy : sPolicies) { 176 Slog.d(TAG, "Policy: " + policy.toString()); 177 } 178 } 179 } 180 181 return true; 182 } 183 184 /** 185 * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy} 186 * instance will be created and returned in the process. During the pass all other 187 * tag elements will be skipped. 188 * 189 * @param parser an XmlPullParser object representing a signer element. 190 * @return the constructed {@link Policy} instance 191 * @throws IOException 192 * @throws XmlPullParserException 193 * @throws IllegalArgumentException if any of the validation checks fail while 194 * parsing tag values. 195 * @throws IllegalStateException if any of the invariants fail when constructing 196 * the {@link Policy} instance. 197 */ 198 private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException, 199 XmlPullParserException { 200 201 parser.require(XmlPullParser.START_TAG, null, "signer"); 202 Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); 203 204 // Check for a cert attached to the signer tag. We allow a signature 205 // to appear as an attribute as well as those attached to cert tags. 206 String cert = parser.getAttributeValue(null, "signature"); 207 if (cert != null) { 208 pb.addSignature(cert); 209 } 210 211 while (parser.next() != XmlPullParser.END_TAG) { 212 if (parser.getEventType() != XmlPullParser.START_TAG) { 213 continue; 214 } 215 216 String tagName = parser.getName(); 217 if ("seinfo".equals(tagName)) { 218 String seinfo = parser.getAttributeValue(null, "value"); 219 pb.setGlobalSeinfoOrThrow(seinfo); 220 readSeinfo(parser); 221 } else if ("package".equals(tagName)) { 222 readPackageOrThrow(parser, pb); 223 } else if ("cert".equals(tagName)) { 224 String sig = parser.getAttributeValue(null, "signature"); 225 pb.addSignature(sig); 226 readCert(parser); 227 } else { 228 skip(parser); 229 } 230 } 231 232 return pb.build(); 233 } 234 235 /** 236 * Loop over a default element looking for seinfo child tags. A {@link Policy} 237 * instance will be created and returned in the process. All other tags encountered 238 * will be skipped. 239 * 240 * @param parser an XmlPullParser object representing a default element. 241 * @return the constructed {@link Policy} instance 242 * @throws IOException 243 * @throws XmlPullParserException 244 * @throws IllegalArgumentException if any of the validation checks fail while 245 * parsing tag values. 246 * @throws IllegalStateException if any of the invariants fail when constructing 247 * the {@link Policy} instance. 248 */ 249 private static Policy readDefaultOrThrow(XmlPullParser parser) throws IOException, 250 XmlPullParserException { 251 252 parser.require(XmlPullParser.START_TAG, null, "default"); 253 Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); 254 pb.setAsDefaultPolicy(); 255 256 while (parser.next() != XmlPullParser.END_TAG) { 257 if (parser.getEventType() != XmlPullParser.START_TAG) { 258 continue; 259 } 260 261 String tagName = parser.getName(); 262 if ("seinfo".equals(tagName)) { 263 String seinfo = parser.getAttributeValue(null, "value"); 264 pb.setGlobalSeinfoOrThrow(seinfo); 265 readSeinfo(parser); 266 } else { 267 skip(parser); 268 } 269 } 270 271 return pb.build(); 272 } 273 274 /** 275 * Loop over a package element looking for seinfo child tags. If found return the 276 * value attribute of the seinfo tag, otherwise return null. All other tags encountered 277 * will be skipped. 278 * 279 * @param parser an XmlPullParser object representing a package element. 280 * @param pb a Policy.PolicyBuilder instance to build 281 * @throws IOException 282 * @throws XmlPullParserException 283 * @throws IllegalArgumentException if any of the validation checks fail while 284 * parsing tag values. 285 * @throws IllegalStateException if there is a duplicate seinfo tag for the current 286 * package tag. 287 */ 288 private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws 289 IOException, XmlPullParserException { 290 parser.require(XmlPullParser.START_TAG, null, "package"); 291 String pkgName = parser.getAttributeValue(null, "name"); 292 293 while (parser.next() != XmlPullParser.END_TAG) { 294 if (parser.getEventType() != XmlPullParser.START_TAG) { 295 continue; 296 } 297 298 String tagName = parser.getName(); 299 if ("seinfo".equals(tagName)) { 300 String seinfo = parser.getAttributeValue(null, "value"); 301 pb.addInnerPackageMapOrThrow(pkgName, seinfo); 302 readSeinfo(parser); 303 } else { 304 skip(parser); 305 } 306 } 307 } 308 309 private static void readCert(XmlPullParser parser) throws IOException, 310 XmlPullParserException { 311 parser.require(XmlPullParser.START_TAG, null, "cert"); 312 parser.nextTag(); 313 } 314 315 private static void readSeinfo(XmlPullParser parser) throws IOException, 316 XmlPullParserException { 317 parser.require(XmlPullParser.START_TAG, null, "seinfo"); 318 parser.nextTag(); 319 } 320 321 private static void skip(XmlPullParser p) throws IOException, XmlPullParserException { 322 if (p.getEventType() != XmlPullParser.START_TAG) { 323 throw new IllegalStateException(); 324 } 325 int depth = 1; 326 while (depth != 0) { 327 switch (p.next()) { 328 case XmlPullParser.END_TAG: 329 depth--; 330 break; 331 case XmlPullParser.START_TAG: 332 depth++; 333 break; 334 } 335 } 336 } 337 338 /** 339 * Applies a security label to a package based on an seinfo tag taken from a matched 340 * policy. All signature based policy stanzas are consulted first and, if no match 341 * is found, the default policy stanza is then consulted. The security label is 342 * attached to the ApplicationInfo instance of the package in the event that a matching 343 * policy was found. 344 * 345 * @param pkg object representing the package to be labeled. 346 * @return boolean which determines whether a non null seinfo label was assigned 347 * to the package. A null value simply represents that no policy matched. 348 */ 349 public static boolean assignSeinfoValue(PackageParser.Package pkg) { 350 synchronized (sPolicies) { 351 for (Policy policy : sPolicies) { 352 String seinfo = policy.getMatchedSeinfo(pkg); 353 if (seinfo != null) { 354 pkg.applicationInfo.seinfo = seinfo; 355 if (DEBUG_POLICY_INSTALL) { 356 Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " + 357 "seinfo=" + seinfo); 358 } 359 return true; 360 } 361 } 362 } 363 364 if (DEBUG_POLICY_INSTALL) { 365 Slog.i(TAG, "package (" + pkg.packageName + ") doesn't match any policy; " + 366 "seinfo will remain null"); 367 } 368 return false; 369 } 370 371 /** 372 * Determines if a recursive restorecon on /data/data and /data/user is needed. 373 * It does this by comparing the SHA-1 of the seapp_contexts file against the 374 * stored hash at /data/system/seapp_hash. 375 * 376 * @return Returns true if the restorecon should occur or false otherwise. 377 */ 378 public static boolean shouldRestorecon() { 379 // Any error with the seapp_contexts file should be fatal 380 byte[] currentHash = null; 381 try { 382 currentHash = returnHash(SEAPP_CONTEXTS); 383 } catch (IOException ioe) { 384 Slog.e(TAG, "Error with hashing seapp_contexts.", ioe); 385 return false; 386 } 387 388 // Push past any error with the stored hash file 389 byte[] storedHash = null; 390 try { 391 storedHash = IoUtils.readFileAsByteArray(SEAPP_HASH_FILE); 392 } catch (IOException ioe) { 393 Slog.w(TAG, "Error opening " + SEAPP_HASH_FILE + ". Assuming first boot."); 394 } 395 396 return (storedHash == null || !MessageDigest.isEqual(storedHash, currentHash)); 397 } 398 399 /** 400 * Stores the SHA-1 of the seapp_contexts to /data/system/seapp_hash. 401 */ 402 public static void setRestoreconDone() { 403 try { 404 final byte[] currentHash = returnHash(SEAPP_CONTEXTS); 405 dumpHash(new File(SEAPP_HASH_FILE), currentHash); 406 } catch (IOException ioe) { 407 Slog.e(TAG, "Error with saving hash to " + SEAPP_HASH_FILE, ioe); 408 } 409 } 410 411 /** 412 * Dump the contents of a byte array to a specified file. 413 * 414 * @param file The file that receives the byte array content. 415 * @param content A byte array that will be written to the specified file. 416 * @throws IOException if any failed I/O operation occured. 417 * Included is the failure to atomically rename the tmp 418 * file used in the process. 419 */ 420 private static void dumpHash(File file, byte[] content) throws IOException { 421 FileOutputStream fos = null; 422 File tmp = null; 423 try { 424 tmp = File.createTempFile("seapp_hash", ".journal", file.getParentFile()); 425 tmp.setReadable(true); 426 fos = new FileOutputStream(tmp); 427 fos.write(content); 428 fos.getFD().sync(); 429 if (!tmp.renameTo(file)) { 430 throw new IOException("Failure renaming " + file.getCanonicalPath()); 431 } 432 } finally { 433 if (tmp != null) { 434 tmp.delete(); 435 } 436 IoUtils.closeQuietly(fos); 437 } 438 } 439 440 /** 441 * Return the SHA-1 of a file. 442 * 443 * @param file The path to the file given as a string. 444 * @return Returns the SHA-1 of the file as a byte array. 445 * @throws IOException if any failed I/O operations occured. 446 */ 447 private static byte[] returnHash(String file) throws IOException { 448 try { 449 final byte[] contents = IoUtils.readFileAsByteArray(file); 450 return MessageDigest.getInstance("SHA-1").digest(contents); 451 } catch (NoSuchAlgorithmException nsae) { 452 throw new RuntimeException(nsae); // impossible 453 } 454 } 455 456 private static boolean useOverridePolicy() { 457 try { 458 final String overrideVersion = IoUtils.readFileAsString(DATA_VERSION_FILE); 459 final String baseVersion = IoUtils.readFileAsString(BASE_VERSION_FILE); 460 if (overrideVersion.equals(baseVersion)) { 461 return true; 462 } 463 Slog.e(TAG, "Override policy version '" + overrideVersion + "' doesn't match " + 464 "base version '" + baseVersion + "'. Skipping override policy files."); 465 } catch (FileNotFoundException fnfe) { 466 // Override version file doesn't have to exist so silently ignore. 467 } catch (IOException ioe) { 468 Slog.w(TAG, "Skipping override policy files.", ioe); 469 } 470 return false; 471 } 472 } 473 474 /** 475 * Holds valid policy representations of individual stanzas from a mac_permissions.xml 476 * file. Each instance can further be used to assign seinfo values to apks using the 477 * {@link Policy#getMatchedSeinfo} method. To create an instance of this use the 478 * {@link PolicyBuilder} pattern class, where each instance is validated against a set 479 * of invariants before being built and returned. Each instance can be guaranteed to 480 * hold one valid policy stanza as outlined in the external/sepolicy/mac_permissions.xml 481 * file. 482 * <p> 483 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 484 * signer based Policy instance with only inner package name refinements. 485 * </p> 486 * <pre> 487 * {@code 488 * Policy policy = new Policy.PolicyBuilder() 489 * .addSignature("308204a8...") 490 * .addSignature("483538c8...") 491 * .addInnerPackageMapOrThrow("com.foo.", "bar") 492 * .addInnerPackageMapOrThrow("com.foo.other", "bar") 493 * .build(); 494 * } 495 * </pre> 496 * <p> 497 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 498 * signer based Policy instance with only a global seinfo tag. 499 * </p> 500 * <pre> 501 * {@code 502 * Policy policy = new Policy.PolicyBuilder() 503 * .addSignature("308204a8...") 504 * .addSignature("483538c8...") 505 * .setGlobalSeinfoOrThrow("paltform") 506 * .build(); 507 * } 508 * </pre> 509 * <p> 510 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 511 * default based Policy instance. 512 * </p> 513 * <pre> 514 * {@code 515 * Policy policy = new Policy.PolicyBuilder() 516 * .setAsDefaultPolicy() 517 * .setGlobalSeinfoOrThrow("default") 518 * .build(); 519 * } 520 * </pre> 521 */ 522 final class Policy { 523 524 private final String mSeinfo; 525 private final boolean mDefaultStanza; 526 private final Set<Signature> mCerts; 527 private final Map<String, String> mPkgMap; 528 529 // Use the PolicyBuilder pattern to instantiate 530 private Policy(PolicyBuilder builder) { 531 mSeinfo = builder.mSeinfo; 532 mDefaultStanza = builder.mDefaultStanza; 533 mCerts = Collections.unmodifiableSet(builder.mCerts); 534 mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); 535 } 536 537 /** 538 * Return all the certs stored with this policy stanza. 539 * 540 * @return A set of Signature objects representing all the certs stored 541 * with the policy. 542 */ 543 public Set<Signature> getSignatures() { 544 return mCerts; 545 } 546 547 /** 548 * Return whether this policy object represents a default stanza. 549 * 550 * @return A boolean indicating if this object represents a default policy stanza. 551 */ 552 public boolean isDefaultStanza() { 553 return mDefaultStanza; 554 } 555 556 /** 557 * Return whether this policy object contains package name mapping refinements. 558 * 559 * @return A boolean indicating if this object has inner package name mappings. 560 */ 561 public boolean hasInnerPackages() { 562 return !mPkgMap.isEmpty(); 563 } 564 565 /** 566 * Return the mapping of all package name refinements. 567 * 568 * @return A Map object whose keys are the package names and whose values are 569 * the seinfo assignments. 570 */ 571 public Map<String, String> getInnerPackages() { 572 return mPkgMap; 573 } 574 575 /** 576 * Return whether the policy object has a global seinfo tag attached. 577 * 578 * @return A boolean indicating if this stanza has a global seinfo tag. 579 */ 580 public boolean hasGlobalSeinfo() { 581 return mSeinfo != null; 582 } 583 584 @Override 585 public String toString() { 586 StringBuilder sb = new StringBuilder(); 587 if (mDefaultStanza) { 588 sb.append("defaultStanza=true "); 589 } 590 591 for (Signature cert : mCerts) { 592 sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... "); 593 } 594 595 if (mSeinfo != null) { 596 sb.append("seinfo=" + mSeinfo); 597 } 598 599 for (String name : mPkgMap.keySet()) { 600 sb.append(" " + name + "=" + mPkgMap.get(name)); 601 } 602 603 return sb.toString(); 604 } 605 606 /** 607 * <p> 608 * Determine the seinfo value to assign to an apk. The appropriate seinfo value 609 * is determined using the following steps: 610 * </p> 611 * <ul> 612 * <li> If this Policy instance is defined as a default stanza: 613 * <ul><li>Return the global seinfo value</li></ul> 614 * </li> 615 * <li> If this Policy instance is defined as a signer stanza: 616 * <ul> 617 * <li> All certs used to sign the apk and all certs stored with this policy 618 * instance are tested for set equality. If this fails then null is returned. 619 * </li> 620 * <li> If all certs match then an appropriate inner package stanza is 621 * searched based on package name alone. If matched, the stored seinfo 622 * value for that mapping is returned. 623 * </li> 624 * <li> If all certs matched and no inner package stanza matches then return 625 * the global seinfo value. The returned value can be null in this case. 626 * </li> 627 * </ul> 628 * </li> 629 * </ul> 630 * <p> 631 * In all cases, a return value of null should be interpreted as the apk failing 632 * to match this Policy instance; i.e. failing this policy stanza. 633 * </p> 634 * @param pkg the apk to check given as a PackageParser.Package object 635 * @return A string representing the seinfo matched during policy lookup. 636 * A value of null can also be returned if no match occured. 637 */ 638 public String getMatchedSeinfo(PackageParser.Package pkg) { 639 if (!mDefaultStanza) { 640 // Check for exact signature matches across all certs. 641 Signature[] certs = mCerts.toArray(new Signature[0]); 642 if (!Signature.areExactMatch(certs, pkg.mSignatures)) { 643 return null; 644 } 645 646 // Check for inner package name matches given that the 647 // signature checks already passed. 648 String seinfoValue = mPkgMap.get(pkg.packageName); 649 if (seinfoValue != null) { 650 return seinfoValue; 651 } 652 } 653 654 // Return the global seinfo value (even if it's null). 655 return mSeinfo; 656 } 657 658 /** 659 * A nested builder class to create {@link Policy} instances. A {@link Policy} 660 * class instance represents one valid policy stanza found in a mac_permissions.xml 661 * file. A valid policy stanza is defined to be either a signer or default stanza 662 * which obeys the rules outlined in external/sepolicy/mac_permissions.xml. The 663 * {@link #build} method ensures a set of invariants are upheld enforcing the correct 664 * stanza structure before returning a valid Policy object. 665 */ 666 public static final class PolicyBuilder { 667 668 private String mSeinfo; 669 private boolean mDefaultStanza; 670 private final Set<Signature> mCerts; 671 private final Map<String, String> mPkgMap; 672 673 public PolicyBuilder() { 674 mCerts = new HashSet<Signature>(2); 675 mPkgMap = new HashMap<String, String>(2); 676 } 677 678 /** 679 * Sets this stanza as a default stanza. All policy stanzas are assumed to 680 * be signer stanzas unless this method is explicitly called. Default stanzas 681 * are treated differently with respect to allowable child tags, ordering and 682 * when and how policy decisions are enforced. 683 * 684 * @return The reference to this PolicyBuilder. 685 */ 686 public PolicyBuilder setAsDefaultPolicy() { 687 mDefaultStanza = true; 688 return this; 689 } 690 691 /** 692 * Adds a signature to the set of certs used for validation checks. The purpose 693 * being that all contained certs will need to be matched against all certs 694 * contained with an apk. 695 * 696 * @param cert the signature to add given as a String. 697 * @return The reference to this PolicyBuilder. 698 * @throws IllegalArgumentException if the cert value fails validation; 699 * null or is an invalid hex-encoded ASCII string. 700 */ 701 public PolicyBuilder addSignature(String cert) { 702 if (cert == null) { 703 String err = "Invalid signature value " + cert; 704 throw new IllegalArgumentException(err); 705 } 706 707 mCerts.add(new Signature(cert)); 708 return this; 709 } 710 711 /** 712 * Set the global seinfo tag for this policy stanza. The global seinfo tag 713 * represents the seinfo element that is used in one of two ways depending on 714 * its context. When attached to a signer tag the global seinfo represents an 715 * assignment when there isn't a further inner package refinement in policy. 716 * When used with a default tag, it represents the only allowable assignment 717 * value. 718 * 719 * @param seinfo the seinfo value given as a String. 720 * @return The reference to this PolicyBuilder. 721 * @throws IllegalArgumentException if the seinfo value fails validation; 722 * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. 723 * @throws IllegalStateException if an seinfo value has already been found 724 */ 725 public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { 726 if (!validateValue(seinfo)) { 727 String err = "Invalid seinfo value " + seinfo; 728 throw new IllegalArgumentException(err); 729 } 730 731 if (mSeinfo != null && !mSeinfo.equals(seinfo)) { 732 String err = "Duplicate seinfo tag found"; 733 throw new IllegalStateException(err); 734 } 735 736 mSeinfo = seinfo; 737 return this; 738 } 739 740 /** 741 * Create a package name to seinfo value mapping. Each mapping represents 742 * the seinfo value that will be assigned to the described package name. 743 * These localized mappings allow the global seinfo to be overriden. This 744 * mapping provides no value when used in conjunction with a default stanza; 745 * enforced through the {@link #build} method. 746 * 747 * @param pkgName the android package name given to the app 748 * @param seinfo the seinfo value that will be assigned to the passed pkgName 749 * @return The reference to this PolicyBuilder. 750 * @throws IllegalArgumentException if the seinfo value fails validation; 751 * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. 752 * Or, if the package name isn't a valid android package name. 753 * @throws IllegalStateException if trying to reset a package mapping with a 754 * different seinfo value. 755 */ 756 public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { 757 if (!validateValue(pkgName)) { 758 String err = "Invalid package name " + pkgName; 759 throw new IllegalArgumentException(err); 760 } 761 if (!validateValue(seinfo)) { 762 String err = "Invalid seinfo value " + seinfo; 763 throw new IllegalArgumentException(err); 764 } 765 766 String pkgValue = mPkgMap.get(pkgName); 767 if (pkgValue != null && !pkgValue.equals(seinfo)) { 768 String err = "Conflicting seinfo value found"; 769 throw new IllegalStateException(err); 770 } 771 772 mPkgMap.put(pkgName, seinfo); 773 return this; 774 } 775 776 /** 777 * General validation routine for the attribute strings of an element. Checks 778 * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. 779 * 780 * @param name the string to validate. 781 * @return boolean indicating if the string was valid. 782 */ 783 private boolean validateValue(String name) { 784 if (name == null) 785 return false; 786 787 // Want to match on [0-9a-zA-Z_.] 788 if (!name.matches("\\A[\\.\\w]+\\z")) { 789 return false; 790 } 791 792 return true; 793 } 794 795 /** 796 * <p> 797 * Create a {@link Policy} instance based on the current configuration. This 798 * method checks for certain policy invariants used to enforce certain guarantees 799 * about the expected structure of a policy stanza. 800 * Those invariants are: 801 * </p> 802 * <ul> 803 * <li> If a default stanza 804 * <ul> 805 * <li> an attached global seinfo tag must be present </li> 806 * <li> no signatures and no package names can be present </li> 807 * </ul> 808 * </li> 809 * <li> If a signer stanza 810 * <ul> 811 * <li> at least one cert must be found </li> 812 * <li> either a global seinfo value is present OR at least one 813 * inner package mapping must be present BUT not both. </li> 814 * </ul> 815 * </li> 816 * </ul> 817 * 818 * @return an instance of {@link Policy} with the options set from this builder 819 * @throws IllegalStateException if an invariant is violated. 820 */ 821 public Policy build() { 822 Policy p = new Policy(this); 823 824 if (p.mDefaultStanza) { 825 if (p.mSeinfo == null) { 826 String err = "Missing global seinfo tag with default stanza."; 827 throw new IllegalStateException(err); 828 } 829 if (p.mCerts.size() != 0) { 830 String err = "Certs not allowed with default stanza."; 831 throw new IllegalStateException(err); 832 } 833 if (!p.mPkgMap.isEmpty()) { 834 String err = "Inner package mappings not allowed with default stanza."; 835 throw new IllegalStateException(err); 836 } 837 } else { 838 if (p.mCerts.size() == 0) { 839 String err = "Missing certs with signer tag. Expecting at least one."; 840 throw new IllegalStateException(err); 841 } 842 if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) { 843 String err = "Only seinfo tag XOR package tags are allowed within " + 844 "a signer stanza."; 845 throw new IllegalStateException(err); 846 } 847 } 848 849 return p; 850 } 851 } 852 } 853 854 /** 855 * Comparision imposing an ordering on Policy objects. It is understood that Policy 856 * objects can only take one of three forms and ordered according to the following 857 * set of rules most specific to least. 858 * <ul> 859 * <li> signer stanzas with inner package mappings </li> 860 * <li> signer stanzas with global seinfo tags </li> 861 * <li> default stanza </li> 862 * </ul> 863 * This comparison also checks for duplicate entries on the input selectors. Any 864 * found duplicates will be flagged and can be checked with {@link #foundDuplicate}. 865 */ 866 867 final class PolicyComparator implements Comparator<Policy> { 868 869 private boolean duplicateFound = false; 870 871 public boolean foundDuplicate() { 872 return duplicateFound; 873 } 874 875 @Override 876 public int compare(Policy p1, Policy p2) { 877 878 // Give precedence to signature stanzas over default stanzas 879 if (p1.isDefaultStanza() != p2.isDefaultStanza()) { 880 return p1.isDefaultStanza() ? 1 : -1; 881 } 882 883 // Give precedence to stanzas with inner package mappings 884 if (p1.hasInnerPackages() != p2.hasInnerPackages()) { 885 return p1.hasInnerPackages() ? -1 : 1; 886 } 887 888 // Check for duplicate entries 889 if (p1.getSignatures().equals(p2.getSignatures())) { 890 // Checks if default stanza or a signer w/o inner package names 891 if (p1.hasGlobalSeinfo()) { 892 duplicateFound = true; 893 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 894 } 895 896 // Look for common inner package name mappings 897 final Map<String, String> p1Packages = p1.getInnerPackages(); 898 final Map<String, String> p2Packages = p2.getInnerPackages(); 899 if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) { 900 duplicateFound = true; 901 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); 902 } 903 } 904 905 return 0; 906 } 907 } 908