From 6461ac34e087a2bff536a01c26432d32be33f088 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 7 Jul 2026 09:51:32 +0200 Subject: [PATCH] Improve representation of DCAP measurements by enforcing all registers to be present --- crates/attestation/src/measurements.rs | 168 +++++++++++++++++-------- 1 file changed, 117 insertions(+), 51 deletions(-) diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index db8c2c9..b0d723d 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -75,11 +75,81 @@ fn parse_azure_pcr_index(value: &str) -> Result { Ok(index) } +#[derive(Clone, PartialEq)] +pub struct DcapMeasurements { + pub mrtd: [u8; 48], + pub rtmr0: [u8; 48], + pub rtmr1: [u8; 48], + pub rtmr2: [u8; 48], + pub rtmr3: [u8; 48], +} + +impl DcapMeasurements { + pub fn new( + mrtd: [u8; 48], + rtmr0: [u8; 48], + rtmr1: [u8; 48], + rtmr2: [u8; 48], + rtmr3: [u8; 48], + ) -> Self { + Self { mrtd, rtmr0, rtmr1, rtmr2, rtmr3 } + } + + fn from_map( + mut measurements: HashMap, + ) -> Result { + Ok(Self { + mrtd: measurements + .remove(&DcapMeasurementRegister::MRTD) + .ok_or_else(|| MeasurementFormatError::MissingValue("MRTD".to_string()))?, + rtmr0: measurements + .remove(&DcapMeasurementRegister::RTMR0) + .ok_or_else(|| MeasurementFormatError::MissingValue("RTMR0".to_string()))?, + rtmr1: measurements + .remove(&DcapMeasurementRegister::RTMR1) + .ok_or_else(|| MeasurementFormatError::MissingValue("RTMR1".to_string()))?, + rtmr2: measurements + .remove(&DcapMeasurementRegister::RTMR2) + .ok_or_else(|| MeasurementFormatError::MissingValue("RTMR2".to_string()))?, + rtmr3: measurements + .remove(&DcapMeasurementRegister::RTMR3) + .ok_or_else(|| MeasurementFormatError::MissingValue("RTMR3".to_string()))?, + }) + } + + fn iter(&self) -> impl Iterator { + [ + (DcapMeasurementRegister::MRTD, &self.mrtd), + (DcapMeasurementRegister::RTMR0, &self.rtmr0), + (DcapMeasurementRegister::RTMR1, &self.rtmr1), + (DcapMeasurementRegister::RTMR2, &self.rtmr2), + (DcapMeasurementRegister::RTMR3, &self.rtmr3), + ] + .into_iter() + } + + fn get(&self, register: &DcapMeasurementRegister) -> &[u8; 48] { + match register { + DcapMeasurementRegister::MRTD => &self.mrtd, + DcapMeasurementRegister::RTMR0 => &self.rtmr0, + DcapMeasurementRegister::RTMR1 => &self.rtmr1, + DcapMeasurementRegister::RTMR2 => &self.rtmr2, + DcapMeasurementRegister::RTMR3 => &self.rtmr3, + } + } +} + +impl fmt::Debug for DcapMeasurements { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + DcapHexDebug(self).fmt(f) + } +} + /// Represents a set of measurements values for one of the supported CVM /// platforms #[derive(Clone, PartialEq)] pub enum MultiMeasurements { - Dcap(HashMap), + Dcap(DcapMeasurements), Azure(HashMap), NoAttestation, } @@ -87,9 +157,7 @@ pub enum MultiMeasurements { impl fmt::Debug for MultiMeasurements { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Self::Dcap(measurements) => { - f.debug_tuple("DCAP").field(&DcapHexDebug(measurements)).finish() - } + Self::Dcap(measurements) => f.debug_tuple("DCAP").field(measurements).finish(), Self::Azure(measurements) => { f.debug_tuple("Azure").field(&AzureHexDebug(measurements)).finish() } @@ -99,17 +167,14 @@ impl fmt::Debug for MultiMeasurements { } /// Used to display DCAP measurements as hex -struct DcapHexDebug<'a>(&'a HashMap); +struct DcapHexDebug<'a>(&'a DcapMeasurements); impl fmt::Debug for DcapHexDebug<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut entries: Vec<_> = self.0.iter().collect(); - entries.sort_by_key(|(register, _)| (*register).clone() as u8); - let mut map = f.debug_map(); - for (register, value) in entries { + for (register, value) in self.0.iter() { let hex_value = hex::encode(value); - map.entry(register, &hex_value); + map.entry(®ister, &hex_value); } map.finish() } @@ -146,7 +211,7 @@ impl MultiMeasurements { let measurements_map = match self { MultiMeasurements::Dcap(dcap_measurements) => dcap_measurements .iter() - .map(|(register, value)| ((register.clone() as u8).to_string(), hex::encode(value))) + .map(|(register, value)| ((register as u8).to_string(), hex::encode(value))) .collect(), MultiMeasurements::Azure(azure_measurements) => azure_measurements .iter() @@ -192,7 +257,7 @@ impl MultiMeasurements { )) }) .collect::>()?; - Self::Dcap(measurements_map) + Self::Dcap(DcapMeasurements::from_map(measurements_map)?) } }) } @@ -208,13 +273,13 @@ impl MultiMeasurements { return Err(DcapVerificationError::SgxNotSupported); } }; - Ok(Self::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, report.mr_td), - (DcapMeasurementRegister::RTMR0, report.rt_mr0), - (DcapMeasurementRegister::RTMR1, report.rt_mr1), - (DcapMeasurementRegister::RTMR2, report.rt_mr2), - (DcapMeasurementRegister::RTMR3, report.rt_mr3), - ]))) + Ok(Self::Dcap(DcapMeasurements::new( + report.mr_td, + report.rt_mr0, + report.rt_mr1, + report.rt_mr2, + report.rt_mr3, + ))) } pub fn from_pcrs<'a>(pcrs: impl Iterator) -> Self { @@ -225,13 +290,13 @@ impl MultiMeasurements { /// Mock TDX measurement values used in tests #[cfg(any(test, feature = "mock"))] pub fn mock_dcap_measurements() -> MultiMeasurements { - MultiMeasurements::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, mock_tdx::MOCK_MRTD), - (DcapMeasurementRegister::RTMR0, mock_tdx::MOCK_RTMR0), - (DcapMeasurementRegister::RTMR1, mock_tdx::MOCK_RTMR1), - (DcapMeasurementRegister::RTMR2, mock_tdx::MOCK_RTMR2), - (DcapMeasurementRegister::RTMR3, mock_tdx::MOCK_RTMR3), - ])) + MultiMeasurements::Dcap(DcapMeasurements::new( + mock_tdx::MOCK_MRTD, + mock_tdx::MOCK_RTMR0, + mock_tdx::MOCK_RTMR1, + mock_tdx::MOCK_RTMR2, + mock_tdx::MOCK_RTMR3, + )) } /// An error when converting measurements / to or from HTTP header format @@ -381,9 +446,9 @@ impl MeasurementPolicy { if let ExpectedMeasurements::Dcap(expected) = &measurement_record.measurements { // All measurements in our policy must be given and must match for (k, v) in expected.iter() { - match dcap_measurements.get(k) { - Some(actual_value) if v.iter().any(|v| actual_value == v) => {} - _ => return false, + let actual_value = dcap_measurements.get(k); + if !v.iter().any(|v| actual_value == v) { + return false; } } return true; @@ -578,6 +643,10 @@ mod tests { use super::*; + fn test_dcap_measurements(mrtd: [u8; 48], rtmr0: [u8; 48]) -> MultiMeasurements { + MultiMeasurements::Dcap(DcapMeasurements::new(mrtd, rtmr0, [0u8; 48], [0u8; 48], [0u8; 48])) + } + #[tokio::test] async fn test_read_measurements_file() { let specific_measurements = @@ -702,6 +771,18 @@ mod tests { } } + #[test] + fn test_dcap_header_format_rejects_incomplete_measurements() { + let input = serde_json::to_string(&HashMap::from([("0", hex::encode([0u8; 48]))])).unwrap(); + + let result = MultiMeasurements::from_header_format(&input, AttestationType::DcapTdx); + + assert!(matches!( + result, + Err(MeasurementFormatError::MissingValue(register)) if register == "RTMR0" + )); + } + #[tokio::test] async fn test_check_measurement_with_or_semantics() { let json = r#"[ @@ -722,18 +803,15 @@ mod tests { let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); // First value should match - let measurements1 = - MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0u8; 48])])); + let measurements1 = test_dcap_measurements([0u8; 48], [0u8; 48]); assert!(policy.check_measurement(&measurements1).is_ok()); // Second value should also match - let measurements2 = - MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x11u8; 48])])); + let measurements2 = test_dcap_measurements([0x11u8; 48], [0u8; 48]); assert!(policy.check_measurement(&measurements2).is_ok()); // Different value should not match - let measurements3 = - MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x22u8; 48])])); + let measurements3 = test_dcap_measurements([0x22u8; 48], [0u8; 48]); assert!(policy.check_measurement(&measurements3).is_err()); } @@ -810,24 +888,15 @@ mod tests { let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); // Both match (single + first of any) - let measurements1 = MultiMeasurements::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, [0u8; 48]), - (DcapMeasurementRegister::RTMR0, [0x11u8; 48]), - ])); + let measurements1 = test_dcap_measurements([0u8; 48], [0x11u8; 48]); assert!(policy.check_measurement(&measurements1).is_ok()); // Both match (single + second of any) - let measurements2 = MultiMeasurements::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, [0u8; 48]), - (DcapMeasurementRegister::RTMR0, [0x22u8; 48]), - ])); + let measurements2 = test_dcap_measurements([0u8; 48], [0x22u8; 48]); assert!(policy.check_measurement(&measurements2).is_ok()); // Single matches but any doesn't - let measurements3 = MultiMeasurements::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, [0u8; 48]), - (DcapMeasurementRegister::RTMR0, [0x33u8; 48]), - ])); + let measurements3 = test_dcap_measurements([0u8; 48], [0x33u8; 48]); assert!(policy.check_measurement(&measurements3).is_err()); } @@ -1023,10 +1092,7 @@ mod tests { #[test] fn test_multi_measurements_debug_prints_hex() { let register_value = [0xabu8; 48]; - let dcap = MultiMeasurements::Dcap(HashMap::from([( - DcapMeasurementRegister::MRTD, - register_value, - )])); + let dcap = test_dcap_measurements(register_value, [0u8; 48]); let dcap_debug = format!("{dcap:?}"); assert!(dcap_debug.contains("DCAP")); assert!(dcap_debug.contains(&hex::encode(register_value)));