Test: Protected VM fails if images are signed by unknown
Arrange:
- prepare VM images signed with a test key
Act:
- start a protected VM
Assert:
- a boot process fails due to pubkey mismatch between pvmfw and
bootloader
Bug: 218934597
Test: atest MicrodroidHostTestCases
Change-Id: I05755ddf32839ef531ca9a11b2939bbc251ff1fb
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 10bcbf4..67a0e8d 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -10,6 +10,7 @@
"general-tests",
],
libs: [
+ "gson-prebuilt-jar",
"tradefed",
],
static_libs: [
@@ -19,6 +20,31 @@
data: [
":MicrodroidTestApp",
":microdroid_general_sepolicy.conf",
+ ":test.com.android.virt.pem",
+ ":test-payload-metadata",
],
- data_native_bins: ["sepolicy-analyze"],
+ data_native_bins: [
+ "sepolicy-analyze",
+ // For re-sign test
+ "avbtool",
+ "img2simg",
+ "lpmake",
+ "lpunpack",
+ "sign_virt_apex",
+ "simg2img",
+ ],
+ // java_test_host doesn't have data_native_libs but jni_libs can be used to put
+ // native modules under ./lib directory.
+ // This works because host tools have rpath (../lib and ./lib).
+ jni_libs: [
+ "libbase",
+ "libc++",
+ "libcrypto_utils",
+ "libcrypto",
+ "libext4_utils",
+ "liblog",
+ "liblp",
+ "libsparse",
+ "libz",
+ ],
}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index e3f1968..e45e524 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -43,7 +43,7 @@
public abstract class VirtualizationTestCaseBase extends BaseHostJUnit4Test {
protected static final String TEST_ROOT = "/data/local/tmp/virt/";
- private static final String VIRT_APEX = "/apex/com.android.virt/";
+ protected static final String VIRT_APEX = "/apex/com.android.virt/";
private static final int TEST_VM_ADB_PORT = 8000;
private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
private static final String INSTANCE_IMG = "instance.img";
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 10b90d3..d0bc91a 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -16,12 +16,14 @@
package android.virt.test;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.result.TestDescription;
@@ -33,14 +35,23 @@
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
@RunWith(DeviceJUnit4ClassRunner.class)
public class MicrodroidTestCase extends VirtualizationTestCaseBase {
@@ -77,6 +88,11 @@
return 0;
}
+ private boolean isProtectedVmSupported() throws DeviceNotAvailableException {
+ return getDevice().getBooleanProperty("ro.boot.hypervisor.protected_vm.supported",
+ false);
+ }
+
@Test
public void testCreateVmRequiresPermission() throws Exception {
// Revoke the MANAGE_VIRTUAL_MACHINE permission for the test app
@@ -98,6 +114,195 @@
.contains("android.permission.MANAGE_VIRTUAL_MACHINE permission"));
}
+ // Helper classes for (de)serialization of VM raw configs
+ static class VmRawConfig {
+ String bootloader;
+ List<Disk> disks;
+ int memory_mib;
+ @SerializedName("protected")
+ boolean isProtected;
+ }
+
+ static class Disk {
+ List<Partition> partitions;
+ boolean writable;
+ public void addPartition(String label, String path) {
+ if (partitions == null) {
+ partitions = new ArrayList<Partition>();
+ }
+ Partition partition = new Partition();
+ partition.label = label;
+ partition.path = path;
+ partitions.add(partition);
+ }
+ }
+
+ static class Partition {
+ String label;
+ String path;
+ boolean writable;
+ }
+
+ private String getPathForPackage(String packageName)
+ throws DeviceNotAvailableException {
+ CommandRunner android = new CommandRunner(getDevice());
+ String pathLine = android.run("pm", "path", packageName);
+ assertTrue("package not found", pathLine.startsWith("package:"));
+ return pathLine.substring("package:".length());
+ }
+
+ private void resignVirtApex(File virtApexDir, File signingKey) {
+ File signVirtApex = findTestFile("sign_virt_apex");
+
+ RunUtil runUtil = new RunUtil();
+ // Set the parent dir on the PATH (e.g. <workdir>/bin)
+ String separator = System.getProperty("path.separator");
+ String path = signVirtApex.getParentFile().getPath() + separator + System.getenv("PATH");
+ runUtil.setEnvVariable("PATH", path);
+
+ String resignCommand = String.format("sign_virt_apex %s %s",
+ signingKey.getPath(),
+ virtApexDir.getPath());
+ CommandResult result = runUtil.runTimedCmd(
+ 20 * 1000,
+ "/bin/bash",
+ "-c",
+ resignCommand);
+ String out = result.getStdout();
+ String err = result.getStderr();
+ assertEquals(
+ "resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n",
+ CommandStatus.SUCCESS, result.getStatus());
+ }
+
+ private String runMicrodroidWithResignedImages(boolean isProtected, boolean daemonize,
+ String consolePath) throws DeviceNotAvailableException, IOException {
+ CommandRunner android = new CommandRunner(getDevice());
+
+ File virtApexDir = FileUtil.createTempDir("virt_apex");
+
+ // Pull the virt apex's etc/ directory (which contains images and microdroid.json)
+ File virtApexEtcDir = new File(virtApexDir, "etc");
+ // We need only etc/ directory for images
+ assertTrue(virtApexEtcDir.mkdirs());
+ assertTrue(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir));
+
+ File testKey = findTestFile("test.com.android.virt.pem");
+ resignVirtApex(virtApexDir, testKey);
+
+ // Push back re-signed virt APEX contents and updated microdroid.json
+ getDevice().pushDir(virtApexDir, TEST_ROOT);
+
+ // Create the idsig file for the APK
+ final String apkPath = getPathForPackage(PACKAGE_NAME);
+ final String idSigPath = TEST_ROOT + "idsig";
+ android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath);
+
+ // Create the instance image for the VM
+ final String instanceImgPath = TEST_ROOT + "instance.img";
+ android.run(VIRT_APEX + "bin/vm", "create-partition", "--type instance",
+ instanceImgPath, Integer.toString(10 * 1024 * 1024));
+
+ // payload-metadata is prepared on host with the two APEXes and APK
+ final String payloadMetadataPath = TEST_ROOT + "payload-metadata.img";
+ getDevice().pushFile(findTestFile("test-payload-metadata.img"), payloadMetadataPath);
+
+ // Since Java APP can't start a VM with a custom image, here, we start a VM using `vm run`
+ // command with a VM Raw config which is equiv. to what virtualizationservice creates with
+ // a VM App config.
+ //
+ // 1. use etc/microdroid.json as base
+ // 2. add partitions: bootconfig, vbmeta, instance image
+ // 3. add a payload image disk with
+ // - payload-metadata
+ // - apexes
+ // - test apk
+ // - its idsig
+
+ // Load etc/microdroid.json
+ Gson gson = new Gson();
+ File microdroidConfigFile = new File(virtApexEtcDir, "microdroid.json");
+ VmRawConfig config = gson.fromJson(new FileReader(microdroidConfigFile),
+ VmRawConfig.class);
+
+ // Replace paths so that the config uses re-signed images from TEST_ROOT
+ config.bootloader = config.bootloader.replace(VIRT_APEX, TEST_ROOT);
+ for (Disk disk : config.disks) {
+ for (Partition part : disk.partitions) {
+ part.path = part.path.replace(VIRT_APEX, TEST_ROOT);
+ }
+ }
+
+ // Add partitions to the second disk
+ Disk secondDisk = config.disks.get(1);
+ secondDisk.addPartition("vbmeta",
+ TEST_ROOT + "etc/fs/microdroid_vbmeta_bootconfig.img");
+ secondDisk.addPartition("bootconfig",
+ TEST_ROOT + "etc/microdroid_bootconfig.full_debuggable");
+ secondDisk.addPartition("vm-instance", instanceImgPath);
+
+ // Add payload image disk with partitions:
+ // - payload-metadata
+ // - apexes: com.android.os.statsd, com.android.adbd
+ // - apk and idsig
+ Disk payloadDisk = new Disk();
+ payloadDisk.addPartition("payload-metadata", payloadMetadataPath);
+ String[] apexes = {"com.android.os.statsd", "com.android.adbd"};
+ for (int i = 0; i < apexes.length; i++) {
+ String apexPath = getPathForPackage(apexes[i]);
+ String filename = apexes[i] + ".apex";
+ File localApexFile = new File(virtApexDir, filename);
+ String remoteApexFile = TEST_ROOT + filename;
+ // Since `adb shell vm` can't access apex_data_file, we `adb pull/push` apex files.
+ getDevice().pullFile(apexPath, localApexFile);
+ getDevice().pushFile(localApexFile, remoteApexFile);
+ payloadDisk.addPartition("microdroid-apex-" + i, remoteApexFile);
+ }
+ payloadDisk.addPartition("microdroid-apk", apkPath);
+ payloadDisk.addPartition("microdroid-apk-idsig", idSigPath);
+ config.disks.add(payloadDisk);
+
+ config.isProtected = isProtected;
+
+ // Write updated raw config
+ final String configPath = TEST_ROOT + "raw_config.json";
+ getDevice().pushString(gson.toJson(config), configPath);
+
+ final String logPath = TEST_ROOT + "log";
+ final String ret = android.runWithTimeout(
+ 60 * 1000,
+ VIRT_APEX + "bin/vm run",
+ daemonize ? "--daemonize" : "",
+ (consolePath != null) ? "--console " + consolePath : "",
+ "--log " + logPath,
+ configPath);
+ Pattern pattern = Pattern.compile("with CID (\\d+)");
+ Matcher matcher = pattern.matcher(ret);
+ assertTrue(matcher.find());
+ return matcher.group(1);
+ }
+
+ @Test
+ public void testBootFailsWhenProtectedVmStartsWithImagesSignedWithDifferentKey()
+ throws Exception {
+ assumeTrue(isProtectedVmSupported());
+ String consolePath = TEST_ROOT + "console";
+ // Run VM without --daemonize. It will shut down due to boot failure.
+ runMicrodroidWithResignedImages(/*protected=*/true, /*daemonize=*/false, consolePath);
+ assertThat(getDevice().pullFileContents(consolePath),
+ containsString("pvmfw boot failed"));
+ }
+
+ @Test
+ public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey()
+ throws Exception {
+ String cid = runMicrodroidWithResignedImages(/*protected=*/false,
+ /*daemonize=*/true, /*consolePath=*/null);
+ // Adb connection to the microdroid means that boot succeeded.
+ adbConnectToMicrodroid(getDevice(), cid);
+ shutdownMicrodroid(getDevice(), cid);
+ }
+
@Test
public void testMicrodroidBoots() throws Exception {
final String configPath = "assets/vm_config.json"; // path inside the APK