compos: Sign fs-verity digest of output files

compsvc now needs to be initialized with the signing key blob to finish
the whole compilation flow, otherwise it'll be an illegal state error.
"init-key" command is added to compos_key_cmd for that purpose.

Also, move some fs-verity related code into fsverity.rs.

Bug: 161471326
Test: ComposHostTestCases
Change-Id: I80db78ce11dc1f49e9ee36af47ad98f200aa4388
diff --git a/compos/aidl/com/android/compos/CompilationResult.aidl b/compos/aidl/com/android/compos/CompilationResult.aidl
new file mode 100644
index 0000000..7a50765
--- /dev/null
+++ b/compos/aidl/com/android/compos/CompilationResult.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package com.android.compos;
+
+/** {@hide} */
+parcelable CompilationResult {
+    /** Exit code of dex2oat */
+    byte exitCode;
+
+    /** raw signature of the output oat's fs-verity digest, may be empty */
+    byte[] oatSignature;
+
+    /** raw signature of the output vdex's fs-verity digest, may be empty */
+    byte[] vdexSignature;
+
+    /** raw signature of the output image's fs-verity digest, may be empty */
+    byte[] imageSignature;
+}
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index aaba86c..3a74940 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -17,6 +17,7 @@
 package com.android.compos;
 
 import com.android.compos.CompOsKeyData;
+import com.android.compos.CompilationResult;
 import com.android.compos.Metadata;
 
 /** {@hide} */
@@ -32,7 +33,7 @@
     void initializeSigningKey(in byte[] keyBlob);
 
     /**
-     * Execute a command composed of the args, in a context that may be specified in the Metadata,
+     * Run dex2oat command with provided args, in a context that may be specified in the Metadata,
      * e.g. with file descriptors pre-opened. The service is responsible to decide what executables
      * it may run.
      *
@@ -40,9 +41,9 @@
      *             which may not be used by the service. The service may be configured to always use
      *             a fixed executable, or possibly use the 0-th args are the executable lookup hint.
      * @param metadata Additional information of the execution
-     * @return exit code of the program
+     * @return a CompilationResult
      */
-    byte execute(in String[] args, in Metadata metadata);
+    CompilationResult compile(in String[] args, in Metadata metadata);
 
     /**
      * Generate a new public/private key pair suitable for signing CompOs output files.
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index e65324a..d22e119 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -434,6 +434,28 @@
     return {};
 }
 
+static Result<void> initializeKey(TargetVm& vm, const std::string& blob_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 status = service->initializeSigningKey(blob.value());
+    if (!status.isOk()) {
+        return Error() << "Failed to initialize signing key: " << status.getDescription();
+    }
+    return {};
+}
+
 static Result<void> makeInstanceImage(const std::string& image_path) {
     ndk::SpAIBinder binder(AServiceManager_waitForService("android.system.virtualizationservice"));
     auto service = IVirtualizationService::fromBinder(binder);
@@ -511,6 +533,13 @@
         } else {
             std::cerr << result.error() << '\n';
         }
+    } else if (argc == 3 && argv[1] == "init-key"sv) {
+        auto result = initializeKey(vm, argv[2]);
+        if (result.ok()) {
+            return 0;
+        } else {
+            std::cerr << result.error() << '\n';
+        }
     } else if (argc == 3 && argv[1] == "make-instance"sv) {
         auto result = makeInstanceImage(argv[2]);
         if (result.ok()) {
@@ -519,11 +548,12 @@
             std::cerr << result.error() << '\n';
         }
     } else {
-        std::cerr << "Usage: compos_key_cmd [OPTIONS] generate|verify|sign|make-instance\n"
+        std::cerr << "Usage: compos_key_cmd [OPTIONS] generate|verify|sign|make-instance|init-key\n"
                   << "  generate <blob file> <public key file> Generate new key pair and write\n"
                   << "    the private key blob and public key to the specified files.\n "
                   << "  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 <blob file> <files to be signed> Generate signatures for one or\n"
                   << "    more files using the supplied private key blob. Signature is stored in\n"
                   << "    <filename>.signature\n"
diff --git a/compos/src/compilation.rs b/compos/src/compilation.rs
index 24266e6..0199eb5 100644
--- a/compos/src/compilation.rs
+++ b/compos/src/compilation.rs
@@ -15,15 +15,13 @@
  */
 
 use anyhow::{anyhow, bail, Context, Result};
-use libc::getxattr;
 use log::error;
 use minijail::{self, Minijail};
-use std::ffi::CString;
 use std::fs::File;
-use std::io;
 use std::os::unix::io::{AsRawFd, RawFd};
 use std::path::Path;
 
+use crate::fsverity;
 use authfs_aidl_interface::aidl::com::android::virt::fs::{
     AuthFsConfig::AuthFsConfig, IAuthFs::IAuthFs, IAuthFsService::IAuthFsService,
     InputFdAnnotation::InputFdAnnotation, OutputFdAnnotation::OutputFdAnnotation,
@@ -35,12 +33,13 @@
 /// meaningless in the current process.
 pub type PseudoRawFd = i32;
 
-const SHA256_HASH_SIZE: usize = 32;
-type Sha256Hash = [u8; SHA256_HASH_SIZE];
-
 pub enum CompilerOutput {
     /// Fs-verity digests of output files, if the compiler finishes successfully.
-    Digests { oat: Sha256Hash, vdex: Sha256Hash, image: Sha256Hash },
+    Digests {
+        oat: fsverity::Sha256Digest,
+        vdex: fsverity::Sha256Digest,
+        image: fsverity::Sha256Digest,
+    },
     /// Exit code returned by the compiler, if not 0.
     ExitCode(i8),
 }
@@ -82,9 +81,9 @@
 
     match jail_result {
         Ok(()) => Ok(CompilerOutput::Digests {
-            oat: fsverity_measure(oat_file.as_raw_fd())?,
-            vdex: fsverity_measure(vdex_file.as_raw_fd())?,
-            image: fsverity_measure(image_file.as_raw_fd())?,
+            oat: fsverity::measure(oat_file.as_raw_fd())?,
+            vdex: fsverity::measure(vdex_file.as_raw_fd())?,
+            image: fsverity::measure(image_file.as_raw_fd())?,
         }),
         Err(minijail::Error::ReturnCode(exit_code)) => {
             error!("dex2oat failed with exit code {}", exit_code);
@@ -186,22 +185,3 @@
     let _pid = jail.run_remap(executable, preserve_fds.as_slice(), args)?;
     Ok(jail)
 }
-
-fn fsverity_measure(fd: RawFd) -> Result<Sha256Hash> {
-    // TODO(b/196635431): Unfortunately, the FUSE API doesn't allow authfs to implement the standard
-    // fs-verity ioctls. Until the kernel allows, use the alternative xattr that authfs provides.
-    let path = CString::new(format!("/proc/self/fd/{}", fd).as_str()).unwrap();
-    let name = CString::new("authfs.fsverity.digest").unwrap();
-    let mut buf = [0u8; SHA256_HASH_SIZE];
-    // SAFETY: getxattr should not write beyond the given buffer size.
-    let size = unsafe {
-        getxattr(path.as_ptr(), name.as_ptr(), buf.as_mut_ptr() as *mut libc::c_void, buf.len())
-    };
-    if size < 0 {
-        bail!("Failed to getxattr: {}", io::Error::last_os_error());
-    } else if size != SHA256_HASH_SIZE as isize {
-        bail!("Unexpected hash size: {}", size);
-    } else {
-        Ok(buf)
-    }
-}
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index b55fb7c..4a19030 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -19,16 +19,19 @@
 //! actual compiler.
 
 use anyhow::Result;
-use log::{debug, warn};
+use log::warn;
+use std::default::Default;
 use std::ffi::CString;
 use std::path::PathBuf;
 use std::sync::{Arc, RwLock};
 
 use crate::compilation::{compile, CompilerOutput};
 use crate::compos_key_service::CompOsKeyService;
+use crate::fsverity;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::IAuthFsService;
 use compos_aidl_interface::aidl::com::android::compos::{
     CompOsKeyData::CompOsKeyData,
+    CompilationResult::CompilationResult,
     ICompOsService::{BnCompOsService, ICompOsService},
     Metadata::Metadata,
 };
@@ -55,6 +58,20 @@
     key_blob: Arc<RwLock<Vec<u8>>>,
 }
 
+impl CompOsService {
+    fn generate_raw_fsverity_signature(
+        &self,
+        key_blob: &[u8],
+        fsverity_digest: &fsverity::Sha256Digest,
+    ) -> Vec<u8> {
+        let formatted_digest = fsverity::to_formatted_digest(fsverity_digest);
+        self.key_service.do_sign(key_blob, &formatted_digest[..]).unwrap_or_else(|e| {
+            warn!("Failed to sign the fsverity digest, returning empty signature.  Error: {}", e);
+            Vec::new()
+        })
+    }
+}
+
 impl Interface for CompOsService {}
 
 impl ICompOsService for CompOsService {
@@ -68,7 +85,7 @@
         }
     }
 
-    fn execute(&self, args: &[String], metadata: &Metadata) -> BinderResult<i8> {
+    fn compile(&self, args: &[String], metadata: &Metadata) -> BinderResult<CompilationResult> {
         let authfs_service = get_authfs_service()?;
         let output = compile(&self.dex2oat_path, args, authfs_service, metadata).map_err(|e| {
             new_binder_exception(
@@ -78,13 +95,27 @@
         })?;
         match output {
             CompilerOutput::Digests { oat, vdex, image } => {
-                // TODO(b/161471326): Sign the output on succeed.
-                debug!("oat fs-verity digest: {:02x?}", oat);
-                debug!("vdex fs-verity digest: {:02x?}", vdex);
-                debug!("image fs-verity digest: {:02x?}", image);
-                Ok(0)
+                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,
+                    })
+                }
             }
-            CompilerOutput::ExitCode(exit_code) => Ok(exit_code),
+            CompilerOutput::ExitCode(exit_code) => {
+                Ok(CompilationResult { exitCode: exit_code, ..Default::default() })
+            }
         }
     }
 
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 48e37b6..6396556 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -21,6 +21,7 @@
 mod compilation;
 mod compos_key_service;
 mod compsvc;
+mod fsverity;
 mod signer;
 
 use crate::common::{SERVICE_NAME, VSOCK_PORT};
diff --git a/compos/src/fsverity.rs b/compos/src/fsverity.rs
new file mode 100644
index 0000000..a1e2314
--- /dev/null
+++ b/compos/src/fsverity.rs
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+use anyhow::{bail, Result};
+use libc::getxattr;
+use std::ffi::CString;
+use std::io;
+use std::os::unix::io::RawFd;
+
+/// Magic used in fs-verity digest
+const FS_VERITY_MAGIC: &[u8; 8] = b"FSVerity";
+
+/// Hash algorithm to use from linux/fsverity.h
+const FS_VERITY_HASH_ALG_SHA256: u8 = 1;
+
+const SHA256_HASH_SIZE: usize = 32;
+
+/// Size of `struct fsverity_formatted_digest` with SHA-256 in bytes.
+const FORMATTED_SHA256_DIGEST_SIZE: usize = 12 + SHA256_HASH_SIZE;
+
+/// Bytes of `struct fsverity_formatted_digest` in Linux with SHA-256.
+pub type FormattedSha256Digest = [u8; FORMATTED_SHA256_DIGEST_SIZE];
+
+/// Bytes of SHA256 digest
+pub type Sha256Digest = [u8; SHA256_HASH_SIZE];
+
+/// Returns the fs-verity measurement/digest. Currently only SHA256 is supported.
+pub fn measure(fd: RawFd) -> Result<Sha256Digest> {
+    // TODO(b/196635431): Unfortunately, the FUSE API doesn't allow authfs to implement the standard
+    // fs-verity ioctls. Until the kernel allows, use the alternative xattr that authfs provides.
+    let path = CString::new(format!("/proc/self/fd/{}", fd).as_str()).unwrap();
+    let name = CString::new("authfs.fsverity.digest").unwrap();
+    let mut buf = [0u8; SHA256_HASH_SIZE];
+    // SAFETY: getxattr should not write beyond the given buffer size.
+    let size = unsafe {
+        getxattr(path.as_ptr(), name.as_ptr(), buf.as_mut_ptr() as *mut libc::c_void, buf.len())
+    };
+    if size < 0 {
+        bail!("Failed to getxattr: {}", io::Error::last_os_error());
+    } else if size != SHA256_HASH_SIZE as isize {
+        bail!("Unexpected hash size: {}", size);
+    } else {
+        Ok(buf)
+    }
+}
+
+pub fn to_formatted_digest(digest: &Sha256Digest) -> FormattedSha256Digest {
+    let mut formatted_digest: FormattedSha256Digest = [0; FORMATTED_SHA256_DIGEST_SIZE];
+    formatted_digest[0..8].copy_from_slice(FS_VERITY_MAGIC);
+    formatted_digest[8..10].copy_from_slice(&(FS_VERITY_HASH_ALG_SHA256 as u16).to_le_bytes());
+    formatted_digest[10..12].copy_from_slice(&(SHA256_HASH_SIZE as u16).to_le_bytes());
+    formatted_digest[12..].copy_from_slice(digest);
+    formatted_digest
+}
diff --git a/compos/src/pvm_exec.rs b/compos/src/pvm_exec.rs
index 69eebbf..99eddfc 100644
--- a/compos/src/pvm_exec.rs
+++ b/compos/src/pvm_exec.rs
@@ -14,20 +14,22 @@
  * limitations under the License.
  */
 
-//! pvm_exec is a proxy/wrapper command to run a command remotely. It does not transport the
-//! program and just pass the command line arguments to compsvc to execute. The most important task
+//! pvm_exec is a proxy/wrapper command to run compilation task remotely. The most important task
 //! for this program is to run a `fd_server` that serves remote file read/write requests.
 //!
-//! Example:
-//! $ adb shell exec 3</dev/zero 4<>/dev/null pvm_exec --in-fd 3 --out-fd 4 -- sleep 10
+//! It currently works as a command line wrapper to make it easy to schedule an existing dex2oat
+//! task to run in the VM.
 //!
-//! Note the immediate argument right after "--" (e.g. "sleep" in the example above) is not really
-//! used. It is only for ergonomics.
+//! Example:
+//! $ adb shell exec 3</input/dex 4<>/output/oat ... pvm_exec --in-fd 3 --out-fd 4 -- dex2oat64 ...
+//!
+//! Note the immediate argument "dex2oat64" right after "--" is not really used. It is only for
+//! ergonomics.
 
 use anyhow::{bail, Context, Result};
 use binder::unstable_api::{new_spibinder, AIBinder};
 use binder::FromIBinder;
-use log::{error, warn};
+use log::{debug, error, warn};
 use minijail::Minijail;
 use nix::fcntl::{fcntl, FcntlArg::F_GETFD};
 use nix::sys::stat::fstat;
@@ -187,14 +189,22 @@
 
     // 3. Send the command line args to the remote to execute.
     let service = if let Some(cid) = cid { get_rpc_binder(cid) } else { get_local_service() }?;
-    let exit_code = service.execute(&args, &metadata).context("Binder call failed")?;
+    let result = service.compile(&args, &metadata).context("Binder call failed")?;
+
+    // TODO: store/use the signature
+    debug!(
+        "Signature length: oat {}, vdex {}, image {}",
+        result.oatSignature.len(),
+        result.vdexSignature.len(),
+        result.imageSignature.len()
+    );
 
     // Be explicit about the lifetime, which should last at least until the task is finished.
     drop(fd_server_lifetime);
 
-    if exit_code > 0 {
-        error!("remote execution failed with exit code {}", exit_code);
-        exit(exit_code as i32);
+    if result.exitCode > 0 {
+        error!("remote execution failed with exit code {}", result.exitCode);
+        exit(result.exitCode as i32);
     }
     Ok(())
 }
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index f69b7b7..8409f44 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -40,6 +40,9 @@
     /** Path to odrefresh on Microdroid */
     private static final String ODREFRESH_BIN = "/apex/com.android.art/bin/odrefresh";
 
+    /** Path to compos_key_cmd on Microdroid */
+    private static final String COMPOS_KEY_CMD_BIN = "/apex/com.android.compos/bin/compos_key_cmd";
+
     /** Output directory of odrefresh */
     private static final String ODREFRESH_OUTPUT_DIR =
             "/data/misc/apexdata/com.android.art/dalvik-cache";
@@ -100,6 +103,15 @@
                 android.runForResultWithTimeout(ODREFRESH_TIMEOUT_MS, ODREFRESH_BIN, "--check");
         assertThat(result.getExitCode()).isEqualTo(OKAY);
 
+        // Initialize the service with the generated key. Should succeed.
+        android.run(
+                COMPOS_KEY_CMD_BIN,
+                "--cid " + mCid,
+                "generate",
+                TEST_ROOT + "test_key.blob",
+                TEST_ROOT + "test_key.pubkey");
+        android.run(COMPOS_KEY_CMD_BIN, "--cid " + mCid, "init-key", TEST_ROOT + "test_key.blob");
+
         // Expect the compilation in Compilation OS to finish successfully.
         {
             long start = System.currentTimeMillis();