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