Merge "Grant crosvm CAP_IPC_LOCK via filesystem config."
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 353b597..471b0cf 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -14,6 +14,7 @@
         "libandroid_logger",
         "libanyhow",
         "libauthfs_crypto_bindgen",
+        "libauthfs_fsverity_metadata",
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libcfg_if",
@@ -69,15 +70,12 @@
     srcs: [
         "testdata/cert.der",
         "testdata/input.4k",
-        "testdata/input.4k.fsv_sig",
-        "testdata/input.4k.merkle_dump",
+        "testdata/input.4k.fsv_meta",
         "testdata/input.4k1",
-        "testdata/input.4k1.fsv_sig",
-        "testdata/input.4k1.merkle_dump",
+        "testdata/input.4k1.fsv_meta",
         "testdata/input.4m",
-        "testdata/input.4m.fsv_sig",
-        "testdata/input.4m.merkle_dump",
-        "testdata/input.4m.merkle_dump.bad",
+        "testdata/input.4m.fsv_meta",
+        "testdata/input.4m.fsv_meta.bad_merkle",
     ],
 }
 
diff --git a/authfs/fd_server/Android.bp b/authfs/fd_server/Android.bp
index 327df0d..9499cd2 100644
--- a/authfs/fd_server/Android.bp
+++ b/authfs/fd_server/Android.bp
@@ -9,6 +9,7 @@
         "authfs_aidl_interface-rust",
         "libandroid_logger",
         "libanyhow",
+        "libauthfs_fsverity_metadata",
         "libbinder_common",
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index c2206c8..125b991 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -38,6 +38,9 @@
 use authfs_aidl_interface::binder::{
     BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
 };
+use authfs_fsverity_metadata::{
+    get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
+};
 use binder_common::{new_binder_exception, new_binder_service_specific_error};
 
 /// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
@@ -51,13 +54,8 @@
         /// The file to read from. fs-verity metadata can be retrieved from this file's FD.
         file: File,
 
-        /// Alternative Merkle tree stored in another file.
-        /// TODO(205987437): Replace with .fsv_meta file.
-        alt_merkle_tree: Option<File>,
-
-        /// Alternative signature stored in another file.
-        /// TODO(205987437): Replace with .fsv_meta file.
-        alt_signature: Option<File>,
+        // Alternative metadata storing merkle tree and signature.
+        alt_metadata: Option<Box<FSVerityMetadata>>,
     },
 
     /// A readable/writable file to serve by this server. This backing file should just be a
@@ -139,23 +137,23 @@
         let offset: u64 = validate_and_cast_offset(offset)?;
 
         self.handle_fd(id, |config| match config {
-            FdConfig::Readonly { file, alt_merkle_tree, .. } => {
-                if let Some(tree_file) = &alt_merkle_tree {
-                    read_into_buf(tree_file, size, offset).map_err(|e| {
+            FdConfig::Readonly { file, alt_metadata, .. } => {
+                let mut buf = vec![0; size];
+
+                let s = if let Some(metadata) = &alt_metadata {
+                    metadata.read_merkle_tree(offset, &mut buf).map_err(|e| {
                         error!("readFsverityMerkleTree: read error: {}", e);
                         new_errno_error(Errno::EIO)
-                    })
+                    })?
                 } else {
-                    let mut buf = vec![0; size];
-                    let s = fsverity::read_merkle_tree(file.as_raw_fd(), offset, &mut buf)
-                        .map_err(|e| {
-                            error!("readFsverityMerkleTree: failed to retrieve merkle tree: {}", e);
-                            new_errno_error(Errno::EIO)
-                        })?;
-                    debug_assert!(s <= buf.len(), "Shouldn't return more bytes than asked");
-                    buf.truncate(s);
-                    Ok(buf)
-                }
+                    fsverity::read_merkle_tree(file.as_raw_fd(), offset, &mut buf).map_err(|e| {
+                        error!("readFsverityMerkleTree: failed to retrieve merkle tree: {}", e);
+                        new_errno_error(Errno::EIO)
+                    })?
+                };
+                debug_assert!(s <= buf.len(), "Shouldn't return more bytes than asked");
+                buf.truncate(s);
+                Ok(buf)
             }
             FdConfig::ReadWrite(_file) => {
                 // For a writable file, Merkle tree is not expected to be served since Auth FS
@@ -169,15 +167,16 @@
 
     fn readFsveritySignature(&self, id: i32) -> BinderResult<Vec<u8>> {
         self.handle_fd(id, |config| match config {
-            FdConfig::Readonly { file, alt_signature, .. } => {
-                if let Some(sig_file) = &alt_signature {
-                    // Supposedly big enough buffer size to store signature.
-                    let size = MAX_REQUESTING_DATA as usize;
-                    let offset = 0;
-                    read_into_buf(sig_file, size, offset).map_err(|e| {
-                        error!("readFsveritySignature: read error: {}", e);
-                        new_errno_error(Errno::EIO)
-                    })
+            FdConfig::Readonly { file, alt_metadata, .. } => {
+                if let Some(metadata) = &alt_metadata {
+                    if let Some(signature) = &metadata.signature {
+                        Ok(signature.clone())
+                    } else {
+                        Err(new_binder_exception(
+                            ExceptionCode::SERVICE_SPECIFIC,
+                            "metadata doesn't contain a signature".to_string(),
+                        ))
+                    }
                 } else {
                     let mut buf = vec![0; MAX_REQUESTING_DATA as usize];
                     let s = fsverity::read_signature(file.as_raw_fd(), &mut buf).map_err(|e| {
@@ -267,11 +266,12 @@
             FdConfig::InputDir(dir) => {
                 let file = open_readonly_at(dir.as_raw_fd(), &path_buf).map_err(new_errno_error)?;
 
-                // TODO(205987437): Provide the corresponding ".fsv_meta" file when it's created.
-                Ok((
-                    file.as_raw_fd(),
-                    FdConfig::Readonly { file, alt_merkle_tree: None, alt_signature: None },
-                ))
+                let metadata_path_buf = get_fsverity_metadata_path(&path_buf);
+                let metadata = open_readonly_at(dir.as_raw_fd(), &metadata_path_buf)
+                    .ok()
+                    .and_then(|f| parse_fsverity_metadata(f).ok());
+
+                Ok((file.as_raw_fd(), FdConfig::Readonly { file, alt_metadata: metadata }))
             }
             FdConfig::OutputDir(_) => {
                 Err(new_errno_error(Errno::ENOSYS)) // TODO: Implement when needed
diff --git a/authfs/fd_server/src/main.rs b/authfs/fd_server/src/main.rs
index f17b7e8..fe0475f 100644
--- a/authfs/fd_server/src/main.rs
+++ b/authfs/fd_server/src/main.rs
@@ -34,6 +34,7 @@
 use std::os::unix::io::FromRawFd;
 
 use aidl::{FdConfig, FdService};
+use authfs_fsverity_metadata::parse_fsverity_metadata;
 
 const RPC_SERVICE_PORT: u32 = 3264; // TODO: support dynamic port for multiple fd_server instances
 
@@ -54,17 +55,19 @@
 fn parse_arg_ro_fds(arg: &str) -> Result<(i32, FdConfig)> {
     let result: Result<Vec<i32>, _> = arg.split(':').map(|x| x.parse::<i32>()).collect();
     let fds = result?;
-    if fds.len() > 3 {
+    if fds.len() > 2 {
         bail!("Too many options: {}", arg);
     }
     Ok((
         fds[0],
         FdConfig::Readonly {
             file: fd_to_file(fds[0])?,
-            // Alternative Merkle tree, if provided
-            alt_merkle_tree: fds.get(1).map(|fd| fd_to_file(*fd)).transpose()?,
-            // Alternative signature, if provided
-            alt_signature: fds.get(2).map(|fd| fd_to_file(*fd)).transpose()?,
+            // Alternative metadata source, if provided
+            alt_metadata: fds
+                .get(1)
+                .map(|fd| fd_to_file(*fd))
+                .transpose()?
+                .and_then(|f| parse_fsverity_metadata(f).ok()),
         },
     ))
 }
diff --git a/authfs/src/auth.rs b/authfs/src/auth.rs
index 71ad858..729b4d2 100644
--- a/authfs/src/auth.rs
+++ b/authfs/src/auth.rs
@@ -20,7 +20,7 @@
 // verify the signature, not the full certificate chain.
 
 pub trait Authenticator {
-    fn verify(&self, signature: &[u8], signed_data: &[u8]) -> io::Result<bool>;
+    fn verify(&self, signature: Option<&[u8]>, signed_data: &[u8]) -> io::Result<bool>;
 }
 
 pub struct FakeAuthenticator {
@@ -39,7 +39,7 @@
 }
 
 impl Authenticator for FakeAuthenticator {
-    fn verify(&self, _signature_pem: &[u8], _signed_data: &[u8]) -> io::Result<bool> {
+    fn verify(&self, _signature_pem: Option<&[u8]>, _signed_data: &[u8]) -> io::Result<bool> {
         Ok(self.should_allow)
     }
 }
diff --git a/authfs/src/fsverity/metadata/Android.bp b/authfs/src/fsverity/metadata/Android.bp
new file mode 100644
index 0000000..39ce515
--- /dev/null
+++ b/authfs/src/fsverity/metadata/Android.bp
@@ -0,0 +1,20 @@
+rust_bindgen {
+    name: "libauthfs_fsverity_metadata_bindgen",
+    wrapper_src: "metadata.hpp",
+    crate_name: "authfs_fsverity_metadata_bindgen",
+    source_stem: "metadata_bindings",
+    apex_available: ["com.android.virt"],
+}
+
+rust_library {
+    name: "libauthfs_fsverity_metadata",
+    crate_name: "authfs_fsverity_metadata",
+    srcs: [
+        "metadata.rs",
+    ],
+    rustlibs: [
+        "libauthfs_fsverity_metadata_bindgen",
+    ],
+    edition: "2018",
+    apex_available: ["com.android.virt"],
+}
diff --git a/authfs/src/fsverity/metadata/metadata.hpp b/authfs/src/fsverity/metadata/metadata.hpp
new file mode 100644
index 0000000..7bbd3da
--- /dev/null
+++ b/authfs/src/fsverity/metadata/metadata.hpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AUTHFS_FSVERITY_METADATA_H
+#define AUTHFS_FSVERITY_METADATA_H
+
+// This file contains the format of fs-verity metadata (.fsv_meta).
+//
+// The header format of .fsv_meta is:
+//
+// +-----------+---------------------------------------------+------------+
+// |  Address  |  Description                                |    Size    |
+// +-----------+---------------------------------------------+------------+
+// |  0x0000   |  32-bit LE, version of the format           |     4      |
+// |           |                                             |            |
+// |  0x0004   |  fsverity_descriptor (see linux/fsverity.h) |    256     |
+// |           |                                             |            |
+// |  0x0104   |  32-bit LE, type of signature               |     4      |
+// |           |  (0: NONE, 1: PKCS7, 2: RAW)                |            |
+// |           |                                             |            |
+// |  0x0108   |  32-bit LE, size of signature               |     4      |
+// |           |                                             |            |
+// |  0x010C   |  signature                                  | See 0x0108 |
+// +-----------+---------------------------------------------+------------+
+//
+// After the header, merkle tree dump exists at the first 4K boundary. Usually it's 0x1000, but it
+// could be, for example, 0x2000 or 0x3000, depending on the size of header.
+//
+// TODO(b/193113326): sync with build/make/tools/releasetools/fsverity_metadata_generator.py
+
+#include <stddef.h>
+#include <stdint.h>
+#include <linux/fsverity.h>
+
+const uint64_t CHUNK_SIZE = 4096;
+
+enum class FSVERITY_SIGNATURE_TYPE : __le32 {
+    NONE = 0,
+    PKCS7 = 1,
+    RAW = 2,
+};
+
+struct fsverity_metadata_header {
+    __le32 version;
+    fsverity_descriptor descriptor;
+    FSVERITY_SIGNATURE_TYPE signature_type;
+    __le32 signature_size;
+} __attribute__((packed));
+
+#endif   // AUTHFS_FSVERITY_METADATA_H
diff --git a/authfs/src/fsverity/metadata/metadata.rs b/authfs/src/fsverity/metadata/metadata.rs
new file mode 100644
index 0000000..0092bee
--- /dev/null
+++ b/authfs/src/fsverity/metadata/metadata.rs
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! Rust bindgen interface for FSVerity Metadata file (.fsv_meta)
+use authfs_fsverity_metadata_bindgen::{
+    fsverity_metadata_header, FSVERITY_SIGNATURE_TYPE_NONE, FSVERITY_SIGNATURE_TYPE_PKCS7,
+    FSVERITY_SIGNATURE_TYPE_RAW,
+};
+
+use std::cmp::min;
+use std::os::unix::fs::MetadataExt;
+
+/// Structure for parsed metadata.
+pub struct FSVerityMetadata {
+    /// Header for the metadata.
+    pub header: fsverity_metadata_header,
+
+    /// Optional signature for the metadata.
+    pub signature: Option<Vec<u8>>,
+
+    metadata_file: File,
+
+    merkle_tree_offset: u64,
+}
+
+impl FSVerityMetadata {
+    /// Read the raw Merkle tree from the metadata, if it exists. The API semantics is similar to a
+    /// regular pread(2), and may not return full requested buffer.
+    pub fn read_merkle_tree(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
+        let start = self.merkle_tree_offset + offset;
+        let end = min(self.metadata_file.metadata()?.size(), start + buf.len() as u64);
+        let read_size = (end - start) as usize;
+        debug_assert!(read_size <= buf.len());
+        self.metadata_file.read_exact_at(&mut buf[..read_size], start)?;
+        Ok(read_size)
+    }
+}
+
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::{self, Read, Seek};
+use std::mem::{size_of, zeroed};
+use std::os::unix::fs::FileExt;
+use std::path::{Path, PathBuf};
+use std::slice::from_raw_parts_mut;
+
+/// Common block and page size in Linux.
+pub const CHUNK_SIZE: u64 = authfs_fsverity_metadata_bindgen::CHUNK_SIZE;
+
+/// Derive a path of metadata for a given path.
+/// e.g. "system/framework/foo.jar" -> "system/framework/foo.jar.fsv_meta"
+pub fn get_fsverity_metadata_path(path: &Path) -> PathBuf {
+    let mut os_string: OsString = path.into();
+    os_string.push(".fsv_meta");
+    os_string.into()
+}
+
+/// Parse metadata from given file, and returns a structure for the metadata.
+pub fn parse_fsverity_metadata(mut metadata_file: File) -> io::Result<Box<FSVerityMetadata>> {
+    let header_size = size_of::<fsverity_metadata_header>();
+
+    // SAFETY: the header doesn't include any pointers
+    let header: fsverity_metadata_header = unsafe {
+        let mut header: fsverity_metadata_header = zeroed();
+        let buffer = from_raw_parts_mut(
+            &mut header as *mut fsverity_metadata_header as *mut u8,
+            header_size,
+        );
+        metadata_file.read_exact(buffer)?;
+
+        // TODO(inseob): This doesn't seem ideal. Maybe we can consider nom?
+        header.version = u32::from_le(header.version);
+        header.descriptor.data_size = u64::from_le(header.descriptor.data_size);
+        header.signature_type = u32::from_le(header.signature_type);
+        header.signature_size = u32::from_le(header.signature_size);
+        header
+    };
+
+    if header.version != 1 {
+        return Err(io::Error::new(io::ErrorKind::Other, "unsupported metadata version"));
+    }
+
+    let signature = match header.signature_type {
+        FSVERITY_SIGNATURE_TYPE_NONE => None,
+        FSVERITY_SIGNATURE_TYPE_PKCS7 | FSVERITY_SIGNATURE_TYPE_RAW => {
+            // TODO: unpad pkcs7?
+            let mut buf = vec![0u8; header.signature_size as usize];
+            metadata_file.read_exact(&mut buf)?;
+            Some(buf)
+        }
+        _ => return Err(io::Error::new(io::ErrorKind::Other, "unknown signature type")),
+    };
+
+    // merkle tree is at the next 4K boundary
+    let merkle_tree_offset =
+        (metadata_file.stream_position()? + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
+
+    Ok(Box::new(FSVerityMetadata { header, signature, metadata_file, merkle_tree_offset }))
+}
diff --git a/authfs/src/fsverity/verifier.rs b/authfs/src/fsverity/verifier.rs
index 4a18c6a..1add37a 100644
--- a/authfs/src/fsverity/verifier.rs
+++ b/authfs/src/fsverity/verifier.rs
@@ -137,7 +137,7 @@
         authenticator: &A,
         chunked_file: F,
         file_size: u64,
-        sig: Vec<u8>,
+        sig: Option<&[u8]>,
         merkle_tree: M,
     ) -> Result<VerifiedFileReader<F, M>, FsverityError> {
         let mut buf = [0u8; CHUNK_SIZE as usize];
@@ -147,7 +147,7 @@
         }
         let root_hash = Sha256Hasher::new()?.update(&buf[..])?.finalize()?;
         let formatted_digest = build_fsverity_formatted_digest(&root_hash, file_size)?;
-        let valid = authenticator.verify(&sig, &formatted_digest)?;
+        let valid = authenticator.verify(sig, &formatted_digest)?;
         if valid {
             Ok(VerifiedFileReader { chunked_file, file_size, merkle_tree, root_hash })
         } else {
@@ -175,9 +175,9 @@
     use crate::auth::FakeAuthenticator;
     use crate::file::ReadByChunk;
     use anyhow::Result;
+    use authfs_fsverity_metadata::{parse_fsverity_metadata, FSVerityMetadata};
     use std::cmp::min;
-    use std::fs::{self, File};
-    use std::io::Read;
+    use std::fs::File;
     use std::os::unix::fs::FileExt;
 
     struct LocalFileReader {
@@ -210,7 +210,17 @@
         }
     }
 
-    type LocalVerifiedFileReader = VerifiedFileReader<LocalFileReader, LocalFileReader>;
+    type LocalVerifiedFileReader = VerifiedFileReader<LocalFileReader, MerkleTreeReader>;
+
+    pub struct MerkleTreeReader {
+        metadata: Box<FSVerityMetadata>,
+    }
+
+    impl ReadByChunk for MerkleTreeReader {
+        fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+            self.metadata.read_merkle_tree(chunk_index * CHUNK_SIZE, buf)
+        }
+    }
 
     fn total_chunk_number(file_size: u64) -> u64 {
         (file_size + 4095) / 4096
@@ -219,28 +229,28 @@
     // Returns a reader with fs-verity verification and the file size.
     fn new_reader_with_fsverity(
         content_path: &str,
-        merkle_tree_path: &str,
-        signature_path: &str,
+        metadata_path: &str,
     ) -> Result<(LocalVerifiedFileReader, u64)> {
         let file_reader = LocalFileReader::new(File::open(content_path)?)?;
         let file_size = file_reader.len();
-        let merkle_tree = LocalFileReader::new(File::open(merkle_tree_path)?)?;
-        let mut sig = Vec::new();
-        let _ = File::open(signature_path)?.read_to_end(&mut sig)?;
+        let metadata = parse_fsverity_metadata(File::open(metadata_path)?)?;
         let authenticator = FakeAuthenticator::always_succeed();
         Ok((
-            VerifiedFileReader::new(&authenticator, file_reader, file_size, sig, merkle_tree)?,
+            VerifiedFileReader::new(
+                &authenticator,
+                file_reader,
+                file_size,
+                metadata.signature.clone().as_deref(),
+                MerkleTreeReader { metadata },
+            )?,
             file_size,
         ))
     }
 
     #[test]
     fn fsverity_verify_full_read_4k() -> Result<()> {
-        let (file_reader, file_size) = new_reader_with_fsverity(
-            "testdata/input.4k",
-            "testdata/input.4k.merkle_dump",
-            "testdata/input.4k.fsv_sig",
-        )?;
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4k", "testdata/input.4k.fsv_meta")?;
 
         for i in 0..total_chunk_number(file_size) {
             let mut buf = [0u8; 4096];
@@ -251,11 +261,8 @@
 
     #[test]
     fn fsverity_verify_full_read_4k1() -> Result<()> {
-        let (file_reader, file_size) = new_reader_with_fsverity(
-            "testdata/input.4k1",
-            "testdata/input.4k1.merkle_dump",
-            "testdata/input.4k1.fsv_sig",
-        )?;
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4k1", "testdata/input.4k1.fsv_meta")?;
 
         for i in 0..total_chunk_number(file_size) {
             let mut buf = [0u8; 4096];
@@ -266,11 +273,8 @@
 
     #[test]
     fn fsverity_verify_full_read_4m() -> Result<()> {
-        let (file_reader, file_size) = new_reader_with_fsverity(
-            "testdata/input.4m",
-            "testdata/input.4m.merkle_dump",
-            "testdata/input.4m.fsv_sig",
-        )?;
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4m", "testdata/input.4m.fsv_meta")?;
 
         for i in 0..total_chunk_number(file_size) {
             let mut buf = [0u8; 4096];
@@ -283,8 +287,7 @@
     fn fsverity_verify_bad_merkle_tree() -> Result<()> {
         let (file_reader, _) = new_reader_with_fsverity(
             "testdata/input.4m",
-            "testdata/input.4m.merkle_dump.bad", // First leaf node is corrupted.
-            "testdata/input.4m.fsv_sig",
+            "testdata/input.4m.fsv_meta.bad_merkle", // First leaf node is corrupted.
         )?;
 
         // A lowest broken node (a 4K chunk that contains 128 sha256 hashes) will fail the read
@@ -304,10 +307,15 @@
         let authenticator = FakeAuthenticator::always_fail();
         let file_reader = LocalFileReader::new(File::open("testdata/input.4m")?)?;
         let file_size = file_reader.len();
-        let merkle_tree = LocalFileReader::new(File::open("testdata/input.4m.merkle_dump")?)?;
-        let sig = fs::read("testdata/input.4m.fsv_sig")?;
-        assert!(VerifiedFileReader::new(&authenticator, file_reader, file_size, sig, merkle_tree)
-            .is_err());
+        let metadata = parse_fsverity_metadata(File::open("testdata/input.4m.fsv_meta")?)?;
+        assert!(VerifiedFileReader::new(
+            &authenticator,
+            file_reader,
+            file_size,
+            metadata.signature.clone().as_deref(),
+            MerkleTreeReader { metadata },
+        )
+        .is_err());
         Ok(())
     }
 }
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index 421cc02..858b099 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -27,7 +27,7 @@
 //! of the actual file name, the exposed file names through AuthFS are currently integer, e.g.
 //! /mountpoint/42.
 
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use log::error;
 use std::convert::TryInto;
 use std::path::{Path, PathBuf};
@@ -166,7 +166,7 @@
     remote_fd: i32,
     file_size: u64,
 ) -> Result<AuthFsEntry> {
-    let signature = service.readFsveritySignature(remote_fd).context("Failed to read signature")?;
+    let signature = service.readFsveritySignature(remote_fd).ok();
 
     let authenticator = FakeAuthenticator::always_succeed();
     Ok(AuthFsEntry::VerifiedReadonly {
@@ -174,7 +174,7 @@
             &authenticator,
             RemoteFileReader::new(service.clone(), remote_fd),
             file_size,
-            signature,
+            signature.as_deref(),
             RemoteMerkleTreeReader::new(service.clone(), remote_fd),
         )?,
         file_size,
diff --git a/authfs/testdata/README.md b/authfs/testdata/README.md
index 113fe62..cf641a9 100644
--- a/authfs/testdata/README.md
+++ b/authfs/testdata/README.md
@@ -1,9 +1,10 @@
 fs-verity signing
 =================
 With a key pair, fs-verity signature can be generated by simply running
-`fsverity` command line tool from
+`fsverity_metadata_generator` command line tool, which uses
 [fsverity-util](https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git).
 
 ```
-fsverity sign test.data test.data.fsv_sig --key=key.pem --cert=cert.pem
+fsverity_metadata_generator --fsverity-path {fsverity_path} --key key.pem --key-format pem \
+    --cert cert.pem --signature pkcs7 --output test.data.fsv_meta test.data
 ```
diff --git a/authfs/testdata/input.4k.fsv_meta b/authfs/testdata/input.4k.fsv_meta
new file mode 100644
index 0000000..d6a899e
--- /dev/null
+++ b/authfs/testdata/input.4k.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4k.fsv_sig b/authfs/testdata/input.4k.fsv_sig
deleted file mode 100644
index 247a297..0000000
--- a/authfs/testdata/input.4k.fsv_sig
+++ /dev/null
Binary files differ
diff --git a/authfs/testdata/input.4k.merkle_dump b/authfs/testdata/input.4k.merkle_dump
deleted file mode 100644
index d93cd33..0000000
--- a/authfs/testdata/input.4k.merkle_dump
+++ /dev/null
Binary files differ
diff --git a/authfs/testdata/input.4k1.fsv_meta b/authfs/testdata/input.4k1.fsv_meta
new file mode 100644
index 0000000..acbbb40
--- /dev/null
+++ b/authfs/testdata/input.4k1.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4k1.fsv_sig b/authfs/testdata/input.4k1.fsv_sig
deleted file mode 100644
index 02f0056..0000000
--- a/authfs/testdata/input.4k1.fsv_sig
+++ /dev/null
Binary files differ
diff --git a/authfs/testdata/input.4k1.merkle_dump b/authfs/testdata/input.4k1.merkle_dump
deleted file mode 100644
index 4ebdc3c..0000000
--- a/authfs/testdata/input.4k1.merkle_dump
+++ /dev/null
Binary files differ
diff --git a/authfs/testdata/input.4m.merkle_dump.bad b/authfs/testdata/input.4m.fsv_meta
similarity index 89%
copy from authfs/testdata/input.4m.merkle_dump.bad
copy to authfs/testdata/input.4m.fsv_meta
index eec67ca..447a780 100644
--- a/authfs/testdata/input.4m.merkle_dump.bad
+++ b/authfs/testdata/input.4m.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4m.merkle_dump.bad b/authfs/testdata/input.4m.fsv_meta.bad_merkle
similarity index 89%
rename from authfs/testdata/input.4m.merkle_dump.bad
rename to authfs/testdata/input.4m.fsv_meta.bad_merkle
index eec67ca..fd61c3e 100644
--- a/authfs/testdata/input.4m.merkle_dump.bad
+++ b/authfs/testdata/input.4m.fsv_meta.bad_merkle
Binary files differ
diff --git a/authfs/testdata/input.4m.fsv_sig b/authfs/testdata/input.4m.fsv_sig
deleted file mode 100644
index 12adca3..0000000
--- a/authfs/testdata/input.4m.fsv_sig
+++ /dev/null
Binary files differ
diff --git a/authfs/testdata/input.4m.merkle_dump b/authfs/testdata/input.4m.merkle_dump
deleted file mode 100644
index b369bab..0000000
--- a/authfs/testdata/input.4m.merkle_dump
+++ /dev/null
Binary files differ
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
index 643e2b4..cc358f2 100644
--- a/authfs/tests/AndroidTest.xml
+++ b/authfs/tests/AndroidTest.xml
@@ -40,20 +40,14 @@
         <option name="push-file" key="input.4m" value="/data/local/tmp/authfs/input.4m" />
         <option name="push-file" key="input.4k1" value="/data/local/tmp/authfs/input.4k1" />
         <option name="push-file" key="input.4k" value="/data/local/tmp/authfs/input.4k" />
-        <option name="push-file" key="input.4m.fsv_sig"
-            value="/data/local/tmp/authfs/input.4m.fsv_sig" />
-        <option name="push-file" key="input.4k1.fsv_sig"
-            value="/data/local/tmp/authfs/input.4k1.fsv_sig" />
-        <option name="push-file" key="input.4k.fsv_sig"
-            value="/data/local/tmp/authfs/input.4k.fsv_sig" />
-        <option name="push-file" key="input.4m.merkle_dump"
-            value="/data/local/tmp/authfs/input.4m.merkle_dump" />
-        <option name="push-file" key="input.4m.merkle_dump.bad"
-            value="/data/local/tmp/authfs/input.4m.merkle_dump.bad" />
-        <option name="push-file" key="input.4k1.merkle_dump"
-            value="/data/local/tmp/authfs/input.4k1.merkle_dump" />
-        <option name="push-file" key="input.4k.merkle_dump"
-            value="/data/local/tmp/authfs/input.4k.merkle_dump" />
+        <option name="push-file" key="input.4m.fsv_meta"
+            value="/data/local/tmp/authfs/input.4m.fsv_meta" />
+        <option name="push-file" key="input.4k1.fsv_meta"
+            value="/data/local/tmp/authfs/input.4k1.fsv_meta" />
+        <option name="push-file" key="input.4k.fsv_meta"
+            value="/data/local/tmp/authfs/input.4k.fsv_meta" />
+        <option name="push-file" key="input.4m.fsv_meta.bad_merkle"
+            value="/data/local/tmp/authfs/input.4m.fsv_meta.bad_merkle" />
     </target_preparer>
 
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 101a349..acaead0 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -161,9 +161,8 @@
     public void testReadWithFsverityVerification_RemoteFile() throws Exception {
         // Setup
         runFdServerOnAndroid(
-                "--open-ro 3:input.4m --open-ro 4:input.4m.merkle_dump --open-ro 5:input.4m.fsv_sig"
-                        + " --open-ro 6:input.4m",
-                "--ro-fds 3:4:5 --ro-fds 6");
+                "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta --open-ro 6:input.4m",
+                "--ro-fds 3:4 --ro-fds 6");
 
         runAuthFsOnMicrodroid(
                 "--remote-ro-file-unverified 6 --remote-ro-file 3:cert.der --cid "
@@ -186,10 +185,9 @@
     public void testReadWithFsverityVerification_RemoteSmallerFile() throws Exception {
         // Setup
         runFdServerOnAndroid(
-                "--open-ro 3:input.4k --open-ro 4:input.4k.merkle_dump --open-ro"
-                    + " 5:input.4k.fsv_sig --open-ro 6:input.4k1 --open-ro 7:input.4k1.merkle_dump"
-                    + " --open-ro 8:input.4k1.fsv_sig",
-                "--ro-fds 3:4:5 --ro-fds 6:7:8");
+                "--open-ro 3:input.4k --open-ro 4:input.4k.fsv_meta --open-ro"
+                    + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta",
+                "--ro-fds 3:4 --ro-fds 6:7");
         runAuthFsOnMicrodroid(
                 "--remote-ro-file 3:cert.der --remote-ro-file 6:cert.der --cid " + VMADDR_CID_HOST);
 
@@ -209,9 +207,8 @@
     public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception {
         // Setup
         runFdServerOnAndroid(
-                "--open-ro 3:input.4m --open-ro 4:input.4m.merkle_dump.bad "
-                        + "--open-ro 5:input.4m.fsv_sig",
-                "--ro-fds 3:4:5");
+                "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle",
+                "--ro-fds 3:4");
         runAuthFsOnMicrodroid("--remote-ro-file 3:cert.der --cid " + VMADDR_CID_HOST);
 
         // Verify
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index 194180b..395a09b 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -104,14 +104,4 @@
      * @return whether the inputs are valid and correspond to each other.
      */
     boolean verifySigningKey(in byte[] keyBlob, in byte[] publicKey);
-
-    /**
-     * Signs some data with the initialized key. The call will fail with EX_ILLEGAL_STATE if not
-     * yet initialized.
-     *
-     * @param data The data to be signed. (Large data sizes may cause failure.)
-     * @return the signature.
-     */
-    // STOPSHIP(b/193241041): We must not expose this from the PVM.
-    byte[] sign(in byte[] data);
 }
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index f40da9c..564a380 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -59,7 +59,6 @@
     ],
 
     prebuilts: [
-        "CompOSPayloadApp.apk.idsig",
         "com.android.compos.init.rc",
     ],
 }
diff --git a/compos/apk/Android.bp b/compos/apk/Android.bp
index 3a68b8e..c6192b9 100644
--- a/compos/apk/Android.bp
+++ b/compos/apk/Android.bp
@@ -3,42 +3,7 @@
 }
 
 android_app {
-    name: "CompOSPayloadApp.unsigned",
+    name: "CompOSPayloadApp",
     sdk_version: "current",
     apex_available: ["com.android.compos"],
 }
-
-// TODO(b/190409306) this is temporary until we have a solid way to pass merkle tree
-java_genrule {
-    name: "CompOSPayloadApp.signing",
-    out: [
-        "CompOSPayloadApp.apk",
-        "CompOSPayloadApp.apk.idsig",
-    ],
-    srcs: [":CompOSPayloadApp.unsigned"],
-    tools: ["apksigner"],
-    tool_files: ["test.keystore"],
-    cmd: "$(location apksigner) sign " +
-        "--ks $(location test.keystore) " +
-        "--ks-pass=pass:testkey --key-pass=pass:testkey " +
-        "--in $(in) " +
-        "--out $(genDir)/CompOSPayloadApp.apk",
-    // $(genDir)/CompOSPayloadApp.apk.idsig is generated implicitly
-}
-
-android_app_import {
-    name: "CompOSPayloadApp",
-    // Make sure the build system doesn't try to resign the APK
-    dex_preopt: {
-        enabled: false,
-    },
-    apk: ":CompOSPayloadApp.signing{CompOSPayloadApp.apk}",
-    presigned: true,
-    filename: "CompOSPayloadApp.apk",
-    apex_available: ["com.android.compos"],
-}
-
-prebuilt_etc {
-    name: "CompOSPayloadApp.apk.idsig",
-    src: ":CompOSPayloadApp.signing{CompOSPayloadApp.apk.idsig}",
-}
diff --git a/compos/apk/test.keystore b/compos/apk/test.keystore
deleted file mode 100644
index 2946641..0000000
--- a/compos/apk/test.keystore
+++ /dev/null
Binary files differ
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 5893fd6..39e7c0a 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -11,11 +11,14 @@
         "android.system.virtualizationservice-rust",
         "compos_aidl_interface-rust",
         "libanyhow",
+        "libbinder_common",
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "liblog_rust",
+        "libnum_traits",
         "librustutils",
     ],
+    proc_macros: ["libnum_derive"],
     shared_libs: [
         "libbinder_rpc_unstable",
     ],
diff --git a/compos/composd/src/util.rs b/compos/common/binder.rs
similarity index 62%
rename from compos/composd/src/util.rs
rename to compos/common/binder.rs
index 54d7751..6bd3957 100644
--- a/compos/composd/src/util.rs
+++ b/compos/common/binder.rs
@@ -14,16 +14,21 @@
  * limitations under the License.
  */
 
-use android_system_composd::binder::Result as BinderResult;
+//! Helper for converting Error types to what Binder expects
+
 use anyhow::Result;
-use binder_common::new_binder_service_specific_error;
-use log::error;
+use binder::public_api::{ExceptionCode, Result as BinderResult};
+use binder_common::new_binder_exception;
+use log::warn;
 use std::fmt::Debug;
 
+/// Convert a Result<T, E> to BinderResult<T> to allow it to be returned from a binder RPC,
+/// preserving the content as far as possible.
+/// Also log the error if there is one.
 pub fn to_binder_result<T, E: Debug>(result: Result<T, E>) -> BinderResult<T> {
     result.map_err(|e| {
         let message = format!("{:?}", e);
-        error!("Returning binder error: {}", &message);
-        new_binder_service_specific_error(-1, message)
+        warn!("Returning binder error: {}", &message);
+        new_binder_exception(ExceptionCode::SERVICE_SPECIFIC, message)
     })
 }
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 9c23fac..3bb066f 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -72,6 +72,7 @@
     pub fn start(
         service: &dyn IVirtualizationService,
         instance_image: File,
+        idsig: &Path,
         parameters: &VmParameters,
     ) -> Result<VmInstance> {
         let instance_fd = ParcelFileDescriptor::new(instance_image);
@@ -83,9 +84,18 @@
             .context("Failed to open config APK file")?;
         let apk_fd = ParcelFileDescriptor::new(apk_fd);
 
-        let idsig_fd = File::open(apex_dir.join("etc/CompOSPayloadApp.apk.idsig"))
-            .context("Failed to open config APK idsig file")?;
-        let idsig_fd = ParcelFileDescriptor::new(idsig_fd);
+        if !idsig.exists() {
+            // Prepare idsig file via VirtualizationService
+            let idsig_file = File::create(idsig).context("Failed to create idsig file")?;
+            let idsig_fd = ParcelFileDescriptor::new(idsig_file);
+            service
+                .createOrUpdateIdsigFile(&apk_fd, &idsig_fd)
+                .context("Failed to update idsig file")?;
+        }
+
+        // Open idsig as read-only
+        let idsig_file = File::open(idsig).context("Failed to open idsig file")?;
+        let idsig_fd = ParcelFileDescriptor::new(idsig_file);
 
         let (console_fd, log_fd, debug_level) = if parameters.debug_mode {
             // Console output and the system log output from the VM are redirected to file.
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
index 9b07030..9a4d0e3 100644
--- a/compos/common/lib.rs
+++ b/compos/common/lib.rs
@@ -16,7 +16,9 @@
 
 //! Common items used by CompOS server and/or clients
 
+pub mod binder;
 pub mod compos_client;
+pub mod odrefresh;
 pub mod timeouts;
 
 /// Special CID indicating "any".
@@ -53,6 +55,9 @@
 /// The file that holds the instance image for a CompOS instance.
 pub const INSTANCE_IMAGE_FILE: &str = "instance.img";
 
+/// The file that holds the idsig for the CompOS Payload APK.
+pub const IDSIG_FILE: &str = "idsig";
+
 /// The path within our config APK of our default VM configuration file, used at boot time.
 pub const DEFAULT_VM_CONFIG_PATH: &str = "assets/vm_config.json";
 
diff --git a/compos/common/odrefresh.rs b/compos/common/odrefresh.rs
new file mode 100644
index 0000000..7fe6ed5
--- /dev/null
+++ b/compos/common/odrefresh.rs
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! Helpers for running odrefresh
+
+use anyhow::{anyhow, Result};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+
+/// The path to the odrefresh binary
+pub const ODREFRESH_PATH: &str = "/apex/com.android.art/bin/odrefresh";
+
+// The highest "standard" exit code defined in sysexits.h (as EX__MAX); odrefresh error codes
+// start above here to avoid clashing.
+// TODO: What if this changes?
+const EX_MAX: i8 = 78;
+
+/// The defined odrefresh exit codes - see art/odrefresh/include/odrefresh/odrefresh.h
+#[derive(Debug, PartialEq, Eq, FromPrimitive)]
+#[repr(i8)]
+pub enum ExitCode {
+    /// No compilation required, all artifacts look good
+    Okay = 0,
+    /// Compilation required
+    CompilationRequired = EX_MAX + 1,
+    /// New artifacts successfully generated
+    CompilationSuccess = EX_MAX + 2,
+    /// Compilation failed
+    CompilationFailed = EX_MAX + 3,
+    /// Removal of existing invalid artifacts failed
+    CleanupFailed = EX_MAX + 4,
+}
+
+impl ExitCode {
+    /// Map an integer to the corresponding ExitCode enum, if there is one
+    pub fn from_i32(exit_code: i32) -> Result<Self> {
+        FromPrimitive::from_i32(exit_code)
+            .ok_or_else(|| anyhow!("Unexpected odrefresh exit code: {}", exit_code))
+    }
+}
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index f8b3d16..0305cd4 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -58,6 +58,7 @@
 using aidl::android::system::virtualizationservice::VirtualMachineConfig;
 using aidl::com::android::compos::CompOsKeyData;
 using aidl::com::android::compos::ICompOsService;
+using android::base::Dirname;
 using android::base::ErrnoError;
 using android::base::Error;
 using android::base::Fdopen;
@@ -73,8 +74,6 @@
 
 constexpr const char* kConfigApkPath =
         "/apex/com.android.compos/app/CompOSPayloadApp/CompOSPayloadApp.apk";
-constexpr const char* kConfigApkIdsigPath =
-        "/apex/com.android.compos/etc/CompOSPayloadApp.apk.idsig";
 
 // These are paths inside the APK
 constexpr const char* kDefaultConfigFilePath = "assets/vm_config.json";
@@ -211,6 +210,8 @@
             return Error() << "Can't specify both cid and image file.";
         }
 
+        // Start a new VM with a given instance.img
+
         // We need a thread pool to receive VM callbacks.
         ABinderProcess_startThreadPool();
 
@@ -242,10 +243,25 @@
             return ErrnoError() << "Failed to open config APK";
         }
 
+        // Prepare an idsig file
+        std::string idsigPath = Dirname(mInstanceImageFile) + "/idsig";
+        {
+            ScopedFileDescriptor idsigFd(TEMP_FAILURE_RETRY(
+                    open(idsigPath.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC,
+                         S_IRUSR | S_IWUSR | S_IRGRP)));
+            if (idsigFd.get() == -1) {
+                return ErrnoError() << "Failed to create an idsig file";
+            }
+            auto status = service->createOrUpdateIdsigFile(apkFd, idsigFd);
+            if (!status.isOk()) {
+                return Error() << status.getDescription();
+            }
+        }
+
         ScopedFileDescriptor idsigFd(
-                TEMP_FAILURE_RETRY(open(kConfigApkIdsigPath, O_RDONLY | O_CLOEXEC)));
+                TEMP_FAILURE_RETRY(open(idsigPath.c_str(), O_RDONLY | O_CLOEXEC)));
         if (idsigFd.get() == -1) {
-            return ErrnoError() << "Failed to open config APK signature";
+            return ErrnoError() << "Failed to open an idsig file";
         }
 
         ScopedFileDescriptor instanceFd(
@@ -405,121 +421,6 @@
     return result;
 }
 
-static Result<std::vector<uint8_t>> computeDigest(const std::string& file) {
-    unique_fd fd(TEMP_FAILURE_RETRY(open(file.c_str(), O_RDONLY | O_CLOEXEC)));
-    if (!fd.ok()) {
-        return ErrnoError() << "Failed to open";
-    }
-
-    struct stat filestat;
-    if (fstat(fd, &filestat) != 0) {
-        return ErrnoError() << "Failed to fstat";
-    }
-
-    struct libfsverity_merkle_tree_params params = {
-            .version = 1,
-            .hash_algorithm = FS_VERITY_HASH_ALG_SHA256,
-            .file_size = static_cast<uint64_t>(filestat.st_size),
-            .block_size = 4096,
-    };
-
-    auto read_callback = [](void* file, void* buf, size_t count) {
-        int* fd = static_cast<int*>(file);
-        if (TEMP_FAILURE_RETRY(read(*fd, buf, count)) < 0) return -errno;
-        return 0;
-    };
-
-    struct libfsverity_digest* digest;
-    int ret = libfsverity_compute_digest(&fd, read_callback, &params, &digest);
-    if (ret < 0) {
-        return Error(-ret) << "Failed to compute fs-verity digest";
-    }
-    std::unique_ptr<libfsverity_digest, decltype(&std::free)> digestOwner{digest, std::free};
-
-    return std::vector(&digest->digest[0], &digest->digest[digest->digest_size]);
-}
-
-static std::string toHex(const std::vector<uint8_t>& digest) {
-    std::stringstream ss;
-    for (auto it = digest.begin(); it != digest.end(); ++it) {
-        ss << std::setfill('0') << std::setw(2) << std::hex << static_cast<unsigned>(*it);
-    }
-    return ss.str();
-}
-
-static Result<void> signInfo(TargetVm& vm, const std::string& blob_file,
-                             const std::string& info_file, const std::vector<std::string>& files) {
-    unique_fd info_fd(
-            TEMP_FAILURE_RETRY(open(info_file.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
-                                    S_IRUSR | S_IWUSR | S_IRGRP)));
-    if (!info_fd.ok()) {
-        return ErrnoError() << "Unable to create " << info_file;
-    }
-
-    std::string signature_file = info_file + ".signature";
-    unique_fd signature_fd(TEMP_FAILURE_RETRY(open(signature_file.c_str(),
-                                                   O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC,
-                                                   S_IRUSR | S_IWUSR | S_IRGRP)));
-    if (!signature_fd.ok()) {
-        return ErrnoError() << "Unable to create " << signature_file;
-    }
-
-    auto cid = vm.resolveCid();
-    if (!cid.ok()) {
-        return cid.error();
-    }
-    auto service = getService(*cid);
-    if (!service) {
-        return Error() << "No service";
-    }
-
-    auto blob = readBytesFromFile(blob_file);
-    if (!blob.ok()) {
-        return blob.error();
-    }
-
-    auto initialized = service->initializeSigningKey(blob.value());
-    if (!initialized.isOk()) {
-        return Error() << "Failed to initialize signing key: " << initialized.getDescription();
-    }
-
-    std::map<std::string, std::string> file_digests;
-
-    for (auto& file : files) {
-        auto digest = computeDigest(file);
-        if (!digest.ok()) {
-            return digest.error();
-        }
-        file_digests.emplace(file, toHex(*digest));
-    }
-
-    OdsignInfo info;
-    info.mutable_file_hashes()->insert(file_digests.begin(), file_digests.end());
-
-    std::vector<uint8_t> serialized(info.ByteSizeLong());
-    if (!info.SerializeToArray(serialized.data(), serialized.size())) {
-        return Error() << "Failed to serialize protobuf";
-    }
-
-    if (!WriteFully(info_fd, serialized.data(), serialized.size()) ||
-        close(info_fd.release()) != 0) {
-        return Error() << "Failed to write info file";
-    }
-
-    std::vector<uint8_t> signature;
-    auto status = service->sign(serialized, &signature);
-    if (!status.isOk()) {
-        return Error() << "Failed to sign: " << status.getDescription();
-    }
-
-    if (!WriteFully(signature_fd, signature.data(), signature.size()) ||
-        close(signature_fd.release()) != 0) {
-        return Error() << "Failed to write signature";
-    }
-
-    return {};
-}
-
 static Result<void> initializeKey(TargetVm& vm, const std::string& blob_file) {
     auto cid = vm.resolveCid();
     if (!cid.ok()) {
@@ -628,17 +529,6 @@
         } else {
             std::cerr << result.error() << '\n';
         }
-    } else if (argc >= 5 && argv[1] == "sign-info"sv) {
-        const std::string blob_file = argv[2];
-        const std::string info_file = argv[3];
-        const std::vector<std::string> files{&argv[4], &argv[argc]};
-        auto result = signInfo(vm, blob_file, info_file, files);
-        if (result.ok()) {
-            std::cerr << "Info file generated and signed.\n";
-            return 0;
-        } else {
-            std::cerr << result.error() << '\n';
-        }
     } else if (argc == 3 && argv[1] == "init-key"sv) {
         auto result = initializeKey(vm, argv[2]);
         if (result.ok()) {
@@ -662,9 +552,6 @@
                   << "  verify <blob file> <public key file> Verify that the content of the\n"
                   << "    specified private key blob and public key files are valid.\n "
                   << "  init-key <blob file> Initialize the service key.\n"
-                  << "  sign-info <blob file> <info file> <files to be signed> Generate\n"
-                  << "    an info file listing the paths and root digests of each of the files to\n"
-                  << "    be signed, along with a signature of that file.\n"
                   << "\n"
                   << "OPTIONS: --log <log file> --debug --staged\n"
                   << "    (--cid <cid> | --start <image file>)\n"
diff --git a/compos/composd/Android.bp b/compos/composd/Android.bp
index 735b9a5..3190395 100644
--- a/compos/composd/Android.bp
+++ b/compos/composd/Android.bp
@@ -20,13 +20,11 @@
         "libcomposd_native_rust",
         "libminijail_rust",
         "libnix",
-        "libnum_traits",
         "liblibc",
         "liblog_rust",
         "librustutils",
         "libshared_child",
     ],
-    proc_macros: ["libnum_derive"],
     apex_available: [
         "com.android.compos",
     ],
diff --git a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
index e73963d..8156265 100644
--- a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
+++ b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
@@ -31,7 +31,7 @@
     ICompilationTask startStagedApexCompile(ICompilationTaskCallback callback);
 
     /**
-     * Run "odrefresh --dalvik-cache=pending-test --force-compile" in a test instance of CompOS.
+     * Run odrefresh in a test instance of CompOS until completed or failed.
      *
      * This compiles BCP extensions and system server, even if the system artifacts are up to date,
      * and writes the results to a test directory to avoid disrupting any real artifacts in
@@ -42,28 +42,4 @@
      * a reference to the ICompilationTask until compilation completes or is cancelled.
      */
     ICompilationTask startTestCompile(ICompilationTaskCallback callback);
-
-    /**
-     * Run odrefresh in a test instance of CompOS until completed or failed.
-     *
-     * This compiles BCP extensions and system server, even if the system artifacts are up to date,
-     * and writes the results to a test directory to avoid disrupting any real artifacts in
-     * existence.
-     *
-     * Compilation continues in the background, and success/failure is reported via the supplied
-     * callback, unless the returned ICompilationTask is cancelled. The caller should maintain
-     * a reference to the ICompilationTask until compilation completes or is cancelled.
-     */
-    ICompilationTask startAsyncOdrefresh(ICompilationTaskCallback callback);
-
-    /**
-     * Run odrefresh in a test instance of CompOS until completed or failed.
-     *
-     * This compiles BCP extensions and system server, even if the system artifacts are up to date,
-     * and writes the results to a test directory to avoid disrupting any real artifacts in
-     * existence.
-     *
-     * TODO(205750213): Change the API to async.
-     */
-    byte startTestOdrefresh();
 }
diff --git a/compos/composd/src/compilation_task.rs b/compos/composd/src/compilation_task.rs
deleted file mode 100644
index 18f5aac..0000000
--- a/compos/composd/src/compilation_task.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-use crate::instance_starter::CompOsInstance;
-use crate::odrefresh::{self, Odrefresh};
-use android_system_composd::aidl::android::system::composd::{
-    ICompilationTask::ICompilationTask, ICompilationTaskCallback::ICompilationTaskCallback,
-};
-use android_system_composd::binder::{Interface, Result as BinderResult, Strong};
-use anyhow::Result;
-use log::{error, warn};
-use std::sync::{Arc, Mutex};
-use std::thread;
-
-#[derive(Clone)]
-pub struct CompilationTask {
-    running_task: Arc<Mutex<Option<RunningTask>>>,
-}
-
-impl Interface for CompilationTask {}
-
-impl ICompilationTask for CompilationTask {
-    fn cancel(&self) -> BinderResult<()> {
-        let task = self.take();
-        if let Some(task) = task {
-            if let Err(e) = task.odrefresh.kill() {
-                warn!("Failed to kill running task: {:?}", e)
-            }
-        }
-        Ok(())
-    }
-}
-
-impl CompilationTask {
-    /// Return the current running task, if any, removing it from this CompilationTask.
-    /// Once removed, meaning the task has ended or been canceled, further calls will always return
-    /// None.
-    fn take(&self) -> Option<RunningTask> {
-        self.running_task.lock().unwrap().take()
-    }
-
-    pub fn start_staged_apex_compile(
-        comp_os: Arc<CompOsInstance>,
-        callback: &Strong<dyn ICompilationTaskCallback>,
-    ) -> Result<CompilationTask> {
-        // TODO: Write to pending
-        // TODO: Delete any existing artifacts
-        let odrefresh = Odrefresh::spawn_compile("test-artifacts")?;
-        let odrefresh = Arc::new(odrefresh);
-        let task =
-            RunningTask { odrefresh: odrefresh.clone(), comp_os, callback: callback.clone() };
-        let task = CompilationTask { running_task: Arc::new(Mutex::new(Some(task))) };
-
-        task.clone().start_waiting_thread(odrefresh);
-
-        Ok(task)
-    }
-
-    pub fn start_test_compile(
-        comp_os: Arc<CompOsInstance>,
-        callback: &Strong<dyn ICompilationTaskCallback>,
-    ) -> Result<CompilationTask> {
-        let odrefresh = Odrefresh::spawn_forced_compile("test-artifacts")?;
-        let odrefresh = Arc::new(odrefresh);
-        let task =
-            RunningTask { odrefresh: odrefresh.clone(), comp_os, callback: callback.clone() };
-        let task = CompilationTask { running_task: Arc::new(Mutex::new(Some(task))) };
-
-        task.clone().start_waiting_thread(odrefresh);
-
-        Ok(task)
-    }
-
-    fn start_waiting_thread(self, odrefresh: Arc<Odrefresh>) {
-        thread::spawn(move || {
-            let exit_code = odrefresh.wait_for_exit();
-            let task = self.take();
-            // We don't do the callback if cancel has already happened.
-            if let Some(task) = task {
-                let result = match exit_code {
-                    Ok(odrefresh::ExitCode::CompilationSuccess) => task.callback.onSuccess(),
-                    Ok(exit_code) => {
-                        error!("Unexpected odrefresh result: {:?}", exit_code);
-                        task.callback.onFailure()
-                    }
-                    Err(e) => {
-                        error!("Running odrefresh failed: {:?}", e);
-                        task.callback.onFailure()
-                    }
-                };
-                if let Err(e) = result {
-                    warn!("Failed to deliver callback: {:?}", e);
-                }
-            }
-        });
-    }
-}
-
-struct RunningTask {
-    odrefresh: Arc<Odrefresh>,
-    callback: Strong<dyn ICompilationTaskCallback>,
-    #[allow(dead_code)] // Keeps the CompOS VM alive
-    comp_os: Arc<CompOsInstance>,
-}
diff --git a/compos/composd/src/composd_main.rs b/compos/composd/src/composd_main.rs
index 2915a58..df088bc 100644
--- a/compos/composd/src/composd_main.rs
+++ b/compos/composd/src/composd_main.rs
@@ -18,15 +18,12 @@
 //! responsible for managing the lifecycle of the CompOS VM instances, providing key management for
 //! them, and orchestrating trusted compilation.
 
-mod compilation_task;
 mod fd_server_helper;
 mod instance_manager;
 mod instance_starter;
 mod internal_service;
-mod odrefresh;
 mod odrefresh_task;
 mod service;
-mod util;
 
 use crate::instance_manager::InstanceManager;
 use android_system_composd::binder::{register_lazy_service, ProcessState};
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 8189fe0..91a0e61 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -26,7 +26,7 @@
 use compos_aidl_interface::binder::{ParcelFileDescriptor, Strong};
 use compos_common::compos_client::{VmInstance, VmParameters};
 use compos_common::{
-    COMPOS_DATA_ROOT, INSTANCE_IMAGE_FILE, PRIVATE_KEY_BLOB_FILE, PUBLIC_KEY_FILE,
+    COMPOS_DATA_ROOT, IDSIG_FILE, INSTANCE_IMAGE_FILE, PRIVATE_KEY_BLOB_FILE, PUBLIC_KEY_FILE,
 };
 use log::{info, warn};
 use std::env;
@@ -51,6 +51,7 @@
     instance_name: String,
     instance_root: PathBuf,
     instance_image: PathBuf,
+    idsig: PathBuf,
     key_blob: PathBuf,
     public_key: PathBuf,
     vm_parameters: VmParameters,
@@ -59,14 +60,16 @@
 impl InstanceStarter {
     pub fn new(instance_name: &str, vm_parameters: VmParameters) -> Self {
         let instance_root = Path::new(COMPOS_DATA_ROOT).join(instance_name);
-        let instant_root_path = instance_root.as_path();
-        let instance_image = instant_root_path.join(INSTANCE_IMAGE_FILE);
-        let key_blob = instant_root_path.join(PRIVATE_KEY_BLOB_FILE);
-        let public_key = instant_root_path.join(PUBLIC_KEY_FILE);
+        let instance_root_path = instance_root.as_path();
+        let instance_image = instance_root_path.join(INSTANCE_IMAGE_FILE);
+        let idsig = instance_root_path.join(IDSIG_FILE);
+        let key_blob = instance_root_path.join(PRIVATE_KEY_BLOB_FILE);
+        let public_key = instance_root_path.join(PUBLIC_KEY_FILE);
         Self {
             instance_name: instance_name.to_owned(),
             instance_root,
             instance_image,
+            idsig,
             key_blob,
             public_key,
             vm_parameters,
@@ -124,6 +127,8 @@
         let _ = fs::create_dir(&self.instance_root);
 
         self.create_instance_image(virtualization_service)?;
+        // Delete existing idsig file. Ignore error in case idsig doesn't exist.
+        let _ = fs::remove_file(&self.idsig);
 
         let compos_instance = self.start_vm(virtualization_service)?;
         let service = &compos_instance.service;
@@ -170,9 +175,13 @@
             .write(true)
             .open(&self.instance_image)
             .context("Failed to open instance image")?;
-        let vm_instance =
-            VmInstance::start(virtualization_service, instance_image, &self.vm_parameters)
-                .context("Starting VM")?;
+        let vm_instance = VmInstance::start(
+            virtualization_service,
+            instance_image,
+            &self.idsig,
+            &self.vm_parameters,
+        )
+        .context("Starting VM")?;
         let service = vm_instance.get_service().context("Connecting to CompOS")?;
         Ok(CompOsInstance { vm_instance, service, lazy_service_guard: Default::default() })
     }
diff --git a/compos/composd/src/internal_service.rs b/compos/composd/src/internal_service.rs
index ebebaae..3fad6d4 100644
--- a/compos/composd/src/internal_service.rs
+++ b/compos/composd/src/internal_service.rs
@@ -17,7 +17,7 @@
 //! Implementation of ICompilationInternal, called from odrefresh during compilation.
 
 use crate::instance_manager::InstanceManager;
-use crate::util::to_binder_result;
+use compos_common::binder::to_binder_result;
 use android_system_composd_internal::aidl::android::system::composd::internal::ICompilationInternal::{
     BnCompilationInternal, ICompilationInternal,
 };
diff --git a/compos/composd/src/odrefresh.rs b/compos/composd/src/odrefresh.rs
deleted file mode 100644
index d6a3435..0000000
--- a/compos/composd/src/odrefresh.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-//! Handle the details of executing odrefresh to generate compiled artifacts.
-
-use crate::fd_server_helper::FdServerConfig;
-use anyhow::{bail, Context, Result};
-use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
-use compos_aidl_interface::binder::Strong;
-use compos_common::timeouts::{need_extra_time, EXTENDED_TIMEOUTS};
-use compos_common::VMADDR_CID_ANY;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use rustutils::system_properties;
-use shared_child::SharedChild;
-use std::fs::{File, OpenOptions};
-use std::os::unix::fs::OpenOptionsExt;
-use std::os::unix::io::AsRawFd;
-use std::path::Path;
-use std::process::Command;
-
-// TODO: What if this changes?
-const EX_MAX: i8 = 78;
-
-const ODREFRESH_BIN: &str = "/apex/com.android.art/bin/odrefresh";
-const ART_APEX_DATA: &str = "/data/misc/apexdata/com.android.art";
-
-#[derive(Debug, PartialEq, Eq, FromPrimitive)]
-#[repr(i8)]
-pub enum ExitCode {
-    // Copied from art/odrefresh/include/odrefresh/odrefresh.h
-    Okay = 0i8,
-    CompilationRequired = EX_MAX + 1,
-    CompilationSuccess = EX_MAX + 2,
-    CompilationFailed = EX_MAX + 3,
-    CleanupFailed = EX_MAX + 4,
-}
-
-pub struct Odrefresh {
-    child: SharedChild,
-}
-
-impl Odrefresh {
-    pub fn spawn_compile(target_dir: &str) -> Result<Self> {
-        Self::spawn_odrefresh(target_dir, "--compile")
-    }
-
-    pub fn spawn_forced_compile(target_dir: &str) -> Result<Self> {
-        Self::spawn_odrefresh(target_dir, "--force-compile")
-    }
-
-    fn spawn_odrefresh(target_dir: &str, compile_arg: &str) -> Result<Self> {
-        // We don`t need to capture stdout/stderr - odrefresh writes to the log
-        let mut cmdline = Command::new(ODREFRESH_BIN);
-        if need_extra_time()? {
-            cmdline
-                .arg(format!(
-                    "--max-execution-seconds={}",
-                    EXTENDED_TIMEOUTS.odrefresh_max_execution_time.as_secs()
-                ))
-                .arg(format!(
-                    "--max-child-process-seconds={}",
-                    EXTENDED_TIMEOUTS.odrefresh_max_child_process_time.as_secs()
-                ));
-        }
-        cmdline
-            .arg(format!("--use-compilation-os={}", VMADDR_CID_ANY as i32))
-            .arg(format!("--dalvik-cache={}", target_dir))
-            .arg(compile_arg);
-        let child = SharedChild::spawn(&mut cmdline).context("Running odrefresh")?;
-        Ok(Odrefresh { child })
-    }
-
-    pub fn wait_for_exit(&self) -> Result<ExitCode> {
-        // No timeout here - but clients can kill the process, which will end the wait.
-        let status = self.child.wait()?;
-        if let Some(exit_code) = status.code().and_then(FromPrimitive::from_i32) {
-            Ok(exit_code)
-        } else {
-            bail!("odrefresh exited with {}", status)
-        }
-    }
-
-    pub fn kill(&self) -> Result<()> {
-        self.child.kill().context("Killing odrefresh process failed")
-    }
-}
-
-pub fn run_in_vm(service: Strong<dyn ICompOsService>, target_dir_name: &str) -> Result<ExitCode> {
-    let staging_dir = open_dir(composd_native::palette_create_odrefresh_staging_directory()?)?;
-    let system_dir = open_dir(Path::new("/system"))?;
-    let output_dir = open_dir(Path::new(ART_APEX_DATA))?;
-
-    // Spawn a fd_server to serve the FDs.
-    let fd_server_config = FdServerConfig {
-        ro_dir_fds: vec![system_dir.as_raw_fd()],
-        rw_dir_fds: vec![staging_dir.as_raw_fd(), output_dir.as_raw_fd()],
-        ..Default::default()
-    };
-    let fd_server_raii = fd_server_config.into_fd_server()?;
-
-    let zygote_arch = system_properties::read("ro.zygote")?;
-    let exit_code = service.odrefresh(
-        system_dir.as_raw_fd(),
-        output_dir.as_raw_fd(),
-        staging_dir.as_raw_fd(),
-        target_dir_name,
-        &zygote_arch,
-    )?;
-
-    drop(fd_server_raii);
-    if let Some(exit_code) = FromPrimitive::from_i8(exit_code) {
-        Ok(exit_code)
-    } else {
-        bail!("odrefresh exited with {}", exit_code)
-    }
-}
-
-/// Returns an owned FD of the directory. It currently returns a `File` as a FD owner, but
-/// it's better to use `std::os::unix::io::OwnedFd` once/if it becomes standard.
-fn open_dir(path: &Path) -> Result<File> {
-    OpenOptions::new()
-        .custom_flags(libc::O_DIRECTORY)
-        .read(true) // O_DIRECTORY can only be opened with read
-        .open(path)
-        .with_context(|| format!("Failed to open {:?} directory as path fd", path))
-}
diff --git a/compos/composd/src/odrefresh_task.rs b/compos/composd/src/odrefresh_task.rs
index 64565dd..56b697e 100644
--- a/compos/composd/src/odrefresh_task.rs
+++ b/compos/composd/src/odrefresh_task.rs
@@ -14,18 +14,28 @@
  * limitations under the License.
  */
 
+//! Handle running odrefresh in the VM, with an async interface to allow cancellation
+
+use crate::fd_server_helper::FdServerConfig;
 use crate::instance_starter::CompOsInstance;
-use crate::odrefresh;
 use android_system_composd::aidl::android::system::composd::{
     ICompilationTask::ICompilationTask, ICompilationTaskCallback::ICompilationTaskCallback,
 };
 use android_system_composd::binder::{Interface, Result as BinderResult, Strong};
-use anyhow::Result;
+use anyhow::{Context, Result};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
+use compos_common::odrefresh::ExitCode;
 use log::{error, warn};
+use rustutils::system_properties;
+use std::fs::{remove_dir_all, File, OpenOptions};
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
 use std::sync::{Arc, Mutex};
 use std::thread;
 
+const ART_APEX_DATA: &str = "/data/misc/apexdata/com.android.art";
+
 #[derive(Clone)]
 pub struct OdrefreshTask {
     running_task: Arc<Mutex<Option<RunningTask>>>,
@@ -42,6 +52,12 @@
     }
 }
 
+struct RunningTask {
+    callback: Strong<dyn ICompilationTaskCallback>,
+    #[allow(dead_code)] // Keeps the CompOS VM alive
+    comp_os: Arc<CompOsInstance>,
+}
+
 impl OdrefreshTask {
     /// Return the current running task, if any, removing it from this CompilationTask.
     /// Once removed, meaning the task has ended or been canceled, further calls will always return
@@ -66,13 +82,13 @@
 
     fn start_thread(self, service: Strong<dyn ICompOsService>, target_dir_name: String) {
         thread::spawn(move || {
-            let exit_code = odrefresh::run_in_vm(service, &target_dir_name);
+            let exit_code = run_in_vm(service, &target_dir_name);
 
             let task = self.take();
             // We don't do the callback if cancel has already happened.
             if let Some(task) = task {
                 let result = match exit_code {
-                    Ok(odrefresh::ExitCode::CompilationSuccess) => task.callback.onSuccess(),
+                    Ok(ExitCode::CompilationSuccess) => task.callback.onSuccess(),
                     Ok(exit_code) => {
                         error!("Unexpected odrefresh result: {:?}", exit_code);
                         task.callback.onFailure()
@@ -90,8 +106,49 @@
     }
 }
 
-struct RunningTask {
-    callback: Strong<dyn ICompilationTaskCallback>,
-    #[allow(dead_code)] // Keeps the CompOS VM alive
-    comp_os: Arc<CompOsInstance>,
+fn run_in_vm(service: Strong<dyn ICompOsService>, target_dir_name: &str) -> Result<ExitCode> {
+    let output_root = Path::new(ART_APEX_DATA);
+
+    // We need to remove the target directory because odrefresh running in compos will create it
+    // (and can't see the existing one, since authfs doesn't show it existing files in an output
+    // directory).
+    let target_path = output_root.join(target_dir_name);
+    if target_path.exists() {
+        remove_dir_all(&target_path)
+            .with_context(|| format!("Failed to delete {}", target_path.display()))?;
+    }
+
+    let staging_dir = open_dir(composd_native::palette_create_odrefresh_staging_directory()?)?;
+    let system_dir = open_dir(Path::new("/system"))?;
+    let output_dir = open_dir(output_root)?;
+
+    // Spawn a fd_server to serve the FDs.
+    let fd_server_config = FdServerConfig {
+        ro_dir_fds: vec![system_dir.as_raw_fd()],
+        rw_dir_fds: vec![staging_dir.as_raw_fd(), output_dir.as_raw_fd()],
+        ..Default::default()
+    };
+    let fd_server_raii = fd_server_config.into_fd_server()?;
+
+    let zygote_arch = system_properties::read("ro.zygote")?;
+    let exit_code = service.odrefresh(
+        system_dir.as_raw_fd(),
+        output_dir.as_raw_fd(),
+        staging_dir.as_raw_fd(),
+        target_dir_name,
+        &zygote_arch,
+    )?;
+
+    drop(fd_server_raii);
+    ExitCode::from_i32(exit_code.into())
+}
+
+/// Returns an owned FD of the directory. It currently returns a `File` as a FD owner, but
+/// it's better to use `std::os::unix::io::OwnedFd` once/if it becomes standard.
+fn open_dir(path: &Path) -> Result<File> {
+    OpenOptions::new()
+        .custom_flags(libc::O_DIRECTORY)
+        .read(true) // O_DIRECTORY can only be opened with read
+        .open(path)
+        .with_context(|| format!("Failed to open {:?} directory as path fd", path))
 }
diff --git a/compos/composd/src/service.rs b/compos/composd/src/service.rs
index 6ce462c..093e428 100644
--- a/compos/composd/src/service.rs
+++ b/compos/composd/src/service.rs
@@ -17,11 +17,8 @@
 //! Implementation of IIsolatedCompilationService, called from system server when compilation is
 //! desired.
 
-use crate::compilation_task::CompilationTask;
 use crate::instance_manager::InstanceManager;
-use crate::odrefresh;
 use crate::odrefresh_task::OdrefreshTask;
-use crate::util::to_binder_result;
 use android_system_composd::aidl::android::system::composd::{
     ICompilationTask::{BnCompilationTask, ICompilationTask},
     ICompilationTaskCallback::ICompilationTaskCallback,
@@ -31,6 +28,7 @@
     self, BinderFeatures, ExceptionCode, Interface, Status, Strong, ThreadState,
 };
 use anyhow::{Context, Result};
+use compos_common::binder::to_binder_result;
 use rustutils::{users::AID_ROOT, users::AID_SYSTEM};
 use std::sync::Arc;
 
@@ -63,19 +61,6 @@
         check_permissions()?;
         to_binder_result(self.do_start_test_compile(callback))
     }
-
-    fn startAsyncOdrefresh(
-        &self,
-        callback: &Strong<dyn ICompilationTaskCallback>,
-    ) -> binder::Result<Strong<dyn ICompilationTask>> {
-        check_permissions()?;
-        to_binder_result(self.do_start_async_odrefresh(callback))
-    }
-
-    fn startTestOdrefresh(&self) -> binder::Result<i8> {
-        check_permissions()?;
-        to_binder_result(self.do_odrefresh_for_test())
-    }
 }
 
 impl IsolatedCompilationService {
@@ -86,7 +71,9 @@
         // TODO: Try to start the current instance with staged APEXes to see if it works?
         let comp_os = self.instance_manager.start_pending_instance().context("Starting CompOS")?;
 
-        let task = CompilationTask::start_staged_apex_compile(comp_os, callback)?;
+        // TODO: Write to compos-pending instead
+        let target_dir_name = "test-artifacts".to_owned();
+        let task = OdrefreshTask::start(comp_os, target_dir_name, callback)?;
 
         Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
     }
@@ -97,33 +84,11 @@
     ) -> Result<Strong<dyn ICompilationTask>> {
         let comp_os = self.instance_manager.start_test_instance().context("Starting CompOS")?;
 
-        let task = CompilationTask::start_test_compile(comp_os, callback)?;
-
-        Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
-    }
-
-    fn do_start_async_odrefresh(
-        &self,
-        callback: &Strong<dyn ICompilationTaskCallback>,
-    ) -> Result<Strong<dyn ICompilationTask>> {
-        let comp_os = self.instance_manager.start_test_instance().context("Starting CompOS")?;
-
         let target_dir_name = "test-artifacts".to_owned();
         let task = OdrefreshTask::start(comp_os, target_dir_name, callback)?;
 
         Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
     }
-
-    fn do_odrefresh_for_test(&self) -> Result<i8> {
-        let compos = self
-            .instance_manager
-            .start_test_instance()
-            .context("Starting CompOS for odrefresh test")?;
-        let service = compos.get_service();
-
-        let exit_code = odrefresh::run_in_vm(service, "test-artifacts")?;
-        Ok(exit_code as i8)
-    }
 }
 
 fn check_permissions() -> binder::Result<()> {
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index 37d5378..546c4af 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -34,9 +34,11 @@
 
 fn main() -> Result<()> {
     let app = clap::App::new("composd_cmd").arg(
-        clap::Arg::with_name("command").index(1).takes_value(true).required(true).possible_values(
-            &["staged-apex-compile", "forced-compile-test", "forced-odrefresh", "async-odrefresh"],
-        ),
+        clap::Arg::with_name("command")
+            .index(1)
+            .takes_value(true)
+            .required(true)
+            .possible_values(&["staged-apex-compile", "test-compile"]),
     );
     let args = app.get_matches();
     let command = args.value_of("command").unwrap();
@@ -45,9 +47,7 @@
 
     match command {
         "staged-apex-compile" => run_staged_apex_compile()?,
-        "forced-compile-test" => run_forced_compile_for_test()?,
-        "forced-odrefresh" => run_forced_odrefresh_for_test()?,
-        "async-odrefresh" => run_async_odrefresh_for_test()?,
+        "test-compile" => run_test_compile()?,
         _ => panic!("Unexpected command {}", command),
     }
 
@@ -108,14 +108,10 @@
     run_async_compilation(|service, callback| service.startStagedApexCompile(callback))
 }
 
-fn run_forced_compile_for_test() -> Result<()> {
+fn run_test_compile() -> Result<()> {
     run_async_compilation(|service, callback| service.startTestCompile(callback))
 }
 
-fn run_async_odrefresh_for_test() -> Result<()> {
-    run_async_compilation(|service, callback| service.startAsyncOdrefresh(callback))
-}
-
 fn run_async_compilation<F>(start_compile_fn: F) -> Result<()>
 where
     F: FnOnce(
@@ -155,11 +151,3 @@
         }
     }
 }
-
-fn run_forced_odrefresh_for_test() -> Result<()> {
-    let service = wait_for_interface::<dyn IIsolatedCompilationService>("android.system.composd")
-        .context("Failed to connect to composd service")?;
-    let compilation_result = service.startTestOdrefresh().context("Compilation failed")?;
-    println!("odrefresh exit code: {:?}", compilation_result);
-    Ok(())
-}
diff --git a/compos/src/artifact_signer.rs b/compos/src/artifact_signer.rs
index 54b8d7a..a4b47d6 100644
--- a/compos/src/artifact_signer.rs
+++ b/compos/src/artifact_signer.rs
@@ -19,7 +19,7 @@
 
 #![allow(dead_code)] // Will be used soon
 
-use crate::compos_key_service::CompOsKeyService;
+use crate::compos_key_service::Signer;
 use crate::fsverity;
 use anyhow::{anyhow, Context, Result};
 use odsign_proto::odsign_info::OdsignInfo;
@@ -27,31 +27,25 @@
 use std::fs::File;
 use std::io::Write;
 use std::os::unix::io::AsRawFd;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 
 const TARGET_DIRECTORY: &str = "/data/misc/apexdata/com.android.art/dalvik-cache";
 const SIGNATURE_EXTENSION: &str = ".signature";
 
 /// Accumulates and then signs information about generated artifacts.
 pub struct ArtifactSigner<'a> {
-    key_blob: Vec<u8>,
-    key_service: &'a CompOsKeyService,
-    base_directory: PathBuf,
+    base_directory: &'a Path,
     file_digests: Vec<(String, String)>, // (File name, digest in hex)
 }
 
 impl<'a> ArtifactSigner<'a> {
     /// base_directory specifies the directory under which the artifacts are currently located;
     /// they will eventually be moved under TARGET_DIRECTORY once they are verified and activated.
-    pub fn new(
-        key_blob: Vec<u8>,
-        key_service: &'a CompOsKeyService,
-        base_directory: PathBuf,
-    ) -> Self {
-        ArtifactSigner { key_blob, key_service, base_directory, file_digests: Vec::new() }
+    pub fn new(base_directory: &'a Path) -> Self {
+        Self { base_directory, file_digests: Vec::new() }
     }
 
-    pub fn add_artifact(&mut self, path: PathBuf) -> Result<()> {
+    pub fn add_artifact(&mut self, path: &Path) -> Result<()> {
         // The path we store is where the file will be when it is verified, not where it is now.
         let suffix = path
             .strip_prefix(&self.base_directory)
@@ -59,7 +53,7 @@
         let target_path = Path::new(TARGET_DIRECTORY).join(suffix);
         let target_path = target_path.to_str().ok_or_else(|| anyhow!("Invalid path"))?;
 
-        let file = File::open(&path)?;
+        let file = File::open(path).with_context(|| format!("Opening {}", path.display()))?;
         let digest = fsverity::measure(file.as_raw_fd())?;
         let digest = to_hex_string(&digest);
 
@@ -69,20 +63,22 @@
 
     /// Consume this ArtifactSigner and write details of all its artifacts to the given path,
     /// with accompanying sigature file.
-    pub fn write_info_and_signature(self, info_path: &Path) -> Result<()> {
+    pub fn write_info_and_signature(self, signer: Signer, info_path: &Path) -> Result<()> {
         let mut info = OdsignInfo::new();
         info.mut_file_hashes().extend(self.file_digests.into_iter());
         let bytes = info.write_to_bytes()?;
 
-        let signature = self.key_service.sign(&self.key_blob, &bytes)?;
+        let signature = signer.sign(&bytes)?;
 
-        let mut file = File::create(info_path)?;
+        let mut file =
+            File::create(info_path).with_context(|| format!("Creating {}", info_path.display()))?;
         file.write_all(&bytes)?;
 
         let mut signature_name = info_path.file_name().unwrap().to_owned();
         signature_name.push(SIGNATURE_EXTENSION);
         let signature_path = info_path.with_file_name(&signature_name);
-        let mut signature_file = File::create(&signature_path)?;
+        let mut signature_file = File::create(&signature_path)
+            .with_context(|| format!("Creating {}", signature_path.display()))?;
         signature_file.write_all(&signature)?;
 
         Ok(())
diff --git a/compos/src/compilation.rs b/compos/src/compilation.rs
index f8a66c2..2ca4dd4 100644
--- a/compos/src/compilation.rs
+++ b/compos/src/compilation.rs
@@ -18,10 +18,12 @@
 use log::{debug, error, info};
 use minijail::{self, Minijail};
 use std::env;
-use std::fs::File;
+use std::fs::{read_dir, File};
 use std::os::unix::io::{AsRawFd, RawFd};
-use std::path::{Path, PathBuf};
+use std::path::{self, Path, PathBuf};
 
+use crate::artifact_signer::ArtifactSigner;
+use crate::compos_key_service::Signer;
 use crate::fsverity;
 use authfs_aidl_interface::aidl::com::android::virt::fs::{
     AuthFsConfig::{
@@ -34,6 +36,7 @@
 };
 use authfs_aidl_interface::binder::{ParcelFileDescriptor, Strong};
 use compos_aidl_interface::aidl::com::android::compos::FdAnnotation::FdAnnotation;
+use compos_common::odrefresh::ExitCode;
 
 const FD_SERVER_PORT: i32 = 3264; // TODO: support dynamic port
 
@@ -58,28 +61,56 @@
     image: ParcelFileDescriptor,
 }
 
-pub fn odrefresh(
-    odrefresh_path: &Path,
-    target_dir_name: &str,
+pub struct OdrefreshContext<'a> {
     system_dir_fd: i32,
     output_dir_fd: i32,
     staging_dir_fd: i32,
-    zygote_arch: &str,
+    target_dir_name: &'a str,
+    zygote_arch: &'a str,
+}
+
+impl<'a> OdrefreshContext<'a> {
+    pub fn new(
+        system_dir_fd: i32,
+        output_dir_fd: i32,
+        staging_dir_fd: i32,
+        target_dir_name: &'a str,
+        zygote_arch: &'a str,
+    ) -> Result<Self> {
+        if system_dir_fd < 0 || output_dir_fd < 0 || staging_dir_fd < 0 {
+            bail!("The remote FDs are expected to be non-negative");
+        }
+        if zygote_arch != "zygote64" && zygote_arch != "zygote64_32" {
+            bail!("Invalid zygote arch");
+        }
+        // Disallow any sort of path traversal
+        if target_dir_name.contains(path::MAIN_SEPARATOR) {
+            bail!("Invalid target directory {}", target_dir_name);
+        }
+
+        Ok(Self { system_dir_fd, output_dir_fd, staging_dir_fd, target_dir_name, zygote_arch })
+    }
+}
+
+pub fn odrefresh(
+    odrefresh_path: &Path,
+    context: OdrefreshContext,
     authfs_service: Strong<dyn IAuthFsService>,
-) -> Result<i8> {
+    signer: Signer,
+) -> Result<ExitCode> {
     // Mount authfs (via authfs_service). The authfs instance unmounts once the `authfs` variable
     // is out of scope.
     let authfs_config = AuthFsConfig {
         port: FD_SERVER_PORT,
         inputDirFdAnnotations: vec![InputDirFdAnnotation {
-            fd: system_dir_fd,
+            fd: context.system_dir_fd,
             // TODO(206869687): Replace /dev/null with the real path when possible.
             manifestPath: "/dev/null".to_string(),
             prefix: "/system".to_string(),
         }],
         outputDirFdAnnotations: vec![
-            OutputDirFdAnnotation { fd: output_dir_fd },
-            OutputDirFdAnnotation { fd: staging_dir_fd },
+            OutputDirFdAnnotation { fd: context.output_dir_fd },
+            OutputDirFdAnnotation { fd: context.staging_dir_fd },
         ],
         ..Default::default()
     };
@@ -87,21 +118,21 @@
     let mountpoint = PathBuf::from(authfs.getMountPoint()?);
 
     let mut android_root = mountpoint.clone();
-    android_root.push(system_dir_fd.to_string());
+    android_root.push(context.system_dir_fd.to_string());
     android_root.push("system");
     env::set_var("ANDROID_ROOT", &android_root);
+    debug!("ANDROID_ROOT={:?}", &android_root);
 
-    let mut art_apex_data = mountpoint.clone();
-    art_apex_data.push(output_dir_fd.to_string());
+    let art_apex_data = mountpoint.join(context.output_dir_fd.to_string());
     env::set_var("ART_APEX_DATA", &art_apex_data);
+    debug!("ART_APEX_DATA={:?}", &art_apex_data);
 
-    let mut staging_dir = mountpoint;
-    staging_dir.push(staging_dir_fd.to_string());
+    let staging_dir = mountpoint.join(context.staging_dir_fd.to_string());
 
     let args = vec![
         "odrefresh".to_string(),
-        format!("--zygote-arch={}", zygote_arch),
-        format!("--dalvik-cache={}", target_dir_name),
+        format!("--zygote-arch={}", context.zygote_arch),
+        format!("--dalvik-cache={}", context.target_dir_name),
         "--no-refresh".to_string(),
         format!("--staging-dir={}", staging_dir.display()),
         "--force-compile".to_string(),
@@ -109,17 +140,46 @@
     debug!("Running odrefresh with args: {:?}", &args);
     let jail = spawn_jailed_task(odrefresh_path, &args, Vec::new() /* fd_mapping */)
         .context("Spawn odrefresh")?;
-    match jail.wait() {
-        // TODO(161471326): On success, sign all files in the output directory.
-        Ok(()) => Ok(0i8),
-        Err(minijail::Error::ReturnCode(exit_code)) => {
-            info!("odrefresh exited with exit code {}", exit_code);
-            Ok(exit_code as i8)
-        }
+    let exit_code = match jail.wait() {
+        Ok(_) => Result::<u8>::Ok(0),
+        Err(minijail::Error::ReturnCode(exit_code)) => Ok(exit_code),
         Err(e) => {
             bail!("Unexpected minijail error: {}", e)
         }
+    }?;
+
+    let exit_code = ExitCode::from_i32(exit_code.into())?;
+    info!("odrefresh exited with {:?}", exit_code);
+
+    if exit_code == ExitCode::CompilationSuccess {
+        // authfs only shows us the files we created, so it's ok to just sign everything under
+        // the target directory.
+        let target_dir = art_apex_data.join(context.target_dir_name);
+        let mut artifact_signer = ArtifactSigner::new(&target_dir);
+        add_artifacts(&target_dir, &mut artifact_signer)?;
+
+        artifact_signer.write_info_and_signature(signer, &target_dir.join("compos.info"))?;
     }
+
+    Ok(exit_code)
+}
+
+fn add_artifacts(target_dir: &Path, artifact_signer: &mut ArtifactSigner) -> Result<()> {
+    for entry in
+        read_dir(&target_dir).with_context(|| format!("Traversing {}", target_dir.display()))?
+    {
+        let entry = entry?;
+        let file_type = entry.file_type()?;
+        if file_type.is_dir() {
+            add_artifacts(&entry.path(), artifact_signer)?;
+        } else if file_type.is_file() {
+            artifact_signer.add_artifact(&entry.path())?;
+        } else {
+            // authfs shouldn't create anything else, but just in case
+            bail!("Unexpected file type in artifacts: {:?}", entry);
+        }
+    }
+    Ok(())
 }
 
 /// Runs the compiler with given flags with file descriptors described in `fd_annotation` retrieved
diff --git a/compos/src/compos_key_service.rs b/compos/src/compos_key_service.rs
index f6caac9..086a162 100644
--- a/compos/src/compos_key_service.rs
+++ b/compos/src/compos_key_service.rs
@@ -101,7 +101,7 @@
         let mut data = [0u8; 32];
         self.random.fill(&mut data).context("No random data")?;
 
-        let signature = self.sign(key_blob, &data)?;
+        let signature = self.new_signer(key_blob).sign(&data)?;
 
         let public_key =
             signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key);
@@ -110,8 +110,19 @@
         Ok(())
     }
 
-    pub fn sign(&self, key_blob: &[u8], data: &[u8]) -> Result<Vec<u8>> {
-        let key_descriptor = KeyDescriptor { blob: Some(key_blob.to_vec()), ..BLOB_KEY_DESCRIPTOR };
+    pub fn new_signer(&self, key_blob: &[u8]) -> Signer {
+        Signer { key_blob: key_blob.to_vec(), security_level: self.security_level.clone() }
+    }
+}
+
+pub struct Signer {
+    key_blob: Vec<u8>,
+    security_level: Strong<dyn IKeystoreSecurityLevel>,
+}
+
+impl Signer {
+    pub fn sign(self, data: &[u8]) -> Result<Vec<u8>> {
+        let key_descriptor = KeyDescriptor { blob: Some(self.key_blob), ..BLOB_KEY_DESCRIPTOR };
         let operation_parameters = [PURPOSE_SIGN, ALGORITHM, PADDING, DIGEST];
         let forced = false;
 
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 4540cd8..8f1e205 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -18,16 +18,17 @@
 //! file descriptors backed by authfs (via authfs_service) and pass the file descriptors to the
 //! actual compiler.
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use binder_common::new_binder_exception;
+use compos_common::binder::to_binder_result;
 use log::warn;
 use std::default::Default;
 use std::env;
 use std::path::PathBuf;
 use std::sync::RwLock;
 
-use crate::compilation::{compile_cmd, odrefresh, CompilerOutput};
-use crate::compos_key_service::CompOsKeyService;
+use crate::compilation::{compile_cmd, odrefresh, CompilerOutput, OdrefreshContext};
+use crate::compos_key_service::{CompOsKeyService, Signer};
 use crate::fsverity;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::IAuthFsService;
 use compos_aidl_interface::aidl::com::android::compos::{
@@ -39,10 +40,10 @@
 use compos_aidl_interface::binder::{
     BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Strong,
 };
+use compos_common::odrefresh::ODREFRESH_PATH;
 
 const AUTHFS_SERVICE_NAME: &str = "authfs_service";
 const DEX2OAT_PATH: &str = "/apex/com.android.art/bin/dex2oat64";
-const ODREFRESH_PATH: &str = "/apex/com.android.art/bin/odrefresh";
 
 /// Constructs a binder object that implements ICompOsService.
 pub fn new_binder() -> Result<Strong<dyn ICompOsService>> {
@@ -65,14 +66,19 @@
 impl CompOsService {
     fn generate_raw_fsverity_signature(
         &self,
-        key_blob: &[u8],
         fsverity_digest: &fsverity::Sha256Digest,
-    ) -> Vec<u8> {
+    ) -> BinderResult<Vec<u8>> {
         let formatted_digest = fsverity::to_formatted_digest(fsverity_digest);
-        self.key_service.sign(key_blob, &formatted_digest[..]).unwrap_or_else(|e| {
-            warn!("Failed to sign the fsverity digest, returning empty signature.  Error: {}", e);
-            Vec::new()
-        })
+        to_binder_result(self.new_signer()?.sign(&formatted_digest[..]))
+    }
+
+    fn new_signer(&self) -> BinderResult<Signer> {
+        let key = &*self.key_blob.read().unwrap();
+        if key.is_empty() {
+            Err(new_binder_exception(ExceptionCode::ILLEGAL_STATE, "Key is not initialized"))
+        } else {
+            Ok(self.key_service.new_signer(key))
+        }
     }
 }
 
@@ -110,36 +116,20 @@
         target_dir_name: &str,
         zygote_arch: &str,
     ) -> BinderResult<i8> {
-        if system_dir_fd < 0 || output_dir_fd < 0 || staging_dir_fd < 0 {
-            return Err(new_binder_exception(
-                ExceptionCode::ILLEGAL_ARGUMENT,
-                "The remote FDs are expected to be non-negative",
-            ));
-        }
-        if zygote_arch != "zygote64" && zygote_arch != "zygote64_32" {
-            return Err(new_binder_exception(
-                ExceptionCode::ILLEGAL_ARGUMENT,
-                "Invalid zygote arch",
-            ));
-        }
-
-        let authfs_service = get_authfs_service()?;
-        odrefresh(
-            &self.odrefresh_path,
-            target_dir_name,
+        let context = to_binder_result(OdrefreshContext::new(
             system_dir_fd,
             output_dir_fd,
             staging_dir_fd,
+            target_dir_name,
             zygote_arch,
-            authfs_service,
-        )
-        .map_err(|e| {
-            warn!("odrefresh failed: {}", e);
-            new_binder_exception(
-                ExceptionCode::SERVICE_SPECIFIC,
-                format!("odrefresh failed: {}", e),
-            )
-        })
+        ))?;
+
+        let authfs_service = get_authfs_service()?;
+        let exit_code = to_binder_result(
+            odrefresh(&self.odrefresh_path, context, authfs_service, self.new_signer()?)
+                .context("odrefresh failed"),
+        )?;
+        Ok(exit_code as i8)
     }
 
     fn compile_cmd(
@@ -148,32 +138,21 @@
         fd_annotation: &FdAnnotation,
     ) -> BinderResult<CompilationResult> {
         let authfs_service = get_authfs_service()?;
-        let output =
-            compile_cmd(&self.dex2oat_path, args, authfs_service, fd_annotation).map_err(|e| {
-                new_binder_exception(
-                    ExceptionCode::SERVICE_SPECIFIC,
-                    format!("Compilation failed: {}", e),
-                )
-            })?;
+        let output = to_binder_result(
+            compile_cmd(&self.dex2oat_path, args, authfs_service, fd_annotation)
+                .context("Compilation failed"),
+        )?;
         match output {
             CompilerOutput::Digests { oat, vdex, image } => {
-                let key = &*self.key_blob.read().unwrap();
-                if key.is_empty() {
-                    Err(new_binder_exception(
-                        ExceptionCode::ILLEGAL_STATE,
-                        "Key is not initialized",
-                    ))
-                } else {
-                    let oat_signature = self.generate_raw_fsverity_signature(key, &oat);
-                    let vdex_signature = self.generate_raw_fsverity_signature(key, &vdex);
-                    let image_signature = self.generate_raw_fsverity_signature(key, &image);
-                    Ok(CompilationResult {
-                        exitCode: 0,
-                        oatSignature: oat_signature,
-                        vdexSignature: vdex_signature,
-                        imageSignature: image_signature,
-                    })
-                }
+                let oat_signature = self.generate_raw_fsverity_signature(&oat)?;
+                let vdex_signature = self.generate_raw_fsverity_signature(&vdex)?;
+                let image_signature = self.generate_raw_fsverity_signature(&image)?;
+                Ok(CompilationResult {
+                    exitCode: 0,
+                    oatSignature: oat_signature,
+                    vdexSignature: vdex_signature,
+                    imageSignature: image_signature,
+                })
             }
             CompilerOutput::ExitCode(exit_code) => {
                 Ok(CompilationResult { exitCode: exit_code, ..Default::default() })
@@ -186,30 +165,17 @@
     }
 
     fn generateSigningKey(&self) -> BinderResult<CompOsKeyData> {
-        self.key_service
-            .generate()
-            .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))
+        to_binder_result(self.key_service.generate())
     }
 
     fn verifySigningKey(&self, key_blob: &[u8], public_key: &[u8]) -> BinderResult<bool> {
         Ok(if let Err(e) = self.key_service.verify(key_blob, public_key) {
-            warn!("Signing key verification failed: {}", e.to_string());
+            warn!("Signing key verification failed: {:?}", e);
             false
         } else {
             true
         })
     }
-
-    fn sign(&self, data: &[u8]) -> BinderResult<Vec<u8>> {
-        let key = &*self.key_blob.read().unwrap();
-        if key.is_empty() {
-            Err(new_binder_exception(ExceptionCode::ILLEGAL_STATE, "Key is not initialized"))
-        } else {
-            self.key_service
-                .sign(key, data)
-                .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))
-        }
-    }
 }
 
 fn get_authfs_service() -> BinderResult<Strong<dyn IAuthFsService>> {
diff --git a/compos/tests/Android.bp b/compos/tests/Android.bp
index 7e00d7b..d380059 100644
--- a/compos/tests/Android.bp
+++ b/compos/tests/Android.bp
@@ -14,7 +14,4 @@
         "VirtualizationTestHelper",
     ],
     test_suites: ["general-tests"],
-    data: [
-        ":CompOSPayloadApp.signing",
-    ],
 }
diff --git a/compos/tests/java/android/compos/test/ComposKeyTestCase.java b/compos/tests/java/android/compos/test/ComposKeyTestCase.java
index eacf3fb..961f34a 100644
--- a/compos/tests/java/android/compos/test/ComposKeyTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposKeyTestCase.java
@@ -120,44 +120,15 @@
                         TEST_ROOT + "test_key.pubkey",
                         TEST_ROOT + "test_key2.blob");
         assertThat(result.getStatus()).isEqualTo(CommandStatus.FAILED);
-
-        // Now, continue to test the signing operation. It's the best to do this in a new test
-        // method. Since we boot a VM for each test method, and booting a VM on cuttlefish/GCE is
-        // very slow, a new test method unfortunately makes the whole test module to exceed the
-        // timeout configured in the test infrastructure.
-
-        // Generate key - should succeed
-        android.run(
-                COMPOS_KEY_CMD_BIN,
-                "--cid " + mCid,
-                "generate",
-                TEST_ROOT + "test_key3.blob",
-                TEST_ROOT + "test_key3.pubkey");
-
-        // Generate some data to sign in a writable directory
-        android.run("echo something > /data/local/tmp/something.txt");
-
-        // Sign something - should succeed
-        android.run(
-                COMPOS_KEY_CMD_BIN,
-                "--cid " + mCid,
-                "sign-info",
-                TEST_ROOT + "test_key3.blob",
-                TEST_ROOT + "test.info",
-                "/data/local/tmp/something.txt");
-
-        // Check existence of the output signature - should succeed
-        android.run("test -f " + TEST_ROOT + "test.info.signature");
     }
 
     private void startVm() throws Exception {
-        final String apkName = "CompOSPayloadApp.apk";
         final String packageName = "com.android.compos.payload";
         mCid =
                 startMicrodroid(
                         getDevice(),
                         getBuild(),
-                        apkName,
+                        /* apkName, no need to install */ null,
                         packageName,
                         "assets/vm_test_config.json",
                         /* debug */ true,
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index 5f4bd00..8906361 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -87,21 +87,24 @@
         }
 
         // Save the expected checksum for the output directory.
-        String expectedChecksumSnapshot = checksumDirectoryContent(android, ODREFRESH_OUTPUT_DIR);
+        String expectedChecksumSnapshot = checksumDirectoryContentPartial(android,
+                ODREFRESH_OUTPUT_DIR);
 
-        // Let --check clean up the output.
+        // --check may delete the output.
         CommandResult result = runOdrefresh(android, "--check");
         assertThat(result.getExitCode()).isEqualTo(OKAY);
 
-        // Make sure we generate a fresh instance
+        // Make sure we generate a fresh instance.
         android.tryRun("rm", "-rf", COMPOS_TEST_ROOT);
+        // TODO: remove once composd starts to clean up the directory.
+        android.tryRun("rm", "-rf", ODREFRESH_OUTPUT_DIR);
 
         // Expect the compilation in Compilation OS to finish successfully.
         {
             long start = System.currentTimeMillis();
             result =
                     android.runForResultWithTimeout(
-                            ODREFRESH_TIMEOUT_MS, COMPOSD_CMD_BIN, "forced-compile-test");
+                            ODREFRESH_TIMEOUT_MS, COMPOSD_CMD_BIN, "test-compile");
             long elapsed = System.currentTimeMillis() - start;
             assertThat(result.getExitCode()).isEqualTo(0);
             CLog.i("Comp OS compilation took " + elapsed + "ms");
@@ -109,22 +112,22 @@
         killVmAndReconnectAdb();
 
         // Save the actual checksum for the output directory.
-        String actualChecksumSnapshot = checksumDirectoryContent(android, ODREFRESH_OUTPUT_DIR);
-
-        // Expect the output to be valid.
-        result = runOdrefresh(android, "--verify");
-        assertThat(result.getExitCode()).isEqualTo(OKAY);
-        // --check can delete the output, so run later.
-        result = runOdrefresh(android, "--check");
-        assertThat(result.getExitCode()).isEqualTo(OKAY);
+        String actualChecksumSnapshot = checksumDirectoryContentPartial(android,
+                ODREFRESH_OUTPUT_DIR);
 
         // Expect the output of Comp OS to be the same as compiled on Android.
         assertThat(actualChecksumSnapshot).isEqualTo(expectedChecksumSnapshot);
+
+        // Expect extra files generated by CompOS exist.
+        android.assumeSuccess("test -f " + ODREFRESH_OUTPUT_DIR + "/compos.info");
+        android.assumeSuccess("test -f " + ODREFRESH_OUTPUT_DIR + "/compos.info.signature");
     }
 
     private CommandResult runOdrefresh(CommandRunner android, String command) throws Exception {
         return android.runForResultWithTimeout(
                 ODREFRESH_TIMEOUT_MS,
+                // TODO(b/210472252): Remove this when the VM handles STANDALONE_SYSTEMSERVER_JARS
+                "STANDALONE_SYSTEMSERVER_JARS=",
                 ODREFRESH_BIN,
                 "--dalvik-cache=" + TEST_ARTIFACTS_DIR,
                 command);
@@ -147,8 +150,14 @@
         android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
     }
 
-    private String checksumDirectoryContent(CommandRunner runner, String path) throws Exception {
-        // Sort by filename (second column) to make comparison easier.
-        return runner.run("find " + path + " -type f -exec sha256sum {} \\; | sort -k2");
+    private String checksumDirectoryContentPartial(CommandRunner runner, String path)
+            throws Exception {
+        // Sort by filename (second column) to make comparison easier. Filter out compos.info and
+        // compos.info.signature since it's only generated by CompOS.
+        // TODO(b/210473615): Remove irrelevant APEXes (i.e. those aren't contributing to the
+        // classpaths, thus not in the VM) from cache-info.xml.
+        return runner.run("cd " + path + "; find -type f -exec sha256sum {} \\;"
+                + "| grep -v cache-info.xml | grep -v compos.info"
+                + "| sort -k2");
     }
 }
diff --git a/compos/verify_key/verify_key.rs b/compos/verify_key/verify_key.rs
index e0ed5e5..a028264 100644
--- a/compos/verify_key/verify_key.rs
+++ b/compos/verify_key/verify_key.rs
@@ -21,7 +21,7 @@
 use compos_aidl_interface::binder::ProcessState;
 use compos_common::compos_client::{VmInstance, VmParameters};
 use compos_common::{
-    COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR, INSTANCE_IMAGE_FILE, PENDING_INSTANCE_DIR,
+    COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR, IDSIG_FILE, INSTANCE_IMAGE_FILE, PENDING_INSTANCE_DIR,
     PRIVATE_KEY_BLOB_FILE, PUBLIC_KEY_FILE, TEST_INSTANCE_DIR,
 };
 use std::fs::{self, File};
@@ -99,6 +99,7 @@
     let blob = instance_dir.join(PRIVATE_KEY_BLOB_FILE);
     let public_key = instance_dir.join(PUBLIC_KEY_FILE);
     let instance_image = instance_dir.join(INSTANCE_IMAGE_FILE);
+    let idsig = instance_dir.join(IDSIG_FILE);
 
     let blob = read_small_file(blob).context("Failed to read key blob")?;
     let public_key = read_small_file(public_key).context("Failed to read public key")?;
@@ -108,6 +109,7 @@
     let vm_instance = VmInstance::start(
         &*virtualization_service,
         instance_image,
+        &idsig,
         &VmParameters { debug_mode, ..Default::default() },
     )?;
     let service = vm_instance.get_service()?;
diff --git a/idsig/src/hashtree.rs b/idsig/src/hashtree.rs
index 1ce2879..63f83ea 100644
--- a/idsig/src/hashtree.rs
+++ b/idsig/src/hashtree.rs
@@ -100,7 +100,7 @@
             // and split it.
             let prev = &levels[n - 1];
             let cur_and_prev = &mut hash_tree[cur.start..prev.end];
-            let (cur, prev) = cur_and_prev.split_at_mut(prev.start);
+            let (cur, prev) = cur_and_prev.split_at_mut(prev.start - cur.start);
             let mut cur = Cursor::new(cur);
             prev.chunks(block_size).for_each(|data| {
                 let h = hash_one_block(data, salt, block_size, algorithm);
@@ -200,7 +200,7 @@
     #[test]
     fn compare_with_golden_output() -> Result<()> {
         // The golden outputs are generated by using the `fsverity` utility.
-        let sizes = ["512", "4K", "1M", "10000000"];
+        let sizes = ["512", "4K", "1M", "10000000", "272629760"];
         for size in sizes.iter() {
             let input_name = format!("testdata/input.{}", size);
             let mut input = File::open(&input_name)?;
diff --git a/idsig/testdata/create.sh b/idsig/testdata/create.sh
index eadfdb2..1a15d2b 100755
--- a/idsig/testdata/create.sh
+++ b/idsig/testdata/create.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-sizes="512 4K 1M 10000000"
+sizes="512 4K 1M 10000000 272629760"
 for size in $sizes; do
   echo $size
   dd if=/dev/random of=input.$size bs=$size count=1
diff --git a/idsig/testdata/input.272629760 b/idsig/testdata/input.272629760
new file mode 100644
index 0000000..5bb6753
--- /dev/null
+++ b/idsig/testdata/input.272629760
Binary files differ
diff --git a/idsig/testdata/input.272629760.descriptor b/idsig/testdata/input.272629760.descriptor
new file mode 100644
index 0000000..70e0744
--- /dev/null
+++ b/idsig/testdata/input.272629760.descriptor
Binary files differ
diff --git a/idsig/testdata/input.272629760.hash b/idsig/testdata/input.272629760.hash
new file mode 100644
index 0000000..f2c68cc
--- /dev/null
+++ b/idsig/testdata/input.272629760.hash
Binary files differ
diff --git a/microdroid/build.prop b/microdroid/build.prop
index ada945d..4ae50c0 100644
--- a/microdroid/build.prop
+++ b/microdroid/build.prop
@@ -6,3 +6,6 @@
 # TODO(b/189164487): support build related properties
 ro.build.version.release=11
 ro.build.version.security_patch=2021-07-05
+
+# Payload metadata partition
+apexd.payload_metadata.path=/dev/block/by-name/payload-metadata
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index fccf031..4420a49 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -34,6 +34,7 @@
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
 use std::fs::{self, create_dir, File, OpenOptions};
+use std::io::BufRead;
 use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::path::Path;
 use std::process::{Child, Command, Stdio};
@@ -435,7 +436,31 @@
 
 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(|| -> Result<Vec<u8>> {
+        let f = File::open("/proc/bootconfig")?;
+
+        // Filter-out androidboot.vbmeta.device which contains UUID of the vbmeta partition. That
+        // UUID could change everytime when the same VM is started because the composite disk image
+        // is ephemeral. A change in UUID is okay as long as other configs (e.g.
+        // androidboot.vbmeta.digest) remain same.
+        Ok(std::io::BufReader::new(f)
+            .lines()
+            // note: this try_fold is to early return when we fail to read a line from the file
+            .try_fold(Vec::new(), |mut lines, line| {
+                line.map(|s| {
+                    lines.push(s);
+                    lines
+                })
+            })?
+            .into_iter()
+            .filter(|line| {
+                let tokens: Vec<&str> = line.splitn(2, '=').collect();
+                // note: if `line` doesn't have =, tokens[0] is the entire line.
+                tokens[0].trim() != "androidboot.vbmeta.device"
+            })
+            .flat_map(|line| (line + "\n").into_bytes())
+            .collect())
+    })
 }
 
 fn load_config(path: &Path) -> Result<VmPayloadConfig> {
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 8d9a7e3..c71d6ac 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -186,9 +186,11 @@
             throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(androidDevice);
 
-        // Install APK
-        File apkFile = findTestFile(buildInfo, apkName);
-        androidDevice.installPackage(apkFile, /* reinstall */ true);
+        // Install APK if necessary
+        if (apkName != null) {
+            File apkFile = findTestFile(buildInfo, apkName);
+            androidDevice.installPackage(apkFile, /* reinstall */ true);
+        }
 
         // Get the path to the installed apk. Note that
         // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect