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