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
diff --git a/apkdmverity/Android.bp b/apkdmverity/Android.bp
index 403e726..06d4500 100644
--- a/apkdmverity/Android.bp
+++ b/apkdmverity/Android.bp
@@ -18,7 +18,6 @@
         "liblibc",
         "libnix",
         "libnum_traits",
-        "librustutils",
         "libscopeguard",
         "libuuid",
     ],
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index a8a8f15..dbf3131 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -29,7 +29,6 @@
 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 @@
     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!(
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 664402f..86f6d0a 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -18,9 +18,13 @@
     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
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index c69d875..23a61d9 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -17,7 +17,9 @@
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libbyteorder",
+        "libglob",
         "libidsig",
+        "libitertools",
         "libkernlog",
         "liblibc",
         "liblog_rust",
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 8ba6f51..aadb71f 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -314,6 +314,7 @@
 #[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]>,
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index efe6126..fccf031 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -23,7 +23,9 @@
 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 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 @@
 };
 
 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 @@
         !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 @@
     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 @@
 
     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 @@
         );
     }
 
+    // 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 @@
     // 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 @@
     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> {