Merge "Implement methods in AppIntegrityManagerServiceImpl."

This commit is contained in:
Song Pan
2019-12-23 15:21:59 +00:00
committed by Android (Google) Code Review
11 changed files with 1136 additions and 27 deletions

View File

@@ -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<AppIntegrityManager>() {
@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 {

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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<Rule> 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;
}
}
}

View File

@@ -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<Rule> 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<Rule> 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<Rule> 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);
}
}
}

View File

@@ -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;
* <p>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<Rule> 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<>();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<BroadcastReceiver> 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<Rule> 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<Intent> 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<Rule> 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<Intent> 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<IntentFilter> 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<BroadcastReceiver> 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<AppInstallMetadata> 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<BroadcastReceiver> 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<BroadcastReceiver> 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<Message> 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;
}
}