Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 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 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 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 | |
Robert Craig | 4385343 | 2014-03-04 11:57:23 -0500 | [diff] [blame] | 25 | import libcore.io.IoUtils; |
| 26 | |
Jeff Sharkey | 0e62384c | 2016-01-13 18:52:55 -0700 | [diff] [blame] | 27 | import org.xmlpull.v1.XmlPullParser; |
| 28 | import org.xmlpull.v1.XmlPullParserException; |
| 29 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 30 | import java.io.File; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 31 | import java.io.FileReader; |
| 32 | import java.io.IOException; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 33 | import java.util.ArrayList; |
| 34 | import java.util.Collections; |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 35 | import java.util.Comparator; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 36 | import java.util.HashMap; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 37 | import java.util.HashSet; |
| 38 | import java.util.List; |
| 39 | import java.util.Map; |
| 40 | import java.util.Set; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 41 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 42 | /** |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | * |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 47 | * {@hide} |
| 48 | */ |
| 49 | public final class SELinuxMMAC { |
| 50 | |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 51 | static final String TAG = "SELinuxMMAC"; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 52 | |
| 53 | private static final boolean DEBUG_POLICY = false; |
| 54 | private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 55 | private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 56 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 57 | // All policy stanzas read from mac_permissions.xml. This is also the lock |
| 58 | // to synchronize access during policy load and access attempts. |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 59 | private static List<Policy> sPolicies = new ArrayList<>(); |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 60 | |
Jeff Sharkey | 0e62384c | 2016-01-13 18:52:55 -0700 | [diff] [blame] | 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"); |
Robert Craig | f877829 | 2014-03-14 11:09:09 -0400 | [diff] [blame] | 64 | |
Jeff Vander Stoep | 0985937 | 2015-10-12 08:28:56 -0700 | [diff] [blame] | 65 | // Append privapp to existing seinfo label |
| 66 | private static final String PRIVILEGED_APP_STR = ":privapp"; |
| 67 | |
Chad Brubaker | 45abcf4 | 2016-10-07 10:13:10 -0700 | [diff] [blame] | 68 | // Append ephemeral to existing seinfo label |
| 69 | private static final String EPHEMERAL_APP_STR = ":ephemeralapp"; |
Jeff Vander Stoep | a4407bf | 2015-10-30 13:15:58 -0700 | [diff] [blame] | 70 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 |
Nick Kralevich | e91dba0 | 2016-04-13 07:49:28 -0700 | [diff] [blame] | 77 | * located at system/sepolicy/mac_permissions.xml. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | */ |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 84 | public static boolean readInstallPolicy() { |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 85 | // Temp structure to hold the rules while we parse the xml file |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 86 | List<Policy> policies = new ArrayList<>(); |
Robert Craig | 9989115 | 2014-09-02 07:16:52 -0400 | [diff] [blame] | 87 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 88 | FileReader policyFile = null; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 89 | XmlPullParser parser = Xml.newPullParser(); |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 90 | try { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 91 | policyFile = new FileReader(MAC_PERMISSIONS); |
| 92 | Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS); |
Robert Craig | f877829 | 2014-03-14 11:09:09 -0400 | [diff] [blame] | 93 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 94 | parser.setInput(policyFile); |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 95 | parser.nextTag(); |
| 96 | parser.require(XmlPullParser.START_TAG, null, "policy"); |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 97 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 98 | while (parser.next() != XmlPullParser.END_TAG) { |
| 99 | if (parser.getEventType() != XmlPullParser.START_TAG) { |
| 100 | continue; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 101 | } |
| 102 | |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 103 | switch (parser.getName()) { |
| 104 | case "signer": |
| 105 | policies.add(readSignerOrThrow(parser)); |
| 106 | break; |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 107 | default: |
| 108 | skip(parser); |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 109 | } |
| 110 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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()); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 120 | return false; |
Robert Craig | f877829 | 2014-03-14 11:09:09 -0400 | [diff] [blame] | 121 | } catch (IOException ioe) { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 122 | Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 123 | return false; |
| 124 | } finally { |
Robert Craig | f877829 | 2014-03-14 11:09:09 -0400 | [diff] [blame] | 125 | IoUtils.closeQuietly(policyFile); |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 126 | } |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 127 | |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 134 | } |
| 135 | |
| 136 | synchronized (sPolicies) { |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 137 | sPolicies = policies; |
| 138 | |
| 139 | if (DEBUG_POLICY_ORDER) { |
| 140 | for (Policy policy : sPolicies) { |
| 141 | Slog.d(TAG, "Policy: " + policy.toString()); |
| 142 | } |
| 143 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 144 | } |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 145 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 146 | return true; |
| 147 | } |
| 148 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 { |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 165 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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) { |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 178 | continue; |
| 179 | } |
| 180 | |
| 181 | String tagName = parser.getName(); |
| 182 | if ("seinfo".equals(tagName)) { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 183 | String seinfo = parser.getAttributeValue(null, "value"); |
| 184 | pb.setGlobalSeinfoOrThrow(seinfo); |
| 185 | readSeinfo(parser); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 186 | } else if ("package".equals(tagName)) { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 187 | readPackageOrThrow(parser, pb); |
| 188 | } else if ("cert".equals(tagName)) { |
| 189 | String sig = parser.getAttributeValue(null, "signature"); |
| 190 | pb.addSignature(sig); |
| 191 | readCert(parser); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 192 | } else { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 193 | skip(parser); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 194 | } |
| 195 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 196 | |
| 197 | return pb.build(); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 198 | } |
| 199 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 200 | /** |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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. |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 213 | */ |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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"); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 218 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 219 | while (parser.next() != XmlPullParser.END_TAG) { |
| 220 | if (parser.getEventType() != XmlPullParser.START_TAG) { |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 221 | continue; |
| 222 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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); |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 231 | } |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 232 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | } |
Robert Craig | 99a626c | 2013-12-02 10:24:23 -0500 | [diff] [blame] | 262 | } |
| 263 | |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 264 | /** |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 265 | * Applies a security label to a package based on an seinfo tag taken from a matched |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 270 | * |
Dianne Hackborn | 0aa5163 | 2014-03-19 17:49:09 -0700 | [diff] [blame] | 271 | * @param pkg object representing the package to be labeled. |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 272 | */ |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 273 | public static void assignSeinfoValue(PackageParser.Package pkg) { |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 274 | synchronized (sPolicies) { |
| 275 | for (Policy policy : sPolicies) { |
| 276 | String seinfo = policy.getMatchedSeinfo(pkg); |
Robert Craig | 83b54ec | 2014-07-01 13:53:11 -0700 | [diff] [blame] | 277 | if (seinfo != null) { |
| 278 | pkg.applicationInfo.seinfo = seinfo; |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 279 | break; |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 280 | } |
| 281 | } |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 282 | } |
| 283 | |
Chad Brubaker | 45abcf4 | 2016-10-07 10:13:10 -0700 | [diff] [blame] | 284 | if (pkg.applicationInfo.isEphemeralApp()) |
| 285 | pkg.applicationInfo.seinfo += EPHEMERAL_APP_STR; |
Jeff Vander Stoep | a4407bf | 2015-10-30 13:15:58 -0700 | [diff] [blame] | 286 | |
Jeff Vander Stoep | 0985937 | 2015-10-12 08:28:56 -0700 | [diff] [blame] | 287 | if (pkg.applicationInfo.isPrivilegedApp()) |
| 288 | pkg.applicationInfo.seinfo += PRIVILEGED_APP_STR; |
| 289 | |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 290 | if (DEBUG_POLICY_INSTALL) { |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 291 | Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " + |
| 292 | "seinfo=" + pkg.applicationInfo.seinfo); |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 293 | } |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 294 | } |
Robert Craig | d3f8d03 | 2013-03-25 06:33:03 -0400 | [diff] [blame] | 295 | } |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 |
Nick Kralevich | e91dba0 | 2016-04-13 07:49:28 -0700 | [diff] [blame] | 303 | * hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 304 | * file. |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 305 | * <p> |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 306 | * The following is an example of how to use {@link Policy.PolicyBuilder} to create a |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 322 | * </p> |
| 323 | * <pre> |
| 324 | * {@code |
| 325 | * Policy policy = new Policy.PolicyBuilder() |
| 326 | * .addSignature("308204a8...") |
| 327 | * .addSignature("483538c8...") |
| 328 | * .setGlobalSeinfoOrThrow("paltform") |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 329 | * .build(); |
| 330 | * } |
| 331 | * </pre> |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 332 | */ |
| 333 | final class Policy { |
| 334 | |
| 335 | private final String mSeinfo; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | /** |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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(); |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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 | /** |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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> |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 410 | * </li> |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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) { |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 432 | } |
| 433 | |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 448 | * file. A valid policy stanza is defined to be a signer stanza which obeys the rules |
Nick Kralevich | e91dba0 | 2016-04-13 07:49:28 -0700 | [diff] [blame] | 449 | * outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 450 | * ensures a set of invariants are upheld enforcing the correct stanza structure |
| 451 | * before returning a valid Policy object. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 452 | */ |
| 453 | public static final class PolicyBuilder { |
| 454 | |
| 455 | private String mSeinfo; |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | /** |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 486 | * when attached to a signer tag represents the assignment when there isn't a |
| 487 | * further inner package refinement in policy. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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. |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 513 | * These localized mappings allow the global seinfo to be overriden. |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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> |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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> |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 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 | |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 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); |
Robert Craig | 2e1f052 | 2014-11-19 13:56:10 -0500 | [diff] [blame] | 589 | } |
| 590 | |
| 591 | return p; |
| 592 | } |
| 593 | } |
| 594 | } |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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> |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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 | |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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())) { |
Robert Craig | 5e16bc5 | 2015-08-28 12:11:41 -0400 | [diff] [blame] | 626 | // Checks if signer w/o inner package names |
Robert Craig | 4caa6b1 | 2015-04-10 11:02:33 -0400 | [diff] [blame] | 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 | } |