Merge "[attestation] Validate client VM's Microdroid kernel in service VM" into main
diff --git a/service_vm/fake_chain/Android.bp b/service_vm/fake_chain/Android.bp
index ebc185d..2bc7b4e 100644
--- a/service_vm/fake_chain/Android.bp
+++ b/service_vm/fake_chain/Android.bp
@@ -24,6 +24,7 @@
visibility: [
"//packages/modules/Virtualization/rialto:__subpackages__",
],
+ prefer_rlib: true,
rustlibs: [
"libcstr",
],
@@ -40,6 +41,7 @@
"libcoset",
"libdiced_open_dice",
"liblog_rust",
+ "libmicrodroid_kernel_hashes",
],
}
diff --git a/service_vm/fake_chain/src/client_vm.rs b/service_vm/fake_chain/src/client_vm.rs
index eb8654b..44ea898 100644
--- a/service_vm/fake_chain/src/client_vm.rs
+++ b/service_vm/fake_chain/src/client_vm.rs
@@ -24,21 +24,16 @@
use coset::CborSerializable;
use cstr::cstr;
use diced_open_dice::{
- retry_bcc_format_config_descriptor, retry_bcc_main_flow, Config, DiceArtifacts,
+ hash, retry_bcc_format_config_descriptor, retry_bcc_main_flow, Config, DiceArtifacts,
DiceConfigValues, DiceError, DiceMode, InputValues, OwnedDiceArtifacts, Result, HASH_SIZE,
HIDDEN_SIZE,
};
use log::error;
+use microdroid_kernel_hashes::{INITRD_DEBUG_HASH, KERNEL_HASH};
type CborResult<T> = result::Result<T, ciborium::value::Error>;
/// All the following data are generated with urandom.
-const CODE_HASH_KERNEL: [u8; HASH_SIZE] = [
- 0xc8, 0x54, 0x6c, 0xad, 0x9d, 0xe7, 0x25, 0xc7, 0x2b, 0xed, 0x07, 0xe1, 0xe9, 0x1a, 0xb0, 0xd0,
- 0xa7, 0x7f, 0x43, 0xb9, 0xe4, 0x56, 0x79, 0x0d, 0x7d, 0xd8, 0xc5, 0xdd, 0xad, 0x0d, 0x31, 0x85,
- 0xaf, 0x94, 0x02, 0xd8, 0x9d, 0x70, 0xab, 0xba, 0xac, 0xc7, 0x12, 0x80, 0xec, 0x7b, 0x9b, 0x65,
- 0xec, 0x6b, 0xdd, 0x64, 0x94, 0xd0, 0x9a, 0x3a, 0x09, 0xf2, 0x49, 0xdb, 0x60, 0x3c, 0x50, 0x30,
-];
const CODE_HASH_PAYLOAD: [u8; HASH_SIZE] = [
0x08, 0x78, 0xc2, 0x5b, 0xe7, 0xea, 0x3d, 0x62, 0x70, 0x22, 0xd9, 0x1c, 0x4f, 0x3c, 0x2e, 0x2f,
0x0f, 0x97, 0xa4, 0x6f, 0x6d, 0xd5, 0xe6, 0x4a, 0x6d, 0xbe, 0x34, 0x2e, 0x56, 0x04, 0xaf, 0xef,
@@ -111,7 +106,7 @@
// so the authority hash is the same.
let authority_hash = service_vm::AUTHORITY_HASH_SERVICE_VM;
let input_values = InputValues::new(
- CODE_HASH_KERNEL,
+ kernel_code_hash()?,
Config::Descriptor(config_descriptor.as_slice()),
authority_hash,
DiceMode::kDiceModeDebug,
@@ -179,3 +174,8 @@
},
]
}
+
+fn kernel_code_hash() -> Result<[u8; HASH_SIZE]> {
+ let code_hash = [KERNEL_HASH, INITRD_DEBUG_HASH].concat();
+ hash(&code_hash)
+}
diff --git a/service_vm/kernel/Android.bp b/service_vm/kernel/Android.bp
new file mode 100644
index 0000000..79158e6
--- /dev/null
+++ b/service_vm/kernel/Android.bp
@@ -0,0 +1,31 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_binary_host {
+ name: "extract_microdroid_kernel_hashes",
+ srcs: ["extract_microdroid_kernel_hashes.py"],
+}
+
+genrule {
+ name: "microdroid_kernel_hashes_rs",
+ srcs: [":microdroid_kernel"],
+ out: ["lib.rs"],
+ tools: [
+ "extract_microdroid_kernel_hashes",
+ "avbtool",
+ ],
+ cmd: "$(location extract_microdroid_kernel_hashes) $(location avbtool) $(in) > $(out)",
+}
+
+rust_library_rlib {
+ name: "libmicrodroid_kernel_hashes",
+ srcs: [":microdroid_kernel_hashes_rs"],
+ crate_name: "microdroid_kernel_hashes",
+ prefer_rlib: true,
+ no_stdlibs: true,
+ stdlibs: [
+ "libcompiler_builtins.rust_sysroot",
+ "libcore.rust_sysroot",
+ ],
+}
diff --git a/service_vm/kernel/extract_microdroid_kernel_hashes.py b/service_vm/kernel/extract_microdroid_kernel_hashes.py
new file mode 100644
index 0000000..538c787
--- /dev/null
+++ b/service_vm/kernel/extract_microdroid_kernel_hashes.py
@@ -0,0 +1,62 @@
+"""Extracts the following hashes from the AVB footer of Microdroid's kernel:
+
+- kernel hash
+- initrd_normal hash
+- initrd_debug hash
+
+The hashes are written to stdout as a Rust file.
+"""
+#!/usr/bin/env python3
+
+import sys
+import subprocess
+from typing import Dict
+
+PARTITION_NAME_BOOT = 'boot'
+PARTITION_NAME_INITRD_NORMAL = 'initrd_normal'
+PARTITION_NAME_INITRD_DEBUG = 'initrd_debug'
+
+def main(args):
+ """Main function."""
+ avbtool = args[0]
+ kernel_image_path = args[1]
+ hashes = collect_hashes(avbtool, kernel_image_path)
+ assert hashes.keys() == {PARTITION_NAME_BOOT,
+ PARTITION_NAME_INITRD_NORMAL,
+ PARTITION_NAME_INITRD_DEBUG}, hashes.keys()
+
+ print("//! This file is generated by extract_microdroid_kernel_hashes.py.")
+ print("//! It contains the hashes of the kernel and initrds.\n")
+ print("#![no_std]\n#![allow(missing_docs)]\n")
+
+ print("pub const KERNEL_HASH: &[u8] = &["
+ f"{format_hex_string(hashes[PARTITION_NAME_BOOT])}];\n")
+ print("pub const INITRD_NORMAL_HASH: &[u8] = &["
+ f"{format_hex_string(hashes[PARTITION_NAME_INITRD_NORMAL])}];\n")
+ print("pub const INITRD_DEBUG_HASH: &[u8] = &["
+ f"{format_hex_string(hashes[PARTITION_NAME_INITRD_DEBUG])}];\n")
+
+def collect_hashes(avbtool: str, kernel_image_path: str) -> Dict[str, str]:
+ """Collects the hashes from the AVB footer of the kernel image."""
+ hashes = {}
+ with subprocess.Popen(
+ [avbtool, 'print_partition_digests', '--image', kernel_image_path],
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
+ stdout, _ = proc.communicate()
+ for line in stdout.decode("utf-8").split("\n"):
+ line = line.replace(" ", "").split(":")
+ if len(line) == 2:
+ partition_name, hash_ = line
+ hashes[partition_name] = hash_
+ return hashes
+
+def format_hex_string(hex_string: str) -> str:
+ """Formats a hex string into a Rust array."""
+ assert len(hex_string) % 2 == 0, \
+ "Hex string must have even length: " + hex_string
+ return ", ".join(["\n0x" + hex_string[i:i+2] if i % 32 == 0
+ else "0x" + hex_string[i:i+2]
+ for i in range(0, len(hex_string), 2)])
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/service_vm/requests/Android.bp b/service_vm/requests/Android.bp
index 52b54b4..57da012 100644
--- a/service_vm/requests/Android.bp
+++ b/service_vm/requests/Android.bp
@@ -24,6 +24,7 @@
"libder_nostd",
"libdiced_open_dice_nostd",
"liblog_rust_nostd",
+ "libmicrodroid_kernel_hashes",
"libserde_nostd",
"libservice_vm_comm_nostd",
"libspki_nostd",
diff --git a/service_vm/requests/src/client_vm.rs b/service_vm/requests/src/client_vm.rs
index cfdac2d..5b4735d 100644
--- a/service_vm/requests/src/client_vm.rs
+++ b/service_vm/requests/src/client_vm.rs
@@ -16,15 +16,20 @@
//! client VM.
use crate::cert;
-use crate::dice::{validate_client_vm_dice_chain_prefix_match, ClientVmDiceChain};
+use crate::dice::{
+ validate_client_vm_dice_chain_prefix_match, ClientVmDiceChain, DiceChainEntryPayload,
+};
use crate::keyblob::decrypt_private_key;
use alloc::vec::Vec;
-use bssl_avf::{rand_bytes, sha256, EcKey, PKey};
+use bssl_avf::{rand_bytes, sha256, Digester, EcKey, PKey};
+use cbor_util::value_to_array;
+use ciborium::value::Value;
use core::result;
-use coset::{CborSerializable, CoseSign};
+use coset::{AsCborValue, CborSerializable, CoseSign, CoseSign1};
use der::{Decode, Encode};
-use diced_open_dice::DiceArtifacts;
+use diced_open_dice::{DiceArtifacts, HASH_SIZE};
use log::error;
+use microdroid_kernel_hashes::{INITRD_DEBUG_HASH, INITRD_NORMAL_HASH, KERNEL_HASH};
use service_vm_comm::{ClientVmAttestationParams, Csr, CsrPayload, RequestProcessingError};
use x509_cert::{certificate::Certificate, name::Name};
@@ -47,13 +52,22 @@
// Validates the prefix of the Client VM DICE chain in the CSR.
let service_vm_dice_chain =
dice_artifacts.bcc().ok_or(RequestProcessingError::MissingDiceChain)?;
+ let service_vm_dice_chain =
+ value_to_array(Value::from_slice(service_vm_dice_chain)?, "service_vm_dice_chain")?;
let client_vm_dice_chain =
- validate_client_vm_dice_chain_prefix_match(&csr.dice_cert_chain, service_vm_dice_chain)?;
+ value_to_array(Value::from_slice(&csr.dice_cert_chain)?, "client_vm_dice_chain")?;
+ validate_client_vm_dice_chain_prefix_match(&client_vm_dice_chain, &service_vm_dice_chain)?;
// Validates the signatures in the Client VM DICE chain and extracts the partially decoded
// DiceChainEntryPayloads.
let client_vm_dice_chain =
ClientVmDiceChain::validate_signatures_and_parse_dice_chain(client_vm_dice_chain)?;
+ // The last entry in the service VM DICE chain describes the service VM, which should
+ // be signed with the same key as the kernel image.
+ let service_vm_entry = service_vm_dice_chain.last().unwrap();
+ validate_kernel_authority_hash(client_vm_dice_chain.microdroid_kernel(), service_vm_entry)?;
+ validate_kernel_code_hash(&client_vm_dice_chain)?;
+
// AAD is empty as defined in service_vm/comm/client_vm_csr.cddl.
let aad = &[];
@@ -121,3 +135,60 @@
let digest = sha256(message)?;
key.ecdsa_sign(&digest)
}
+
+/// Validates that the authority hash of the Microdroid kernel in the Client VM DICE chain
+/// matches the authority hash of the service VM entry in the service VM DICE chain, because
+/// the Microdroid kernel is signed with the same key as the one used for the service VM.
+fn validate_kernel_authority_hash(
+ kernel: &DiceChainEntryPayload,
+ service_vm_entry: &Value,
+) -> Result<()> {
+ if expected_kernel_authority_hash(service_vm_entry)? == kernel.authority_hash {
+ Ok(())
+ } else {
+ error!("The authority hash of the Microdroid kernel does not match the expected value");
+ Err(RequestProcessingError::InvalidDiceChain)
+ }
+}
+
+/// Validates that the kernel code hash in the Client VM DICE chain matches the code hashes
+/// embedded during the build time.
+fn validate_kernel_code_hash(dice_chain: &ClientVmDiceChain) -> Result<()> {
+ let kernel = dice_chain.microdroid_kernel();
+ if expected_kernel_code_hash_normal()? == kernel.code_hash {
+ return Ok(());
+ }
+ if expected_kernel_code_hash_debug()? == kernel.code_hash {
+ if dice_chain.all_entries_are_secure() {
+ error!("The Microdroid kernel has debug initrd but the DICE chain is secure");
+ return Err(RequestProcessingError::InvalidDiceChain);
+ }
+ return Ok(());
+ }
+ error!("The kernel code hash in the Client VM DICE chain does not match any expected values");
+ Err(RequestProcessingError::InvalidDiceChain)
+}
+
+fn expected_kernel_code_hash_normal() -> bssl_avf::Result<Vec<u8>> {
+ let mut code_hash = [0u8; 64];
+ code_hash[0..32].copy_from_slice(KERNEL_HASH);
+ code_hash[32..].copy_from_slice(INITRD_NORMAL_HASH);
+ Digester::sha512().digest(&code_hash)
+}
+
+fn expected_kernel_code_hash_debug() -> bssl_avf::Result<Vec<u8>> {
+ let mut code_hash = [0u8; 64];
+ code_hash[0..32].copy_from_slice(KERNEL_HASH);
+ code_hash[32..].copy_from_slice(INITRD_DEBUG_HASH);
+ Digester::sha512().digest(&code_hash)
+}
+
+fn expected_kernel_authority_hash(service_vm_entry: &Value) -> Result<[u8; HASH_SIZE]> {
+ let cose_sign1 = CoseSign1::from_cbor_value(service_vm_entry.clone())?;
+ let payload = cose_sign1.payload.ok_or_else(|| {
+ error!("No payload found in the service VM DICE chain entry");
+ RequestProcessingError::InternalError
+ })?;
+ let service_vm = DiceChainEntryPayload::from_slice(&payload)?;
+ Ok(service_vm.authority_hash)
+}
diff --git a/service_vm/requests/src/dice.rs b/service_vm/requests/src/dice.rs
index 557b678..8c804da 100644
--- a/service_vm/requests/src/dice.rs
+++ b/service_vm/requests/src/dice.rs
@@ -126,7 +126,7 @@
Ok(())
}
- fn microdroid_kernel(&self) -> &DiceChainEntryPayload {
+ pub(crate) fn microdroid_kernel(&self) -> &DiceChainEntryPayload {
&self.payloads[self.payloads.len() - 2]
}
@@ -147,15 +147,11 @@
/// Validates that the `client_vm_dice_chain` matches the `service_vm_dice_chain` up to the pvmfw
/// entry.
///
-/// Returns a CBOR value array of the client VM's DICE chain if the verification succeeds.
+/// Returns `Ok(())` if the verification succeeds.
pub(crate) fn validate_client_vm_dice_chain_prefix_match(
- client_vm_dice_chain: &[u8],
- service_vm_dice_chain: &[u8],
-) -> Result<Vec<Value>> {
- let client_vm_dice_chain =
- value_to_array(Value::from_slice(client_vm_dice_chain)?, "client_vm_dice_chain")?;
- let service_vm_dice_chain =
- value_to_array(Value::from_slice(service_vm_dice_chain)?, "service_vm_dice_chain")?;
+ client_vm_dice_chain: &[Value],
+ service_vm_dice_chain: &[Value],
+) -> Result<()> {
if service_vm_dice_chain.len() < 3 {
// The service VM's DICE chain must contain the root key and at least two other entries
// that describe:
@@ -180,7 +176,7 @@
);
return Err(RequestProcessingError::InvalidDiceChain);
}
- Ok(client_vm_dice_chain)
+ Ok(())
}
#[derive(Debug, Clone)]
@@ -208,11 +204,8 @@
#[allow(dead_code)]
subject_public_key: PublicKey,
mode: DiceMode,
- /// TODO(b/271275206): Verify Microdroid kernel authority and code hashes.
- #[allow(dead_code)]
- code_hash: [u8; HASH_SIZE],
- #[allow(dead_code)]
- authority_hash: [u8; HASH_SIZE],
+ pub(crate) code_hash: [u8; HASH_SIZE],
+ pub(crate) authority_hash: [u8; HASH_SIZE],
config_descriptor: ConfigDescriptor,
}
@@ -230,42 +223,42 @@
error!("No payload found in the DICE chain entry");
RequestProcessingError::InvalidDiceChain
})?;
- let entries = value_to_map(Value::from_slice(&payload)?, "DiceChainEntryPayload")?;
- build_payload(entries)
+ Self::from_slice(&payload)
}
-}
-fn build_payload(entries: Vec<(Value, Value)>) -> Result<DiceChainEntryPayload> {
- let mut builder = PayloadBuilder::default();
- for (key, value) in entries.into_iter() {
- let key: i64 = value_to_num(key, "DiceChainEntryPayload key")?;
- match key {
- SUBJECT_PUBLIC_KEY => {
- let subject_public_key = value_to_bytes(value, "subject_public_key")?;
- let subject_public_key = CoseKey::from_slice(&subject_public_key)?.try_into()?;
- builder.subject_public_key(subject_public_key)?;
+ pub(crate) fn from_slice(data: &[u8]) -> Result<Self> {
+ let entries = value_to_map(Value::from_slice(data)?, "DiceChainEntryPayload")?;
+ let mut builder = PayloadBuilder::default();
+ for (key, value) in entries.into_iter() {
+ let key: i64 = value_to_num(key, "DiceChainEntryPayload key")?;
+ match key {
+ SUBJECT_PUBLIC_KEY => {
+ let subject_public_key = value_to_bytes(value, "subject_public_key")?;
+ let subject_public_key =
+ CoseKey::from_slice(&subject_public_key)?.try_into()?;
+ builder.subject_public_key(subject_public_key)?;
+ }
+ MODE => builder.mode(to_mode(value)?)?,
+ CODE_HASH => {
+ let code_hash = value_to_byte_array(value, "DiceChainEntryPayload code_hash")?;
+ builder.code_hash(code_hash)?;
+ }
+ AUTHORITY_HASH => {
+ let authority_hash =
+ value_to_byte_array(value, "DiceChainEntryPayload authority_hash")?;
+ builder.authority_hash(authority_hash)?;
+ }
+ CONFIG_DESC => {
+ let config_descriptor = value_to_bytes(value, "config_descriptor")?;
+ let config_descriptor = ConfigDescriptor::from_slice(&config_descriptor)?;
+ builder.config_descriptor(config_descriptor)?;
+ }
+ _ => {}
}
- MODE => builder.mode(to_mode(value)?)?,
- CODE_HASH => {
- let code_hash = value_to_byte_array(value, "DiceChainEntryPayload code_hash")?;
- builder.code_hash(code_hash)?;
- }
- AUTHORITY_HASH => {
- let authority_hash =
- value_to_byte_array(value, "DiceChainEntryPayload authority_hash")?;
- builder.authority_hash(authority_hash)?;
- }
- CONFIG_DESC => {
- let config_descriptor = value_to_bytes(value, "config_descriptor")?;
- let config_descriptor = ConfigDescriptor::from_slice(&config_descriptor)?;
- builder.config_descriptor(config_descriptor)?;
- }
- _ => {}
}
+ builder.build()
}
- builder.build()
}
-
/// Represents a partially decoded `ConfigurationDescriptor`.
///
/// The whole `ConfigurationDescriptor` is defined in: