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 0000000000000..e4c422951dc37 Binary files /dev/null and b/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/test.apk differ 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; } }