blob: 89705560db7df056c400b1fdfa2e08b6fe24e502 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.pm;
import android.content.pm.PackageParser;
import android.content.pm.Signature;
import android.os.Environment;
import android.os.SystemProperties;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Slog;
import android.util.Xml;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Centralized access to SELinux MMAC (middleware MAC) implementation. This
* class is responsible for loading the appropriate mac_permissions.xml file
* as well as providing an interface for assigning seinfo values to apks.
*
* {@hide}
*/
public final class SELinuxMMAC {
static final String TAG = "SELinuxMMAC";
private static final boolean DEBUG_POLICY = false;
private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false;
private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false;
// All policy stanzas read from mac_permissions.xml. This is also the lock
// to synchronize access during policy load and access attempts.
private static List<Policy> sPolicies = new ArrayList<>();
private static final String PROP_FORCE_RESTORECON = "sys.force_restorecon";
/** Path to version on rootfs */
private static final File VERSION_FILE = new File("/selinux_version");
/** Path to MAC permissions on system image */
private static final File MAC_PERMISSIONS = new File(Environment.getRootDirectory(),
"/etc/security/mac_permissions.xml");
/** Path to app contexts on rootfs */
private static final File SEAPP_CONTEXTS = new File("/seapp_contexts");
/** Calculated hash of {@link #SEAPP_CONTEXTS} */
private static final byte[] SEAPP_CONTEXTS_HASH = returnHash(SEAPP_CONTEXTS);
/** Attribute where {@link #SEAPP_CONTEXTS_HASH} is stored */
private static final String XATTR_SEAPP_HASH = "user.seapp_hash";
// Append privapp to existing seinfo label
private static final String PRIVILEGED_APP_STR = ":privapp";
// Append autoplay to existing seinfo label
private static final String AUTOPLAY_APP_STR = ":autoplayapp";
/**
* Load the mac_permissions.xml file containing all seinfo assignments used to
* label apps. The loaded mac_permissions.xml file is determined by the
* MAC_PERMISSIONS class variable which is set at class load time which itself
* is based on the USE_OVERRIDE_POLICY class variable. For further guidance on
* the proper structure of a mac_permissions.xml file consult the source code
* located at system/sepolicy/mac_permissions.xml.
*
* @return boolean indicating if policy was correctly loaded. A value of false
* typically indicates a structural problem with the xml or incorrectly
* constructed policy stanzas. A value of true means that all stanzas
* were loaded successfully; no partial loading is possible.
*/
public static boolean readInstallPolicy() {
// Temp structure to hold the rules while we parse the xml file
List<Policy> policies = new ArrayList<>();
FileReader policyFile = null;
XmlPullParser parser = Xml.newPullParser();
try {
policyFile = new FileReader(MAC_PERMISSIONS);
Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS);
parser.setInput(policyFile);
parser.nextTag();
parser.require(XmlPullParser.START_TAG, null, "policy");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case "signer":
policies.add(readSignerOrThrow(parser));
break;
default:
skip(parser);
}
}
} catch (IllegalStateException | IllegalArgumentException |
XmlPullParserException ex) {
StringBuilder sb = new StringBuilder("Exception @");
sb.append(parser.getPositionDescription());
sb.append(" while parsing ");
sb.append(MAC_PERMISSIONS);
sb.append(":");
sb.append(ex);
Slog.w(TAG, sb.toString());
return false;
} catch (IOException ioe) {
Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe);
return false;
} finally {
IoUtils.closeQuietly(policyFile);
}
// Now sort the policy stanzas
PolicyComparator policySort = new PolicyComparator();
Collections.sort(policies, policySort);
if (policySort.foundDuplicate()) {
Slog.w(TAG, "ERROR! Duplicate entries found parsing " + MAC_PERMISSIONS);
return false;
}
synchronized (sPolicies) {
sPolicies = policies;
if (DEBUG_POLICY_ORDER) {
for (Policy policy : sPolicies) {
Slog.d(TAG, "Policy: " + policy.toString());
}
}
}
return true;
}
/**
* Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy}
* instance will be created and returned in the process. During the pass all other
* tag elements will be skipped.
*
* @param parser an XmlPullParser object representing a signer element.
* @return the constructed {@link Policy} instance
* @throws IOException
* @throws XmlPullParserException
* @throws IllegalArgumentException if any of the validation checks fail while
* parsing tag values.
* @throws IllegalStateException if any of the invariants fail when constructing
* the {@link Policy} instance.
*/
private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException,
XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "signer");
Policy.PolicyBuilder pb = new Policy.PolicyBuilder();
// Check for a cert attached to the signer tag. We allow a signature
// to appear as an attribute as well as those attached to cert tags.
String cert = parser.getAttributeValue(null, "signature");
if (cert != null) {
pb.addSignature(cert);
}
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String tagName = parser.getName();
if ("seinfo".equals(tagName)) {
String seinfo = parser.getAttributeValue(null, "value");
pb.setGlobalSeinfoOrThrow(seinfo);
readSeinfo(parser);
} else if ("package".equals(tagName)) {
readPackageOrThrow(parser, pb);
} else if ("cert".equals(tagName)) {
String sig = parser.getAttributeValue(null, "signature");
pb.addSignature(sig);
readCert(parser);
} else {
skip(parser);
}
}
return pb.build();
}
/**
* Loop over a package element looking for seinfo child tags. If found return the
* value attribute of the seinfo tag, otherwise return null. All other tags encountered
* will be skipped.
*
* @param parser an XmlPullParser object representing a package element.
* @param pb a Policy.PolicyBuilder instance to build
* @throws IOException
* @throws XmlPullParserException
* @throws IllegalArgumentException if any of the validation checks fail while
* parsing tag values.
* @throws IllegalStateException if there is a duplicate seinfo tag for the current
* package tag.
*/
private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws
IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "package");
String pkgName = parser.getAttributeValue(null, "name");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String tagName = parser.getName();
if ("seinfo".equals(tagName)) {
String seinfo = parser.getAttributeValue(null, "value");
pb.addInnerPackageMapOrThrow(pkgName, seinfo);
readSeinfo(parser);
} else {
skip(parser);
}
}
}
private static void readCert(XmlPullParser parser) throws IOException,
XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "cert");
parser.nextTag();
}
private static void readSeinfo(XmlPullParser parser) throws IOException,
XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "seinfo");
parser.nextTag();
}
private static void skip(XmlPullParser p) throws IOException, XmlPullParserException {
if (p.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (p.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
/**
* Applies a security label to a package based on an seinfo tag taken from a matched
* policy. All signature based policy stanzas are consulted and, if no match is
* found, the default seinfo label of 'default' (set in ApplicationInfo object) is
* used. The security label is attached to the ApplicationInfo instance of the package
* in the event that a matching policy was found.
*
* @param pkg object representing the package to be labeled.
*/
public static void assignSeinfoValue(PackageParser.Package pkg) {
synchronized (sPolicies) {
for (Policy policy : sPolicies) {
String seinfo = policy.getMatchedSeinfo(pkg);
if (seinfo != null) {
pkg.applicationInfo.seinfo = seinfo;
break;
}
}
}
if (pkg.applicationInfo.isAutoPlayApp())
pkg.applicationInfo.seinfo += AUTOPLAY_APP_STR;
if (pkg.applicationInfo.isPrivilegedApp())
pkg.applicationInfo.seinfo += PRIVILEGED_APP_STR;
if (DEBUG_POLICY_INSTALL) {
Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " +
"seinfo=" + pkg.applicationInfo.seinfo);
}
}
/**
* Determines if a recursive restorecon on the given package data directory
* is needed. It does this by comparing the SHA-1 of the seapp_contexts file
* against the stored hash in an xattr.
* <p>
* Note that the xattr isn't in the 'security' namespace, so this should
* only be run on directories owned by the system.
*
* @return Returns true if the restorecon should occur or false otherwise.
*/
public static boolean isRestoreconNeeded(File file) {
// To investigate boot timing, allow a property to always force restorecon
if (SystemProperties.getBoolean(PROP_FORCE_RESTORECON, false)) {
return true;
}
try {
final byte[] buf = new byte[20];
final int len = Os.getxattr(file.getAbsolutePath(), XATTR_SEAPP_HASH, buf);
if ((len == 20) && Arrays.equals(SEAPP_CONTEXTS_HASH, buf)) {
return false;
}
} catch (ErrnoException e) {
if (e.errno != OsConstants.ENODATA) {
Slog.e(TAG, "Failed to read seapp hash for " + file, e);
}
}
return true;
}
/**
* Stores the SHA-1 of the seapp_contexts into an xattr.
* <p>
* Note that the xattr isn't in the 'security' namespace, so this should
* only be run on directories owned by the system.
*/
public static void setRestoreconDone(File file) {
try {
Os.setxattr(file.getAbsolutePath(), XATTR_SEAPP_HASH, SEAPP_CONTEXTS_HASH, 0);
} catch (ErrnoException e) {
Slog.e(TAG, "Failed to persist seapp hash in " + file, e);
}
}
/**
* Return the SHA-1 of a file.
*
* @param file The path to the file given as a string.
* @return Returns the SHA-1 of the file as a byte array.
*/
private static byte[] returnHash(File file) {
try {
final byte[] contents = IoUtils.readFileAsByteArray(file.getAbsolutePath());
return MessageDigest.getInstance("SHA-1").digest(contents);
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
/**
* Holds valid policy representations of individual stanzas from a mac_permissions.xml
* file. Each instance can further be used to assign seinfo values to apks using the
* {@link Policy#getMatchedSeinfo} method. To create an instance of this use the
* {@link PolicyBuilder} pattern class, where each instance is validated against a set
* of invariants before being built and returned. Each instance can be guaranteed to
* hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml
* file.
* <p>
* The following is an example of how to use {@link Policy.PolicyBuilder} to create a
* signer based Policy instance with only inner package name refinements.
* </p>
* <pre>
* {@code
* Policy policy = new Policy.PolicyBuilder()
* .addSignature("308204a8...")
* .addSignature("483538c8...")
* .addInnerPackageMapOrThrow("com.foo.", "bar")
* .addInnerPackageMapOrThrow("com.foo.other", "bar")
* .build();
* }
* </pre>
* <p>
* The following is an example of how to use {@link Policy.PolicyBuilder} to create a
* signer based Policy instance with only a global seinfo tag.
* </p>
* <pre>
* {@code
* Policy policy = new Policy.PolicyBuilder()
* .addSignature("308204a8...")
* .addSignature("483538c8...")
* .setGlobalSeinfoOrThrow("paltform")
* .build();
* }
* </pre>
*/
final class Policy {
private final String mSeinfo;
private final Set<Signature> mCerts;
private final Map<String, String> mPkgMap;
// Use the PolicyBuilder pattern to instantiate
private Policy(PolicyBuilder builder) {
mSeinfo = builder.mSeinfo;
mCerts = Collections.unmodifiableSet(builder.mCerts);
mPkgMap = Collections.unmodifiableMap(builder.mPkgMap);
}
/**
* Return all the certs stored with this policy stanza.
*
* @return A set of Signature objects representing all the certs stored
* with the policy.
*/
public Set<Signature> getSignatures() {
return mCerts;
}
/**
* Return whether this policy object contains package name mapping refinements.
*
* @return A boolean indicating if this object has inner package name mappings.
*/
public boolean hasInnerPackages() {
return !mPkgMap.isEmpty();
}
/**
* Return the mapping of all package name refinements.
*
* @return A Map object whose keys are the package names and whose values are
* the seinfo assignments.
*/
public Map<String, String> getInnerPackages() {
return mPkgMap;
}
/**
* Return whether the policy object has a global seinfo tag attached.
*
* @return A boolean indicating if this stanza has a global seinfo tag.
*/
public boolean hasGlobalSeinfo() {
return mSeinfo != null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Signature cert : mCerts) {
sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... ");
}
if (mSeinfo != null) {
sb.append("seinfo=" + mSeinfo);
}
for (String name : mPkgMap.keySet()) {
sb.append(" " + name + "=" + mPkgMap.get(name));
}
return sb.toString();
}
/**
* <p>
* Determine the seinfo value to assign to an apk. The appropriate seinfo value
* is determined using the following steps:
* </p>
* <ul>
* <li> All certs used to sign the apk and all certs stored with this policy
* instance are tested for set equality. If this fails then null is returned.
* </li>
* <li> If all certs match then an appropriate inner package stanza is
* searched based on package name alone. If matched, the stored seinfo
* value for that mapping is returned.
* </li>
* <li> If all certs matched and no inner package stanza matches then return
* the global seinfo value. The returned value can be null in this case.
* </li>
* </ul>
* <p>
* In all cases, a return value of null should be interpreted as the apk failing
* to match this Policy instance; i.e. failing this policy stanza.
* </p>
* @param pkg the apk to check given as a PackageParser.Package object
* @return A string representing the seinfo matched during policy lookup.
* A value of null can also be returned if no match occured.
*/
public String getMatchedSeinfo(PackageParser.Package pkg) {
// Check for exact signature matches across all certs.
Signature[] certs = mCerts.toArray(new Signature[0]);
if (!Signature.areExactMatch(certs, pkg.mSignatures)) {
return null;
}
// Check for inner package name matches given that the
// signature checks already passed.
String seinfoValue = mPkgMap.get(pkg.packageName);
if (seinfoValue != null) {
return seinfoValue;
}
// Return the global seinfo value.
return mSeinfo;
}
/**
* A nested builder class to create {@link Policy} instances. A {@link Policy}
* class instance represents one valid policy stanza found in a mac_permissions.xml
* file. A valid policy stanza is defined to be a signer stanza which obeys the rules
* outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method
* ensures a set of invariants are upheld enforcing the correct stanza structure
* before returning a valid Policy object.
*/
public static final class PolicyBuilder {
private String mSeinfo;
private final Set<Signature> mCerts;
private final Map<String, String> mPkgMap;
public PolicyBuilder() {
mCerts = new HashSet<Signature>(2);
mPkgMap = new HashMap<String, String>(2);
}
/**
* Adds a signature to the set of certs used for validation checks. The purpose
* being that all contained certs will need to be matched against all certs
* contained with an apk.
*
* @param cert the signature to add given as a String.
* @return The reference to this PolicyBuilder.
* @throws IllegalArgumentException if the cert value fails validation;
* null or is an invalid hex-encoded ASCII string.
*/
public PolicyBuilder addSignature(String cert) {
if (cert == null) {
String err = "Invalid signature value " + cert;
throw new IllegalArgumentException(err);
}
mCerts.add(new Signature(cert));
return this;
}
/**
* Set the global seinfo tag for this policy stanza. The global seinfo tag
* when attached to a signer tag represents the assignment when there isn't a
* further inner package refinement in policy.
*
* @param seinfo the seinfo value given as a String.
* @return The reference to this PolicyBuilder.
* @throws IllegalArgumentException if the seinfo value fails validation;
* null, zero length or contains non-valid characters [^a-zA-Z_\._0-9].
* @throws IllegalStateException if an seinfo value has already been found
*/
public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) {
if (!validateValue(seinfo)) {
String err = "Invalid seinfo value " + seinfo;
throw new IllegalArgumentException(err);
}
if (mSeinfo != null && !mSeinfo.equals(seinfo)) {
String err = "Duplicate seinfo tag found";
throw new IllegalStateException(err);
}
mSeinfo = seinfo;
return this;
}
/**
* Create a package name to seinfo value mapping. Each mapping represents
* the seinfo value that will be assigned to the described package name.
* These localized mappings allow the global seinfo to be overriden.
*
* @param pkgName the android package name given to the app
* @param seinfo the seinfo value that will be assigned to the passed pkgName
* @return The reference to this PolicyBuilder.
* @throws IllegalArgumentException if the seinfo value fails validation;
* null, zero length or contains non-valid characters [^a-zA-Z_\.0-9].
* Or, if the package name isn't a valid android package name.
* @throws IllegalStateException if trying to reset a package mapping with a
* different seinfo value.
*/
public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) {
if (!validateValue(pkgName)) {
String err = "Invalid package name " + pkgName;
throw new IllegalArgumentException(err);
}
if (!validateValue(seinfo)) {
String err = "Invalid seinfo value " + seinfo;
throw new IllegalArgumentException(err);
}
String pkgValue = mPkgMap.get(pkgName);
if (pkgValue != null && !pkgValue.equals(seinfo)) {
String err = "Conflicting seinfo value found";
throw new IllegalStateException(err);
}
mPkgMap.put(pkgName, seinfo);
return this;
}
/**
* General validation routine for the attribute strings of an element. Checks
* if the string is non-null, positive length and only contains [a-zA-Z_\.0-9].
*
* @param name the string to validate.
* @return boolean indicating if the string was valid.
*/
private boolean validateValue(String name) {
if (name == null)
return false;
// Want to match on [0-9a-zA-Z_.]
if (!name.matches("\\A[\\.\\w]+\\z")) {
return false;
}
return true;
}
/**
* <p>
* Create a {@link Policy} instance based on the current configuration. This
* method checks for certain policy invariants used to enforce certain guarantees
* about the expected structure of a policy stanza.
* Those invariants are:
* </p>
* <ul>
* <li> at least one cert must be found </li>
* <li> either a global seinfo value is present OR at least one
* inner package mapping must be present BUT not both. </li>
* </ul>
* @return an instance of {@link Policy} with the options set from this builder
* @throws IllegalStateException if an invariant is violated.
*/
public Policy build() {
Policy p = new Policy(this);
if (p.mCerts.isEmpty()) {
String err = "Missing certs with signer tag. Expecting at least one.";
throw new IllegalStateException(err);
}
if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) {
String err = "Only seinfo tag XOR package tags are allowed within " +
"a signer stanza.";
throw new IllegalStateException(err);
}
return p;
}
}
}
/**
* Comparision imposing an ordering on Policy objects. It is understood that Policy
* objects can only take one of three forms and ordered according to the following
* set of rules most specific to least.
* <ul>
* <li> signer stanzas with inner package mappings </li>
* <li> signer stanzas with global seinfo tags </li>
* </ul>
* This comparison also checks for duplicate entries on the input selectors. Any
* found duplicates will be flagged and can be checked with {@link #foundDuplicate}.
*/
final class PolicyComparator implements Comparator<Policy> {
private boolean duplicateFound = false;
public boolean foundDuplicate() {
return duplicateFound;
}
@Override
public int compare(Policy p1, Policy p2) {
// Give precedence to stanzas with inner package mappings
if (p1.hasInnerPackages() != p2.hasInnerPackages()) {
return p1.hasInnerPackages() ? -1 : 1;
}
// Check for duplicate entries
if (p1.getSignatures().equals(p2.getSignatures())) {
// Checks if signer w/o inner package names
if (p1.hasGlobalSeinfo()) {
duplicateFound = true;
Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
}
// Look for common inner package name mappings
final Map<String, String> p1Packages = p1.getInnerPackages();
final Map<String, String> p2Packages = p2.getInnerPackages();
if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) {
duplicateFound = true;
Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
}
}
return 0;
}
}