diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index f09a6487e..1fa97c5e6 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -419,23 +419,27 @@ fn find_event_payloads(runtime_events: &[RuntimeEvent], name: &str) -> Vec Result { - let config = if config.is_empty() { - fallback_config - } else { - config - }; +/// The VM config to decode: the caller-supplied copy or the one carried in the +/// attestation. When both are given they must be identical, so no reader can +/// authorize on one copy and decode another. +pub fn resolve_vm_config<'a>(external: &'a str, embedded: &'a str) -> Result<&'a str> { + if external.is_empty() { + return Ok(embedded); + } + if !embedded.is_empty() && external != embedded { + bail!("vm_config does not match the config carried in the attestation"); + } + Ok(external) +} + +fn parse_vm_config(config: &str) -> Result { + // No vm config for nitro enclave let config = if config.is_empty() { "{}" } else { config }; - let config = vm_config_json_from_config(config).unwrap_or(Cow::Borrowed(config)); - serde_json::from_str(&config).context("Failed to parse vm config") + serde_json::from_str(config).context("Failed to parse vm config") } -fn vm_config_json_from_config(config: &str) -> Option> { - let value = serde_json::from_str::(config).ok()?; - value - .get("vm_config") - .and_then(|value| value.as_str()) - .map(|vm_config| Cow::Owned(vm_config.to_string())) +fn decode_vm_config_with_fallback(config: &str, fallback_config: &str) -> Result { + parse_vm_config(resolve_vm_config(config, fallback_config)?) } fn mr_config_document_from_value(value: &serde_json::Value) -> Result> { @@ -453,16 +457,7 @@ fn mr_config_document_from_config(config: &str) -> Result> { let Ok(value) = serde_json::from_str::(config) else { return Ok(None); }; - if let Some(mr_config) = mr_config_document_from_value(&value)? { - return Ok(Some(mr_config)); - } - - let Some(vm_config) = value.get("vm_config").and_then(|value| value.as_str()) else { - return Ok(None); - }; - let vm_config = serde_json::from_str::(vm_config) - .context("Failed to parse nested vm_config for amd sev-snp mr_config")?; - mr_config_document_from_value(&vm_config) + mr_config_document_from_value(&value) } pub use dstack_types::TeeVariant; @@ -1685,11 +1680,10 @@ fn decode_app_info_sev_snp( external_vm_config: &str, ) -> Result { let parsed = crate::amd_sev_snp::parse_unverified_amd_snp_report(report)?; + let config = resolve_vm_config(external_vm_config, embedded_config)?; let mr_config_document = if let Some(mr_config) = mr_config { Cow::Borrowed(mr_config) - } else if let Some(mr_config) = mr_config_document_from_config(external_vm_config)? { - Cow::Owned(mr_config) - } else if let Some(mr_config) = mr_config_document_from_config(embedded_config)? { + } else if let Some(mr_config) = mr_config_document_from_config(config)? { Cow::Owned(mr_config) } else { bail!("amd sev-snp mr_config is missing"); @@ -1697,8 +1691,7 @@ fn decode_app_info_sev_snp( let mr_config = verify_snp_mr_config_host_data(mr_config_document.as_ref(), &parsed.host_data)?; let key_provider_info = key_provider_info_from_mr_config(&mr_config)?; - let os_image_hash = - decode_vm_config_with_fallback(external_vm_config, embedded_config)?.os_image_hash; + let os_image_hash = parse_vm_config(config)?.os_image_hash; let mrs = decode_mr_sev_snp(&parsed.measurement, &parsed.host_data); Ok(AppInfo { @@ -1983,17 +1976,8 @@ impl Attestation { } /// Decode the VM config from the external or embedded config - pub fn decode_vm_config<'a>(&'a self, mut config: &'a str) -> Result { - if config.is_empty() { - config = &self.config; - } - if config.is_empty() { - // No vm config for nitro enclave - config = "{}"; - } - let vm_config: VmConfig = - serde_json::from_str(config).context("Failed to parse vm config")?; - Ok(vm_config) + pub fn decode_vm_config(&self, config: &str) -> Result { + decode_vm_config_with_fallback(config, &self.config) } /// Decode the app info from the platform-specific app info source. diff --git a/dstack/dstack-mr/src/sev.rs b/dstack/dstack-mr/src/sev.rs index 37e51c8a3..b413e8b43 100644 --- a/dstack/dstack-mr/src/sev.rs +++ b/dstack/dstack-mr/src/sev.rs @@ -1077,38 +1077,17 @@ pub struct SnpLaunchInputs { /// `mr_config` document out of a VMM `vm_config` string. /// /// The fields are intentionally explicit so missing SNP launch inputs fail -/// closed instead of falling back to TDX event-log decoding. Both the top-level -/// shape and the legacy nested `vm_config` string shape are accepted. +/// closed instead of falling back to TDX event-log decoding. pub fn parse_snp_inputs_from_vm_config(vm_config: &str) -> Result { - let value: serde_json::Value = + let parsed: SevSnpMeasurementVmConfig = serde_json::from_str(vm_config).context("failed to parse vm_config for amd sev-snp")?; - let parsed: SevSnpMeasurementVmConfig = serde_json::from_value(value.clone()) - .context("failed to parse vm_config for amd sev-snp")?; - let nested = value - .get("vm_config") - .and_then(|value| value.as_str()) - .map(|vm_config| { - serde_json::from_str::(vm_config) - .context("failed to parse nested vm_config for amd sev-snp") - }) - .transpose()?; let measurement_document = parsed .sev_snp_measurement - .or_else(|| { - nested - .as_ref() - .and_then(|nested| nested.sev_snp_measurement.clone()) - }) - .ok_or_else(|| anyhow::anyhow!("sev_snp_measurement is required for amd sev-snp"))?; - let os_image_hash = if !parsed.os_image_hash.is_empty() { - parsed.os_image_hash - } else { - nested - .as_ref() - .map(|nested| nested.os_image_hash.clone()) - .filter(|hash| !hash.is_empty()) - .ok_or_else(|| anyhow::anyhow!("os_image_hash is required for amd sev-snp"))? - }; + .context("sev_snp_measurement is required for amd sev-snp")?; + let os_image_hash = parsed.os_image_hash; + if os_image_hash.is_empty() { + bail!("os_image_hash is required for amd sev-snp"); + } let document: SnpMeasurementDocument = serde_json::from_str(&measurement_document) .context("invalid amd sev-snp measurement document")?; dstack_types::SevOsImageMeasurementDocument::new( @@ -1122,8 +1101,7 @@ pub fn parse_snp_inputs_from_vm_config(vm_config: &str) -> Result Result { let boot_info = if att.report.amd_snp_report().is_some() { - let vm_config_str = if vm_config_str.is_empty() { - att.config.as_str() - } else { - vm_config_str - }; amd_attest::build_amd_snp_boot_info_from_verified_attestation_and_vm_config( att, - vm_config_str, + resolve_vm_config(vm_config_str, &att.config)?, )? } else { build_boot_info(att, use_boottime_mr, vm_config_str)? @@ -1159,6 +1154,28 @@ mod tests { assert_eq!(boot_info.app_id, vec![0x11; 20]); } + #[test] + fn build_boot_info_for_attestation_rejects_a_vm_config_differing_from_the_attested_one() { + let input = valid_snp_measurement_input(); + let measurement = compute_expected_measurement(&input).unwrap(); + let mr_config = valid_snp_mr_config(); + let vm_config = snp_vm_config(&input, &mr_config); + let attestation = verified_snp_attestation_with_config( + measurement, + [0xab; 64], + vm_config.clone(), + &mr_config, + ); + let mut request: serde_json::Value = serde_json::from_str(&vm_config).unwrap(); + request["cpu_count"] = 8.into(); + + let err = build_boot_info_for_attestation(&attestation, false, &request.to_string()) + .expect_err("a vm_config differing from the attested one must be rejected"); + assert!(format!("{err:#}").contains("does not match the config carried")); + build_boot_info_for_attestation(&attestation, false, &vm_config) + .expect("an identical copy is accepted"); + } + #[test] fn build_boot_info_for_attestation_accepts_self_contained_snp_input_without_config() { let input = valid_snp_measurement_input(); diff --git a/dstack/verifier/src/verification.rs b/dstack/verifier/src/verification.rs index 2173fb273..f4f862db2 100644 --- a/dstack/verifier/src/verification.rs +++ b/dstack/verifier/src/verification.rs @@ -22,8 +22,8 @@ use dstack_mr::{tdx::TdxRtmr0AcpiHashes, TdxMeasurements}; use dstack_types::{sha256sum, TdxAttestationVariant, VmConfig}; use hex_literal::hex; use ra_tls::attestation::{ - AppInfo, Attestation, AttestationQuote, AttestationVerifier, DstackVerifiedReport, NitroPcrs, - VerifiedAttestation, VersionedAttestation, + resolve_vm_config, AppInfo, Attestation, AttestationQuote, AttestationVerifier, + DstackVerifiedReport, NitroPcrs, VerifiedAttestation, VersionedAttestation, }; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; @@ -764,16 +764,10 @@ impl CvmVerifier { attestation: &VerifiedAttestation, details: &mut VerificationDetails, ) -> Result { - // The raw config string used for platform-specific binding: the explicit - // request `vm_config` when supplied, otherwise the one embedded in the - // attestation (mirroring `decode_vm_config`'s own fallback). - let raw_config = if vm_config.is_empty() { - attestation.config.clone() - } else { - vm_config.clone() - }; + // The raw config string used for platform-specific binding. + let raw_config = resolve_vm_config(&vm_config, &attestation.config)?; let mut vm_config = attestation - .decode_vm_config(&vm_config) + .decode_vm_config(raw_config) .context("Failed to decode VM config")?; match &attestation.quote { AttestationQuote::DstackGcpTdx(_) => { @@ -823,7 +817,7 @@ impl CvmVerifier { AttestationQuote::DstackAmdSevSnp(_) => { self.verify_os_image_hash_for_dstack_sev( attestation, - &raw_config, + raw_config, &mut vm_config, details, )?; diff --git a/sdk/simulator/attestation.bin b/sdk/simulator/attestation.bin index 2c4aef253..0703bf6b6 100644 Binary files a/sdk/simulator/attestation.bin and b/sdk/simulator/attestation.bin differ diff --git a/sdk/simulator/sys-config.json b/sdk/simulator/sys-config.json index 4993438b3..8f75ae85f 100644 --- a/sdk/simulator/sys-config.json +++ b/sdk/simulator/sys-config.json @@ -8,5 +8,5 @@ "pccs_url": "", "docker_registry": "", "host_api_url": "vsock://2:12000/api", - "vm_config": "{\"os_image_hash\":\"64f0d1545cd510a8dfed7ad609d105b5d41f0cb2afcfdda8867ede00c88add7a\",\"cpu_count\":1,\"memory_size\":2147483648}" + "vm_config": "{\"os_image_hash\":\"e61be43dd1cc6a6f5edefd7c51b608983780839b0000ce60dd99737dcffe09b9\",\"cpu_count\":8,\"memory_size\":17179869184,\"qemu_version\":\"8.2.2\",\"pci_hole64_size\":0,\"hugepages\":false,\"num_gpus\":0,\"num_nvswitches\":0,\"hotplug_off\":false,\"image\":\"dstack-0.6.0\",\"host_share_mode\":\"vvfat\",\"spec_version\":1}" }