diff --git a/docs/intel_tdx.md b/docs/intel_tdx.md index efa4007c2..0a82d73da 100644 --- a/docs/intel_tdx.md +++ b/docs/intel_tdx.md @@ -189,3 +189,27 @@ devices responsible for handling PCI hotplug (PCI hotplug controller, PCI Express Bus and Generic Event Device) will not be allowed, therefore the corresponding drivers will not be loaded and the PCI hotplug feature will not be supported. + +## Measurement configuration registers + +A TD carries three owner/configuration measurement registers that are baked +into its attestation report: `MRCONFIGID`, `MROWNER` and `MROWNERCONFIG`. Each +is a 384-bit (48-byte) SHA384 digest chosen by the tenant or platform owner. + +They can be supplied through the matching `--platform` options as hex strings +of exactly 96 characters (48 bytes): + +```bash +./cloud-hypervisor \ + --platform tdx=on,mrconfigid=<96-hex-chars>,mrowner=<96-hex-chars>,mrownerconfig=<96-hex-chars> \ + --firmware td-shim/target/release/final.bin \ + --kernel bzImage \ + --cmdline "root=/dev/vda3 console=hvc0 rw" \ + --cpus boot=1 \ + --memory size=1G \ + --disk path=tdx_guest_img +``` + +Any register left unset defaults to all zeros, preserving the previous +behavior. + diff --git a/hypervisor/src/kvm/mod.rs b/hypervisor/src/kvm/mod.rs index 26631190f..b7595f6bf 100644 --- a/hypervisor/src/kvm/mod.rs +++ b/hypervisor/src/kvm/mod.rs @@ -1031,7 +1031,14 @@ impl vm::Vm for KvmVm { /// Initialize TDX for this VM /// #[cfg(feature = "tdx")] - fn tdx_init(&self, cpuid: &[CpuIdEntry], max_vcpus: u32) -> vm::Result<()> { + fn tdx_init( + &self, + cpuid: &[CpuIdEntry], + max_vcpus: u32, + mrconfigid: &[u8; 48], + mrowner: &[u8; 48], + mrownerconfig: &[u8; 48], + ) -> vm::Result<()> { let tdx_capabilities = self.tdx_capabilities()?; // `KVM_TDX_INIT_VM` only accepts the configurable leaves reported by @@ -1063,9 +1070,9 @@ impl vm::Vm for KvmVm { let data = KvmTdxInitVm { attributes, xfam, - mrconfigid: [0; 6], - mrowner: [0; 6], - mrownerconfig: [0; 6], + mrconfigid: *mrconfigid, + mrowner: *mrowner, + mrownerconfig: *mrownerconfig, reserved: [0; 12], cpuid: kvm_cpuid2 { nent: filtered_cpuid.len() as u32, diff --git a/hypervisor/src/kvm/tdx.rs b/hypervisor/src/kvm/tdx.rs index 1fef9aea2..0c829ea01 100644 --- a/hypervisor/src/kvm/tdx.rs +++ b/hypervisor/src/kvm/tdx.rs @@ -88,9 +88,13 @@ pub(crate) struct KvmTdxCmd { pub(crate) struct KvmTdxInitVm { pub attributes: u64, pub xfam: u64, - pub mrconfigid: [u64; 6], - pub mrowner: [u64; 6], - pub mrownerconfig: [u64; 6], + // The kernel treats mrconfigid/mrowner/mrownerconfig as raw 48-byte SHA384 + // digests (declared `__u64[6]`, populated by a byte copy), so store them + // as byte arrays to forward the caller's values verbatim without any + // endianness conversion. + pub mrconfigid: [u8; 48], + pub mrowner: [u8; 48], + pub mrownerconfig: [u8; 48], pub reserved: [u64; 12], pub cpuid: kvm_cpuid2, } diff --git a/hypervisor/src/vm.rs b/hypervisor/src/vm.rs index 2d1cbb20d..2c1505e11 100644 --- a/hypervisor/src/vm.rs +++ b/hypervisor/src/vm.rs @@ -408,7 +408,14 @@ pub trait Vm: Send + Sync + Any { } #[cfg(feature = "tdx")] /// Initialize TDX on this VM - fn tdx_init(&self, _cpuid: &[CpuIdEntry], _max_vcpus: u32) -> Result<()> { + fn tdx_init( + &self, + _cpuid: &[CpuIdEntry], + _max_vcpus: u32, + _mrconfigid: &[u8; 48], + _mrowner: &[u8; 48], + _mrownerconfig: &[u8; 48], + ) -> Result<()> { unimplemented!() } #[cfg(feature = "tdx")] diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index 0f326d488..0e452d68c 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -31,7 +31,7 @@ mshv = [ ] pvmemcontrol = ["devices/pvmemcontrol"] sev_snp = ["arch/sev_snp", "hypervisor/sev_snp", "virtio-devices/sev_snp"] -tdx = ["arch/tdx", "hypervisor/tdx"] +tdx = ["arch/tdx", "hex", "hypervisor/tdx"] tracing = ["tracer/tracing"] [dependencies] diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 0742100da..c6bbdd232 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -279,6 +279,14 @@ pub enum ValidationError { #[cfg(feature = "tdx")] #[error("No TDX firmware specified")] TdxFirmwareMissing, + /// Invalid TDX measurement-configuration digest + #[cfg(feature = "tdx")] + #[error("TDX '{0}' must be a 96-character (48-byte) hex SHA384 digest: {1}")] + TdxInvalidMeasurement(&'static str, String), + /// TDX measurement-configuration digest specified without enabling TDX + #[cfg(feature = "tdx")] + #[error("mrconfigid/mrowner/mrownerconfig require 'tdx=on'")] + TdxMeasurementWithoutTdx, /// Insufficient vCPUs for queues #[error("Queue count ({0}) must not exceed boot vCPUs ({1})")] TooManyQueues(usize /* queues */, usize /* vCPUs */), @@ -809,6 +817,12 @@ impl PlatformConfig { .add("oem_strings"); #[cfg(feature = "tdx")] parser.add("tdx"); + #[cfg(feature = "tdx")] + parser.add("mrconfigid"); + #[cfg(feature = "tdx")] + parser.add("mrowner"); + #[cfg(feature = "tdx")] + parser.add("mrownerconfig"); #[cfg(feature = "sev_snp")] parser.add("sev_snp"); parser.parse(platform).map_err(Error::ParsePlatform)?; @@ -839,6 +853,18 @@ impl PlatformConfig { .map_err(Error::ParsePlatform)? .unwrap_or(Toggle(false)) .0; + #[cfg(feature = "tdx")] + let tdx_mrconfigid = parser + .convert::("mrconfigid") + .map_err(Error::ParsePlatform)?; + #[cfg(feature = "tdx")] + let tdx_mrowner = parser + .convert::("mrowner") + .map_err(Error::ParsePlatform)?; + #[cfg(feature = "tdx")] + let tdx_mrownerconfig = parser + .convert::("mrownerconfig") + .map_err(Error::ParsePlatform)?; #[cfg(feature = "sev_snp")] let sev_snp = parser .convert::("sev_snp") @@ -854,11 +880,38 @@ impl PlatformConfig { oem_strings, #[cfg(feature = "tdx")] tdx, + #[cfg(feature = "tdx")] + tdx_mrconfigid, + #[cfg(feature = "tdx")] + tdx_mrowner, + #[cfg(feature = "tdx")] + tdx_mrownerconfig, #[cfg(feature = "sev_snp")] sev_snp, }) } + /// Decode the optional TDX SHA384 measurement-configuration digests + /// (`mrconfigid`, `mrowner`, `mrownerconfig`) from their hex string form + /// into 48-byte arrays. Any register left unset decodes to all zeros. + #[cfg(feature = "tdx")] + pub fn tdx_measurements(&self) -> ValidationResult<([u8; 48], [u8; 48], [u8; 48])> { + fn decode(name: &'static str, value: &Option) -> ValidationResult<[u8; 48]> { + let mut out = [0u8; 48]; + if let Some(s) = value { + hex::decode_to_slice(s, &mut out) + .map_err(|e| ValidationError::TdxInvalidMeasurement(name, e.to_string()))?; + } + Ok(out) + } + + Ok(( + decode("mrconfigid", &self.tdx_mrconfigid)?, + decode("mrowner", &self.tdx_mrowner)?, + decode("mrownerconfig", &self.tdx_mrownerconfig)?, + )) + } + pub fn validate(&self) -> ValidationResult<()> { if self.num_pci_segments == 0 || self.num_pci_segments > MAX_NUM_PCI_SEGMENTS { return Err(ValidationError::InvalidNumPciSegments( @@ -2873,6 +2926,15 @@ impl VmConfig { if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) { return Err(ValidationError::TdxNoCpuHotplug); } + let has_tdx_measurements = self.platform.as_ref().is_some_and(|p| { + p.tdx_mrconfigid.is_some() || p.tdx_mrowner.is_some() || p.tdx_mrownerconfig.is_some() + }); + if has_tdx_measurements && !tdx_enabled { + return Err(ValidationError::TdxMeasurementWithoutTdx); + } + if tdx_enabled { + self.platform.as_ref().unwrap().tdx_measurements()?; + } if tdx_enabled { // For TDX the guest physical address width (GPAW) is fixed at: // 48 - (GPAW-48, 4-level EPT) @@ -4415,6 +4477,38 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" Ok(()) } + #[cfg(feature = "tdx")] + #[test] + fn test_platform_tdx_measurements() -> Result<()> { + // Unset registers decode to all zeros. + let p = PlatformConfig::parse("tdx=on")?; + assert_eq!(p.tdx_measurements(), Ok(([0u8; 48], [0u8; 48], [0u8; 48]))); + + // A valid 96-character hex digest decodes into the matching register. + let hexid = "01".repeat(48); + let p = PlatformConfig::parse(&format!("tdx=on,mrconfigid={hexid}"))?; + let (mrconfigid, mrowner, mrownerconfig) = p.tdx_measurements().unwrap(); + assert_eq!(mrconfigid, [1u8; 48]); + assert_eq!(mrowner, [0u8; 48]); + assert_eq!(mrownerconfig, [0u8; 48]); + + // Wrong length is rejected by validation. + let p = PlatformConfig::parse("tdx=on,mrowner=00")?; + assert!(matches!( + p.tdx_measurements(), + Err(ValidationError::TdxInvalidMeasurement("mrowner", _)) + )); + + // Non-hex characters are rejected too. + let p = PlatformConfig::parse(&format!("tdx=on,mrownerconfig={}", "zz".repeat(48)))?; + assert!(matches!( + p.tdx_measurements(), + Err(ValidationError::TdxInvalidMeasurement("mrownerconfig", _)) + )); + + Ok(()) + } + #[test] fn test_vsock_parsing() -> Result<()> { // socket and cid is required @@ -4808,6 +4902,12 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" oem_strings: None, #[cfg(feature = "tdx")] tdx: false, + #[cfg(feature = "tdx")] + tdx_mrconfigid: None, + #[cfg(feature = "tdx")] + tdx_mrowner: None, + #[cfg(feature = "tdx")] + tdx_mrownerconfig: None, #[cfg(feature = "sev_snp")] sev_snp: false, } @@ -4926,6 +5026,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" tdx_config.cpus.max_phys_bits = 52; tdx_config.validate().unwrap(); assert_eq!(tdx_config.cpus.max_phys_bits, 52); + + // mrconfigid/mrowner/mrownerconfig are rejected without tdx=on. + let mut no_tdx_config = valid_config.clone(); + no_tdx_config.platform = Some(PlatformConfig { + tdx_mrconfigid: Some("01".repeat(48)), + ..platform_fixture() + }); + assert_eq!( + no_tdx_config.validate(), + Err(ValidationError::TdxMeasurementWithoutTdx) + ); } let mut invalid_config = valid_config.clone(); diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index d7ead06d5..2c0b66617 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -984,7 +984,15 @@ impl Vm { if config.lock().unwrap().is_tdx_enabled() { let cpuid = cpu_manager.lock().unwrap().common_cpuid(); let max_vcpus = cpu_manager.lock().unwrap().max_vcpus(); - vm.tdx_init(&cpuid, max_vcpus) + let (mrconfigid, mrowner, mrownerconfig) = config + .lock() + .unwrap() + .platform + .as_ref() + .expect("TDX requires a platform configuration") + .tdx_measurements() + .map_err(Error::ConfigValidation)?; + vm.tdx_init(&cpuid, max_vcpus, &mrconfigid, &mrowner, &mrownerconfig) .map_err(Error::InitializeTdxVm)?; } Ok(()) diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 06deffbff..ebf06358a 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -130,6 +130,19 @@ pub struct PlatformConfig { #[cfg(feature = "tdx")] #[serde(default)] pub tdx: bool, + // Optional SHA384 measurement-configuration digests, hex encoded (96 hex + // characters = 48 bytes each), forwarded verbatim to `KVM_TDX_INIT_VM` as + // the TD's `mrconfigid`, `mrowner` and `mrownerconfig` registers. When + // unset each register is left as all zeros. + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrconfigid: Option, + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrowner: Option, + #[cfg(feature = "tdx")] + #[serde(default)] + pub tdx_mrownerconfig: Option, #[cfg(feature = "sev_snp")] #[serde(default)] pub sev_snp: bool,