From f9bc33ca759a6453f0e200d5bf75524385799aa8 Mon Sep 17 00:00:00 2001 From: John Starks Date: Thu, 21 Nov 2024 06:05:52 +0000 Subject: [PATCH 1/3] RFC: add optional disk table to the VMGS file The disk table lists the disks that the host is allowed to offer, along with their encryption information (cipher + key). If the host offers a disk that is not in the list, fail to boot. This is designed to support unenlightened guests, and is opt in via environment variable. To use, prepopulate the VMGS file with the appropriate disk information, e.g.: ``` vmgstool disk-table -f vmgs.vmgs add --d 00000001-a4a3-11ef-9347-43f645bdedf8 -c xts-aes-256 -k key.bin ``` And in your OpenHCL configuration, ensure OPENHCL_USE_DISK_TABLE=1 is set. If using an encrypted disk, be sure to build openvmm-hcl with the `disk_crypt` feature. --- Cargo.lock | 7 + openhcl/openvmm_hcl/Cargo.toml | 3 + openhcl/openvmm_hcl_resources/Cargo.toml | 1 + openhcl/openvmm_hcl_resources/src/lib.rs | 2 + openhcl/underhill_core/Cargo.toml | 3 + openhcl/underhill_core/src/dispatch/mod.rs | 2 + .../src/dispatch/vtl2_settings_worker.rs | 55 ++++++- openhcl/underhill_core/src/lib.rs | 1 + openhcl/underhill_core/src/options.rs | 6 + openhcl/underhill_core/src/worker.rs | 17 ++ openvmm/openvmm_entry/src/storage_builder.rs | 12 +- vm/devices/get/underhill_config/src/lib.rs | 11 ++ vm/vmgs/vmgs_format/Cargo.toml | 9 ++ vm/vmgs/vmgs_format/build.rs | 6 + vm/vmgs/vmgs_format/src/disk_table.proto | 19 +++ vm/vmgs/vmgs_format/src/lib.rs | 12 ++ vm/vmgs/vmgstool/Cargo.toml | 3 +- vm/vmgs/vmgstool/src/disk_table.rs | 153 ++++++++++++++++++ vm/vmgs/vmgstool/src/main.rs | 31 +++- 19 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 vm/vmgs/vmgs_format/build.rs create mode 100644 vm/vmgs/vmgs_format/src/disk_table.proto create mode 100644 vm/vmgs/vmgstool/src/disk_table.rs diff --git a/Cargo.lock b/Cargo.lock index e3bdc2ef1..c2be06161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4512,6 +4512,7 @@ dependencies = [ "build_rs_guest_arch", "chipset", "debug_worker", + "disk_crypt", "disk_striped", "hyperv_ic", "mesh_worker", @@ -6741,6 +6742,7 @@ dependencies = [ "disk_backend", "disk_backend_resources", "disk_blockdevice", + "disk_crypt_resources", "disk_get_vmgs", "disk_nvme", "firmware_pcat", @@ -6793,6 +6795,7 @@ dependencies = [ "pal_uring", "parking_lot", "profiler_worker", + "prost", "safe_intrinsics", "scsi_buffers", "scsi_core", @@ -6845,6 +6848,7 @@ dependencies = [ "vmcore", "vmgs", "vmgs_broker", + "vmgs_format", "vmgs_resources", "vmm_core", "vmm_core_defs", @@ -8000,6 +8004,8 @@ dependencies = [ "bitfield-struct", "inspect", "open_enum", + "prost", + "prost-build", "static_assertions", "zerocopy", ] @@ -8037,6 +8043,7 @@ dependencies = [ "hcl_compat_uefi_nvram_storage", "hex", "pal_async", + "prost", "serde", "serde_json", "tempfile", diff --git a/openhcl/openvmm_hcl/Cargo.toml b/openhcl/openvmm_hcl/Cargo.toml index ba3060a1b..0901ac6dc 100644 --- a/openhcl/openvmm_hcl/Cargo.toml +++ b/openhcl/openvmm_hcl/Cargo.toml @@ -19,6 +19,9 @@ uidevices = ["openvmm_hcl_resources/uidevices", "openvmm_hcl_resources/vnc_worke # Enable NVMe emulation. nvme = ["openvmm_hcl_resources/nvme", "underhill_entry/vpci"] +# Encrypted disk support. +disk_crypt = ["openvmm_hcl_resources/disk_crypt"] + [target.'cfg(target_os = "linux")'.dependencies] underhill_entry.workspace = true openvmm_hcl_resources.workspace = true diff --git a/openhcl/openvmm_hcl_resources/Cargo.toml b/openhcl/openvmm_hcl_resources/Cargo.toml index 3cde3efe6..bd1feee2e 100644 --- a/openhcl/openvmm_hcl_resources/Cargo.toml +++ b/openhcl/openvmm_hcl_resources/Cargo.toml @@ -15,6 +15,7 @@ vmsocket.workspace = true serial_core.workspace = true vmbus_serial_guest.workspace = true +disk_crypt = { workspace = true, optional = true } disk_striped.workspace = true scsidisk.workspace = true diff --git a/openhcl/openvmm_hcl_resources/src/lib.rs b/openhcl/openvmm_hcl_resources/src/lib.rs index 5391cef12..b501722cc 100644 --- a/openhcl/openvmm_hcl_resources/src/lib.rs +++ b/openhcl/openvmm_hcl_resources/src/lib.rs @@ -35,6 +35,8 @@ vm_resource::register_static_resolvers! { // `BlockDevice` and `NvmeDisk` are registered dynamically since they have // runtime dependencies. disk_striped::StripedDiskResolver, + #[cfg(feature = "disk_crypt")] + disk_crypt::resolver::DiskCryptResolver, // SCSI scsidisk::resolver::SimpleScsiResolver, diff --git a/openhcl/underhill_core/Cargo.toml b/openhcl/underhill_core/Cargo.toml index 72b3aa670..43bd39d37 100644 --- a/openhcl/underhill_core/Cargo.toml +++ b/openhcl/underhill_core/Cargo.toml @@ -48,6 +48,7 @@ chipset_legacy.workspace = true disk_backend.workspace = true disk_backend_resources.workspace = true disk_blockdevice.workspace = true +disk_crypt_resources.workspace = true disk_get_vmgs.workspace = true disk_nvme.workspace = true firmware_uefi.workspace = true @@ -121,6 +122,7 @@ guestmem.workspace = true vmcore.workspace = true vm_resource.workspace = true vmgs = { workspace = true, features = ["encryption_ossl", "save_restore"] } +vmgs_format = { workspace = true, features = ["proto"] } vmgs_broker = { workspace = true, features = ["encryption_ossl"] } vmgs_resources.workspace = true x86defs.workspace = true @@ -155,6 +157,7 @@ futures.workspace = true getrandom.workspace = true libc.workspace = true parking_lot.workspace = true +prost.workspace = true serde = { workspace = true, features = ["derive"] } serde_helpers.workspace = true serde_json.workspace = true diff --git a/openhcl/underhill_core/src/dispatch/mod.rs b/openhcl/underhill_core/src/dispatch/mod.rs index 622073f86..80878bc7c 100644 --- a/openhcl/underhill_core/src/dispatch/mod.rs +++ b/openhcl/underhill_core/src/dispatch/mod.rs @@ -140,6 +140,7 @@ pub(crate) struct LoadedVm { pub nvme_manager: Option, pub emuplat_servicing: EmuplatServicing, pub device_interfaces: Option, + pub disk_table: Option, /// Memory map with IGVM types for each range. pub vtl0_memory_map: Vec<(MemoryRangeWithNode, MemoryMapEntryType)>, @@ -215,6 +216,7 @@ impl LoadedVm { device_config_send, self.get_client.clone(), self.device_interfaces.take().unwrap(), + self.disk_table.take(), ); threadpool.spawn("VTL2 settings services", { diff --git a/openhcl/underhill_core/src/dispatch/vtl2_settings_worker.rs b/openhcl/underhill_core/src/dispatch/vtl2_settings_worker.rs index 131cf69e9..b714d1466 100644 --- a/openhcl/underhill_core/src/dispatch/vtl2_settings_worker.rs +++ b/openhcl/underhill_core/src/dispatch/vtl2_settings_worker.rs @@ -131,6 +131,10 @@ enum Error<'a> { }, #[error("cannot modify IDE configuration at runtime")] StorageCannotModifyIdeAtRuntime, + #[error("unknown disk cipher {cipher}")] + StorageBadCipher { cipher: i32 }, + #[error("disk {disk_id} is missing VMGS disk table entry")] + StorageMissingDiskTableEntry { disk_id: &'a str }, } impl Error<'_> { @@ -179,6 +183,10 @@ impl Error<'_> { Error::StorageChangeMediaFailed { .. } => { Vtl2SettingsErrorCode::StorageChangeMediaFailed } + Error::StorageBadCipher { .. } => Vtl2SettingsErrorCode::StorageInvalidDiskCipher, + Error::StorageMissingDiskTableEntry { .. } => { + Vtl2SettingsErrorCode::StorageMissingDiskTableEntry + } } } } @@ -233,6 +241,7 @@ pub struct Vtl2SettingsWorker { device_config_send: mesh::Sender, get_client: guest_emulation_transport::GuestEmulationTransportClient, interfaces: DeviceInterfaces, + disk_table: Option, } pub struct DeviceInterfaces { @@ -247,12 +256,14 @@ impl Vtl2SettingsWorker { device_config_send: mesh::Sender, get_client: guest_emulation_transport::GuestEmulationTransportClient, interfaces: DeviceInterfaces, + disk_table: Option, ) -> Vtl2SettingsWorker { Vtl2SettingsWorker { old_settings: initial_settings, device_config_send, get_client, interfaces, + disk_table, } } @@ -368,6 +379,7 @@ impl Vtl2SettingsWorker { &StorageContext { uevent_listener, use_nvme_vfio: self.interfaces.use_nvme_vfio, + disk_table: self.disk_table.as_ref(), }, &disk, false, @@ -386,6 +398,7 @@ impl Vtl2SettingsWorker { &StorageContext { uevent_listener, use_nvme_vfio: self.interfaces.use_nvme_vfio, + disk_table: self.disk_table.as_ref(), }, &disk, false, @@ -894,13 +907,14 @@ pub enum StorageDevicePath { async fn make_disk_type_from_physical_devices( ctx: &mut CancelContext, + device_id: &str, storage_context: &StorageContext<'_>, physical_devices: &PhysicalDevices, ntfs_guid: Option, read_only: bool, is_restoring: bool, ) -> Result, Vtl2SettingsErrorInfo> { - let disk_type = match *physical_devices { + let mut disk_type = match *physical_devices { PhysicalDevices::Single { ref device } => { make_disk_type_from_physical_device(ctx, storage_context, device, read_only).await? } @@ -935,10 +949,37 @@ async fn make_disk_type_from_physical_devices( } else { // DEVNOTE: open-source OpenHCL does not currently have a resolver // for `AutoFormattedDiskHandle`. - return Ok(Resource::new(AutoFormattedDiskHandle { + disk_type = Resource::new(AutoFormattedDiskHandle { disk: disk_type, guid: ntfs_guid.into(), - })); + }); + } + } + + if let Some(disk_table) = storage_context.disk_table { + let entry = disk_table + .disks + .iter() + .find(|k| k.disk_id == device_id) + .ok_or(Error::StorageMissingDiskTableEntry { disk_id: device_id })?; + + let cipher = match entry.cipher() { + vmgs_format::DiskCipher::Unspecified => { + return Err(Error::StorageBadCipher { + cipher: entry.cipher, + } + .into()); + } + vmgs_format::DiskCipher::None => None, + vmgs_format::DiskCipher::XtsAes256 => Some(disk_crypt_resources::Cipher::XtsAes256), + }; + + if let Some(cipher) = cipher { + disk_type = Resource::new(disk_crypt_resources::DiskCryptHandle { + disk: disk_type, + cipher, + key: entry.key.clone(), + }); } } @@ -948,6 +989,7 @@ async fn make_disk_type_from_physical_devices( struct StorageContext<'a> { uevent_listener: &'a UeventListener, use_nvme_vfio: bool, + disk_table: Option<&'a vmgs_format::DiskTable>, } #[instrument(skip_all)] @@ -1253,6 +1295,7 @@ async fn make_disk_type( (false, physical_devices) => Some( make_disk_type_from_physical_devices( ctx, + &disk.disk_params().device_id, storage_context, physical_devices, disk.ntfs_guid(), @@ -1264,6 +1307,7 @@ async fn make_disk_type( (true, physical_devices) => { let disk_type = make_disk_type_from_physical_devices( ctx, + &disk.disk_params().device_id, storage_context, physical_devices, disk.ntfs_guid(), @@ -1294,6 +1338,7 @@ async fn make_nvme_disk_config( ) -> Result { let disk_type = make_disk_type_from_physical_devices( ctx, + &namespace.disk_params.device_id, storage_context, &namespace.physical_devices, None, @@ -1422,6 +1467,7 @@ pub async fn create_storage_controllers_from_vtl2_settings( sub_channels: u16, is_restoring: bool, default_io_queue_depth: u32, + disk_table: Option<&vmgs_format::DiskTable>, ) -> Result< ( Option, @@ -1433,6 +1479,7 @@ pub async fn create_storage_controllers_from_vtl2_settings( let storage_context = StorageContext { uevent_listener, use_nvme_vfio, + disk_table, }; let ide_controller = make_ide_controller_config(ctx, &storage_context, settings, is_restoring).await?; @@ -1772,6 +1819,7 @@ impl InitialControllers { use_nvme_vfio: bool, is_restoring: bool, default_io_queue_depth: u32, + disk_table: Option<&vmgs_format::DiskTable>, ) -> anyhow::Result { const VM_CONFIG_TIME_OUT_IN_SECONDS: u64 = 5; let mut context = @@ -1793,6 +1841,7 @@ impl InitialControllers { fixed.scsi_sub_channels, is_restoring, default_io_queue_depth, + disk_table, ) .await? } else { diff --git a/openhcl/underhill_core/src/lib.rs b/openhcl/underhill_core/src/lib.rs index ebcefd9f6..06e60a20a 100644 --- a/openhcl/underhill_core/src/lib.rs +++ b/openhcl/underhill_core/src/lib.rs @@ -322,6 +322,7 @@ async fn launch_workers( no_sidecar_hotplug: opt.no_sidecar_hotplug, gdbstub: opt.gdbstub, hide_isolation: opt.hide_isolation, + disk_table: opt.disk_table, }; let (mut remote_console_cfg, framebuffer_access) = diff --git a/openhcl/underhill_core/src/options.rs b/openhcl/underhill_core/src/options.rs index e5b14d523..a9e743ad6 100644 --- a/openhcl/underhill_core/src/options.rs +++ b/openhcl/underhill_core/src/options.rs @@ -113,6 +113,10 @@ pub struct Options { /// (OPENHCL_NO_SIDECAR_HOTPLUG=1) Leave sidecar VPs remote even if they /// hit exits. pub no_sidecar_hotplug: bool, + + /// (OPENHCL_USE_DISK_TABLE=1) Read the disk table from the VMGS file to + /// determine the allowed set of disks and their associated encryption keys. + pub disk_table: bool, } impl Options { @@ -181,6 +185,7 @@ impl Options { let no_sidecar_hotplug = parse_legacy_env_bool("OPENHCL_NO_SIDECAR_HOTPLUG"); let gdbstub = parse_legacy_env_bool("OPENHCL_GDBSTUB"); let gdbstub_port = parse_legacy_env_number("OPENHCL_GDBSTUB_PORT")?.map(|x| x as u32); + let disk_table = parse_env_bool("OPENHCL_USE_DISK_TABLE"); let mut args = std::env::args().chain(extra_args); // Skip our own filename. @@ -234,6 +239,7 @@ impl Options { hide_isolation, halt_on_guest_halt, no_sidecar_hotplug, + disk_table, }) } diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 2daf9fe10..f85895dc6 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -294,6 +294,8 @@ pub struct UnderhillEnvCfg { pub gdbstub: bool, /// Hide the isolation mode from the guest. pub hide_isolation: bool, + /// Enable and require the disk table. + pub disk_table: bool, } /// Bundle of config + runtime objects for hooking into the underhill remote @@ -1613,6 +1615,19 @@ async fn new_underhill_vm( // Make the GET available for other resources. resolver.add_resolver(get_client.clone()); + // Read the disk table from VMGS. + let disk_table = if env_cfg.disk_table { + let table = match vmgs.read_file(vmgs::FileId::DISK_TABLE).await { + Ok(keys) => ::decode(keys.as_slice()) + .context("failed to decode disk table")?, + Err(vmgs::Error::FileInfoAllocated) => vmgs_format::DiskTable::default(), + Err(err) => return Err(err).context("failed to read disk table")?, + }; + Some(table) + } else { + None + }; + // Spawn the VMGS client for multi-task access. let (vmgs_client, vmgs_handle) = spawn_vmgs_broker(get_spawner, vmgs); resolver.add_resolver(VmgsFileResolver::new(vmgs_client.clone())); @@ -1715,6 +1730,7 @@ async fn new_underhill_vm( env_cfg.nvme_vfio, is_restoring, default_io_queue_depth, + disk_table.as_ref(), ) .instrument(tracing::info_span!("new_initial_controllers")) .await @@ -2859,6 +2875,7 @@ async fn new_underhill_vm( netvsp_state, }, device_interfaces: Some(controllers.device_interfaces), + disk_table, vtl0_memory_map, vmbus_server, diff --git a/openvmm/openvmm_entry/src/storage_builder.rs b/openvmm/openvmm_entry/src/storage_builder.rs index f8c2a0e35..19bfd28a2 100644 --- a/openvmm/openvmm_entry/src/storage_builder.rs +++ b/openvmm/openvmm_entry/src/storage_builder.rs @@ -259,10 +259,18 @@ impl StorageBuilder { } }; + // Use a consistent device ID, based on the location. + let mut device_id = Guid::from_static_str("00000000-a4a3-11ef-9347-43f645bdedf8"); + device_id.data1 = match target { + DiskLocation::Ide(_, _) => unreachable!(), + DiskLocation::Scsi(_) => location, + DiskLocation::Nvme(_) => location | (1 << 31), + }; + luns.push(Lun { location, - device_id: Guid::new_random().to_string(), - vendor_id: "HvLite".to_string(), + device_id: device_id.to_string(), + vendor_id: "OpenVMM".to_string(), product_id: "Disk".to_string(), product_revision_level: "1.0".to_string(), serial_number: "0".to_string(), diff --git a/vm/devices/get/underhill_config/src/lib.rs b/vm/devices/get/underhill_config/src/lib.rs index 46af86141..fa55e8c71 100644 --- a/vm/devices/get/underhill_config/src/lib.rs +++ b/vm/devices/get/underhill_config/src/lib.rs @@ -114,6 +114,13 @@ impl StorageDisk { StorageDisk::Scsi(scsi_disk) => scsi_disk.ntfs_guid, } } + + pub fn disk_params(&self) -> &DiskParameters { + match self { + StorageDisk::Ide(ide_disk) => &ide_disk.disk_params, + StorageDisk::Scsi(scsi_disk) => &scsi_disk.disk_params, + } + } } #[derive(Debug, Clone, Eq, PartialEq, MeshPayload, Inspect)] @@ -348,6 +355,10 @@ pub enum Vtl2SettingsErrorCode { StorageChangeMediaFailed => (Storage, Underhill), /// Invalid NTFS format guid StorageInvalidNtfsFormatGuid => (Storage, Configuration), + /// Missing disk table entry + StorageMissingDiskTableEntry => (Storage, Configuration), + /// Invalid disk cipher + StorageInvalidDiskCipher => (Storage, Configuration), /// Failed to modify NIC NetworkingModifyNicFailed => (Network, Configuration), diff --git a/vm/vmgs/vmgs_format/Cargo.toml b/vm/vmgs/vmgs_format/Cargo.toml index e3367cf4e..4a8c14cf7 100644 --- a/vm/vmgs/vmgs_format/Cargo.toml +++ b/vm/vmgs/vmgs_format/Cargo.toml @@ -6,13 +6,22 @@ name = "vmgs_format" edition = "2021" rust-version.workspace = true +[features] +# Protobuf definitions for the disk table and possibly other things in the +# future. +proto = ["dep:prost", "dep:prost-build"] + [dependencies] open_enum.workspace = true inspect = { workspace = true, optional = true } +prost = { workspace = true, optional = true } bitfield-struct.workspace = true static_assertions.workspace = true zerocopy.workspace = true +[build-dependencies] +prost-build = { workspace = true, optional = true } + [lints] workspace = true diff --git a/vm/vmgs/vmgs_format/build.rs b/vm/vmgs/vmgs_format/build.rs new file mode 100644 index 000000000..59d0c3268 --- /dev/null +++ b/vm/vmgs/vmgs_format/build.rs @@ -0,0 +1,6 @@ +fn main() { + #[cfg(feature = "proto")] + prost_build::Config::new() + .compile_protos(&["src/disk_table.proto"], &["src/"]) + .unwrap(); +} diff --git a/vm/vmgs/vmgs_format/src/disk_table.proto b/vm/vmgs/vmgs_format/src/disk_table.proto new file mode 100644 index 000000000..fe6a5a85a --- /dev/null +++ b/vm/vmgs/vmgs_format/src/disk_table.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package vmgs; + +message DiskTable { + repeated Disk disks = 1; +} + +enum DiskCipher { + DISK_CIPHER_UNSPECIFIED = 0; + DISK_CIPHER_NONE = 1; + DISK_CIPHER_XTS_AES_256 = 2; +} + +message Disk { + string disk_id = 1; + DiskCipher cipher = 2; + bytes key = 3; +} diff --git a/vm/vmgs/vmgs_format/src/lib.rs b/vm/vmgs/vmgs_format/src/lib.rs index b8a62af6d..57c937edb 100644 --- a/vm/vmgs/vmgs_format/src/lib.rs +++ b/vm/vmgs/vmgs_format/src/lib.rs @@ -5,6 +5,9 @@ #![no_std] +#[cfg(feature = "proto")] +pub use proto::*; + use bitfield_struct::bitfield; use core::ops::Index; use core::ops::IndexMut; @@ -16,6 +19,14 @@ use zerocopy::AsBytes; use zerocopy::FromBytes; use zerocopy::FromZeroes; +#[cfg(feature = "proto")] +mod proto { + // Crates used by generated code. Reference them explicitly to ensure that + // automated tools do not remove them. + use prost as _; + include!(concat!(env!("OUT_DIR"), "/vmgs.rs")); +} + /// The suggested default capacity of a VMGS disk in bytes, 4MB. /// /// In some sense, this is not part of the VMGS format, but all known @@ -43,6 +54,7 @@ open_enum! { GUEST_WATCHDOG = 10, HW_KEY_PROTECTOR = 11, GUEST_SECRET_KEY = 13, + DISK_TABLE = 14, EXTENDED_FILE_TABLE = 63, } diff --git a/vm/vmgs/vmgstool/Cargo.toml b/vm/vmgs/vmgstool/Cargo.toml index 096517226..e75ff3605 100644 --- a/vm/vmgs/vmgstool/Cargo.toml +++ b/vm/vmgs/vmgstool/Cargo.toml @@ -24,13 +24,14 @@ pal_async.workspace = true uefi_nvram_specvars.workspace = true uefi_specs.workspace = true vmgs.workspace = true -vmgs_format.workspace = true +vmgs_format = { workspace = true, features = ["proto"] } anyhow.workspace = true async-trait.workspace = true clap = { workspace = true, features = ["derive"] } hex.workspace = true fs-err.workspace = true +prost.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror.workspace = true diff --git a/vm/vmgs/vmgstool/src/disk_table.rs b/vm/vmgs/vmgstool/src/disk_table.rs new file mode 100644 index 000000000..b58e8d3ef --- /dev/null +++ b/vm/vmgs/vmgstool/src/disk_table.rs @@ -0,0 +1,153 @@ +use crate::vmgs_file_open; +use crate::Error; +use crate::FilePathArg; +use crate::KeyPathArg; +use crate::OpenMode; +use clap::Subcommand; +use clap::ValueEnum; +use guid::Guid; +use prost::Message; +use std::path::PathBuf; +use vmgs::FileId; +use vmgs::Vmgs; + +#[derive(Subcommand)] +pub(crate) enum DiskTableOperation { + /// List disks + List, + /// Add a disk entry + Add { + /// The disk identifier + #[clap(long, short)] + disk_id: Guid, + /// The path to a file containing the disk key + #[clap(long, short)] + key_path: Option, + /// The cipher to use for the disk encryption + #[clap(long, short)] + cipher: DiskCipher, + /// Update the key if it already exists + #[clap(long, short)] + allow_overwrite: bool, + }, + /// Remove a disk key + Remove { + /// The disk identifier + #[clap(long)] + disk: String, + }, +} + +#[derive(ValueEnum, Copy, Clone)] +pub(crate) enum DiskCipher { + None, + #[clap(name = "xts-aes-256")] + XtsAes256, +} + +async fn open_file( + file_path: &FilePathArg, + key_path: &KeyPathArg, + open_mode: OpenMode, +) -> Result { + vmgs_file_open( + &file_path.file_path, + key_path.key_path.as_deref(), + open_mode, + false, + ) + .await +} + +pub(crate) async fn do_command( + file_path: FilePathArg, + key_path: KeyPathArg, + operation: DiskTableOperation, +) -> Result<(), Error> { + match operation { + DiskTableOperation::List => { + let mut vmgs = open_file(&file_path, &key_path, OpenMode::ReadOnly).await?; + let table = read_disk_table(&mut vmgs).await?; + for entry in &table.disks { + let cipher = match entry.cipher() { + vmgs_format::DiskCipher::Unspecified => { + format!("unknown ({})", entry.cipher) + } + vmgs_format::DiskCipher::None => format!("none"), + vmgs_format::DiskCipher::XtsAes256 => format!("xts-aes-256"), + }; + println!("{disk_id} {cipher}", disk_id = entry.disk_id); + } + Ok(()) + } + DiskTableOperation::Add { + disk, + disk_key, + cipher, + allow_overwrite, + } => { + let mut vmgs = open_file(&file_path, &key_path, OpenMode::ReadWrite).await?; + let mut table = read_disk_table(&mut vmgs).await?; + let key = if matches!(cipher, DiskCipher::None) { + if disk_key.is_some() { + return Err(Error::UnexpectedDiskKeyFile); + } + Vec::new() + } else { + fs_err::read(disk_key.ok_or(Error::MissingDiskKeyFile)?) + .map_err(Error::DiskKeyFile)? + }; + let new_entry = vmgs_format::Disk { + disk_id: disk.to_string(), + cipher: (match cipher { + DiskCipher::None => vmgs_format::DiskCipher::None, + DiskCipher::XtsAes256 => vmgs_format::DiskCipher::XtsAes256, + }) + .into(), + key, + }; + if let Some(entry) = table + .disks + .iter_mut() + .find(|k| k.disk_id == new_entry.disk_id) + { + if !allow_overwrite { + return Err(Error::DiskEntryExists); + } + *entry = new_entry; + } else { + table.disks.push(new_entry); + } + vmgs.write_file_encrypted(FileId::DISK_TABLE, &table.encode_to_vec()) + .await?; + Ok(()) + } + DiskTableOperation::Remove { disk } => { + let mut vmgs = open_file(&file_path, &key_path, OpenMode::ReadWrite).await?; + let mut table = read_disk_table(&mut vmgs).await?; + let mut i = 0; + table.disks.retain_mut(|k| { + let keep = k.disk_id != disk; + if !keep { + i += 1; + }; + keep + }); + if i == 0 { + return Err(Error::DiskEntryNotFound); + } + vmgs.write_file_encrypted(FileId::DISK_TABLE, &table.encode_to_vec()) + .await?; + Ok(()) + } + } +} + +async fn read_disk_table(vmgs: &mut Vmgs) -> Result { + let data = match vmgs.read_file(FileId::DISK_TABLE).await { + Ok(data) => data, + Err(vmgs::Error::FileInfoAllocated) => Vec::new(), + Err(e) => return Err(e.into()), + }; + vmgs_format::DiskTable::decode(data.as_slice()).map_err(Error::DiskTableCorrupt) +} diff --git a/vm/vmgs/vmgstool/src/main.rs b/vm/vmgs/vmgstool/src/main.rs index a392dee6c..ab2ee0886 100644 --- a/vm/vmgs/vmgstool/src/main.rs +++ b/vm/vmgs/vmgstool/src/main.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +mod disk_table; mod storage_backend; mod uefi_nvram; mod vmgs_json; @@ -9,6 +10,7 @@ use anyhow::Result; use clap::Args; use clap::Parser; use disk_backend::SimpleDisk; +use disk_table::DiskTableOperation; use disk_vhd1::Vhd1Disk; use fs_err::File; use pal_async::DefaultPool; @@ -56,7 +58,7 @@ enum Error { InvalidVmgsFileSize(u64, String), #[error("VMGS file is encrypted, but no encryption key was provided")] AlreadyEncrypted, - #[error("Key file IO")] + #[error("Failed to read key file")] KeyFile(#[source] std::io::Error), #[error("Key must be {0} bytes long, is {1} bytes instead")] InvalidKeySize(u64, u64), @@ -80,6 +82,18 @@ enum Error { Json(String), #[error("File ID {0:?} already exists. Use `--allow-overwrite` to ignore.")] FileIdExists(FileId), + #[error("The specified disk entry already exists. Use `--allow-overwrite` to overwrite.")] + DiskEntryExists, + #[error("The specified disk entry was not found.")] + DiskEntryNotFound, + #[error("The disk table is corrupt")] + DiskTableCorrupt(#[source] prost::DecodeError), + #[error("Failed to read disk key file")] + DiskKeyFile(#[source] std::io::Error), + #[error("Cannot specify a disk key with with no cipher")] + UnexpectedDiskKeyFile, + #[error("Cannot specify a cipher with no disk key")] + MissingDiskKeyFile, } /// Automation requires certain exit codes to be guaranteed @@ -238,6 +252,15 @@ enum Options { #[clap(subcommand)] operation: UefiNvramOperation, }, + /// Disk table operations + DiskTable { + #[command(flatten)] + file_path: FilePathArg, + #[command(flatten)] + key_path: KeyPathArg, + #[clap(subcommand)] + operation: DiskTableOperation, + }, } fn parse_file_id(file_id: &str) -> Result { @@ -256,6 +279,7 @@ fn parse_file_id(file_id: &str) -> Result { "HW_KEY_PROTECTOR" => FileId::HW_KEY_PROTECTOR, "GUEST_SECRET_KEY" => FileId::GUEST_SECRET_KEY, "EXTENDED_FILE_TABLE" => FileId::EXTENDED_FILE_TABLE, + "DISK_TABLE" => FileId::DISK_TABLE, v => FileId(v.parse::()?), }) } @@ -429,6 +453,11 @@ async fn do_main() -> Result<(), Error> { vmgs_file_query_encryption(file_path.file_path).await } Options::UefiNvram { operation } => uefi_nvram::do_command(operation).await, + Options::DiskTable { + file_path, + key_path, + operation, + } => disk_table::do_command(file_path, key_path, operation).await, } } From 8cef4618ab9d6cf6ff97d3332c9d40c1272bb50b Mon Sep 17 00:00:00 2001 From: John Starks Date: Thu, 21 Nov 2024 20:22:02 +0000 Subject: [PATCH 2/3] fix renames --- vm/vmgs/vmgstool/src/disk_table.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vm/vmgs/vmgstool/src/disk_table.rs b/vm/vmgs/vmgstool/src/disk_table.rs index b58e8d3ef..10dd836c7 100644 --- a/vm/vmgs/vmgstool/src/disk_table.rs +++ b/vm/vmgs/vmgstool/src/disk_table.rs @@ -81,24 +81,24 @@ pub(crate) async fn do_command( Ok(()) } DiskTableOperation::Add { - disk, - disk_key, + disk_id, + key_path: disk_key_path, cipher, allow_overwrite, } => { let mut vmgs = open_file(&file_path, &key_path, OpenMode::ReadWrite).await?; let mut table = read_disk_table(&mut vmgs).await?; let key = if matches!(cipher, DiskCipher::None) { - if disk_key.is_some() { + if disk_key_path.is_some() { return Err(Error::UnexpectedDiskKeyFile); } Vec::new() } else { - fs_err::read(disk_key.ok_or(Error::MissingDiskKeyFile)?) + fs_err::read(disk_key_path.ok_or(Error::MissingDiskKeyFile)?) .map_err(Error::DiskKeyFile)? }; let new_entry = vmgs_format::Disk { - disk_id: disk.to_string(), + disk_id: disk_id.to_string(), cipher: (match cipher { DiskCipher::None => vmgs_format::DiskCipher::None, DiskCipher::XtsAes256 => vmgs_format::DiskCipher::XtsAes256, From 2954973fe1c30d6d3b40edb9d0183b2d7ebc29d7 Mon Sep 17 00:00:00 2001 From: John Starks Date: Thu, 21 Nov 2024 20:37:48 +0000 Subject: [PATCH 3/3] clippy --- vm/vmgs/vmgstool/src/disk_table.rs | 4 ++-- vm/vmgs/vmgstool/src/main.rs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/vm/vmgs/vmgstool/src/disk_table.rs b/vm/vmgs/vmgstool/src/disk_table.rs index 10dd836c7..c081eb0da 100644 --- a/vm/vmgs/vmgstool/src/disk_table.rs +++ b/vm/vmgs/vmgstool/src/disk_table.rs @@ -73,8 +73,8 @@ pub(crate) async fn do_command( vmgs_format::DiskCipher::Unspecified => { format!("unknown ({})", entry.cipher) } - vmgs_format::DiskCipher::None => format!("none"), - vmgs_format::DiskCipher::XtsAes256 => format!("xts-aes-256"), + vmgs_format::DiskCipher::None => "none".to_string(), + vmgs_format::DiskCipher::XtsAes256 => "xts-aes-256".to_string(), }; println!("{disk_id} {cipher}", disk_id = entry.disk_id); } diff --git a/vm/vmgs/vmgstool/src/main.rs b/vm/vmgs/vmgstool/src/main.rs index ab2ee0886..993394918 100644 --- a/vm/vmgs/vmgstool/src/main.rs +++ b/vm/vmgs/vmgstool/src/main.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[cfg(with_encryption)] mod disk_table; mod storage_backend; mod uefi_nvram; @@ -10,7 +11,6 @@ use anyhow::Result; use clap::Args; use clap::Parser; use disk_backend::SimpleDisk; -use disk_table::DiskTableOperation; use disk_vhd1::Vhd1Disk; use fs_err::File; use pal_async::DefaultPool; @@ -82,16 +82,22 @@ enum Error { Json(String), #[error("File ID {0:?} already exists. Use `--allow-overwrite` to ignore.")] FileIdExists(FileId), + #[cfg(with_encryption)] #[error("The specified disk entry already exists. Use `--allow-overwrite` to overwrite.")] DiskEntryExists, + #[cfg(with_encryption)] #[error("The specified disk entry was not found.")] DiskEntryNotFound, + #[cfg(with_encryption)] #[error("The disk table is corrupt")] DiskTableCorrupt(#[source] prost::DecodeError), + #[cfg(with_encryption)] #[error("Failed to read disk key file")] DiskKeyFile(#[source] std::io::Error), + #[cfg(with_encryption)] #[error("Cannot specify a disk key with with no cipher")] UnexpectedDiskKeyFile, + #[cfg(with_encryption)] #[error("Cannot specify a cipher with no disk key")] MissingDiskKeyFile, } @@ -253,13 +259,14 @@ enum Options { operation: UefiNvramOperation, }, /// Disk table operations + #[cfg(with_encryption)] DiskTable { #[command(flatten)] file_path: FilePathArg, #[command(flatten)] key_path: KeyPathArg, #[clap(subcommand)] - operation: DiskTableOperation, + operation: disk_table::DiskTableOperation, }, } @@ -453,6 +460,7 @@ async fn do_main() -> Result<(), Error> { vmgs_file_query_encryption(file_path.file_path).await } Options::UefiNvram { operation } => uefi_nvram::do_command(operation).await, + #[cfg(with_encryption)] Options::DiskTable { file_path, key_path,