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 android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.Signature; 24 import android.os.Build; 25 import android.util.Log; 26 27 import androidx.annotation.RequiresApi; 28 import androidx.collection.ArrayMap; 29 import androidx.collection.LruCache; 30 31 import com.google.auto.value.AutoValue; 32 33 import org.jspecify.annotations.NonNull; 34 import org.jspecify.annotations.Nullable; 35 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Set; 41 42 /** 43 * Provides methods to verify the signatures of apps based on the expected signing identities 44 * provided to the {@link Builder#setPermissionAllowMap(Map)} and {@link 45 * Builder#setExpectedIdentities(Map)} builder methods. 46 */ 47 class AppSignatureVerifier { 48 private static final String TAG = AppSignatureVerifier.class.getSimpleName(); 49 private static final String EXPECTED_IDENTITY_QUERY = "expected-identity"; 50 private static final int DEFAULT_CACHE_SIZE = 16; 51 52 private final PackageManager mPackageManager; 53 private final String mDigestAlgorithm; 54 private final Cache mCache; 55 /** 56 * A mapping from permission to allowed packages / signing identities. 57 */ 58 private final Map<String, Map<String, Set<String>>> mPermissionAllowMap; 59 /** 60 * A mapping from package name to expected signing identities. 61 */ 62 private final Map<String, Set<String>> mExpectedIdentities; 63 64 /** 65 * Private constructor; instances should be instantiated through a {@link Builder} obtained 66 * with {@link #builder}. 67 */ AppSignatureVerifier(Context context, Map<String, Map<String, Set<String>>> permissionAllowMap, Map<String, Set<String>> expectedIdentities, String digestAlgorithm, Cache cache)68 AppSignatureVerifier(Context context, 69 Map<String, Map<String, Set<String>>> permissionAllowMap, 70 Map<String, Set<String>> expectedIdentities, 71 String digestAlgorithm, 72 Cache cache) { 73 mPackageManager = context.getPackageManager(); 74 mPermissionAllowMap = permissionAllowMap; 75 mExpectedIdentities = expectedIdentities; 76 mDigestAlgorithm = digestAlgorithm; 77 mCache = cache; 78 } 79 80 /** 81 * Returns a new {@link Builder} that can be used to instantiate a new {@code 82 * AppSignatureVerifier}. 83 */ builder(Context context)84 static Builder builder(Context context) { 85 return new Builder(context); 86 } 87 88 /** 89 * Provides methods to configure a new {@code AppSignatureVerifier} instance. 90 */ 91 static class Builder { 92 private final Context mContext; 93 private String mDigestAlgorithm; 94 private Cache mCache; 95 private Map<String, Map<String, Set<String>>> mPermissionAllowMap; 96 private Map<String, Set<String>> mExpectedIdentities; 97 98 /** 99 * Constructor accepting the {@code context} used to instantiate a new {@code 100 * AppSignatureVerifier}. 101 */ Builder(Context context)102 Builder(Context context) { 103 mContext = context; 104 } 105 106 /** 107 * Sets the {@code digestAlgorithm} to be used by the {@code AppSignatureVerifier}; all 108 * signing identities provided to {@link #setPermissionAllowMap} and 109 * {@link #setExpectedIdentities} must be computed using this same {@code digestAlgorithm}. 110 */ setDigestAlgorithm(String digestAlgorithm)111 Builder setDigestAlgorithm(String digestAlgorithm) { 112 mDigestAlgorithm = digestAlgorithm; 113 return this; 114 } 115 116 /** 117 * Sets the {@code cache} to be used by the {@code AppSignatureVerifier}. 118 */ setCache(Cache cache)119 Builder setCache(Cache cache) { 120 mCache = cache; 121 return this; 122 } 123 124 /** 125 * Sets the {@code permissionAllowMap} to be used by the {@code AppSignatureVerifier}. 126 * 127 * This {@code Map} should contain a mapping from permission names to a mapping of package 128 * names to expected signing identities; each permission can also contain a mapping to 129 * the {@link AppAuthenticator#ALL_PACKAGES_TAG} which allow signing identities to be 130 * specified without knowing the exact packages that will be signed by them. 131 */ setPermissionAllowMap(Map<String, Map<String, Set<String>>> permissionAllowMap)132 Builder setPermissionAllowMap(Map<String, Map<String, Set<String>>> permissionAllowMap) { 133 mPermissionAllowMap = permissionAllowMap; 134 return this; 135 } 136 137 /** 138 * Sets the {@code expectedIdentities} to be used by the {@code AppSignatureVerifier}. 139 * 140 * This {@code Map} should contain a mapping from package name to the expected signing 141 * certificate digest(s). 142 */ setExpectedIdentities(Map<String, Set<String>> expectedIdentities)143 Builder setExpectedIdentities(Map<String, Set<String>> expectedIdentities) { 144 mExpectedIdentities = expectedIdentities; 145 return this; 146 } 147 148 /** 149 * Builds a new {@code AppSignatureVerifier} instance using the provided configuration. 150 */ build()151 AppSignatureVerifier build() { 152 if (mPermissionAllowMap == null) { 153 mPermissionAllowMap = new ArrayMap<>(); 154 } 155 if (mExpectedIdentities == null) { 156 mExpectedIdentities = new ArrayMap<>(); 157 } 158 if (mDigestAlgorithm == null) { 159 mDigestAlgorithm = AppAuthenticator.DEFAULT_DIGEST_ALGORITHM; 160 } 161 if (mCache == null) { 162 mCache = new Cache(DEFAULT_CACHE_SIZE); 163 } 164 return new AppSignatureVerifier(mContext, mPermissionAllowMap, mExpectedIdentities, 165 mDigestAlgorithm, mCache); 166 } 167 } 168 169 /** 170 * Verifies the signing identity of the provided {@code packageName} for the specified {@code 171 * permission}, returning {@code true} if the signing identity matches that declared under 172 * the {@code permission} for the {@code package} as specified to {@link 173 * Builder#setPermissionAllowMap}. 174 */ verifySigningIdentity(String packageName, String permission)175 boolean verifySigningIdentity(String packageName, String permission) { 176 // If there are no declared expected certificate digests for the specified package or 177 // all-packages under the permission then return immediately. 178 Map<String, Set<String>> allowedCertDigests = mPermissionAllowMap.get(permission); 179 if (allowedCertDigests == null) { 180 Log.d(TAG, "No expected signing identities declared for permission " + permission); 181 return false; 182 } 183 Set<String> packageCertDigests = allowedCertDigests.get(packageName); 184 Set<String> allPackagesCertDigests = 185 allowedCertDigests.get(AppAuthenticator.ALL_PACKAGES_TAG); 186 if (packageCertDigests == null && allPackagesCertDigests == null) { 187 return false; 188 } 189 return verifySigningIdentityForQuery(packageName, permission, packageCertDigests, 190 allPackagesCertDigests); 191 } 192 193 /** 194 * Verifies the signing identity of the provided {@code packageName} against the expected 195 * signing identity set through {@link Builder#setExpectedIdentities(Map)}. 196 */ verifyExpectedIdentity(String packageName)197 boolean verifyExpectedIdentity(String packageName) { 198 Set<String> packageCertDigests = mExpectedIdentities.get(packageName); 199 if (packageCertDigests == null) { 200 return false; 201 } 202 return verifySigningIdentityForQuery(packageName, EXPECTED_IDENTITY_QUERY, 203 packageCertDigests, null); 204 } 205 206 /** 207 * Verifies the signing identity of the provided {@code packageName} based on the provided 208 * {@code query} against the expected {@code packageCertDigests}, and where applicable the 209 * {@code allPackageCertDigests}. 210 * 211 * The {@code query} can either be a permission or {@code EXPECTED_IDENTITY_QUERY} when 212 * verifying the identity of another app before establishing communication. 213 */ verifySigningIdentityForQuery(String packageName, String query, Set<String> packageCertDigests, Set<String> allPackagesCertDigests)214 boolean verifySigningIdentityForQuery(String packageName, String query, 215 Set<String> packageCertDigests, Set<String> allPackagesCertDigests) { 216 AppSigningInfo appSigningInfo; 217 try { 218 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 219 appSigningInfo = Api28Impl.getAppSigningInfo(mPackageManager, packageName); 220 } else { 221 appSigningInfo = DefaultImpl.getAppSigningInfo(mPackageManager, packageName); 222 } 223 } catch (AppSignatureVerifierException e) { 224 Log.e(TAG, "Caught an exception obtaining signing info for package " + packageName, e); 225 return false; 226 } 227 // If a previous verification result exists for this package and query, and the package 228 // has not yet been updated, then use the result from the previous verification. An app's 229 // signing identity can only be changed on an update which should result in an update of 230 // the last update time. 231 CacheEntry cacheEntry = mCache.get(packageName, query); 232 if (cacheEntry != null 233 && cacheEntry.getLastUpdateTime() == appSigningInfo.getLastUpdateTime()) { 234 return cacheEntry.getVerificationResult(); 235 } 236 boolean verificationResult; 237 // API levels >= 28 support obtaining the signing lineage of a package after a key 238 // rotation; if the signing lineage is available then verify each entry in the lineage 239 // against the expected signing identities. 240 if (appSigningInfo.getSigningLineage() != null) { 241 verificationResult = verifySigningLineage(appSigningInfo.getSigningLineage(), 242 packageCertDigests, allPackagesCertDigests); 243 } else { 244 verificationResult = verifyCurrentSigners(appSigningInfo.getCurrentSignatures(), 245 packageCertDigests, allPackagesCertDigests); 246 } 247 mCache.put(packageName, query, CacheEntry.create(verificationResult, 248 appSigningInfo.getLastUpdateTime())); 249 return verificationResult; 250 } 251 252 /** 253 * Verifies the provided {@code signatures} signing lineage against the expected signing 254 * identities in the {@code packageCertDigests} and {@code allPackagesCertDigests}. 255 * 256 * <p>A signing identity is successfully verified if any of the signatures in the lineage 257 * matches any of the expected signing certificate digest declarations in the provided {@code 258 * Map}s. 259 */ verifySigningLineage(List<Signature> signatures, Set<String> packageCertDigests, Set<String> allPackagesCertDigests)260 private boolean verifySigningLineage(List<Signature> signatures, Set<String> packageCertDigests, 261 Set<String> allPackagesCertDigests) { 262 for (Signature signature : signatures) { 263 String signatureDigest = AppAuthenticatorUtils.computeDigest(mDigestAlgorithm, 264 signature.toByteArray()); 265 if (packageCertDigests != null && packageCertDigests.contains(signatureDigest)) { 266 return true; 267 } 268 if (allPackagesCertDigests != null 269 && allPackagesCertDigests.contains(signatureDigest)) { 270 return true; 271 } 272 } 273 return false; 274 } 275 276 /** 277 * Verifies the provided current {@code signatures} against the expected signing identities 278 * in the {@code packageCertDigests} and {@code allPackagesCertDigests}. 279 * 280 * <p>A signing identity is successfully verified if all of the current signers are 281 * declared in either of the expected signing certificate digest {@code Map}s. 282 */ verifyCurrentSigners(List<Signature> signatures, Set<String> packageCertDigests, Set<String> allPackagesCertDigests)283 boolean verifyCurrentSigners(List<Signature> signatures, Set<String> packageCertDigests, 284 Set<String> allPackagesCertDigests) { 285 List<String> signatureDigests = new ArrayList<>(signatures.size()); 286 for (Signature signature : signatures) { 287 signatureDigests.add(AppAuthenticatorUtils.computeDigest(mDigestAlgorithm, 288 signature.toByteArray())); 289 } 290 if (packageCertDigests != null && packageCertDigests.containsAll(signatureDigests)) { 291 return true; 292 } 293 return allPackagesCertDigests != null 294 && allPackagesCertDigests.containsAll(signatureDigests); 295 } 296 297 /** 298 * Provides a method to support package signature queries for API levels >= 28. Starting at 299 * API level 28 the platform added support for app signing key rotation, so apps signed by a 300 * single signer can include the entire signing lineage from 301 * {@link AppSigningInfo#getSigningLineage()}. 302 */ 303 @RequiresApi(28) 304 private static class Api28Impl { Api28Impl()305 private Api28Impl() { 306 } 307 308 /** 309 * Returns the {@link AppSigningInfo} for the specified {@code packageName} using the 310 * provided {@code packageManager}, including full signing lineage for apps signed by a 311 * single signer. 312 * 313 * @throws AppSignatureVerifierException if the specified package is not found, or if the 314 * {@code SigningInfo} is not returned for the package. 315 */ 316 @SuppressWarnings("deprecation") getAppSigningInfo(PackageManager packageManager, String packageName)317 static AppSigningInfo getAppSigningInfo(PackageManager packageManager, 318 String packageName) throws AppSignatureVerifierException { 319 PackageInfo packageInfo; 320 try { 321 packageInfo = packageManager.getPackageInfo(packageName, 322 PackageManager.GET_SIGNING_CERTIFICATES); 323 } catch (PackageManager.NameNotFoundException e) { 324 throw new AppSignatureVerifierException("Package " + packageName + " not found", e); 325 } 326 if (packageInfo.signingInfo == null) { 327 throw new AppSignatureVerifierException( 328 "No SigningInfo returned for package " + packageName); 329 } 330 return AppSigningInfo.create(packageName, 331 packageInfo.signingInfo.getApkContentsSigners(), 332 packageInfo.signingInfo.getSigningCertificateHistory(), 333 packageInfo.lastUpdateTime); 334 } 335 } 336 337 /** 338 * Provides a method to support package signature queries for API levels < 28. Prior to API 339 * level 28 the platform only supported returning an app's original signature(s), and an app 340 * signed with a rotated signing key will still return the original signing key when queried 341 * with the {@link PackageManager#GET_SIGNATURES} flag. 342 */ 343 private static class DefaultImpl { DefaultImpl()344 private DefaultImpl() { 345 } 346 347 /** 348 * Returns the {@link AppSigningInfo} for the specified {@code packageName} using the 349 * provided {@code packageManager}, containing only the original / current signer for the 350 * package. 351 * 352 * @throws AppSignatureVerifierException if the specified package is not found, or if the 353 * {@code signatures} are not returned for the package. 354 */ 355 // Suppress the deprecation and GetSignatures warnings for the GET_SIGNATURES flag and the 356 // use of PackageInfo.Signatures since this method is intended for API levels < 28 which 357 // only support these. 358 @SuppressWarnings("deprecation") 359 @SuppressLint("PackageManagerGetSignatures") getAppSigningInfo(PackageManager packageManager, String packageName)360 static AppSigningInfo getAppSigningInfo(PackageManager packageManager, 361 String packageName) throws AppSignatureVerifierException { 362 PackageInfo packageInfo; 363 try { 364 packageInfo = packageManager.getPackageInfo(packageName, 365 PackageManager.GET_SIGNATURES); 366 } catch (PackageManager.NameNotFoundException e) { 367 throw new AppSignatureVerifierException("Package " + packageName + " not found", e); 368 } 369 if (packageInfo.signatures == null) { 370 throw new AppSignatureVerifierException( 371 "No signatures returned for package " + packageName); 372 } 373 // When using the GET_SIGNATURES flag to obtain the app's signing info only the 374 // current signers are returned, so set the lineage to null in the AppSigningInfo. 375 return AppSigningInfo.create(packageName, packageInfo.signatures, null, 376 packageInfo.lastUpdateTime); 377 } 378 } 379 380 /** 381 * Cache containing previous signing identity version results stored by package name and 382 * query where the query is either the permission name or the {@code EXPECTED_IDENTITY_QUERY}. 383 */ 384 static class Cache extends LruCache<String, CacheEntry> { 385 /** 386 * Constructs a new {@code Cache} with the provided {@code maxSize}. 387 */ Cache(int maxSize)388 Cache(int maxSize) { 389 super(maxSize); 390 } 391 392 /** 393 * Returns the {@link CacheEntry} in the cache for the specified {@code packageName} and 394 * {@code query}. 395 */ get(String packageName, String query)396 CacheEntry get(String packageName, String query) { 397 return get(packageName + query); 398 } 399 400 /** 401 * Puts the provided {@code cacheEntry} in the cache for the specified {@code packageName} 402 * and {@code query}. 403 */ put(String packageName, String query, CacheEntry cacheEntry)404 void put(String packageName, String query, CacheEntry cacheEntry) { 405 put(packageName + query, cacheEntry); 406 } 407 } 408 409 /** 410 * Value class containing the verification result and the last update time for an entry in 411 * the {@link Cache}. 412 */ 413 @AutoValue 414 abstract static class CacheEntry { getVerificationResult()415 abstract boolean getVerificationResult(); getLastUpdateTime()416 abstract long getLastUpdateTime(); 417 418 /** 419 * Creates a new instance with the provided {@code verificationResult} and {@code 420 * lastUpdateTime}. 421 */ create(boolean verificationResult, long lastUpdateTime)422 static CacheEntry create(boolean verificationResult, long lastUpdateTime) { 423 return new AutoValue_AppSignatureVerifier_CacheEntry(verificationResult, 424 lastUpdateTime); 425 } 426 } 427 428 /** 429 * Value class containing generic signing info for a package. 430 */ 431 // Suppressing the AutoValue immutable field warning as this class is only used internally 432 // and is not worth bringing in the dependency for an ImmutableList. 433 @SuppressWarnings("AutoValueImmutableFields") 434 @AutoValue 435 abstract static class AppSigningInfo { getPackageName()436 abstract String getPackageName(); getCurrentSignatures()437 abstract List<Signature> getCurrentSignatures(); getSigningLineage()438 abstract @Nullable List<Signature> getSigningLineage(); getLastUpdateTime()439 abstract long getLastUpdateTime(); 440 441 /** 442 * Creates a new instance with the provided {@code packageName}, {@code currentSignatures}, 443 * {@code signingLineage}, and {@code lastUpdateTime}. 444 * 445 * <p>Note, the {@code signingLineage} can be null as this was not available prior to API 446 * level 28, but a non-null value must be specified for the {@code currentSignatures}. 447 */ create(@onNull String packageName, Signature @NonNull [] currentSignatures, Signature[] signingLineage, long lastUpdateTime)448 static AppSigningInfo create(@NonNull String packageName, 449 Signature @NonNull [] currentSignatures, Signature[] signingLineage, 450 long lastUpdateTime) { 451 return new AutoValue_AppSignatureVerifier_AppSigningInfo(packageName, 452 Arrays.asList(currentSignatures), 453 signingLineage != null ? Arrays.asList(signingLineage) : null, 454 lastUpdateTime); 455 } 456 } 457 458 /** 459 * This {@code Exception} is thrown when an unexpected error is encountered when querying for 460 * or verifying package signing identities. 461 */ 462 private static class AppSignatureVerifierException extends Exception { AppSignatureVerifierException(@onNull String message)463 AppSignatureVerifierException(@NonNull String message) { 464 super(message); 465 } 466 AppSignatureVerifierException(@onNull String message, @NonNull Throwable cause)467 AppSignatureVerifierException(@NonNull String message, @NonNull Throwable cause) { 468 super(message, cause); 469 } 470 } 471 } 472