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