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
This commit is contained in:
Jooyung Han 2022-02-22 05:22:23 +09:00
parent b7983a2683
commit 6afd667daf
7 changed files with 281 additions and 8 deletions

View File

@ -142,11 +142,16 @@ sh_test_host {
],
data: [
":com.android.virt",
"test.com.android.virt.pem",
":test.com.android.virt.pem",
],
test_suites: ["general-tests"],
}
filegroup {
name: "test.com.android.virt.pem",
srcs: ["test.com.android.virt.pem"],
}
// custom tool to replace bytes in a file
python_binary_host {
name: "replace_bytes",

View File

@ -269,24 +269,34 @@ Result<void> MakePayload(const Config& config, const std::string& metadata_file,
}
int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <config> <output>\n";
if (argc < 3 || argc > 4) {
std::cerr << "Usage: " << argv[0] << " [--metadata-only] <config> <output>\n";
return 1;
}
int arg_index = 1;
bool metadata_only = false;
if (strcmp(argv[arg_index], "--metadata-only") == 0) {
metadata_only = true;
arg_index++;
}
auto config = LoadConfig(argv[1]);
auto config = LoadConfig(argv[arg_index++]);
if (!config.ok()) {
std::cerr << "bad config: " << config.error() << '\n';
return 1;
}
const std::string output_file(argv[2]);
const std::string metadata_file = AppendFileName(output_file, "-metadata");
const std::string output_file(argv[arg_index++]);
const std::string metadata_file =
metadata_only ? output_file : AppendFileName(output_file, "-metadata");
if (const auto res = MakeMetadata(*config, metadata_file); !res.ok()) {
std::cerr << res.error() << '\n';
return 1;
}
if (metadata_only) {
return 0;
}
if (const auto res = MakePayload(*config, metadata_file, output_file); !res.ok()) {
std::cerr << res.error() << '\n';
return 1;

View File

@ -91,3 +91,11 @@ android_filesystem {
],
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"],
}

View File

@ -10,6 +10,7 @@ java_test_host {
"general-tests",
],
libs: [
"gson-prebuilt-jar",
"tradefed",
],
static_libs: [
@ -19,6 +20,31 @@ java_test_host {
data: [
":MicrodroidTestApp",
":microdroid_general_sepolicy.conf",
":test.com.android.virt.pem",
":test-payload-metadata",
],
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",
],
data_native_bins: ["sepolicy-analyze"],
}

View File

@ -43,7 +43,7 @@ import java.util.regex.Pattern;
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";

View File

@ -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.CommandStatus;
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 @@ public class MicrodroidTestCase extends VirtualizationTestCaseBase {
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 @@ public class MicrodroidTestCase extends VirtualizationTestCaseBase {
.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

View File

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