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/Android.bp b/tests/Android.bp
index 74d58f5..0062846 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -91,3 +91,11 @@
     ],
     type: "cpio",
 }
+
+genrule {
+    name: "test-payload-metadata",
+    tools: ["mk_payload"],
+    cmd: "$(location mk_payload) --metadata-only $(in) $(out)",
+    srcs: ["test-payload-metadata-config.json"],
+    out: ["test-payload-metadata.img"],
+}
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
diff --git a/tests/test-payload-metadata-config.json b/tests/test-payload-metadata-config.json
new file mode 100644
index 0000000..3c56e5f
--- /dev/null
+++ b/tests/test-payload-metadata-config.json
@@ -0,0 +1,19 @@
+{
+  "_comment": "This file is to create a payload-metadata partition for payload.img which is for MicrodroidTestApp to run with assets/vm_config.json",
+  "apexes": [
+    {
+      "name": "com.android.os.statsd",
+      "path": ""
+    },
+    {
+      "name": "com.android.adbd",
+      "path": ""
+    }
+  ],
+  "apk": {
+    "name": "microdroid-apk",
+    "path": "",
+    "idsig_path": ""
+  },
+  "payload_config_path": "/mnt/apk/assets/vm_config.json"
+}
\ No newline at end of file