diff --git a/crates/attestation/README.md b/crates/attestation/README.md index 7a80c42..e15c2dd 100644 --- a/crates/attestation/README.md +++ b/crates/attestation/README.md @@ -190,6 +190,22 @@ attestation type. The measurements can still be checked up-stream by the source client or target service using header injection described below. But it is then up to these external programs to reject unacceptable measurements. +### Alternate `dstack-mr-gcp` format + +`MeasurementPolicy::from_json_bytes` also accepts the object format emitted by +[`dstack-mr-gcp`](https://github.com/flashbots/dstack-mr-gcp). In that format, +the top-level value is a single object instead +of an array of records. + +For this format: + +- `mrtd` and `rtmr0` may contain arrays of acceptable hex values. +- `rtmr1`, `rtmr2`, and `rtmr3` are parsed as fixed hex values. +- `mrconfigid`, `xfam`, and `tdattributes` are ignored by the policy parser. + +The object is normalized into the existing DCAP measurement policy model, so +any `mrtd` value can match with any `rtmr0` value listed in the input. + ### Measurement field names For Azure vTMP attestations, the preferred field names are PCR register diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index db8c2c9..3057ff5 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -5,6 +5,7 @@ use std::{collections::HashMap, fmt, fmt::Formatter, net::IpAddr, path::PathBuf} use dcap_qvl::quote::Report; use http::{HeaderValue, header::InvalidHeaderValue, uri::InvalidUri}; use serde::Deserialize; +use serde_json::Value; use thiserror::Error; use crate::{AttestationError, AttestationType, dcap::DcapVerificationError}; @@ -75,6 +76,49 @@ fn parse_azure_pcr_index(value: &str) -> Result { Ok(index) } +/// Parse a list of hex strings into fixed-size byte arrays +fn parse_hex_values( + values: impl IntoIterator, +) -> Result, MeasurementFormatError> { + values + .into_iter() + .map(|value| hex::decode(value)?.try_into().map_err(|_| MeasurementFormatError::BadLength)) + .collect() +} + +/// Parse a DCAP measurement field from either a single string or an array +fn parse_dcap_measurement_value( + value: &Value, + register_name: &str, +) -> Result, MeasurementFormatError> { + match value { + Value::String(hex_value) => parse_hex_values::(vec![hex_value.clone()]), + Value::Array(values) => { + if values.is_empty() { + return Err(MeasurementFormatError::EmptyExpectedAny(register_name.to_string())); + } + + let hex_values = values + .iter() + .map(|value| { + value.as_str().map(|s| s.to_owned()).ok_or_else(|| { + MeasurementFormatError::Json(serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("expected hex string for register '{register_name}'"), + ))) + }) + }) + .collect::, MeasurementFormatError>>()?; + + parse_hex_values::(hex_values) + } + _ => Err(MeasurementFormatError::Json(serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("expected string or array for register '{register_name}'"), + )))), + } +} + /// Represents a set of measurements values for one of the supported CVM /// platforms #[derive(Clone, PartialEq)] @@ -505,54 +549,119 @@ impl MeasurementPolicy { } } - let records_simple: Vec = serde_json::from_slice(&json_bytes)?; - - let mut measurement_policy = Vec::new(); - - for record in records_simple { - let attestation_type = - serde_json::from_value(serde_json::Value::String(record.attestation_type))?; - - if let Some(measurements) = record.measurements { - let expected_measurements = match attestation_type { - AttestationType::None => ExpectedMeasurements::NoAttestation, - AttestationType::AzureTdx => { - let azure_measurements = measurements - .iter() - .map(|(index_str, entry)| { - let index = parse_azure_pcr_index(index_str)?; - Ok((index, parse_measurement_entry::<32>(entry, index_str)?)) - }) - .collect::>, MeasurementFormatError>>( - )?; - ExpectedMeasurements::Azure(azure_measurements) + let json_value: Value = serde_json::from_slice(&json_bytes)?; + + let measurement_policy = match json_value { + Value::Array(records) => { + let records_simple: Vec = + serde_json::from_value(Value::Array(records))?; + + let mut measurement_policy = Vec::new(); + + for record in records_simple { + let attestation_type = serde_json::from_value::( + Value::String(record.attestation_type), + )?; + + if let Some(measurements) = record.measurements { + let expected_measurements = match attestation_type { + AttestationType::None => ExpectedMeasurements::NoAttestation, + AttestationType::AzureTdx => { + let azure_measurements = + measurements + .iter() + .map(|(index_str, entry)| { + let index = parse_azure_pcr_index(index_str)?; + Ok(( + index, + parse_measurement_entry::<32>(entry, index_str)?, + )) + }) + .collect::>, + MeasurementFormatError, + >>()?; + ExpectedMeasurements::Azure(azure_measurements) + } + AttestationType::DcapTdx | + AttestationType::GcpTdx | + AttestationType::QemuTdx => ExpectedMeasurements::Dcap( + measurements + .iter() + .map(|(index_str, entry)| { + Ok(( + DcapMeasurementRegister::from_policy_key(index_str)?, + parse_measurement_entry::<48>(entry, index_str)?, + )) + }) + .collect::>, + MeasurementFormatError, + >>()?, + ), + }; + + measurement_policy.push(MeasurementRecord { + measurement_id: record.measurement_id.unwrap_or_default(), + measurements: expected_measurements, + }); + } else { + measurement_policy + .push(MeasurementRecord::allow_any_measurement(attestation_type)); + }; + } + + measurement_policy + } + Value::Object(measurements) => { + let mut dcap_measurements = HashMap::new(); + + for (register_name, value) in measurements { + match register_name.as_str() { + "mrconfigid" | "xfam" | "tdattributes" => continue, + _ => { + let register = + DcapMeasurementRegister::from_policy_key(®ister_name).map_err( + |_| { + MeasurementFormatError::Json( + serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "unknown dstack-mr measurement field '{register_name}'" + ), + )), + ) + }, + )?; + + dcap_measurements.insert( + register, + parse_dcap_measurement_value::<48>(&value, ®ister_name)?, + ); + } } - AttestationType::DcapTdx | - AttestationType::GcpTdx | - AttestationType::QemuTdx => ExpectedMeasurements::Dcap( - measurements - .iter() - .map(|(index_str, entry)| { - Ok(( - DcapMeasurementRegister::from_policy_key(index_str)?, - parse_measurement_entry::<48>(entry, index_str)?, - )) - }) - .collect::>, - MeasurementFormatError, - >>()?, + } + + if dcap_measurements.is_empty() { + return Err(MeasurementFormatError::NoExpectedValue( + "dstack-mr measurements".to_string(), + )); + } + + vec![MeasurementRecord { + measurement_id: "dstack-mr-gcp".to_string(), + measurements: ExpectedMeasurements::Dcap(dcap_measurements), + }] + } + _ => { + return Err(MeasurementFormatError::Json(serde_json::Error::io( + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "measurement policy must be a JSON array or object", ), - }; - - measurement_policy.push(MeasurementRecord { - measurement_id: record.measurement_id.unwrap_or_default(), - measurements: expected_measurements, - }); - } else { - measurement_policy.push(MeasurementRecord::allow_any_measurement(attestation_type)); - }; - } + ))); + } + }; Ok(MeasurementPolicy { accepted_measurements: measurement_policy }) } @@ -1001,6 +1110,171 @@ mod tests { } } + #[tokio::test] + async fn test_parse_dstack_mr_gcp_measurements() { + /// Decode a 48-byte hex string for the test fixture + fn hex_48(value: &str) -> [u8; 48] { + hex::decode(value).unwrap().try_into().unwrap() + } + + let json = r#"{ + "rtmr1": "cdf855b56d27967473b885164b3910ab4d81f3db0bd50e114593bd5fd91cf55760de7776c93f4724cefeaf5ac0843e62", + "rtmr2": "438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf", + "rtmr3": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "rtmr0": [ + "c07820997dc2a5e1cc67b05e89852c1a72289e0ec82034bee5b3605cd759328853a758a346522651956afe9222914235", + "c6975b4a5d66fd88bce4d449ed161e0800ce0f5bcd6a3246f2c83407e230474ff56e6e8d5bc9e9d95cf692d458257954", + "8256ae5bb15489ebb0181f9935fe00625751e879d4e3b0a111aaba48da31e5cdd82f379cf4b97411618b02ce1deba1fd", + "46af3396ed9969f670f69142f7b74514598b0f25bb66fc3094402c2de37f4f7493d83cc0860416d3e50f7dda1c34f658", + "c0f93df02880d6c1dc1a5104e04ef691bc41cdc9da49f8834cc1cdc10acdb557758371d19d2466b560be7a66643953fd", + "6927bd0230ca2dba4ad21ee68c1f7e018660d5ad6a99e185eac8adc1f05dd6eeb10178d2a744d22cd14b8945b454712b", + "b90ca4eb0badc04128035ad62f1d1e792f1ca40c99ce25e3cce13f8167eb6265e890c65097518e9f8f5af91519d60d73", + "c3940a1a1f6709fed6d90c34698cb91767fa6261f4469fdcf6da5e36c6f2493fdfd34bc2d1a9f726a17ed88a79b33561", + "8530903ec5cb9aad1737c2bf0e9df958ec0a3ede63fe556415990192b4def86a50d8f6869b6283141b13dd4848b0cea2", + "9efb7193464610d63fbd948901998eda998b3e47e9a0abb72857ba948dcbacd3a17ee75e5081455dccaae208b8294bd9", + "5c942d2f4a08acd594b7d8914362835dcbb12994aedc2128abecf5585807cb8886faf7b9b32611cd4e63eac269632560", + "284f209fece0d49331f2e411c46f6debc3be698bd8587264c90ab0dbdf651046aef3badaae9a7983ea0855a7dabdfa00", + "8d5f7f704ccba0a63157f1ec1214c7f043005b045adae261e0581951965a96350d6196f38751b5dd0e72fca181817efa", + "123ad184172b44083b191b12557f3c923416d8e654ffb390736db331ff2a5bce6c89d14d62cf70e113b98d8f13e78519", + "8ae3d7af48afa0f30fd700a58ca84cd5e0054fbe011d9ed228e30a17db456987e63c6dfd71437aac33ffb9d796088d70", + "3fdfcb2bbf25c9e535f7e4724b1cee79666824cab1565f985d2e1e0218818d538cf6f3bfa5c623c13d6226ae51ea8cd6", + "640b92712990cc8aab4f3786611a8acc3180525abd42a31c06eb7611b8c54a72247dfe8a7a93d3c922f771797a7932de", + "7c8fc1dd62391d416ac64174b833f821b59738d816d96168483300127608e0cf3345840b5bd9325c125dd6b2f595f1b0", + "a191d8250215e05e31fc42fa00f4b7a8729e1fb83b3dacb3def3989b9eaa3f8d199b96759477ce20bbd47c909b6b984c", + "633eeb1778affe65d1b3633527395763602c06e9d7aea52a2a6d5073c33ee1fe78f3a83aeb58edd036de681eee3d1f0f", + "b372a4eac4561e3a8d92028a38e8860a63d7e69c7fcab250aa49d1c951c94b49d0abbe87c353fcd14651f64ac5dde055", + "90d7dbdb795d66669ff44aff1f8ea0de13f5362f1dec68f17fca60364fcc019de18b246c9e173c09102360442dba3261", + "8292abcf17f665c5f63e158a5fd7f2e160ac5b5ae4811532d93c3b5f38a53adebebddaf531aca4ef91d9fb68fb4312a8", + "e1d0235496f93f9475bf0b26d33da5c15831cfc94104d6bea7ab82db027c5f1e917d47dda6953eefae7dcb20ab6f75c4" + ], + "mrtd": [ + "a5844e88897b70c318bef929ef4dfd6c7304c52c4bc9c3f39132f0fdccecf3eb5bab70110ee42a12509a31c037288694", + "8370d8f6d02f2d13e211e91c93fde923049522b241425a29a7bf0071ef49b250af4ef49d852fa3e10065d1b51dfce8fb", + "feb7486608382c1ff0e15b4648ddc0acea6ca974eb53e3529f4c4bd5ffbaa20bf335cb75965cea65fe473aed9647c162" + ], + "mrconfigid": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "xfam": "e700060000000000", + "tdattributes": "0000001000000000" + }"#; + + let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + assert_eq!(policy.accepted_measurements.len(), 1); + + let record = &policy.accepted_measurements[0]; + if let ExpectedMeasurements::Dcap(dcap) = &record.measurements { + assert_eq!(dcap.len(), 5); + assert_eq!(dcap.get(&DcapMeasurementRegister::MRTD).unwrap().len(), 3); + assert_eq!(dcap.get(&DcapMeasurementRegister::RTMR0).unwrap().len(), 24); + assert_eq!(dcap.get(&DcapMeasurementRegister::RTMR1).unwrap().len(), 1); + assert_eq!(dcap.get(&DcapMeasurementRegister::RTMR2).unwrap().len(), 1); + assert_eq!(dcap.get(&DcapMeasurementRegister::RTMR3).unwrap().len(), 1); + + let measurements1 = MultiMeasurements::Dcap(HashMap::from([ + ( + DcapMeasurementRegister::MRTD, + hex_48( + "a5844e88897b70c318bef929ef4dfd6c7304c52c4bc9c3f39132f0fdccecf3eb5bab70110ee42a12509a31c037288694", + ), + ), + ( + DcapMeasurementRegister::RTMR0, + hex_48( + "c07820997dc2a5e1cc67b05e89852c1a72289e0ec82034bee5b3605cd759328853a758a346522651956afe9222914235", + ), + ), + ( + DcapMeasurementRegister::RTMR1, + hex_48( + "cdf855b56d27967473b885164b3910ab4d81f3db0bd50e114593bd5fd91cf55760de7776c93f4724cefeaf5ac0843e62", + ), + ), + ( + DcapMeasurementRegister::RTMR2, + hex_48( + "438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf", + ), + ), + ( + DcapMeasurementRegister::RTMR3, + hex_48( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + ), + ])); + assert!(policy.check_measurement(&measurements1).is_ok()); + + let measurements2 = MultiMeasurements::Dcap(HashMap::from([ + ( + DcapMeasurementRegister::MRTD, + hex_48( + "8370d8f6d02f2d13e211e91c93fde923049522b241425a29a7bf0071ef49b250af4ef49d852fa3e10065d1b51dfce8fb", + ), + ), + ( + DcapMeasurementRegister::RTMR0, + hex_48( + "e1d0235496f93f9475bf0b26d33da5c15831cfc94104d6bea7ab82db027c5f1e917d47dda6953eefae7dcb20ab6f75c4", + ), + ), + ( + DcapMeasurementRegister::RTMR1, + hex_48( + "cdf855b56d27967473b885164b3910ab4d81f3db0bd50e114593bd5fd91cf55760de7776c93f4724cefeaf5ac0843e62", + ), + ), + ( + DcapMeasurementRegister::RTMR2, + hex_48( + "438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf", + ), + ), + ( + DcapMeasurementRegister::RTMR3, + hex_48( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + ), + ])); + assert!(policy.check_measurement(&measurements2).is_ok()); + + let measurements3 = MultiMeasurements::Dcap(HashMap::from([ + ( + DcapMeasurementRegister::MRTD, + hex_48( + "a5844e88897b70c318bef929ef4dfd6c7304c52c4bc9c3f39132f0fdccecf3eb5bab70110ee42a12509a31c037288694", + ), + ), + ( + DcapMeasurementRegister::RTMR0, + hex_48( + "c07820997dc2a5e1cc67b05e89852c1a72289e0ec82034bee5b3605cd759328853a758a346522651956afe9222914235", + ), + ), + ( + DcapMeasurementRegister::RTMR1, + hex_48( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ), + ), + ( + DcapMeasurementRegister::RTMR2, + hex_48( + "438337a98c597535940941a3d9913e04a76d84e4ebf69dbb89e1addc8bae7183579685f0ef3144875dba7d933d9dcabf", + ), + ), + ( + DcapMeasurementRegister::RTMR3, + hex_48( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + ), + ])); + assert!(policy.check_measurement(&measurements3).is_err()); + } else { + panic!("Expected ExpectedMeasurements::Dcap"); + } + } + #[tokio::test] async fn test_parse_invalid_prefixed_azure_pcr_key_error() { let json = r#"[