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();