1 /* 2 * Copyright 2020 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 androidx.security.app.authenticator; 18 19 import static android.os.Process.INVALID_PID; 20 import static android.os.Process.INVALID_UID; 21 22 import android.app.Activity; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.res.Resources; 27 import android.os.Binder; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import androidx.annotation.IntDef; 32 import androidx.annotation.RestrictTo; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.annotation.XmlRes; 35 import androidx.collection.ArrayMap; 36 import androidx.collection.ArraySet; 37 38 import com.google.auto.value.AutoValue; 39 40 import org.jspecify.annotations.NonNull; 41 import org.jspecify.annotations.Nullable; 42 import org.xmlpull.v1.XmlPullParser; 43 import org.xmlpull.v1.XmlPullParserException; 44 import org.xmlpull.v1.XmlPullParserFactory; 45 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.Locale; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * Provides methods to verify the signing identity of other apps on the device. 56 */ 57 // TODO(b/175503230): Add usage details to class level documentation once implementation is 58 // complete. 59 public class AppAuthenticator { 60 private static final String TAG = "AppAuthenticator"; 61 62 /** 63 * This is returned by {@link #checkCallingAppIdentity(String, String)} and 64 * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name 65 * has the expected signing identity for the provided permission. 66 */ 67 public static final int PERMISSION_GRANTED = 0; 68 69 /** 70 * This is returned by {@link #checkCallingAppIdentity(String, String)} and 71 * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name 72 * does not have any of the expected signing identities for the provided permission. 73 * 74 * @see PackageManager#SIGNATURE_NO_MATCH 75 */ 76 public static final int PERMISSION_DENIED_NO_MATCH = -3; 77 78 /** 79 * This is returned by {@link #checkCallingAppIdentity(String, String)} and 80 * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name 81 * does not belong to an app installed on the device. 82 * 83 * @see PackageManager#SIGNATURE_UNKNOWN_PACKAGE 84 */ 85 public static final int PERMISSION_DENIED_UNKNOWN_PACKAGE = -4; 86 87 /** 88 * This is returned by {@link #checkCallingAppIdentity(String, String)} and 89 * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name 90 * does not belong to the provided calling UID, or if the UID is not provided and the 91 * specified package name does not belong to the UID of the calling process as returned by 92 * {@link Binder#getCallingUid()}. 93 */ 94 public static final int PERMISSION_DENIED_PACKAGE_UID_MISMATCH = -5; 95 96 /** 97 * Values returned when checking that a specified package has the expected signing identity 98 * for a particular permission. 99 * 100 */ 101 @RestrictTo(RestrictTo.Scope.LIBRARY) 102 @IntDef(value = { 103 PERMISSION_GRANTED, 104 PERMISSION_DENIED_NO_MATCH, 105 PERMISSION_DENIED_UNKNOWN_PACKAGE, 106 PERMISSION_DENIED_PACKAGE_UID_MISMATCH, 107 }) 108 @Retention(RetentionPolicy.SOURCE) 109 public @interface AppIdentityPermissionResult {} 110 111 /** 112 * This is returned by {@link #checkAppIdentity(String)} when the specified package name has 113 * the expected signing identity. 114 * 115 * @see PackageManager#SIGNATURE_MATCH 116 */ 117 public static final int SIGNATURE_MATCH = 0; 118 119 /** 120 * This is returned by {@link #checkAppIdentity(String)} when the specified package name does 121 * not have the expected signing identity. 122 * 123 * @see PackageManager#SIGNATURE_NO_MATCH 124 */ 125 public static final int SIGNATURE_NO_MATCH = -1; 126 127 /** 128 * Values returned when checking that a specified package has the expected signing identity 129 * on the device. 130 * 131 */ 132 @RestrictTo(RestrictTo.Scope.LIBRARY) 133 @IntDef(value = { 134 SIGNATURE_MATCH, 135 SIGNATURE_NO_MATCH, 136 }) 137 @Retention(RetentionPolicy.SOURCE) 138 public @interface AppIdentityResult {} 139 140 /** 141 * The root tag for an AppAuthenticator XMl config file. 142 */ 143 private static final String ROOT_TAG = "app-authenticator"; 144 /** 145 * The tag to declare a new permission that can be granted to enclosed packages / signing 146 * identities. 147 */ 148 private static final String PERMISSION_TAG = "permission"; 149 /** 150 * The tag to begin declaration of the expected signing identities for the enclosed packages. 151 */ 152 private static final String EXPECTED_IDENTITY_TAG = "expected-identity"; 153 /** 154 * The tag to declare a new signing identity of a package within either a permission or 155 * expected-identity element. 156 */ 157 private static final String PACKAGE_TAG = "package"; 158 /** 159 * The tag to declare all packages signed with the enclosed signing identities are to be 160 * granted to the enclosing permission. 161 */ 162 static final String ALL_PACKAGES_TAG = "all-packages"; 163 /** 164 * The tag to declare a known signing certificate digest for the enclosing package. 165 */ 166 private static final String CERT_DIGEST_TAG = "cert-digest"; 167 /** 168 * The attribute to declare the name within a permission or package element. 169 */ 170 private static final String NAME_ATTRIBUTE = "name"; 171 /** 172 * The default digest algorithm used for all certificate digests if one is not specified in 173 * the root element. 174 */ 175 static final String DEFAULT_DIGEST_ALGORITHM = "SHA-256"; 176 177 private AppSignatureVerifier mAppSignatureVerifier; 178 private AppAuthenticatorUtils mAppAuthenticatorUtils; 179 180 /** 181 * Private constructor; instances should be created through the static factory methods. 182 * 183 * @param appSignatureVerifier the verifier to be used to verify app signing identities 184 * @param appAuthenticatorUtils the utils to be used 185 */ AppAuthenticator(AppSignatureVerifier appSignatureVerifier, AppAuthenticatorUtils appAuthenticatorUtils)186 AppAuthenticator(AppSignatureVerifier appSignatureVerifier, 187 AppAuthenticatorUtils appAuthenticatorUtils) { 188 mAppSignatureVerifier = appSignatureVerifier; 189 mAppAuthenticatorUtils = appAuthenticatorUtils; 190 } 191 192 /** 193 * Allows injection of the {@code appSignatureVerifier} to be used during tests. 194 */ 195 @VisibleForTesting setAppSignatureVerifier(AppSignatureVerifier appSignatureVerifier)196 void setAppSignatureVerifier(AppSignatureVerifier appSignatureVerifier) { 197 mAppSignatureVerifier = appSignatureVerifier; 198 } 199 200 /** 201 * Allows injection of the {@code appAuthenticatorUtils} to be used during tests. 202 * @param appAuthenticatorUtils 203 */ 204 @VisibleForTesting setAppAuthenticatorUtils(AppAuthenticatorUtils appAuthenticatorUtils)205 void setAppAuthenticatorUtils(AppAuthenticatorUtils appAuthenticatorUtils) { 206 mAppAuthenticatorUtils = appAuthenticatorUtils; 207 } 208 209 /** 210 * Enforces the specified {@code packageName} has the expected signing identity for the 211 * provided {@code permission}. 212 * 213 * <p>This method should be used for verifying the identity of a package when the UID / PID 214 * is not available. For instance, this should be used within an activity that was started 215 * with {@link Activity#startActivityForResult(Intent, int)} where the package name is 216 * available from {@link Activity#getCallingPackage()}. For instances where the calling 217 * UID is available but the calling PID is not available, 218 * ({@link #checkCallingAppIdentity(String, String, int)} should be preferred. 219 * 220 * @param packageName the name of the package to be verified 221 * @param permission the name of the permission as specified in the XML from which to verify the 222 * package / signing identity 223 * @throws SecurityException if the signing identity of the package does not match that defined 224 * for the permission 225 */ enforceCallingAppIdentity(@onNull String packageName, @NonNull String permission)226 public void enforceCallingAppIdentity(@NonNull String packageName, @NonNull String permission) { 227 enforceCallingAppIdentity(packageName, permission, INVALID_PID, INVALID_UID); 228 } 229 230 /** 231 * Enforces the specified {@code packageName} belongs to the provided {@code uid} 232 * and has the expected signing identity for the {@code permission}. 233 * 234 * <p>This method should be used for verifying the identity of a package when the UID is 235 * available but the PID is not. For instance, this should be used within an activity that 236 * is started with {@link android.app.ActivityOptions#setShareIdentityEnabled(boolean)} set to 237 * true; the package name would then be accessible via {@link Activity#getLaunchedFromPackage()} 238 * and the UID from {@link Activity#getLaunchedFromUid()}. 239 * 240 * @param packageName the name of the package to be verified 241 * @param permission the name of the permission as specified in the XML from which to verify the 242 * package / signing identity 243 * @param uid the expected uid of the package 244 * @throws SecurityException if the uid does not belong to the specified package, or if the 245 * signing identity of the package does not match that defined for the permission 246 */ enforceCallingAppIdentity(@onNull String packageName, @NonNull String permission, int uid)247 public void enforceCallingAppIdentity(@NonNull String packageName, @NonNull String permission, 248 int uid) { 249 enforceCallingAppIdentity(packageName, permission, INVALID_PID, uid); 250 } 251 252 /** 253 * Enforces the specified {@code packageName} belongs to the provided {@code pid} / {@code uid} 254 * and has the expected signing identity for the {@code permission}. 255 * 256 * <p>This method should be used for verifying the identity of a calling process of an IPC. 257 * 258 * @param packageName the name of the package to be verified 259 * @param permission the name of the permission as specified in the XML from which to verify the 260 * package / signing identity 261 * @param pid the expected pid of the process 262 * @param uid the expected uid of the package 263 * @throws SecurityException if the uid does not belong to the specified package, or if the 264 * signing identity of the package does not match that defined for the permission 265 */ enforceCallingAppIdentity(@onNull String packageName, @NonNull String permission, int pid, int uid)266 public void enforceCallingAppIdentity(@NonNull String packageName, @NonNull String permission, 267 int pid, int uid) { 268 AppAuthenticatorResult result = checkCallingAppIdentityInternal(packageName, permission, 269 pid, uid); 270 if (result.getResultCode() != PERMISSION_GRANTED) { 271 throw new SecurityException(result.getResultMessage()); 272 } 273 } 274 275 /** 276 * Checks the specified {@code packageName} has the expected signing identity for the 277 * provided {@code permission}. 278 * 279 * <p>This method should be used for verifying the identity of a package when the UID / PID 280 * is not available. For instance, this should be used within an activity that was started 281 * with {@link Activity#startActivityForResult(Intent, int)} where the package name is 282 * available from {@link Activity#getCallingPackage()}. For instances where the calling 283 * UID is available but the calling PID is not available, 284 * ({@link #checkCallingAppIdentity(String, String, int)} should be preferred. 285 * 286 * @param packageName the name of the package to be verified 287 * @param permission the name of the permission as specified in the XML from which to verify the 288 * package / signing identity 289 * @return {@link #PERMISSION_GRANTED} if the specified {@code packageName} has the expected 290 * signing identity for the provided {@code permission},<br> 291 * {@link #PERMISSION_DENIED_NO_MATCH} if the specified {@code packageName} does not have 292 * the expected signing identity for the provided {@code permission},<br> 293 * {@link #PERMISSION_DENIED_UNKNOWN_PACKAGE} if the specified {@code packageName} does not 294 * exist on the device,<br> 295 */ 296 @AppIdentityPermissionResult checkCallingAppIdentity(@onNull String packageName, @NonNull String permission)297 public int checkCallingAppIdentity(@NonNull String packageName, @NonNull String permission) { 298 return checkCallingAppIdentity(packageName, permission, INVALID_PID, INVALID_UID); 299 } 300 301 /** 302 * Checks the specified {@code packageName} running under {@code uid} has the expected 303 * signing identity for the provided {@code permission}. 304 * 305 * <p>This method should be used for verifying the identity of a package when the UID is 306 * available but the PID is not. For instance, this should be used within an activity that 307 * is started with {@link android.app.ActivityOptions#setShareIdentityEnabled(boolean)} set to 308 * true; the package name would then be accessible via {@link Activity#getLaunchedFromPackage()} 309 * and the UID from {@link Activity#getLaunchedFromUid()}. 310 * 311 * @param packageName the name of the package to be verified 312 * @param permission the name of the permission as specified in the XML from which to verify the 313 * package / signing identity 314 * @param uid the expected uid of the package 315 * @return {@link #PERMISSION_GRANTED} if the specified {@code packageName} has the expected 316 * signing identity for the provided {@code permission},<br> 317 * {@link #PERMISSION_DENIED_NO_MATCH} if the specified {@code packageName} does not have 318 * the expected signing identity for the provided {@code permission},<br> 319 * {@link #PERMISSION_DENIED_UNKNOWN_PACKAGE} if the specified {@code packageName} does not 320 * exist on the device,<br> 321 * {@link #PERMISSION_DENIED_PACKAGE_UID_MISMATCH} if the specified {@code uid} does not 322 * match the uid assigned to the package 323 */ 324 @AppIdentityPermissionResult checkCallingAppIdentity(@onNull String packageName, @NonNull String permission, int uid)325 public int checkCallingAppIdentity(@NonNull String packageName, @NonNull String permission, 326 int uid) { 327 return checkCallingAppIdentity(packageName, permission, INVALID_PID, uid); 328 } 329 330 /** 331 * Checks the specified {@code packageName} running with {@code pid} and {@code uid} has the 332 * expected signing identity for the provided {@code permission}. 333 * 334 * <p>This method should be used for verifying the identity of a calling process of an IPC. 335 * 336 * @param packageName the name of the package to be verified 337 * @param permission the name of the permission as specified in the XML from which to verify the 338 * package / signing identity 339 * @param pid the expected pid of the process 340 * @param uid the expected uid of the package 341 * @return {@link #PERMISSION_GRANTED} if the specified {@code packageName} has the expected 342 * signing identity for the provided {@code permission},<br> 343 * {@link #PERMISSION_DENIED_NO_MATCH} if the specified {@code packageName} does not have 344 * the expected signing identity for the provided {@code permission},<br> 345 * {@link #PERMISSION_DENIED_UNKNOWN_PACKAGE} if the specified {@code packageName} does not 346 * exist on the device,<br> 347 * {@link #PERMISSION_DENIED_PACKAGE_UID_MISMATCH} if the specified {@code uid} does not 348 * match the uid assigned to the package 349 */ 350 @AppIdentityPermissionResult checkCallingAppIdentity(@onNull String packageName, @NonNull String permission, int pid, int uid)351 public int checkCallingAppIdentity(@NonNull String packageName, @NonNull String permission, 352 int pid, int uid) { 353 AppAuthenticatorResult result = checkCallingAppIdentityInternal(packageName, permission, 354 pid, uid); 355 if (result.getResultCode() != PERMISSION_GRANTED) { 356 Log.e(TAG, result.getResultMessage()); 357 } 358 return result.getResultCode(); 359 } 360 361 /** 362 * Checks the specified {@code packageName} has the expected signing identity for the 363 * provided {@code permission} under the calling {@code pid} and {@code uid}. 364 */ 365 // The pid variable may be used in a future release for platform verification; it is 366 // currently added to the public API and this method to seamlessly make use of any platform 367 // features in the future. 368 @SuppressWarnings("UnusedVariable") checkCallingAppIdentityInternal(String packageName, String permission, int pid, int uid)369 private AppAuthenticatorResult checkCallingAppIdentityInternal(String packageName, 370 String permission, 371 int pid, 372 int uid) { 373 // If a valid UID is provided, verify that the UID of the calling package matches the 374 // specified value. 375 if (uid != INVALID_UID) { 376 int packageUid; 377 try { 378 packageUid = mAppAuthenticatorUtils.getUidForPackage(packageName); 379 } catch (PackageManager.NameNotFoundException e) { 380 return AppAuthenticatorResult.create(PERMISSION_DENIED_UNKNOWN_PACKAGE, 381 "The app " + packageName + " was not found on the device"); 382 } 383 if (packageUid != uid) { 384 return AppAuthenticatorResult.create(PERMISSION_DENIED_PACKAGE_UID_MISMATCH, 385 "The expected UID, " + uid + ", of the app " + packageName 386 + " does not match the actual UID, " + packageUid); 387 } 388 } 389 if (mAppSignatureVerifier.verifySigningIdentity(packageName, permission)) { 390 return AppAuthenticatorResult.create(PERMISSION_GRANTED, null); 391 } 392 return AppAuthenticatorResult.create(PERMISSION_DENIED_NO_MATCH, "The signing" 393 + " identity of app " + packageName + " does not match the expected identity"); 394 } 395 396 397 /** 398 * Enforces the specified {@code packageName} has the expected signing identity as declared in 399 * the {@code <expected-identity>} tag. 400 * 401 * <p>This method should be used when an app's signing identity must be verified; for instance 402 * before a client connects to an exported service this method can be used to verify that the 403 * app comes from the expected developer. 404 * 405 * @param packageName the name of the package to be verified 406 * @throws SecurityException if the signing identity of the package does not match that defined 407 * in the {@code <expected-identity>} tag 408 */ enforceAppIdentity(@onNull String packageName)409 public void enforceAppIdentity(@NonNull String packageName) { 410 if (checkAppIdentity(packageName) != SIGNATURE_MATCH) { 411 throw new SecurityException("The app " + packageName + " does not match the expected " 412 + "signing identity"); 413 } 414 } 415 416 /** 417 * Checks the specified {@code packageName} has the expected signing identity as specified in 418 * the {@code <expected-identity>} tag. 419 * 420 * <p>This method should be used when an app's signing identity must be verified; for instance 421 * before a client connects to an exported service this method can be used to verify that the 422 * app comes from the expected developer. 423 * 424 * @param packageName the name of the package to be verified 425 * @return {@link #SIGNATURE_MATCH} if the specified package has the expected 426 * signing identity 427 */ 428 @AppIdentityResult checkAppIdentity(@onNull String packageName)429 public int checkAppIdentity(@NonNull String packageName) { 430 if (mAppSignatureVerifier.verifyExpectedIdentity(packageName)) { 431 return SIGNATURE_MATCH; 432 } 433 return SIGNATURE_NO_MATCH; 434 } 435 436 /** 437 * Creates a new {@code AppAuthenticator} that can be used to guard resources based on 438 * package name / signing identity as well as allow verification of expected signing identities 439 * before interacting with other apps on a device using the configuration defined in the 440 * provided {@code xmlInputStream}. 441 * 442 * @param context the context within which to create the {@code AppAuthenticator} 443 * @param xmlInputStream the XML {@link InputStream} containing the definitions for the 444 * permissions and expected identities based on packages / expected 445 * signing certificate digests 446 * @return a new {@code AppAuthenticator} that can be used to enforce the signing 447 * identities defined in the provided XML {@code InputStream} 448 * @throws AppAuthenticatorXmlException if the provided XML {@code InputStream} is not in the 449 * proper format to create a new {@code AppAuthenticator} 450 * @throws IOException if an IO error is encountered when attempting to read 451 * the XML {@code InputStream} 452 */ createFromInputStream(@onNull Context context, @NonNull InputStream xmlInputStream)453 public static @NonNull AppAuthenticator createFromInputStream(@NonNull Context context, 454 @NonNull InputStream xmlInputStream) throws AppAuthenticatorXmlException, IOException { 455 XmlPullParser parser; 456 try { 457 parser = XmlPullParserFactory.newInstance().newPullParser(); 458 parser.setInput(xmlInputStream, null); 459 } catch (XmlPullParserException e) { 460 throw new AppAuthenticatorXmlException("Unable to create parser from provided " 461 + "InputStream", e); 462 } 463 return createFromParser(context, parser); 464 } 465 466 /** 467 * Creates a new {@code AppAuthenticator} that can be used to guard resources based on 468 * package name / signing identity as well as allow verification of expected signing identities 469 * before interacting with other apps on a device using the configuration defined in the 470 * provided XML resource. 471 * 472 * @param context the context within which to create the {@code AppAuthenticator} 473 * @param xmlResource the ID of the XML resource containing the definitions for the 474 * permissions and expected identities based on package / expected signing 475 * certificate digests 476 * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities 477 * defined in the provided XML resource 478 * @throws AppAuthenticatorXmlException if the provided XML resource is not in the proper format 479 * to create a new {@code AppAuthenticator} 480 * @throws IOException if an IO error is encountered when attempting to read 481 * the XML resource 482 */ createFromResource(@onNull Context context, @XmlRes int xmlResource)483 public static @NonNull AppAuthenticator createFromResource(@NonNull Context context, 484 @XmlRes int xmlResource) throws AppAuthenticatorXmlException, IOException { 485 Resources resources = context.getResources(); 486 XmlPullParser parser = resources.getXml(xmlResource); 487 return createFromParser(context, parser); 488 } 489 490 /** 491 * Creates a new {@code AppAuthenticator} that can be used to guard resources based on 492 * package name / signing identity as well as allow verification of expected signing identities 493 * before interacting with other apps on a device using the configuration defined in the 494 * provided {@code parser}. 495 * 496 * @param context the context within which to create the {@code AppAuthenticator} 497 * @param parser an {@link XmlPullParser} containing the definitions for the 498 * permissions and expected identities based on package / expected signing 499 * certificate digests 500 * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities 501 * defined in the provided {@code XmlPullParser} 502 * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser} 503 * is not in the proper format to create a new 504 * {@code AppAuthenticator} 505 * @throws IOException if an IO error is encountered when attempting to read 506 * from the {@code XmlPullParser} 507 */ createFromParser(Context context, XmlPullParser parser)508 private static AppAuthenticator createFromParser(Context context, XmlPullParser parser) 509 throws AppAuthenticatorXmlException, IOException { 510 AppAuthenticatorConfig config = createConfigFromParser(parser); 511 return createFromConfig(context, config); 512 } 513 514 /** 515 * Creates a new {@code AppAuthenticator} that can be used to guard resources based on 516 * package name / signing identity as well as allow verification of expected signing identities 517 * before interacting with other apps on a device using the configuration defined in the 518 * provided {@code config}. 519 * 520 * @param context the context within which to create the {@code AppAuthenticator} 521 * @param config an {@link AppAuthenticatorConfig} containing the definitions for the 522 * permissions and expected identities based on package / expected signing 523 * certificate digests 524 * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities 525 * defined in the provided {@code config} 526 */ createFromConfig(Context context, @NonNull AppAuthenticatorConfig config)527 static AppAuthenticator createFromConfig(Context context, 528 @NonNull AppAuthenticatorConfig config) { 529 AppSignatureVerifier verifier = AppSignatureVerifier.builder(context) 530 .setPermissionAllowMap(config.getPermissionAllowMap()) 531 .setExpectedIdentities(config.getExpectedIdentities()) 532 .setDigestAlgorithm(config.getDigestAlgorithm()) 533 .build(); 534 return new AppAuthenticator(verifier, new AppAuthenticatorUtils(context)); 535 } 536 537 /** 538 * Creates a new {@code AppAuthenticatorConfig} that can be used to instantiate a new {@code 539 * AppAuthenticator} with the specified config. 540 * 541 * @param parser an {@link XmlPullParser} containing the definition for the permissions and 542 * expected identities based on package / expected signing certificate digests 543 * @return a new {@code AppAuthenticatorConfig} based on the config declared in the {@code 544 * parser} that can be used to instantiate a new {@code AppAuthenticator}. 545 * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser} 546 * is not in the proper format to create a new 547 * {@code AppAuthenticator} 548 * @throws IOException if an IO error is encountered when attempting to read 549 * from the {@code XmlPullParser} 550 */ createConfigFromParser(XmlPullParser parser)551 static AppAuthenticatorConfig createConfigFromParser(XmlPullParser parser) 552 throws AppAuthenticatorXmlException, IOException { 553 Map<String, Map<String, Set<String>>> permissionAllowMap = new ArrayMap<>(); 554 Map<String, Set<String>> expectedIdentities = new ArrayMap<>(); 555 try { 556 parseToNextStartTag(parser); 557 String tag = parser.getName(); 558 if (TextUtils.isEmpty(tag) || !tag.equalsIgnoreCase(ROOT_TAG)) { 559 throw new AppAuthenticatorXmlException( 560 "Provided XML does not contain the expected root tag: " + ROOT_TAG); 561 } 562 assertExpectedAttribute(parser, ROOT_TAG, null, false); 563 String digestAlgorithm = DEFAULT_DIGEST_ALGORITHM; 564 int eventType = parser.nextTag(); 565 // Each new start tag should be for a new permission / expected-identity. 566 while (eventType == XmlPullParser.START_TAG) { 567 tag = parser.getName(); 568 if (tag.equalsIgnoreCase(PERMISSION_TAG)) { 569 assertExpectedAttribute(parser, PERMISSION_TAG, NAME_ATTRIBUTE, true); 570 String permissionName = parser.getAttributeValue(null, NAME_ATTRIBUTE); 571 if (TextUtils.isEmpty(permissionName)) { 572 throw new AppAuthenticatorXmlException( 573 "The " + PERMISSION_TAG + " tag requires a non-empty value for the " 574 + NAME_ATTRIBUTE + " attribute"); 575 } 576 Map<String, Set<String>> allowedPackageCerts = parsePackages(parser, true); 577 if (permissionAllowMap.containsKey(permissionName)) { 578 permissionAllowMap.get(permissionName).putAll(allowedPackageCerts); 579 } else { 580 permissionAllowMap.put(permissionName, allowedPackageCerts); 581 } 582 } else if (tag.equalsIgnoreCase(EXPECTED_IDENTITY_TAG)) { 583 assertExpectedAttribute(parser, EXPECTED_IDENTITY_TAG, null, true); 584 expectedIdentities.putAll(parsePackages(parser, false)); 585 } else { 586 throw new AppAuthenticatorXmlException( 587 "Expected " + PERMISSION_TAG + " or " + EXPECTED_IDENTITY_TAG 588 + " under root tag at line " + parser.getLineNumber()); 589 } 590 eventType = parser.nextTag(); 591 } 592 return AppAuthenticatorConfig.create(permissionAllowMap, expectedIdentities, 593 digestAlgorithm); 594 } catch (XmlPullParserException e) { 595 throw new AppAuthenticatorXmlException("Caught an exception parsing the provided " 596 + "XML:", e); 597 } 598 } 599 600 /** 601 * Parses package tags from the provided {@code parser}, allowing the {@code all-packages} 602 * tag if the {@code allPackagesAllowed} boolean is true. 603 * 604 * @param parser the {@link XmlPullParser} from which to parser the packages 605 * @param allPackagesAllowed boolean indicating whether the {@code all-packages} element is 606 * allowed 607 * @return a mapping from the enclosed packages to signing identities 608 * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser} 609 * is not in the proper format 610 * @throws IOException if an IO error is encountered when attempting to read 611 * from the {@code XmlPullParser} 612 * @throws XmlPullParserException if any errors are encountered when attempting to 613 * parse the provided {@code XmlPullParser} 614 */ parsePackages(XmlPullParser parser, boolean allPackagesAllowed)615 private static Map<String, Set<String>> parsePackages(XmlPullParser parser, 616 boolean allPackagesAllowed) 617 throws AppAuthenticatorXmlException, IOException, XmlPullParserException { 618 Map<String, Set<String>> allowedPackageCerts = new ArrayMap<>(); 619 int eventType = parser.nextTag(); 620 while (eventType == XmlPullParser.START_TAG) { 621 String tag = parser.getName(); 622 String packageName; 623 if (tag.equalsIgnoreCase(PACKAGE_TAG)) { 624 assertExpectedAttribute(parser, PACKAGE_TAG, NAME_ATTRIBUTE, true); 625 packageName = parser.getAttributeValue(null, NAME_ATTRIBUTE); 626 if (TextUtils.isEmpty(packageName)) { 627 throw new AppAuthenticatorXmlException( 628 "The " + PACKAGE_TAG + " tag requires a non-empty value for the " 629 + NAME_ATTRIBUTE + " attribute"); 630 } 631 } else if (tag.equalsIgnoreCase(ALL_PACKAGES_TAG)) { 632 packageName = ALL_PACKAGES_TAG; 633 if (!allPackagesAllowed) { 634 throw new AppAuthenticatorXmlException("The " + ALL_PACKAGES_TAG 635 + " tag is not allowed within this element on line " 636 + parser.getLineNumber()); 637 } 638 } else { 639 throw new AppAuthenticatorXmlException( 640 "Unexpected tag " + tag + " on line " + parser.getLineNumber() 641 + "; expected " + PACKAGE_TAG + "" + (allPackagesAllowed ? " or " 642 + ALL_PACKAGES_TAG : "")); 643 } 644 Set<String> allowedCertDigests = parseCertDigests(parser); 645 if (allowedCertDigests.isEmpty()) { 646 throw new AppAuthenticatorXmlException("No " + CERT_DIGEST_TAG + " tag found " 647 + "within " + tag + " element on line " + parser.getLineNumber()); 648 } 649 if (allowedPackageCerts.containsKey(packageName)) { 650 allowedPackageCerts.get(packageName).addAll(allowedCertDigests); 651 } else { 652 allowedPackageCerts.put(packageName, allowedCertDigests); 653 } 654 eventType = parser.nextTag(); 655 } 656 return allowedPackageCerts; 657 } 658 659 /** 660 * Parses certificate digests from the provided {@code parser}, returning a {@link Set} of 661 * parsed digests. 662 * 663 * @param parser the {@link XmlPullParser} from which to parser the digests 664 * @return a {@code Set} of certificate digests 665 * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser} 666 * is not in the proper format 667 * @throws IOException if an IO error is encountered when attempting to read 668 * from the {@code XmlPullParser} 669 * @throws XmlPullParserException if any errors are encountered when attempting to 670 * parse the provided {@code XmlPullParser} 671 */ parseCertDigests(XmlPullParser parser)672 private static Set<String> parseCertDigests(XmlPullParser parser) 673 throws AppAuthenticatorXmlException, IOException, 674 XmlPullParserException { 675 Set<String> allowedCertDigests = new ArraySet<>(); 676 int eventType = parser.nextTag(); 677 while (eventType == XmlPullParser.START_TAG) { 678 String tag = parser.getName(); 679 if (!tag.equalsIgnoreCase(CERT_DIGEST_TAG)) { 680 throw new AppAuthenticatorXmlException( 681 "Expected " + CERT_DIGEST_TAG + " on line " + parser.getLineNumber()); 682 } 683 String digest = parser.nextText().trim(); 684 if (TextUtils.isEmpty(digest)) { 685 throw new AppAuthenticatorXmlException("The " + CERT_DIGEST_TAG + " element " 686 + "on line " + parser.getLineNumber() + " must have non-empty text " 687 + "containing the certificate digest of the signer"); 688 } 689 allowedCertDigests.add(normalizeCertDigest(digest)); 690 eventType = parser.nextTag(); 691 } 692 return allowedCertDigests; 693 } 694 695 /** 696 * Normalizes the provided {@code certDigest} to ensure it is in the proper form for {@code 697 * Collection} membership checks when comparing a package's signing certificate digest against 698 * those provided to the {@code AppAuthenticator}. 699 * 700 * @param certDigest the digest to be normalized 701 * @return a normalized form of the provided digest that can be used in subsequent {@code 702 * Collection} membership checks 703 */ normalizeCertDigest(String certDigest)704 static String normalizeCertDigest(String certDigest) { 705 // The AppAuthenticatorUtils#computeDigest method uses lower case characters to compute the 706 // digest. 707 return certDigest.toLowerCase(Locale.US); 708 } 709 710 /** 711 * Moves the provided {@code parser} to the next {@link XmlPullParser#START_TAG} or {@link 712 * XmlPullParser#END_DOCUMENT} if the end of the document is reached, returning the value of 713 * the event type. 714 */ parseToNextStartTag(XmlPullParser parser)715 private static int parseToNextStartTag(XmlPullParser parser) throws IOException, 716 XmlPullParserException { 717 int type; 718 while ((type = parser.next()) != XmlPullParser.START_TAG 719 && type != XmlPullParser.END_DOCUMENT) { 720 // Empty loop to reach the first start tag or end of the document. 721 } 722 return type; 723 } 724 725 /** 726 * Asserts the current {@code tagName} contains only the specified {@code expectedAttribute}, 727 * or no elements if not {@code required}; a null {@code expectedAttribute} can be used to 728 * assert no attributes are provided. 729 * 730 * <p>This method is intended to report if unsupported attributes are specified to warn the 731 * caller that the provided value will not be used by this instance. Since this method is 732 * checking the attributes it must only be called when the current event type is {@link 733 * XmlPullParser#START_TAG}. 734 */ assertExpectedAttribute(XmlPullParser parser, String tagName, String expectedAttribute, boolean required)735 private static void assertExpectedAttribute(XmlPullParser parser, String tagName, 736 String expectedAttribute, boolean required) 737 throws AppAuthenticatorXmlException, XmlPullParserException { 738 int attributeCount = parser.getAttributeCount(); 739 if (attributeCount == -1) { 740 throw new AssertionError( 741 "parser#getAttributeCount called for event type " + parser.getEventType() 742 + " on line " + parser.getLineNumber()); 743 } 744 if (attributeCount == 0 && expectedAttribute != null && required) { 745 throw new AppAuthenticatorXmlException("The attribute " + expectedAttribute + " is " 746 + "required for tag " + tagName + " on line " + parser.getLineNumber()); 747 } 748 StringBuilder unsupportedAttributes = null; 749 for (int i = 0; i < attributeCount; i++) { 750 String attributeName = parser.getAttributeName(i); 751 if (!attributeName.equalsIgnoreCase(expectedAttribute)) { 752 if (unsupportedAttributes == null) { 753 unsupportedAttributes = new StringBuilder(); 754 } else { 755 unsupportedAttributes.append(", "); 756 } 757 unsupportedAttributes.append(attributeName); 758 } 759 } 760 if (unsupportedAttributes != null) { 761 String prefixMessage; 762 if (expectedAttribute == null) { 763 prefixMessage = "Tag " + tagName + " does not support any attributes"; 764 } else { 765 prefixMessage = "Tag " + tagName + " only supports attribute " + expectedAttribute; 766 } 767 throw new AppAuthenticatorXmlException( 768 prefixMessage + "; found the following unsupported attributes on line " 769 + parser.getLineNumber() + ": " + unsupportedAttributes); 770 } 771 } 772 773 /** 774 * Value class containing the configuration for an {@code AppAuthenticator}. 775 */ 776 // Suppressing the AutoValue immutable field warning as this class is only used internally 777 // and is not worth bringing in the dependency for the immutable classes. 778 @SuppressWarnings("AutoValueImmutableFields") 779 @AutoValue 780 abstract static class AppAuthenticatorConfig { 781 /** 782 * Returns a mapping from permission to allowed packages / signing identities. 783 */ getPermissionAllowMap()784 abstract Map<String, Map<String, Set<String>>> getPermissionAllowMap(); 785 786 /** 787 * Returns a mapping from package name to expected signing identities. 788 */ getExpectedIdentities()789 abstract Map<String, Set<String>> getExpectedIdentities(); 790 791 /** 792 * Returns the digest algorithm to be used. 793 */ getDigestAlgorithm()794 abstract String getDigestAlgorithm(); 795 796 /** 797 * Creates a new instance with the provided {@code permissionAllowMap}, {@code 798 * expectedIdentities}, and {@code digestAlgorithm}. 799 * 800 * @param permissionAllowMap the mapping from permission to allowed packages / signing 801 * identities 802 * @param expectedIdentities the mapping from package name to expected signing identities 803 * @param digestAlgorithm the digest algorithm to be used when computing signing 804 * certificate digests 805 * @return a new {@code AppAuthenticatorConfig} that can be used to configure the 806 * AppAuthenticator instance. 807 */ create( Map<String, Map<String, Set<String>>> permissionAllowMap, Map<String, Set<String>> expectedIdentities, String digestAlgorithm)808 static AppAuthenticatorConfig create( 809 Map<String, Map<String, Set<String>>> permissionAllowMap, 810 Map<String, Set<String>> expectedIdentities, String digestAlgorithm) { 811 return new AutoValue_AppAuthenticator_AppAuthenticatorConfig(permissionAllowMap, 812 expectedIdentities, digestAlgorithm); 813 814 } 815 } 816 817 /** 818 * Value class for the result of an {@code AppAuthenticator} query. 819 */ 820 @AutoValue 821 abstract static class AppAuthenticatorResult { 822 /** 823 * Returns the result code for the query. 824 */ getResultCode()825 abstract int getResultCode(); 826 827 /** 828 * Returns the result message for the query; if the query successfully verified an app's 829 * signature matches the expected signing identity this value will be {@code null}. 830 */ getResultMessage()831 abstract @Nullable String getResultMessage(); 832 833 /** 834 * Creates a new instance with the provided {@code resultCode} and {@code resultMessage}. 835 */ create(int resultCode, String resultMessage)836 static AppAuthenticatorResult create(int resultCode, String resultMessage) { 837 return new AutoValue_AppAuthenticator_AppAuthenticatorResult(resultCode, resultMessage); 838 } 839 } 840 } 841