Verify and mount extra apks

virtualizationservice currently passes extra apks and corresponding
idsigs as block devices. microdroid_manager will read the payload config
and mount the apks to /mnt/extra-apk/{index} after verifying them.

Bug: 205224817
Test: atest ComposHostTestCases MicrodroidHostTestCases
Test: manually edit vm config and see apks mounted
Change-Id: I9afa03cb7fabe0ca16b7926c4480d0b32c8bcd04
This commit is contained in:
Inseob Kim 2021-12-01 19:49:00 +09:00
parent e275568b0d
commit 197748b652
6 changed files with 159 additions and 43 deletions

View File

@ -18,7 +18,6 @@ rust_defaults {
"liblibc",
"libnix",
"libnum_traits",
"librustutils",
"libscopeguard",
"libuuid",
],

View File

@ -29,7 +29,6 @@ use anyhow::{bail, Context, Result};
use clap::{App, Arg};
use idsig::{HashAlgorithm, V4Signature};
use itertools::Itertools;
use rustutils::system_properties;
use std::fmt::Debug;
use std::fs;
use std::fs::File;
@ -40,27 +39,26 @@ fn main() -> Result<()> {
let matches = App::new("apkdmverity")
.about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
.arg(Arg::from_usage(
"--apk... <apk_path> <idsig_path> <name> \
'Input APK file, idsig file, and the name of the block device. The APK \
file must be signed using the APK signature scheme 4. The block device \
is created at \"/dev/mapper/<name>\".'",
))
"--apk... <apk_path> <idsig_path> <name> <root_hash> \
'Input APK file, idsig file, name of the block device, and root hash. \
The APK file must be signed using the APK signature scheme 4. The \
block device is created at \"/dev/mapper/<name>\".' root_hash is \
optional; idsig file's root hash will be used if specified as \"none\"."
))
.arg(Arg::with_name("verbose").short("v").long("verbose").help("Shows verbose output"))
.get_matches();
let apks = matches.values_of("apk").unwrap();
assert!(apks.len() % 3 == 0);
let roothash = if let Ok(val) = system_properties::read("microdroid_manager.apk_root_hash") {
Some(util::parse_hexstring(&val)?)
} else {
// This failure is not an error. We will use the roothash read from the idsig file.
None
};
assert!(apks.len() % 4 == 0);
let verbose = matches.is_present("verbose");
for (apk, idsig, name) in apks.tuples() {
for (apk, idsig, name, roothash) in apks.tuples() {
let roothash = if roothash != "none" {
Some(util::parse_hexstring(roothash).expect("failed to parse roothash"))
} else {
None
};
let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
if verbose {
println!(

View File

@ -18,9 +18,13 @@ on early-init
start ueventd
mkdir /mnt/apk 0755 system system
mkdir /mnt/extra-apk 0755 root root
# Microdroid_manager starts apkdmverity/zipfuse/apexd
start microdroid_manager
# restorecon so microdroid_manager can create subdirectories
restorecon /mnt/extra-apk
# Wait for apexd to finish activating APEXes before starting more processes.
wait_for_prop apexd.status activated
perform_apex_config

View File

@ -17,7 +17,9 @@ rust_defaults {
"libbinder_rpc_unstable_bindgen",
"libbinder_rs",
"libbyteorder",
"libglob",
"libidsig",
"libitertools",
"libkernlog",
"liblibc",
"liblog_rust",

View File

@ -314,6 +314,7 @@ fn get_key() -> ZeroOnDropKey {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct MicrodroidData {
pub apk_data: ApkData,
pub extra_apks_data: Vec<ApkData>,
pub apex_data: Vec<ApexData>,
pub bootconfig: Box<[u8]>,
}

View File

@ -23,7 +23,9 @@ use anyhow::{anyhow, bail, ensure, Context, Error, Result};
use apkverify::{get_public_key_der, verify};
use binder::unstable_api::{new_spibinder, AIBinder};
use binder::{FromIBinder, Strong};
use glob::glob;
use idsig::V4Signature;
use itertools::sorted;
use log::{error, info, warn};
use microdroid_metadata::{write_metadata, Metadata};
use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
@ -31,7 +33,7 @@ use once_cell::sync::OnceCell;
use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
use rustutils::system_properties;
use rustutils::system_properties::PropertyWatcher;
use std::fs::{self, File, OpenOptions};
use std::fs::{self, create_dir, File, OpenOptions};
use std::os::unix::io::{FromRawFd, IntoRawFd};
use std::path::Path;
use std::process::{Child, Command, Stdio};
@ -44,13 +46,11 @@ use android_system_virtualmachineservice::aidl::android::system::virtualmachines
};
const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
const APK_DM_VERITY_ARGUMENT: ApkDmverityArgument = {
ApkDmverityArgument {
apk: "/dev/block/by-name/microdroid-apk",
idsig: "/dev/block/by-name/microdroid-apk-idsig",
name: "microdroid-apk",
}
};
const MAIN_APK_PATH: &str = "/dev/block/by-name/microdroid-apk";
const MAIN_APK_IDSIG_PATH: &str = "/dev/block/by-name/microdroid-apk-idsig";
const MAIN_APK_DEVICE_NAME: &str = "microdroid-apk";
const EXTRA_APK_PATH_PATTERN: &str = "/dev/block/by-name/extra-apk-*";
const EXTRA_IDSIG_PATH_PATTERN: &str = "/dev/block/by-name/extra-idsig-*";
const DM_MOUNTED_APK_PATH: &str = "/dev/block/mapper/microdroid-apk";
const APKDMVERITY_BIN: &str = "/system/bin/apkdmverity";
const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
@ -170,7 +170,16 @@ fn try_run_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<i32>
!metadata.payload_config_path.is_empty(),
MicrodroidError::InvalidConfig("No payload_config_path in metadata".to_string())
);
let config = load_config(Path::new(&metadata.payload_config_path))?;
if config.extra_apks.len() != verified_data.extra_apks_data.len() {
return Err(anyhow!(
"config expects {} extra apks, but found only {}",
config.extra_apks.len(),
verified_data.extra_apks_data.len()
));
}
mount_extra_apks(&config)?;
let fake_secret = "This is a placeholder for a value that is derived from the images that are loaded in the VM.";
if let Err(err) = rustutils::system_properties::write("ro.vmsecret.keymint", fake_secret) {
@ -192,6 +201,7 @@ struct ApkDmverityArgument<'a> {
apk: &'a str,
idsig: &'a str,
name: &'a str,
saved_root_hash: Option<&'a RootHash>,
}
fn run_apkdmverity(args: &[ApkDmverityArgument]) -> Result<Child> {
@ -201,6 +211,11 @@ fn run_apkdmverity(args: &[ApkDmverityArgument]) -> Result<Child> {
for argument in args {
cmd.arg("--apk").arg(argument.apk).arg(argument.idsig).arg(argument.name);
if let Some(root_hash) = argument.saved_root_hash {
cmd.arg(&to_hex_string(root_hash));
} else {
cmd.arg("none");
}
}
cmd.spawn().context("Spawn apkdmverity")
@ -236,19 +251,86 @@ fn verify_payload(
);
}
// Verify main APK
let root_hash = saved_data.map(|d| &d.apk_data.root_hash);
let root_hash_from_idsig = get_apk_root_hash_from_idsig()?;
let root_hash_from_idsig = get_apk_root_hash_from_idsig(MAIN_APK_IDSIG_PATH)?;
let root_hash_trustful = root_hash == Some(&root_hash_from_idsig);
// If root_hash can be trusted, pass it to apkdmverity so that it uses the passed root_hash
// instead of the value read from the idsig file.
if root_hash_trustful {
let root_hash = to_hex_string(root_hash.unwrap());
system_properties::write("microdroid_manager.apk_root_hash", &root_hash)?;
let main_apk_argument = {
ApkDmverityArgument {
apk: MAIN_APK_PATH,
idsig: MAIN_APK_IDSIG_PATH,
name: MAIN_APK_DEVICE_NAME,
saved_root_hash: if root_hash_trustful {
Some(root_hash_from_idsig.as_ref())
} else {
None
},
}
};
let mut apkdmverity_arguments = vec![main_apk_argument];
// Verify extra APKs
// For now, we can't read the payload config, so glob APKs and idsigs.
// Later, we'll see if it matches with the payload config.
// sort globbed paths to match apks (extra-apk-{idx}) and idsigs (extra-idsig-{idx})
// e.g. "extra-apk-0" corresponds to "extra-idsig-0"
let extra_apks =
sorted(glob(EXTRA_APK_PATH_PATTERN)?.collect::<Result<Vec<_>, _>>()?).collect::<Vec<_>>();
let extra_idsigs =
sorted(glob(EXTRA_IDSIG_PATH_PATTERN)?.collect::<Result<Vec<_>, _>>()?).collect::<Vec<_>>();
if extra_apks.len() != extra_idsigs.len() {
return Err(anyhow!(
"Extra apks/idsigs mismatch: {} apks but {} idsigs",
extra_apks.len(),
extra_idsigs.len()
));
}
let extra_apks_count = extra_apks.len();
let (extra_apk_names, extra_root_hashes_from_idsig): (Vec<_>, Vec<_>) = extra_idsigs
.iter()
.enumerate()
.map(|(i, extra_idsig)| {
(
format!("extra-apk-{}", i),
get_apk_root_hash_from_idsig(extra_idsig.to_str().unwrap())
.expect("Can't find root hash from extra idsig"),
)
})
.unzip();
let saved_extra_root_hashes: Vec<_> = saved_data
.map(|d| d.extra_apks_data.iter().map(|apk_data| &apk_data.root_hash).collect())
.unwrap_or_else(Vec::new);
let extra_root_hashes_trustful: Vec<_> = extra_root_hashes_from_idsig
.iter()
.enumerate()
.map(|(i, root_hash_from_idsig)| {
saved_extra_root_hashes.get(i).copied() == Some(root_hash_from_idsig)
})
.collect();
for i in 0..extra_apks_count {
apkdmverity_arguments.push({
ApkDmverityArgument {
apk: extra_apks[i].to_str().unwrap(),
idsig: extra_idsigs[i].to_str().unwrap(),
name: &extra_apk_names[i],
saved_root_hash: if extra_root_hashes_trustful[i] {
Some(&extra_root_hashes_from_idsig[i])
} else {
None
},
}
});
}
// Start apkdmverity and wait for the dm-verify block
let mut apkdmverity_child = run_apkdmverity(&[APK_DM_VERITY_ARGUMENT])?;
let mut apkdmverity_child = run_apkdmverity(&apkdmverity_arguments)?;
// While waiting for apkdmverity to mount APK, gathers public keys and root digests from
// APEX payload.
@ -280,26 +362,47 @@ fn verify_payload(
// taken only when the root_hash is un-trustful which can be either when this is the first boot
// of the VM or APK was updated in the host.
// TODO(jooyung): consider multithreading to make this faster
let apk_pubkey = if !root_hash_trustful {
verify(DM_MOUNTED_APK_PATH).context(MicrodroidError::PayloadVerificationFailed(format!(
"failed to verify {}",
DM_MOUNTED_APK_PATH
)))?
} else {
get_public_key_der(DM_MOUNTED_APK_PATH)?
};
let main_apk_pubkey = get_public_key_from_apk(DM_MOUNTED_APK_PATH, root_hash_trustful)?;
let extra_apks_data = extra_root_hashes_from_idsig
.into_iter()
.enumerate()
.map(|(i, extra_root_hash)| {
let mount_path = format!("/dev/block/mapper/{}", &extra_apk_names[i]);
let apk_pubkey = get_public_key_from_apk(&mount_path, extra_root_hashes_trustful[i])?;
Ok(ApkData { root_hash: extra_root_hash, pubkey: apk_pubkey })
})
.collect::<Result<Vec<_>>>()?;
info!("payload verification successful. took {:#?}", start_time.elapsed().unwrap());
// At this point, we can ensure that the root_hash from the idsig file is trusted, either by
// fully verifying the APK or by comparing it with the saved root_hash.
Ok(MicrodroidData {
apk_data: ApkData { root_hash: root_hash_from_idsig, pubkey: apk_pubkey },
apk_data: ApkData { root_hash: root_hash_from_idsig, pubkey: main_apk_pubkey },
extra_apks_data,
apex_data: apex_data_from_payload,
bootconfig: get_bootconfig()?.clone().into_boxed_slice(),
})
}
fn mount_extra_apks(config: &VmPayloadConfig) -> Result<()> {
// For now, only the number of apks is important, as the mount point and dm-verity name is fixed
for i in 0..config.extra_apks.len() {
let mount_dir = format!("/mnt/extra-apk/{}", i);
create_dir(Path::new(&mount_dir)).context("Failed to create mount dir for extra apks")?;
// don't wait, just detach
run_zipfuse(
"fscontext=u:object_r:zipfusefs:s0,context=u:object_r:extra_apk_file:s0",
Path::new(&format!("/dev/block/mapper/extra-apk-{}", i)),
Path::new(&mount_dir),
)
.context("Failed to zipfuse extra apks")?;
}
Ok(())
}
// Waits until linker config is generated
fn wait_for_apex_config_done() -> Result<()> {
let mut prop = PropertyWatcher::new(APEX_CONFIG_DONE_PROP)?;
@ -313,17 +416,26 @@ fn wait_for_apex_config_done() -> Result<()> {
Ok(())
}
fn get_apk_root_hash_from_idsig() -> Result<Box<RootHash>> {
let mut idsig = File::open("/dev/block/by-name/microdroid-apk-idsig")?;
fn get_apk_root_hash_from_idsig(path: &str) -> Result<Box<RootHash>> {
let mut idsig = File::open(path)?;
let idsig = V4Signature::from(&mut idsig)?;
Ok(idsig.hashing_info.raw_root_hash)
}
fn get_public_key_from_apk(apk: &str, root_hash_trustful: bool) -> Result<Box<[u8]>> {
if !root_hash_trustful {
verify(apk).context(MicrodroidError::PayloadVerificationFailed(format!(
"failed to verify {}",
apk
)))
} else {
get_public_key_der(apk)
}
}
fn get_bootconfig() -> Result<&'static Vec<u8>> {
static VAL: OnceCell<Vec<u8>> = OnceCell::new();
VAL.get_or_try_init(|| {
fs::read("/proc/bootconfig").context("Failed to read bootconfig")
})
VAL.get_or_try_init(|| fs::read("/proc/bootconfig").context("Failed to read bootconfig"))
}
fn load_config(path: &Path) -> Result<VmPayloadConfig> {