From 6e3677c0c3a47b9e3347fac4e69fe4c94644c37a Mon Sep 17 00:00:00 2001 From: Song Pan Date: Tue, 29 Oct 2019 14:19:26 +0000 Subject: [PATCH] Implement methods in AppIntegrityManagerServiceImpl. 1. Pushing rules. 2. Getting version/rule provider. 3. Handle integrity verification request Bug:143689885 Test: atest AppIntegrityManagerServiceImplTest Change-Id: I0f2d554efbd3ba115eab10238908c9c2a3f2139c --- .../android/app/SystemServiceRegistry.java | 10 + .../content/integrity/AppInstallMetadata.java | 13 + .../integrity/AppIntegrityManagerService.java | 4 +- .../AppIntegrityManagerServiceImpl.java | 445 +++++++++++++++++- .../integrity/IntegrityFileManager.java | 169 +++++++ .../engine/RuleEvaluationEngine.java | 20 +- .../server/integrity/model/RuleMetadata.java | 41 ++ .../integrity/parser/RuleMetadataParser.java | 67 +++ .../serializer/RuleMetadataSerializer.java | 52 ++ .../test.apk | Bin 0 -> 35076 bytes .../AppIntegrityManagerServiceImplTest.java | 342 +++++++++++++- 11 files changed, 1136 insertions(+), 27 deletions(-) create mode 100644 services/core/java/com/android/server/integrity/IntegrityFileManager.java create mode 100644 services/core/java/com/android/server/integrity/model/RuleMetadata.java create mode 100644 services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java create mode 100644 services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java create mode 100644 services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/test.apk diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 69c37ec42bd08..b5e7f4151eaac 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -50,6 +50,8 @@ import android.content.ContentCaptureOptions; import android.content.Context; import android.content.IRestrictionsManager; import android.content.RestrictionsManager; +import android.content.integrity.AppIntegrityManager; +import android.content.integrity.IAppIntegrityManager; import android.content.om.IOverlayManager; import android.content.om.OverlayManager; import android.content.pm.CrossProfileApps; @@ -1246,6 +1248,14 @@ public final class SystemServiceRegistry { IIncrementalManagerNative.Stub.asInterface(b)); }}); //CHECKSTYLE:ON IndentationCheck + registerService(Context.APP_INTEGRITY_SERVICE, AppIntegrityManager.class, + new CachedServiceFetcher() { + @Override + public AppIntegrityManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow(Context.APP_INTEGRITY_SERVICE); + return new AppIntegrityManager(IAppIntegrityManager.Stub.asInterface(b)); + }}); sInitializing = true; try { diff --git a/core/java/android/content/integrity/AppInstallMetadata.java b/core/java/android/content/integrity/AppInstallMetadata.java index c9634758f63f2..70776c76531bc 100644 --- a/core/java/android/content/integrity/AppInstallMetadata.java +++ b/core/java/android/content/integrity/AppInstallMetadata.java @@ -86,6 +86,19 @@ public final class AppInstallMetadata { return mIsPreInstalled; } + @Override + public String toString() { + return String.format( + "AppInstallMetadata { PackageName = %s, AppCert = %s, InstallerName = %s," + + " InstallerCert = %s, VersionCode = %d, PreInstalled = %b }", + mPackageName, + mAppCertificate, + mInstallerName == null ? "null" : mInstallerName, + mInstallerCertificate == null ? "null" : mInstallerCertificate, + mVersionCode, + mIsPreInstalled); + } + /** Builder class for constructing {@link AppInstallMetadata} objects. */ public static final class Builder { private String mPackageName; diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerService.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerService.java index 005fb696b0891..3762ebb0b9022 100644 --- a/services/core/java/com/android/server/integrity/AppIntegrityManagerService.java +++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerService.java @@ -37,7 +37,7 @@ public class AppIntegrityManagerService extends SystemService { @Override public void onStart() { - mService = new AppIntegrityManagerServiceImpl(mContext); - // TODO: define and publish a binder service. + mService = AppIntegrityManagerServiceImpl.create(mContext); + publishBinderService(Context.APP_INTEGRITY_SERVICE, mService); } } diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java index 5c4479a1a2fe6..4d976838361e2 100644 --- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java +++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java @@ -17,38 +17,98 @@ package com.android.server.integrity; import static android.content.Intent.ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION; +import static android.content.Intent.EXTRA_ORIGINATING_UID; +import static android.content.Intent.EXTRA_PACKAGE_NAME; +import static android.content.Intent.EXTRA_VERSION_CODE; +import static android.content.integrity.AppIntegrityManager.EXTRA_STATUS; +import static android.content.integrity.AppIntegrityManager.STATUS_FAILURE; +import static android.content.integrity.AppIntegrityManager.STATUS_SUCCESS; import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID; +import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.integrity.AppInstallMetadata; +import android.content.integrity.IAppIntegrityManager; +import android.content.integrity.Rule; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.content.pm.Signature; +import android.net.Uri; +import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; +import android.os.RemoteException; import android.util.Slog; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; +import com.android.server.integrity.engine.RuleEvaluationEngine; +import com.android.server.integrity.model.IntegrityCheckResult; +import com.android.server.integrity.model.RuleMetadata; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; /** Implementation of {@link AppIntegrityManagerService}. */ -class AppIntegrityManagerServiceImpl { +public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { private static final String TAG = "AppIntegrityManagerServiceImpl"; private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; + private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); + private static final String PACKAGE_INSTALLER = "com.google.android.packageinstaller"; + private static final String BASE_APK_FILE = "base.apk"; + private static final String ADB_INSTALLER = "adb"; + private static final String UNKNOWN_INSTALLER = ""; + private static final String INSTALLER_CERT_NOT_APPLICABLE = ""; + + // Access to files inside mRulesDir is protected by mRulesLock; private final Context mContext; private final Handler mHandler; private final PackageManagerInternal mPackageManagerInternal; + private final RuleEvaluationEngine mEvaluationEngine; + private final IntegrityFileManager mIntegrityFileManager; - AppIntegrityManagerServiceImpl(Context context) { - mContext = context; - + /** Create an instance of {@link AppIntegrityManagerServiceImpl}. */ + public static AppIntegrityManagerServiceImpl create(Context context) { HandlerThread handlerThread = new HandlerThread("AppIntegrityManagerServiceHandler"); handlerThread.start(); - mHandler = handlerThread.getThreadHandler(); - mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + return new AppIntegrityManagerServiceImpl( + context, + LocalServices.getService(PackageManagerInternal.class), + RuleEvaluationEngine.getRuleEvaluationEngine(), + IntegrityFileManager.getInstance(), + handlerThread.getThreadHandler()); + } + + @VisibleForTesting + AppIntegrityManagerServiceImpl( + Context context, + PackageManagerInternal packageManagerInternal, + RuleEvaluationEngine evaluationEngine, + IntegrityFileManager integrityFileManager, + Handler handler) { + mContext = context; + mPackageManagerInternal = packageManagerInternal; + mEvaluationEngine = evaluationEngine; + mIntegrityFileManager = integrityFileManager; + mHandler = handler; IntentFilter integrityVerificationFilter = new IntentFilter(); integrityVerificationFilter.addAction(ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION); @@ -74,14 +134,371 @@ class AppIntegrityManagerServiceImpl { mHandler); } - // protected broadcasts cannot be sent in the test. - @VisibleForTesting - void handleIntegrityVerification(Intent intent) { + @Override + public void updateRuleSet( + String version, ParceledListSlice rules, IntentSender statusReceiver) + throws RemoteException { + String ruleProvider = getCallerPackageNameOrThrow(); + + mHandler.post( + () -> { + boolean success = true; + try { + mIntegrityFileManager.writeRules(version, ruleProvider, rules.getList()); + } catch (Exception e) { + Slog.e(TAG, "Error writing rules.", e); + success = false; + } + + Intent intent = new Intent(); + intent.putExtra(EXTRA_STATUS, success ? STATUS_SUCCESS : STATUS_FAILURE); + try { + statusReceiver.sendIntent( + mContext, + /* code= */ 0, + intent, + /* onFinished= */ null, + /* handler= */ null); + } catch (IntentSender.SendIntentException e) { + Slog.e(TAG, "Error sending status feedback.", e); + } + }); + } + + @Override + public String getCurrentRuleSetVersion() throws RemoteException { + getCallerPackageNameOrThrow(); + + RuleMetadata ruleMetadata = mIntegrityFileManager.readMetadata(); + return (ruleMetadata != null && ruleMetadata.getVersion() != null) + ? ruleMetadata.getVersion() + : ""; + } + + @Override + public String getCurrentRuleSetProvider() throws RemoteException { + getCallerPackageNameOrThrow(); + + RuleMetadata ruleMetadata = mIntegrityFileManager.readMetadata(); + return (ruleMetadata != null && ruleMetadata.getRuleProvider() != null) + ? ruleMetadata.getRuleProvider() + : ""; + } + + private void handleIntegrityVerification(Intent intent) { int verificationId = intent.getIntExtra(EXTRA_VERIFICATION_ID, -1); - // TODO: implement this method. - Slog.i(TAG, "Received integrity verification intent " + intent.toString()); - Slog.i(TAG, "Extras " + intent.getExtras()); - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); + try { + Slog.i(TAG, "Received integrity verification intent " + intent.toString()); + Slog.i(TAG, "Extras " + intent.getExtras()); + + AppInstallMetadata.Builder builder = new AppInstallMetadata.Builder(); + + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + String installerPackageName = getInstallerPackageName(intent); + String appCert = getAppCertificateFingerprint(intent.getData()); + + builder.setPackageName(getPackageNameNormalized(packageName)); + builder.setAppCertificate(appCert == null ? "" : appCert); + builder.setVersionCode(intent.getIntExtra(EXTRA_VERSION_CODE, -1)); + builder.setInstallerName(getPackageNameNormalized(installerPackageName)); + builder.setInstallerCertificate( + getInstallerCertificateFingerprint(installerPackageName)); + builder.setIsPreInstalled(isSystemApp(packageName)); + + AppInstallMetadata appInstallMetadata = builder.build(); + + Slog.i(TAG, "To be verified: " + appInstallMetadata); + IntegrityCheckResult result = mEvaluationEngine.evaluate(appInstallMetadata); + Slog.i( + TAG, + "Integrity check result: " + + result.getEffect() + + " due to " + + result.getRule()); + mPackageManagerInternal.setIntegrityVerificationResult( + verificationId, + result.getEffect() == IntegrityCheckResult.Effect.ALLOW + ? PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW + : PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT); + } catch (IllegalArgumentException e) { + // This exception indicates something is wrong with the input passed by package manager. + // e.g., someone trying to trick the system. We block installs in this case. + Slog.e(TAG, "Invalid input to integrity verification", e); + + mPackageManagerInternal.setIntegrityVerificationResult( + verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT); + } catch (Exception e) { + // Other exceptions indicate an error within the integrity component implementation and + // we allow them. + Slog.e(TAG, "Error handling integrity verification", e); + + mPackageManagerInternal.setIntegrityVerificationResult( + verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); + } + } + + /** + * Verify the UID and return the installer package name. + * + * @return the package name of the installer, or null if it cannot be determined or it is + * installed via adb. + */ + @Nullable + private String getInstallerPackageName(Intent intent) { + String installer = + intent.getStringExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_PACKAGE); + if (installer == null) { + return ADB_INSTALLER; + } + int installerUid = intent.getIntExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_UID, -1); + if (installerUid < 0) { + Slog.e( + TAG, + "Installer cannot be determined: installer: " + + installer + + " installer UID: " + + installerUid); + return UNKNOWN_INSTALLER; + } + + try { + int actualInstallerUid = + mContext.getPackageManager().getPackageUid(installer, /* flags= */ 0); + if (actualInstallerUid != installerUid) { + // Installer package name can be faked but the installerUid cannot. + Slog.e( + TAG, + "Installer " + + installer + + " has UID " + + actualInstallerUid + + " which doesn't match alleged installer UID " + + installerUid); + return UNKNOWN_INSTALLER; + } + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Installer package " + installer + " not found."); + return UNKNOWN_INSTALLER; + } + + // At this time we can trust "installer". + + // A common way for apps to install packages is to send an intent to PackageInstaller. In + // that case, the installer will always show up as PackageInstaller which is not what we + // want. + if (installer.equals(PACKAGE_INSTALLER)) { + int originatingUid = intent.getIntExtra(EXTRA_ORIGINATING_UID, -1); + if (originatingUid < 0) { + Slog.e(TAG, "Installer is package installer but originating UID not found."); + return UNKNOWN_INSTALLER; + } + String[] installerPackages = + mContext.getPackageManager().getPackagesForUid(originatingUid); + if (installerPackages == null || installerPackages.length == 0) { + Slog.e(TAG, "No package found associated with originating UID " + originatingUid); + return UNKNOWN_INSTALLER; + } + // In the case of multiple package sharing a UID, we just return the first one. + return installerPackages[0]; + } + + return installer; + } + + /** We will use the SHA256 digest of a package name if it is more than 32 bytes long. */ + private String getPackageNameNormalized(String packageName) { + if (packageName.length() <= 32) { + return packageName; + } + + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = messageDigest.digest(packageName.getBytes(StandardCharsets.UTF_8)); + return toHexString(hashBytes); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found", e); + } + } + + private String getAppCertificateFingerprint(Uri dataUri) { + PackageInfo packageInfo = getPackageArchiveInfo(dataUri); + return getFingerprint(getSignature(packageInfo)); + } + + private String getInstallerCertificateFingerprint(String installer) { + if (installer.equals(ADB_INSTALLER) || installer.equals(UNKNOWN_INSTALLER)) { + return INSTALLER_CERT_NOT_APPLICABLE; + } + try { + PackageInfo installerInfo = + mContext.getPackageManager() + .getPackageInfo(installer, PackageManager.GET_SIGNATURES); + return getFingerprint(getSignature(installerInfo)); + } catch (PackageManager.NameNotFoundException e) { + Slog.i(TAG, "Installer package " + installer + " not found."); + return ""; + } + } + + private static Signature getSignature(PackageInfo packageInfo) { + if (packageInfo.signatures == null || packageInfo.signatures.length < 1) { + throw new IllegalArgumentException("Package signature not found in " + packageInfo); + } + // Only the first element is guaranteed to be present. + return packageInfo.signatures[0]; + } + + private static String getFingerprint(Signature cert) { + InputStream input = new ByteArrayInputStream(cert.toByteArray()); + + CertificateFactory factory; + try { + factory = CertificateFactory.getInstance("X509"); + } catch (CertificateException e) { + throw new RuntimeException("Error getting CertificateFactory", e); + } + X509Certificate certificate = null; + try { + if (factory != null) { + certificate = (X509Certificate) factory.generateCertificate(input); + } + } catch (CertificateException e) { + throw new RuntimeException("Error getting X509Certificate", e); + } + + if (certificate == null) { + throw new RuntimeException("X509 Certificate not found"); + } + + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] publicKey = digest.digest(certificate.getEncoded()); + return toHexString(publicKey); + } catch (NoSuchAlgorithmException | CertificateEncodingException e) { + throw new IllegalArgumentException("Error error computing fingerprint", e); + } + } + + private static String toHexString(byte[] bytes) { + // each byte is represented by two hex chars + StringBuffer hexString = new StringBuffer(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + hexString.append(String.format("%02X", bytes[i])); + } + return new String(hexString); + } + + private PackageInfo getPackageArchiveInfo(Uri dataUri) { + File installationPath = getInstallationPath(dataUri); + if (installationPath == null) { + throw new IllegalArgumentException("Installation path is null, package not found"); + } + PackageInfo packageInfo; + try { + // The installation path will be a directory for a multi-apk install on L+ + if (installationPath.isDirectory()) { + packageInfo = getMultiApkInfo(installationPath); + } else { + packageInfo = + mContext.getPackageManager() + .getPackageArchiveInfo( + installationPath.getPath(), PackageManager.GET_SIGNATURES); + } + return packageInfo; + } catch (Exception e) { + throw new IllegalArgumentException("Exception reading " + dataUri, e); + } + } + + private PackageInfo getMultiApkInfo(File multiApkDirectory) { + // The base apk will normally be called base.apk + File baseFile = new File(multiApkDirectory, BASE_APK_FILE); + PackageInfo basePackageInfo = + mContext.getPackageManager() + .getPackageArchiveInfo( + baseFile.getAbsolutePath(), PackageManager.GET_SIGNATURES); + + if (basePackageInfo == null) { + for (File apkFile : multiApkDirectory.listFiles()) { + if (apkFile.isDirectory()) { + continue; + } + + // If we didn't find a base.apk, then try to parse each apk until we find the one + // that succeeds. + basePackageInfo = + mContext.getPackageManager() + .getPackageArchiveInfo( + apkFile.getAbsolutePath(), + PackageManager.GET_SIGNING_CERTIFICATES); + if (basePackageInfo != null) { + Slog.i(TAG, "Found package info from " + apkFile); + break; + } + } + } + + if (basePackageInfo == null) { + throw new IllegalArgumentException( + "Base package info cannot be found from installation directory"); + } + + return basePackageInfo; + } + + private File getInstallationPath(Uri dataUri) { + if (dataUri == null) { + throw new IllegalArgumentException("Null data uri"); + } + + String scheme = dataUri.getScheme(); + if (!"file".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException("Unsupported scheme for " + dataUri); + } + + File installationPath = new File(dataUri.getPath()); + if (!installationPath.exists()) { + throw new IllegalArgumentException("Cannot find file for " + dataUri); + } + if (!installationPath.canRead()) { + throw new IllegalArgumentException("Cannot read file for " + dataUri); + } + return installationPath; + } + + private String getCallerPackageNameOrThrow() { + final String[] allowedRuleProviders = + mContext.getResources() + .getStringArray(R.array.config_integrityRuleProviderPackages); + for (String packageName : allowedRuleProviders) { + try { + // At least in tests, getPackageUid gives "NameNotFound" but getPackagesFromUid + // give the correct package name. + int uid = mContext.getPackageManager().getPackageUid(packageName, 0); + if (uid == Binder.getCallingUid()) { + // Caller is allowed in the config. + if (isSystemApp(packageName)) { + return packageName; + } + } + } catch (PackageManager.NameNotFoundException e) { + // Ignore the exception. We don't expect the app to be necessarily installed. + Slog.i(TAG, "Rule provider package " + packageName + " not installed."); + } + } + throw new SecurityException( + "Only system packages specified in config_integrityRuleProviderPackages are" + + " allowed to call this method."); + } + + private boolean isSystemApp(String packageName) { + try { + PackageInfo existingPackageInfo = + mContext.getPackageManager().getPackageInfo(packageName, /* flags= */ 0); + return existingPackageInfo.applicationInfo != null + && existingPackageInfo.applicationInfo.isSystemApp(); + } catch (PackageManager.NameNotFoundException e) { + return false; + } } } diff --git a/services/core/java/com/android/server/integrity/IntegrityFileManager.java b/services/core/java/com/android/server/integrity/IntegrityFileManager.java new file mode 100644 index 0000000000000..bdf027995fadd --- /dev/null +++ b/services/core/java/com/android/server/integrity/IntegrityFileManager.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity; + +import android.annotation.Nullable; +import android.content.integrity.AppInstallMetadata; +import android.content.integrity.Rule; +import android.os.Environment; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.integrity.model.RuleMetadata; +import com.android.server.integrity.parser.RuleBinaryParser; +import com.android.server.integrity.parser.RuleMetadataParser; +import com.android.server.integrity.parser.RuleParseException; +import com.android.server.integrity.parser.RuleParser; +import com.android.server.integrity.serializer.RuleBinarySerializer; +import com.android.server.integrity.serializer.RuleMetadataSerializer; +import com.android.server.integrity.serializer.RuleSerializeException; +import com.android.server.integrity.serializer.RuleSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +/** Abstraction over the underlying storage of rules and other metadata. */ +public class IntegrityFileManager { + private static final String TAG = "IntegrityFileManager"; + + // TODO: this is a prototype implementation of this class. Thus no tests are included. + // Implementing rule indexing will likely overhaul this class and more tests should be included + // then. + + private static final String METADATA_FILE = "metadata"; + private static final String RULES_FILE = "rules"; + private static final Object RULES_LOCK = new Object(); + + private static IntegrityFileManager sInstance = null; + + private final RuleParser mRuleParser; + private final RuleSerializer mRuleSerializer; + + // mRulesDir contains data of the actual rules currently stored. + private final File mRulesDir; + // mStagingDir is used to store the temporary rules / metadata during updating, since we want to + // update rules atomically. + private final File mStagingDir; + + @Nullable private RuleMetadata mRuleMetadataCache; + + /** Get the singleton instance of this class. */ + public static synchronized IntegrityFileManager getInstance() { + if (sInstance == null) { + sInstance = new IntegrityFileManager(); + } + return sInstance; + } + + private IntegrityFileManager() { + this( + new RuleBinaryParser(), + new RuleBinarySerializer(), + Environment.getDataSystemDirectory()); + } + + @VisibleForTesting + IntegrityFileManager(RuleParser ruleParser, RuleSerializer ruleSerializer, File dataDir) { + mRuleParser = ruleParser; + mRuleSerializer = ruleSerializer; + + mRulesDir = new File(dataDir, "integrity_rules"); + mStagingDir = new File(dataDir, "integrity_staging"); + + if (!mStagingDir.mkdirs() && mRulesDir.mkdirs()) { + Slog.e(TAG, "Error creating staging and rules directory"); + // TODO: maybe throw an exception? + } + + File metadataFile = new File(mRulesDir, METADATA_FILE); + if (metadataFile.exists()) { + try (FileInputStream inputStream = new FileInputStream(metadataFile)) { + mRuleMetadataCache = RuleMetadataParser.parse(inputStream); + } catch (Exception e) { + Slog.e(TAG, "Error reading metadata file.", e); + } + } + } + + /** Write rules to persistent storage. */ + public void writeRules(String version, String ruleProvider, List rules) + throws IOException, RuleSerializeException { + try { + writeMetadata(mStagingDir, ruleProvider, version); + } catch (IOException e) { + Slog.e(TAG, "Error writing metadata.", e); + // We don't consider this fatal so we continue execution. + } + + try (FileOutputStream fileOutputStream = + new FileOutputStream(new File(mStagingDir, RULES_FILE))) { + mRuleSerializer.serialize(rules, Optional.empty(), fileOutputStream); + } + + switchStagingRulesDir(); + } + + /** + * Read rules from persistent storage. + * + * @param appInstallMetadata information about the install used to select rules to read + */ + public List readRules(AppInstallMetadata appInstallMetadata) + throws IOException, RuleParseException { + // TODO: select rules by index + synchronized (RULES_LOCK) { + try (FileInputStream inputStream = + new FileInputStream(new File(mRulesDir, RULES_FILE))) { + List rules = mRuleParser.parse(inputStream); + return rules; + } + } + } + + /** Read the metadata of the current rules in storage. */ + @Nullable + public RuleMetadata readMetadata() { + return mRuleMetadataCache; + } + + private void switchStagingRulesDir() throws IOException { + synchronized (RULES_LOCK) { + File tmpDir = new File(Environment.getDataSystemDirectory(), "temp"); + + if (!(mRulesDir.renameTo(tmpDir) + && mStagingDir.renameTo(mRulesDir) + && tmpDir.renameTo(mStagingDir))) { + throw new IOException("Error switching staging/rules directory"); + } + } + } + + private void writeMetadata(File directory, String ruleProvider, String version) + throws IOException { + mRuleMetadataCache = new RuleMetadata(ruleProvider, version); + + File metadataFile = new File(directory, METADATA_FILE); + + try (FileOutputStream outputStream = new FileOutputStream(metadataFile)) { + RuleMetadataSerializer.serialize(mRuleMetadataCache, outputStream); + } + } +} diff --git a/services/core/java/com/android/server/integrity/engine/RuleEvaluationEngine.java b/services/core/java/com/android/server/integrity/engine/RuleEvaluationEngine.java index b8202b6c0dc6e..1933679f50e43 100644 --- a/services/core/java/com/android/server/integrity/engine/RuleEvaluationEngine.java +++ b/services/core/java/com/android/server/integrity/engine/RuleEvaluationEngine.java @@ -18,7 +18,9 @@ package com.android.server.integrity.engine; import android.content.integrity.AppInstallMetadata; import android.content.integrity.Rule; +import android.util.Slog; +import com.android.server.integrity.IntegrityFileManager; import com.android.server.integrity.model.IntegrityCheckResult; import java.util.ArrayList; @@ -30,17 +32,23 @@ import java.util.List; *

Every app install is evaluated against rules (pushed by the verifier) by the evaluation engine * to allow/block that install. */ -public final class RuleEvaluationEngine { +public class RuleEvaluationEngine { private static final String TAG = "RuleEvaluation"; // The engine for loading rules, retrieving metadata for app installs, and evaluating app // installs against rules. private static RuleEvaluationEngine sRuleEvaluationEngine; + private final IntegrityFileManager mIntegrityFileManager; + + private RuleEvaluationEngine(IntegrityFileManager integrityFileManager) { + mIntegrityFileManager = integrityFileManager; + } + /** Provide a singleton instance of the rule evaluation engine. */ public static synchronized RuleEvaluationEngine getRuleEvaluationEngine() { if (sRuleEvaluationEngine == null) { - return new RuleEvaluationEngine(); + return new RuleEvaluationEngine(IntegrityFileManager.getInstance()); } return sRuleEvaluationEngine; } @@ -58,7 +66,11 @@ public final class RuleEvaluationEngine { } private List loadRules(AppInstallMetadata appInstallMetadata) { - // TODO: Load rules - return new ArrayList<>(); + try { + return mIntegrityFileManager.readRules(appInstallMetadata); + } catch (Exception e) { + Slog.e(TAG, "Error loading rules.", e); + return new ArrayList<>(); + } } } diff --git a/services/core/java/com/android/server/integrity/model/RuleMetadata.java b/services/core/java/com/android/server/integrity/model/RuleMetadata.java new file mode 100644 index 0000000000000..6b582ae7b5f21 --- /dev/null +++ b/services/core/java/com/android/server/integrity/model/RuleMetadata.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity.model; + +import android.annotation.Nullable; + +/** Data class containing relevant metadata associated with a rule set. */ +public class RuleMetadata { + + private final String mRuleProvider; + private final String mVersion; + + public RuleMetadata(String ruleProvider, String version) { + mRuleProvider = ruleProvider; + mVersion = version; + } + + @Nullable + public String getRuleProvider() { + return mRuleProvider; + } + + @Nullable + public String getVersion() { + return mVersion; + } +} diff --git a/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java new file mode 100644 index 0000000000000..28d2e69141038 --- /dev/null +++ b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity.parser; + +import android.annotation.Nullable; +import android.util.Xml; + +import com.android.server.integrity.model.RuleMetadata; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Helper class for parsing rule metadata. */ +public class RuleMetadataParser { + + public static final String RULE_PROVIDER_TAG = "P"; + public static final String VERSION_TAG = "V"; + + /** Parse the rule metadata from an input stream. */ + @Nullable + public static RuleMetadata parse(InputStream inputStream) + throws XmlPullParserException, IOException { + + String ruleProvider = ""; + String version = ""; + + XmlPullParser xmlPullParser = Xml.newPullParser(); + xmlPullParser.setInput(inputStream, StandardCharsets.UTF_8.name()); + + int eventType; + while ((eventType = xmlPullParser.next()) != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + String tag = xmlPullParser.getName(); + switch (tag) { + case RULE_PROVIDER_TAG: + ruleProvider = xmlPullParser.nextText(); + break; + case VERSION_TAG: + version = xmlPullParser.nextText(); + break; + default: + throw new IllegalStateException("Unknown tag in metadata: " + tag); + } + } + } + + return new RuleMetadata(ruleProvider, version); + } +} diff --git a/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java new file mode 100644 index 0000000000000..5c51f31ba8cca --- /dev/null +++ b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity.serializer; + +import static com.android.server.integrity.parser.RuleMetadataParser.RULE_PROVIDER_TAG; +import static com.android.server.integrity.parser.RuleMetadataParser.VERSION_TAG; + +import android.util.Xml; + +import com.android.server.integrity.model.RuleMetadata; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** Helper class for writing rule metadata. */ +public class RuleMetadataSerializer { + /** Serialize the rule metadata to an output stream. */ + public static void serialize(RuleMetadata ruleMetadata, OutputStream outputStream) + throws IOException { + XmlSerializer xmlSerializer = Xml.newSerializer(); + xmlSerializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); + + serializeTaggedValue(xmlSerializer, RULE_PROVIDER_TAG, ruleMetadata.getRuleProvider()); + serializeTaggedValue(xmlSerializer, VERSION_TAG, ruleMetadata.getVersion()); + + xmlSerializer.endDocument(); + } + + private static void serializeTaggedValue(XmlSerializer xmlSerializer, String tag, String value) + throws IOException { + xmlSerializer.startTag(/* namespace= */ null, tag); + xmlSerializer.text(value); + xmlSerializer.endTag(/* namespace= */ null, tag); + } +} diff --git a/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/test.apk b/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/test.apk new file mode 100644 index 0000000000000000000000000000000000000000..e4c422951dc37ec844ccdf08e5b695b601bf846c GIT binary patch literal 35076 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 literal 0 HcmV?d00001 diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java index c080332ca2812..222dac9535686 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java @@ -16,41 +16,369 @@ package com.android.server.integrity; +import static android.content.integrity.AppIntegrityManager.EXTRA_STATUS; +import static android.content.integrity.AppIntegrityManager.STATUS_FAILURE; +import static android.content.integrity.AppIntegrityManager.STATUS_SUCCESS; +import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID; +import static android.content.pm.PackageManager.EXTRA_VERIFICATION_INSTALLER_PACKAGE; +import static android.content.pm.PackageManager.EXTRA_VERIFICATION_INSTALLER_UID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.integrity.AppInstallMetadata; +import android.content.integrity.AtomicFormula; +import android.content.integrity.Rule; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.R; import com.android.server.LocalServices; +import com.android.server.integrity.engine.RuleEvaluationEngine; +import com.android.server.integrity.model.IntegrityCheckResult; +import com.android.server.testutils.TestUtils; +import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; + /** Unit test for {@link com.android.server.integrity.AppIntegrityManagerServiceImpl} */ @RunWith(AndroidJUnit4.class) public class AppIntegrityManagerServiceImplTest { + private static final String TEST_DIR = "AppIntegrityManagerServiceImplTest"; - @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; + private static final String VERSION = "version"; + private static final String TEST_FRAMEWORK_PACKAGE = "com.android.frameworks.servicestests"; + + private static final String PACKAGE_NAME = "com.test.app"; + private static final int VERSION_CODE = 100; + private static final String INSTALLER = TEST_FRAMEWORK_PACKAGE; + // These are obtained by running the test and checking logcat. + private static final String APP_CERT = + "949ADC6CB92FF09E3784D6E9504F26F9BEAC06E60D881D55A6A81160F9CD6FD1"; + private static final String INSTALLER_CERT = + "301AA3CB081134501C45F1422ABC66C24224FD5DED5FDC8F17E697176FD866AA"; + // We use SHA256 for package names longer than 32 characters. + private static final String INSTALLER_SHA256 = + "786933C28839603EB48C50B2A688DC6BE52C833627CB2731FF8466A2AE9F94CD"; + + @org.junit.Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock PackageManagerInternal mPackageManagerInternal; + @Mock Context mMockContext; + @Mock Resources mMockResources; + @Mock RuleEvaluationEngine mRuleEvaluationEngine; + @Mock IntegrityFileManager mIntegrityFileManager; + @Mock Handler mHandler; + private PackageManager mSpyPackageManager; + private File mTestApk; + + private final Context mRealContext = InstrumentationRegistry.getTargetContext(); // under test private AppIntegrityManagerServiceImpl mService; @Before - public void setup() { - LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal); + public void setup() throws Exception { + mTestApk = File.createTempFile("TestApk", /* suffix= */ null); + mTestApk.deleteOnExit(); + try (InputStream inputStream = mRealContext.getAssets().open(TEST_DIR + "/test.apk")) { + Files.copy(inputStream, mTestApk.toPath(), StandardCopyOption.REPLACE_EXISTING); + } - mService = new AppIntegrityManagerServiceImpl(InstrumentationRegistry.getContext()); + mService = + new AppIntegrityManagerServiceImpl( + mMockContext, + mPackageManagerInternal, + mRuleEvaluationEngine, + mIntegrityFileManager, + mHandler); + + mSpyPackageManager = spy(mRealContext.getPackageManager()); + // setup mocks to prevent NPE + when(mMockContext.getPackageManager()).thenReturn(mSpyPackageManager); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getStringArray(anyInt())).thenReturn(new String[] {}); + } + + @After + public void tearDown() throws Exception { + mTestApk.delete(); + } + + // This is not a test of the class, but more of a safeguard that we don't block any install in + // the default case. This is needed because we don't have any emergency kill switch to disable + // this component. + @Test + public void default_allow() throws Exception { + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal); + mService = AppIntegrityManagerServiceImpl.create(mMockContext); + ArgumentCaptor broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mMockContext, times(2)) + .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + Intent intent = makeVerificationIntent(); + + broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent); + + // Since we are not mocking handler in this case, we must wait. + // 2 seconds should be a sensible timeout. + Thread.sleep(2000); + verify(mPackageManagerInternal) + .setIntegrityVerificationResult( + 1, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); } @Test - public void noop() { - // We need this test just as a place holder since an empty test suite is treated as error. + public void updateRuleSet_notAuthorized() throws Exception { + makeUsSystemApp(); + Rule rule = + new Rule( + new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), + Rule.DENY); + TestUtils.assertExpectException( + SecurityException.class, + "Only system packages specified in config_integrityRuleProviderPackages are" + + " allowed to call this method.", + () -> + mService.updateRuleSet( + VERSION, + new ParceledListSlice<>(Arrays.asList(rule)), + /* statusReceiver= */ null)); + } + + @Test + public void updateRuleSet_notSystemApp() throws Exception { + whitelistUsAsRuleProvider(); + Rule rule = + new Rule( + new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), + Rule.DENY); + TestUtils.assertExpectException( + SecurityException.class, + "Only system packages specified in config_integrityRuleProviderPackages are" + + " allowed to call this method.", + () -> + mService.updateRuleSet( + VERSION, + new ParceledListSlice<>(Arrays.asList(rule)), + /* statusReceiver= */ null)); + } + + @Test + public void updateRuleSet_authorized() throws Exception { + whitelistUsAsRuleProvider(); + makeUsSystemApp(); + Rule rule = + new Rule( + new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), + Rule.DENY); + + // no SecurityException + mService.updateRuleSet( + VERSION, new ParceledListSlice<>(Arrays.asList(rule)), mock(IntentSender.class)); + } + + @Test + public void updateRuleSet_correctMethodCall() throws Exception { + whitelistUsAsRuleProvider(); + makeUsSystemApp(); + IntentSender mockReceiver = mock(IntentSender.class); + List rules = + Arrays.asList( + new Rule( + new AtomicFormula.StringAtomicFormula( + AtomicFormula.PACKAGE_NAME, + PACKAGE_NAME, + /* isHashedValue= */ false), + Rule.DENY)); + + mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver); + runJobInHandler(); + + verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules); + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any()); + assertEquals(STATUS_SUCCESS, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1)); + } + + @Test + public void updateRuleSet_fail() throws Exception { + whitelistUsAsRuleProvider(); + makeUsSystemApp(); + doThrow(new IOException()).when(mIntegrityFileManager).writeRules(any(), any(), any()); + IntentSender mockReceiver = mock(IntentSender.class); + List rules = + Arrays.asList( + new Rule( + new AtomicFormula.StringAtomicFormula( + AtomicFormula.PACKAGE_NAME, + PACKAGE_NAME, + /* isHashedValue= */ false), + Rule.DENY)); + + mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver); + runJobInHandler(); + + verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules); + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any()); + assertEquals(STATUS_FAILURE, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1)); + } + + @Test + public void broadcastReceiverRegistration() throws Exception { + ArgumentCaptor intentFilterCaptor = + ArgumentCaptor.forClass(IntentFilter.class); + + verify(mMockContext).registerReceiver(any(), intentFilterCaptor.capture(), any(), any()); + assertEquals(1, intentFilterCaptor.getValue().countActions()); + assertEquals( + Intent.ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION, + intentFilterCaptor.getValue().getAction(0)); + assertEquals(1, intentFilterCaptor.getValue().countDataTypes()); + assertEquals(PACKAGE_MIME_TYPE, intentFilterCaptor.getValue().getDataType(0)); + } + + @Test + public void handleBroadcast_correctArgs() throws Exception { + ArgumentCaptor broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mMockContext) + .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + Intent intent = makeVerificationIntent(); + when(mRuleEvaluationEngine.evaluate(any())).thenReturn(IntegrityCheckResult.allow()); + + broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent); + runJobInHandler(); + + ArgumentCaptor metadataCaptor = + ArgumentCaptor.forClass(AppInstallMetadata.class); + verify(mRuleEvaluationEngine).evaluate(metadataCaptor.capture()); + AppInstallMetadata appInstallMetadata = metadataCaptor.getValue(); + assertEquals(PACKAGE_NAME, appInstallMetadata.getPackageName()); + assertEquals(APP_CERT, appInstallMetadata.getAppCertificate()); + assertEquals(INSTALLER_SHA256, appInstallMetadata.getInstallerName()); + assertEquals(INSTALLER_CERT, appInstallMetadata.getInstallerCertificate()); + assertEquals(VERSION_CODE, appInstallMetadata.getVersionCode()); + assertFalse(appInstallMetadata.isPreInstalled()); + } + + @Test + public void handleBroadcast_allow() throws Exception { + ArgumentCaptor broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mMockContext) + .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + Intent intent = makeVerificationIntent(); + when(mRuleEvaluationEngine.evaluate(any())).thenReturn(IntegrityCheckResult.allow()); + + broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent); + runJobInHandler(); + + verify(mPackageManagerInternal) + .setIntegrityVerificationResult( + 1, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); + } + + @Test + public void handleBroadcast_reject() throws Exception { + ArgumentCaptor broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mMockContext) + .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + when(mRuleEvaluationEngine.evaluate(any())) + .thenReturn( + IntegrityCheckResult.deny( + new Rule( + new AtomicFormula.BooleanAtomicFormula( + AtomicFormula.PRE_INSTALLED, false), + Rule.DENY))); + Intent intent = makeVerificationIntent(); + + broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent); + runJobInHandler(); + + verify(mPackageManagerInternal) + .setIntegrityVerificationResult( + 1, PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT); + } + + private void whitelistUsAsRuleProvider() { + Resources mockResources = mock(Resources.class); + when(mockResources.getStringArray(R.array.config_integrityRuleProviderPackages)) + .thenReturn(new String[] {TEST_FRAMEWORK_PACKAGE}); + when(mMockContext.getResources()).thenReturn(mockResources); + } + + private void runJobInHandler() { + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + // sendMessageAtTime is the first non-final method in the call chain when "post" is invoked. + verify(mHandler).sendMessageAtTime(messageCaptor.capture(), anyLong()); + messageCaptor.getValue().getCallback().run(); + } + + private void makeUsSystemApp() throws Exception { + PackageInfo packageInfo = + mRealContext.getPackageManager().getPackageInfo(TEST_FRAMEWORK_PACKAGE, 0); + packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + doReturn(packageInfo) + .when(mSpyPackageManager) + .getPackageInfo(eq(TEST_FRAMEWORK_PACKAGE), anyInt()); + } + + private Intent makeVerificationIntent() throws Exception { + Intent intent = new Intent(); + intent.setDataAndType(Uri.fromFile(mTestApk), PACKAGE_MIME_TYPE); + intent.setAction(Intent.ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION); + intent.putExtra(EXTRA_VERIFICATION_ID, 1); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, PACKAGE_NAME); + intent.putExtra(EXTRA_VERIFICATION_INSTALLER_PACKAGE, INSTALLER); + intent.putExtra( + EXTRA_VERIFICATION_INSTALLER_UID, + mRealContext.getPackageManager().getPackageUid(INSTALLER, /* flags= */ 0)); + intent.putExtra(Intent.EXTRA_VERSION_CODE, VERSION_CODE); + return intent; } }