(
+ aligned_alloc(block_size /* alignment */, block_size /* size */)));
+
+ if (read_block_from_device(block_device, physical_block_offset, block_size,
+ buf.get()) < 0) {
+ return -1;
+ }
+ char* p = buf.get() + offset_within_block;
+ printf("before: %hhx\n", *p);
+ *p ^= 0xff;
+ printf("after: %hhx\n", *p);
+ if (write_block_to_device(block_device, physical_block_offset, block_size,
+ buf.get()) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
new file mode 100644
index 0000000000000..761c5ceb2413c
--- /dev/null
+++ b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
@@ -0,0 +1,496 @@
+/*
+ * 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.apkverity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.RootPermissionTest;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * This test makes sure app installs with fs-verity signature, and on-access verification works.
+ *
+ * When an app is installed, all or none of the files should have their corresponding .fsv_sig
+ * signature file. Otherwise, install will fail.
+ *
+ *
Once installed, file protected by fs-verity is verified by kernel every time a block is loaded
+ * from disk to memory. The file is immutable by design, enforced by filesystem.
+ *
+ *
In order to make sure a block of the file is readable only if the underlying block on disk
+ * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical
+ * address against the block device.
+ *
+ *
Requirements to run this test:
+ *
+ * - Device is rootable
+ * - The filesystem supports fs-verity
+ * - The feature flag is enabled
+ *
+ */
+@RootPermissionTest
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ApkVerityTest extends BaseHostJUnit4Test {
+ private static final String TARGET_PACKAGE = "com.android.apkverity";
+
+ private static final String BASE_APK = "ApkVerityTestApp.apk";
+ private static final String BASE_APK_DM = "ApkVerityTestApp.dm";
+ private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk";
+ private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm";
+
+ private static final String INSTALLED_BASE_APK = "base.apk";
+ private static final String INSTALLED_BASE_DM = "base.dm";
+ private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk";
+ private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm";
+ private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig";
+ private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig";
+ private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig";
+ private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig";
+
+ private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer";
+ private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der";
+
+ private static final String APK_VERITY_STANDARD_MODE = "2";
+
+ /** Only 4K page is supported by fs-verity currently. */
+ private static final int FSVERITY_PAGE_SIZE = 4096;
+
+ private ITestDevice mDevice;
+ private String mKeyId;
+
+ @Before
+ public void setUp() throws DeviceNotAvailableException {
+ mDevice = getDevice();
+
+ String apkVerityMode = mDevice.getProperty("ro.apk_verity.mode");
+ assumeTrue(APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
+
+ mKeyId = expectRemoteCommandToSucceed(
+ "mini-keyctl padd asymmetric fsv_test .fs-verity < " + CERT_PATH).trim();
+ if (!mKeyId.matches("^\\d+$")) {
+ String keyId = mKeyId;
+ mKeyId = null;
+ fail("Key ID is not decimal: " + keyId);
+ }
+
+ uninstallPackage(TARGET_PACKAGE);
+ }
+
+ @After
+ public void tearDown() throws DeviceNotAvailableException {
+ uninstallPackage(TARGET_PACKAGE);
+
+ if (mKeyId != null) {
+ expectRemoteCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
+ }
+ }
+
+ @Test
+ public void testFsverityKernelSupports() throws DeviceNotAvailableException {
+ ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
+ expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity");
+ }
+
+ @Test
+ public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG);
+ verifyInstalledFilesHaveFsverity();
+ }
+
+ @Test
+ public void testInstallBaseWithWrongSignature()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFile(BASE_APK)
+ .addFile(SPLIT_APK_DM + ".fsv_sig",
+ BASE_APK + ".fsv_sig")
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallBaseWithSplit()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .addFileAndSignature(SPLIT_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG,
+ INSTALLED_SPLIT_APK,
+ INSTALLED_SPLIT_APK_FSV_SIG);
+ verifyInstalledFilesHaveFsverity();
+ }
+
+ @Test
+ public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .addFileAndSignature(BASE_APK_DM)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG,
+ INSTALLED_BASE_DM,
+ INSTALLED_BASE_DM_FSV_SIG);
+ verifyInstalledFilesHaveFsverity();
+ }
+
+ @Test
+ public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .addFileAndSignature(BASE_APK_DM)
+ .addFileAndSignature(SPLIT_APK)
+ .addFileAndSignature(SPLIT_APK_DM)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG,
+ INSTALLED_BASE_DM,
+ INSTALLED_BASE_DM_FSV_SIG,
+ INSTALLED_SPLIT_APK,
+ INSTALLED_SPLIT_APK_FSV_SIG,
+ INSTALLED_SPLIT_DM,
+ INSTALLED_SPLIT_DM_FSV_SIG);
+ verifyInstalledFilesHaveFsverity();
+ }
+
+ @Test
+ public void testInstallSplitOnly()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG);
+
+ new InstallMultiple()
+ .inheritFrom(TARGET_PACKAGE)
+ .addFileAndSignature(SPLIT_APK)
+ .run();
+
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG,
+ INSTALLED_SPLIT_APK,
+ INSTALLED_SPLIT_APK_FSV_SIG);
+ verifyInstalledFilesHaveFsverity();
+ }
+
+ @Test
+ public void testInstallSplitOnlyMissingSignature()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG);
+
+ new InstallMultiple()
+ .inheritFrom(TARGET_PACKAGE)
+ .addFile(SPLIT_APK)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallSplitOnlyWithoutBaseSignature()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFile(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ verifyInstalledFiles(INSTALLED_BASE_APK);
+
+ new InstallMultiple()
+ .inheritFrom(TARGET_PACKAGE)
+ .addFileAndSignature(SPLIT_APK)
+ .run();
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_SPLIT_APK,
+ INSTALLED_SPLIT_APK_FSV_SIG);
+
+ }
+
+ @Test
+ public void testInstallOnlyBaseHasFsvSig()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .addFile(BASE_APK_DM)
+ .addFile(SPLIT_APK)
+ .addFile(SPLIT_APK_DM)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallOnlyDmHasFsvSig()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFile(BASE_APK)
+ .addFileAndSignature(BASE_APK_DM)
+ .addFile(SPLIT_APK)
+ .addFile(SPLIT_APK_DM)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallOnlySplitHasFsvSig()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFile(BASE_APK)
+ .addFile(BASE_APK_DM)
+ .addFileAndSignature(SPLIT_APK)
+ .addFile(SPLIT_APK_DM)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallBaseWithFsvSigThenSplitWithout()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFileAndSignature(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ verifyInstalledFiles(
+ INSTALLED_BASE_APK,
+ INSTALLED_BASE_APK_FSV_SIG);
+
+ new InstallMultiple()
+ .addFile(SPLIT_APK)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testInstallBaseWithoutFsvSigThenSplitWith()
+ throws DeviceNotAvailableException, FileNotFoundException {
+ new InstallMultiple()
+ .addFile(BASE_APK)
+ .run();
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ verifyInstalledFiles(INSTALLED_BASE_APK);
+
+ new InstallMultiple()
+ .addFileAndSignature(SPLIT_APK)
+ .runExpectingFailure();
+ }
+
+ @Test
+ public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException {
+ new InstallMultiple().addFileAndSignature(BASE_APK).run();
+ String apkPath = getApkPath(TARGET_PACKAGE);
+
+ assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE));
+ expectRemoteCommandToFail("echo -n '' >> " + apkPath);
+ expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null");
+ }
+
+ @Test
+ public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException {
+ new InstallMultiple().addFileAndSignature(BASE_APK).run();
+ String apkPath = getApkPath(TARGET_PACKAGE);
+
+ long apkSize = getFileSizeInBytes(apkPath);
+ long offsetFirstByte = 0;
+
+ // The first two pages should be both readable at first.
+ assertTrue(canReadByte(apkPath, offsetFirstByte));
+ if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
+ assertTrue(canReadByte(apkPath, offsetFirstByte + FSVERITY_PAGE_SIZE));
+ }
+
+ // Damage the file directly against the block device.
+ damageFileAgainstBlockDevice(apkPath, offsetFirstByte);
+
+ // Expect actual read from disk to fail but only at damaged page.
+ dropCaches();
+ assertFalse(canReadByte(apkPath, offsetFirstByte));
+ if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) {
+ long lastByteOfTheSamePage =
+ offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1;
+ assertFalse(canReadByte(apkPath, lastByteOfTheSamePage));
+ assertTrue(canReadByte(apkPath, lastByteOfTheSamePage + 1));
+ }
+ }
+
+ @Test
+ public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException {
+ new InstallMultiple().addFileAndSignature(BASE_APK).run();
+ String apkPath = getApkPath(TARGET_PACKAGE);
+
+ long apkSize = getFileSizeInBytes(apkPath);
+ long offsetOfLastByte = apkSize - 1;
+
+ // The first two pages should be both readable at first.
+ assertTrue(canReadByte(apkPath, offsetOfLastByte));
+ if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
+ assertTrue(canReadByte(apkPath, offsetOfLastByte - FSVERITY_PAGE_SIZE));
+ }
+
+ // Damage the file directly against the block device.
+ damageFileAgainstBlockDevice(apkPath, offsetOfLastByte);
+
+ // Expect actual read from disk to fail but only at damaged page.
+ dropCaches();
+ assertFalse(canReadByte(apkPath, offsetOfLastByte));
+ if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) {
+ long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE;
+ assertFalse(canReadByte(apkPath, firstByteOfTheSamePage));
+ assertTrue(canReadByte(apkPath, firstByteOfTheSamePage - 1));
+ }
+ }
+
+ private void verifyInstalledFilesHaveFsverity() throws DeviceNotAvailableException {
+ // Verify that all files are protected by fs-verity
+ String apkPath = getApkPath(TARGET_PACKAGE);
+ String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
+ long kTargetOffset = 0;
+ for (String basename : expectRemoteCommandToSucceed("ls " + appDir).split("\n")) {
+ if (basename.endsWith(".apk") || basename.endsWith(".dm")) {
+ String path = appDir + "/" + basename;
+ damageFileAgainstBlockDevice(path, kTargetOffset);
+
+ // Retry is sometimes needed to pass the test. Package manager may have FD leaks
+ // (see b/122744005 as example) that prevents the file in question to be evicted
+ // from filesystem cache. Forcing GC workarounds the problem.
+ int retry = 5;
+ for (; retry > 0; retry--) {
+ dropCaches();
+ if (!canReadByte(path, kTargetOffset)) {
+ break;
+ }
+ try {
+ Thread.sleep(1000);
+ String pid = expectRemoteCommandToSucceed("pidof system_server");
+ mDevice.executeShellV2Command("kill -10 " + pid); // force GC
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ assertTrue("Read from " + path + " should fail", retry > 0);
+ }
+ }
+ }
+
+ private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException {
+ String apkPath = getApkPath(TARGET_PACKAGE);
+ String appDir = apkPath.substring(0, apkPath.lastIndexOf("/"));
+ HashSet actualFiles = new HashSet<>(Arrays.asList(
+ expectRemoteCommandToSucceed("ls " + appDir).split("\n")));
+ assertTrue(actualFiles.remove("lib"));
+ assertTrue(actualFiles.remove("oat"));
+
+ HashSet expectedFiles = new HashSet<>(Arrays.asList(filenames));
+ assertEquals(expectedFiles, actualFiles);
+ }
+
+ private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)
+ throws DeviceNotAvailableException {
+ assertTrue(path.startsWith("/data/"));
+ ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data");
+ expectRemoteCommandToSucceed(String.join(" ", DAMAGING_EXECUTABLE,
+ mountPoint.filesystem, path, Long.toString(offsetOfTargetingByte)));
+ }
+
+ private String getApkPath(String packageName) throws DeviceNotAvailableException {
+ String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk");
+ int index = line.trim().indexOf(":");
+ assertTrue(index >= 0);
+ return line.substring(index + 1);
+ }
+
+ private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException {
+ return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim());
+ }
+
+ private void dropCaches() throws DeviceNotAvailableException {
+ expectRemoteCommandToSucceed("sync && echo 1 > /proc/sys/vm/drop_caches");
+ }
+
+ private boolean canReadByte(String filePath, long offset) throws DeviceNotAvailableException {
+ CommandResult result = mDevice.executeShellV2Command(
+ "dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset));
+ return result.getStatus() == CommandStatus.SUCCESS;
+ }
+
+ private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
+ CommandResult result = mDevice.executeShellV2Command(cmd);
+ assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
+ result.getStatus());
+ return result.getStdout();
+ }
+
+ private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
+ CommandResult result = mDevice.executeShellV2Command(cmd);
+ assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(),
+ result.getStatus() != CommandStatus.SUCCESS);
+ }
+
+ private class InstallMultiple extends BaseInstallMultiple {
+ InstallMultiple() {
+ super(getDevice(), getBuild());
+ }
+
+ InstallMultiple addFileAndSignature(String filename) {
+ try {
+ addFile(filename);
+ addFile(filename + ".fsv_sig");
+ } catch (FileNotFoundException e) {
+ fail("Missing test file: " + e);
+ }
+ return this;
+ }
+ }
+}
diff --git a/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java b/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java
new file mode 100644
index 0000000000000..02e73d157dde5
--- /dev/null
+++ b/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java
@@ -0,0 +1,140 @@
+/*
+ * 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.apkverity;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for invoking the install-multiple command via ADB. Subclass this for less typing:
+ *
+ * private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public
+ * InstallMultiple() { super(getDevice(), null); } }
+ */
+/*package*/ class BaseInstallMultiple> {
+
+ private final ITestDevice mDevice;
+ private final IBuildInfo mBuild;
+
+ private final List mArgs = new ArrayList<>();
+ private final Map mFileToRemoteMap = new HashMap<>();
+
+ /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) {
+ mDevice = device;
+ mBuild = buildInfo;
+ addArg("-g");
+ }
+
+ T addArg(String arg) {
+ mArgs.add(arg);
+ return (T) this;
+ }
+
+ T addFile(String filename) throws FileNotFoundException {
+ return addFile(filename, filename);
+ }
+
+ T addFile(String filename, String remoteName) throws FileNotFoundException {
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
+ mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName);
+ return (T) this;
+ }
+
+ T inheritFrom(String packageName) {
+ addArg("-r");
+ addArg("-p " + packageName);
+ return (T) this;
+ }
+
+ void run() throws DeviceNotAvailableException {
+ run(true);
+ }
+
+ void runExpectingFailure() throws DeviceNotAvailableException {
+ run(false);
+ }
+
+ private void run(boolean expectingSuccess) throws DeviceNotAvailableException {
+ final ITestDevice device = mDevice;
+
+ // Create an install session
+ final StringBuilder cmd = new StringBuilder();
+ cmd.append("pm install-create");
+ for (String arg : mArgs) {
+ cmd.append(' ').append(arg);
+ }
+
+ String result = device.executeShellCommand(cmd.toString());
+ TestCase.assertTrue(result, result.startsWith("Success"));
+
+ final int start = result.lastIndexOf("[");
+ final int end = result.lastIndexOf("]");
+ int sessionId = -1;
+ try {
+ if (start != -1 && end != -1 && start < end) {
+ sessionId = Integer.parseInt(result.substring(start + 1, end));
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Failed to parse install session: " + result);
+ }
+ if (sessionId == -1) {
+ throw new IllegalStateException("Failed to create install session: " + result);
+ }
+
+ // Push our files into session. Ideally we'd use stdin streaming,
+ // but ddmlib doesn't support it yet.
+ for (final Map.Entry entry : mFileToRemoteMap.entrySet()) {
+ final File file = entry.getKey();
+ final String remoteName = entry.getValue();
+ final String remotePath = "/data/local/tmp/" + file.getName();
+ if (!device.pushFile(file, remotePath)) {
+ throw new IllegalStateException("Failed to push " + file);
+ }
+
+ cmd.setLength(0);
+ cmd.append("pm install-write");
+ cmd.append(' ').append(sessionId);
+ cmd.append(' ').append(remoteName);
+ cmd.append(' ').append(remotePath);
+
+ result = device.executeShellCommand(cmd.toString());
+ TestCase.assertTrue(result, result.startsWith("Success"));
+ }
+
+ // Everything staged; let's pull trigger
+ cmd.setLength(0);
+ cmd.append("pm install-commit");
+ cmd.append(' ').append(sessionId);
+
+ result = device.executeShellCommand(cmd.toString());
+ if (expectingSuccess) {
+ TestCase.assertTrue(result, result.contains("Success"));
+ } else {
+ TestCase.assertFalse(result, result.contains("Success"));
+ }
+ }
+}
diff --git a/tests/ApkVerityTest/testdata/Android.bp b/tests/ApkVerityTest/testdata/Android.bp
new file mode 100644
index 0000000000000..c10b0cef21d76
--- /dev/null
+++ b/tests/ApkVerityTest/testdata/Android.bp
@@ -0,0 +1,77 @@
+// 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.
+
+filegroup {
+ name: "ApkVerityTestKeyPem",
+ srcs: ["ApkVerityTestKey.pem"],
+}
+
+filegroup {
+ name: "ApkVerityTestCertPem",
+ srcs: ["ApkVerityTestCert.pem"],
+}
+
+filegroup {
+ name: "ApkVerityTestCertDer",
+ srcs: ["ApkVerityTestCert.der"],
+}
+
+filegroup {
+ name: "ApkVerityTestAppDm",
+ srcs: ["ApkVerityTestApp.dm"],
+}
+
+filegroup {
+ name: "ApkVerityTestAppSplitDm",
+ srcs: ["ApkVerityTestAppSplit.dm"],
+}
+
+genrule_defaults {
+ name: "apk_verity_sig_gen_default",
+ tools: ["fsverity"],
+ tool_files: [":ApkVerityTestKeyPem", ":ApkVerityTestCertPem"],
+ cmd: "$(location fsverity) sign $(in) $(out) " +
+ "--key=$(location :ApkVerityTestKeyPem) " +
+ "--cert=$(location :ApkVerityTestCertPem) " +
+ "> /dev/null",
+}
+
+genrule {
+ name: "ApkVerityTestAppFsvSig",
+ defaults: ["apk_verity_sig_gen_default"],
+ srcs: [":ApkVerityTestApp"],
+ out: ["ApkVerityTestApp.apk.fsv_sig"],
+}
+
+genrule {
+ name: "ApkVerityTestAppDmFsvSig",
+ defaults: ["apk_verity_sig_gen_default"],
+ srcs: [":ApkVerityTestAppDm"],
+ out: ["ApkVerityTestApp.dm.fsv_sig"],
+}
+
+genrule {
+ name: "ApkVerityTestAppSplitFsvSig",
+ defaults: ["apk_verity_sig_gen_default"],
+ srcs: [":ApkVerityTestAppSplit"],
+ out: ["ApkVerityTestAppSplit.apk.fsv_sig"],
+}
+
+genrule {
+ name: "ApkVerityTestAppSplitDmFsvSig",
+ defaults: ["apk_verity_sig_gen_default"],
+ srcs: [":ApkVerityTestAppSplitDm"],
+ out: ["ApkVerityTestAppSplit.dm.fsv_sig"],
+}
+
diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm b/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm
new file mode 100644
index 0000000000000..e53a861313662
Binary files /dev/null and b/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm differ
diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm b/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm
new file mode 100644
index 0000000000000..75396f1ba730f
Binary files /dev/null and b/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm differ
diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.der b/tests/ApkVerityTest/testdata/ApkVerityTestCert.der
new file mode 100644
index 0000000000000..fe9029b53aa1b
Binary files /dev/null and b/tests/ApkVerityTest/testdata/ApkVerityTestCert.der differ
diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem b/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem
new file mode 100644
index 0000000000000..6c0b7b1f635a5
--- /dev/null
+++ b/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFLjCCAxagAwIBAgIJAKZbtMlZZwtdMA0GCSqGSIb3DQEBCwUAMCwxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHQW5kcm9pZDAeFw0xODEyMTky
+MTA5MzVaFw0xOTAxMTgyMTA5MzVaMCwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD
+QTEQMA4GA1UECgwHQW5kcm9pZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+ggIBAKnrw4WiFgFBq6vXqcLc97iwvcYPZmeIjQqYRF+CHwXBXx8IyDlMfPrgyIYo
+ZLkosnUK/Exuypdu6UEtdqtYPknC6w9z4YkxqsKtyxyB1b13ptcTHh3bf2N8bqGr
+8gWWLxj0QjumCtFi7Z/TCwB5t3b3gtC+0jVfABSWrm5PNkgk7jIP+4KeYLDCDfiJ
+XH3uHu6OASiSHTOnrmLWSaSw0y6G4OFthHqQnMywasly0r6m+Mif+K0ZUV7hBRi/
+SfqcJ1HTCXTJMskEyV6Qx2sHF/VbK2gdUv56z6OVRNSs/FxPBiWVMuZZKh1FpBVI
+gbGxusf2Awwtc+Soxr4/P1YFcrwfA/ff9FK3Yg/Cd3ZMGbzUkbEMEkE5BW7Gbjmx
+wz3mYTiRfa2L/Bl4MiMqNi0tfORLkmg+V/EItzfhZ/HsXMOCBsnuj4KnFslmbamz
+t9opypj2JLGk+lXhZ5gFNFw8tYH1AnG1AIXe5u+6Fq2nQ1y/ncGUTR5Sw4de/Gee
+C0UgR+KiFEdKupMKbXgSKl+0QPz/i2eSpcDOKMwZ4WiNrkbccbCyr38so+j5DfWF
+IeZA9a/IlysA6G8yU2TfXBc65VCIEQRJOQdUOZFDO8OSoqGP+fbA6edpmovGw+TH
+sM/NkmpEXpQm7BVOI4oVjdf4pKPp0zaW2YcaA3xU2w6eF17pAgMBAAGjUzBRMB0G
+A1UdDgQWBBRGpHYy7yiLEYalGuF1va6zJKGD/zAfBgNVHSMEGDAWgBRGpHYy7yiL
+EYalGuF1va6zJKGD/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC
+AQAao6ZBM122F0pYb2QLahIyyGEr3LfSdBGID4068pVik4ncIefFz36Xf9AFxRQd
+KHmwRYNPHiLRIEGdtqplC5pZDeHz41txIArNIZKzDWOYtdcFyCz8umuj912BmsoM
+YUQhT6F1sX53SWcKxEP/aJ2kltSlPFX99e3Vx9eRkceV1oe2NM6ZG8hnYCfCAMeJ
+jRTpbqCGaAsEHFtIx6wt3zEtUXIVg4aYFQs/qjTjeP8ByIj0b4lZrceEoTeRimuj
++4aAI+jBxLkwaN3hseQHzRNpgPehIVV/0RU92yzOD/WN4YwE6rwjKEI1lihHNBDa
++DwGtGbHmIUzjW1qArig+mzUIhfYIJAxrx20ynPz/Q+C7+iXhTDAYQlxTle0pX8m
+yM2DUdPo97eLOzQ4JDHxtcN3ntTEJKKvrmzKvWuxy/yoLwS7MtLH6RETTHabH3Qd
+CP83X7z8zTyxgPxHdfHo9sgR/4C9RHGJx4OpBTQaiqfjSpDqJSIQdbrHGOQDgYwL
+KQyiQuhukmNgRCB6dJoZJ/MyaNuMsXV9QobsDHW1oSuCvPAihVoWHJxt8m4Ma0jJ
+EIbEPT2Umw1F/P+CeXnVQwhPvzQKHCa+6cC/YdjTqIKLmQV8X3HUBUIMhP2JGDic
+MnUipTm/RwWZVOjCJaFqk5sVq3L0Lyd0XVUWSK1a4IcrsA==
+-----END CERTIFICATE-----
diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem b/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem
new file mode 100644
index 0000000000000..f0746c162421c
--- /dev/null
+++ b/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+p2aUlQOLXU9SjQTirZbdCZP0XHtFsg==
+-----END PRIVATE KEY-----
diff --git a/tests/ApkVerityTest/testdata/README.md b/tests/ApkVerityTest/testdata/README.md
new file mode 100644
index 0000000000000..163cb183a5add
--- /dev/null
+++ b/tests/ApkVerityTest/testdata/README.md
@@ -0,0 +1,13 @@
+This test only runs on rooted / debuggable device.
+
+The test tries to install subsets of base.{apk,dm}, split.{apk,dm} and their
+corresponding .fsv_sig files (generated by build rule). If installed, the
+tests also tries to tamper with the file at absolute disk offset to verify
+if fs-verity is effective.
+
+How to generate dex metadata (.dm)
+==================================
+
+ adb shell profman --generate-test-profile=/data/local/tmp/primary.prof
+ adb pull /data/local/tmp/primary.prof
+ zip foo.dm primary.prof