// Copyright 2023, 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. //! Support for reading and writing to the instance.img. use crate::dice::PartialInputs; use crate::gpt; use crate::gpt::Partition; use crate::gpt::Partitions; use bssl_avf::{self, hkdf, Aead, AeadContext, Digester}; use core::fmt; use core::mem::size_of; use diced_open_dice::DiceMode; use diced_open_dice::Hash; use diced_open_dice::Hidden; use log::trace; use uuid::Uuid; use virtio_drivers::transport::{pci::bus::PciRoot, DeviceType, Transport}; use vmbase::util::ceiling_div; use vmbase::virtio::pci::{PciTransportIterator, VirtIOBlk}; use vmbase::virtio::HalImpl; use zerocopy::AsBytes; use zerocopy::FromBytes; use zerocopy::FromZeroes; pub enum Error { /// Unexpected I/O error while accessing the underlying disk. FailedIo(gpt::Error), /// Impossible to create a new instance.img entry. InstanceImageFull, /// Badly formatted instance.img header block. InvalidInstanceImageHeader, /// No instance.img ("vm-instance") partition found. MissingInstanceImage, /// The instance.img doesn't contain a header. MissingInstanceImageHeader, /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key. RecordedAuthHashMismatch, /// Code hash found in the pvmfw instance.img entry doesn't match the inputs. RecordedCodeHashMismatch, /// DICE mode found in the pvmfw instance.img entry doesn't match the current one. RecordedDiceModeMismatch, /// Size of the instance.img entry being read or written is not supported. UnsupportedEntrySize(usize), /// Failed to create VirtIO Block device. VirtIOBlkCreationFailed(virtio_drivers::Error), /// An error happened during the interaction with BoringSSL. BoringSslFailed(bssl_avf::Error), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"), Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"), Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"), Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"), Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"), Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"), Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"), Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"), Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"), Self::VirtIOBlkCreationFailed(e) => { write!(f, "Failed to create VirtIO Block device: {e}") } Self::BoringSslFailed(e) => { write!(f, "An error happened during the interaction with BoringSSL: {e}") } } } } impl From for Error { fn from(e: bssl_avf::Error) -> Self { Self::BoringSslFailed(e) } } pub type Result = core::result::Result; fn aead_ctx_from_secret(secret: &[u8]) -> Result { let key = hkdf::<32>(secret, /* salt= */ &[], b"vm-instance", Digester::sha512())?; Ok(AeadContext::new(Aead::aes_256_gcm_randnonce(), key.as_slice(), /* tag_len */ None)?) } /// Get the entry from instance.img. This method additionally returns Partition corresponding to /// pvmfw in the instance.img as well as index corresponding to empty header which can be used to /// record instance data with `record_instance_entry`. pub(crate) fn get_recorded_entry( pci_root: &mut PciRoot, secret: &[u8], ) -> Result<(Option, Partition, usize)> { let mut instance_img = find_instance_img(pci_root)?; let entry = locate_entry(&mut instance_img)?; trace!("Found pvmfw instance.img entry: {entry:?}"); match entry { PvmfwEntry::Existing { header_index, payload_size } => { let aead_ctx = aead_ctx_from_secret(secret)?; let mut blk = [0; BLK_SIZE]; if payload_size > blk.len() { // We currently only support single-blk entries. return Err(Error::UnsupportedEntrySize(payload_size)); } let payload_index = header_index + 1; instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?; let payload = &blk[..payload_size]; let mut entry = [0; size_of::()]; // The nonce is generated internally for `aes_256_gcm_randnonce`, so no additional // nonce is required. let decrypted = aead_ctx.open(payload, /* nonce */ &[], /* ad */ &[], &mut entry)?; let body = EntryBody::read_from(decrypted).unwrap(); Ok((Some(body), instance_img, header_index)) } PvmfwEntry::New { header_index } => Ok((None, instance_img, header_index)), } } pub(crate) fn record_instance_entry( body: &EntryBody, secret: &[u8], instance_img: &mut Partition, header_index: usize, ) -> Result<()> { // We currently only support single-blk entries. let mut blk = [0; BLK_SIZE]; let plaintext = body.as_bytes(); let aead_ctx = aead_ctx_from_secret(secret)?; assert!(plaintext.len() + aead_ctx.aead().max_overhead() < blk.len()); let encrypted = aead_ctx.seal(plaintext, /* nonce */ &[], /* ad */ &[], &mut blk)?; let payload_size = encrypted.len(); let payload_index = header_index + 1; instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?; let header = EntryHeader::new(PvmfwEntry::UUID, payload_size); header.write_to_prefix(blk.as_mut_slice()).unwrap(); blk[header.as_bytes().len()..].fill(0); instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?; Ok(()) } #[derive(FromZeroes, FromBytes)] #[repr(C, packed)] struct Header { magic: [u8; Header::MAGIC.len()], version: u16, } impl Header { const MAGIC: &'static [u8] = b"Android-VM-instance"; const VERSION_1: u16 = 1; pub fn is_valid(&self) -> bool { self.magic == Self::MAGIC && self.version() == Self::VERSION_1 } fn version(&self) -> u16 { u16::from_le(self.version) } } fn find_instance_img(pci_root: &mut PciRoot) -> Result { for transport in PciTransportIterator::::new(pci_root) .filter(|t| DeviceType::Block == t.device_type()) { let device = VirtIOBlk::::new(transport).map_err(Error::VirtIOBlkCreationFailed)?; match Partition::get_by_name(device, "vm-instance") { Ok(Some(p)) => return Ok(p), Ok(None) => {} Err(e) => log::warn!("error while reading from disk: {e}"), }; } Err(Error::MissingInstanceImage) } #[derive(Debug)] enum PvmfwEntry { Existing { header_index: usize, payload_size: usize }, New { header_index: usize }, } const BLK_SIZE: usize = Partitions::LBA_SIZE; impl PvmfwEntry { const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825); } fn locate_entry(partition: &mut Partition) -> Result { let mut blk = [0; BLK_SIZE]; let mut indices = partition.indices(); let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?; partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?; // The instance.img header is only used for discovery/validation. let header = Header::read_from_prefix(blk.as_slice()).unwrap(); if !header.is_valid() { return Err(Error::InvalidInstanceImageHeader); } while let Some(header_index) = indices.next() { partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?; let header = EntryHeader::read_from_prefix(blk.as_slice()).unwrap(); match (header.uuid(), header.payload_size()) { (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }), (PvmfwEntry::UUID, payload_size) => { return Ok(PvmfwEntry::Existing { header_index, payload_size }) } (uuid, payload_size) => { trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes"); let n = ceiling_div(payload_size, BLK_SIZE).unwrap(); if n > 0 { let _ = indices.nth(n - 1); // consume } } }; } Err(Error::InstanceImageFull) } /// Marks the start of an instance.img entry. /// /// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition". #[derive(AsBytes, FromZeroes, FromBytes)] #[repr(C, packed)] struct EntryHeader { uuid: u128, payload_size: u64, } impl EntryHeader { fn new(uuid: Uuid, payload_size: usize) -> Self { Self { uuid: uuid.to_u128_le(), payload_size: u64::try_from(payload_size).unwrap().to_le() } } fn uuid(&self) -> Uuid { Uuid::from_u128_le(self.uuid) } fn payload_size(&self) -> usize { usize::try_from(u64::from_le(self.payload_size)).unwrap() } } #[derive(AsBytes, FromZeroes, FromBytes)] #[repr(C)] pub(crate) struct EntryBody { pub code_hash: Hash, pub auth_hash: Hash, pub salt: Hidden, mode: u8, } impl EntryBody { pub(crate) fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self { let mode = match dice_inputs.mode { DiceMode::kDiceModeNotInitialized => 0, DiceMode::kDiceModeNormal => 1, DiceMode::kDiceModeDebug => 2, DiceMode::kDiceModeMaintenance => 3, }; Self { code_hash: dice_inputs.code_hash, auth_hash: dice_inputs.auth_hash, salt: *salt, mode, } } pub(crate) fn mode(&self) -> DiceMode { match self.mode { 1 => DiceMode::kDiceModeNormal, 2 => DiceMode::kDiceModeDebug, 3 => DiceMode::kDiceModeMaintenance, _ => DiceMode::kDiceModeNotInitialized, } } }