diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index a0167f769..63b05e3d9 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -2189,6 +2189,7 @@ dependencies = [ "serde-human-bytes", "serde_jcs", "serde_json", + "serde_with", "sha2 0.10.9", "sha3", "size-parser", @@ -6945,9 +6946,22 @@ dependencies = [ "schemars 1.2.1", "serde_core", "serde_json", + "serde_with_macros", "time", ] +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" diff --git a/dstack/Cargo.toml b/dstack/Cargo.toml index 848809b03..f32315326 100644 --- a/dstack/Cargo.toml +++ b/dstack/Cargo.toml @@ -170,6 +170,7 @@ scale = { version = "3.7.4", package = "parity-scale-codec", features = [ ] } serde = { version = "1.0.228", features = ["derive"], default-features = false } serde-human-bytes = "0.1.2" +serde_with = "3.14.0" semver = "1.0.28" serde_jcs = "0.2.0" rmp-serde = "1.3.1" diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index a3b5b323c..8c78d1867 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -1547,7 +1547,7 @@ struct Mrs { fn key_provider_info_from_mr_config(mr_config: &MrConfigV3) -> Result> { serde_json::to_vec(&KeyProviderInfo::new( mr_config.key_provider_name().to_string(), - hex::encode(&mr_config.key_provider_id), + hex::encode(mr_config.key_provider_id.as_deref().unwrap_or_default()), )) .context("Failed to serialize key provider info") } @@ -1607,8 +1607,8 @@ fn decode_app_info_sev_snp( let mrs = decode_mr_sev_snp(&parsed.measurement, &parsed.host_data); Ok(AppInfo { - app_id: mr_config.app_id, - instance_id: mr_config.instance_id, + app_id: mr_config.app_id.unwrap_or_default(), + instance_id: mr_config.instance_id.unwrap_or_default(), device_id: sha256(parsed.chip_id).to_vec(), mr_system: mrs.mr_system, mr_aggregated: mrs.mr_aggregated, diff --git a/dstack/dstack-attest/tests/sev_snp_verify.rs b/dstack/dstack-attest/tests/sev_snp_verify.rs index 13a87759a..38a19145c 100644 --- a/dstack/dstack-attest/tests/sev_snp_verify.rs +++ b/dstack/dstack-attest/tests/sev_snp_verify.rs @@ -102,7 +102,7 @@ fn verify_sev_snp_attestation_bin() { ); // The HOST_DATA-bound app identity is recovered from the mr_config document. assert_eq!( - hex::encode(&binding.mr_config.app_id), + hex::encode(binding.mr_config.app_id.as_deref().unwrap_or_default()), "86e59625be93207bc2351c4d1bba20037cec8e16", "mr_config app_id bound by HOST_DATA" ); diff --git a/dstack/dstack-mr/src/sev.rs b/dstack/dstack-mr/src/sev.rs index 34dd2ab53..59ff62f58 100644 --- a/dstack/dstack-mr/src/sev.rs +++ b/dstack/dstack-mr/src/sev.rs @@ -911,13 +911,15 @@ pub fn validate_mr_config(mr_config: &MrConfigV3) -> Result<()> { if mr_config.version != 3 { bail!("mr_config version must be 3"); } - ensure_len("mr_config.app_id", &mr_config.app_id, 20)?; + if let Some(app_id) = mr_config.app_id.as_deref() { + ensure_len("mr_config.app_id", app_id, 20)?; + } ensure_len("mr_config.compose_hash", &mr_config.compose_hash, 32)?; if let Some(gpu_policy_hash) = &mr_config.gpu_policy_hash { ensure_len("mr_config.gpu_policy_hash", gpu_policy_hash, 32)?; } - if !mr_config.instance_id.is_empty() { - ensure_len("mr_config.instance_id", &mr_config.instance_id, 20)?; + if let Some(instance_id) = mr_config.instance_id.as_deref() { + ensure_len("mr_config.instance_id", instance_id, 20)?; } Ok(()) } diff --git a/dstack/dstack-types/Cargo.toml b/dstack/dstack-types/Cargo.toml index 526d5192b..5dba0358c 100644 --- a/dstack/dstack-types/Cargo.toml +++ b/dstack/dstack-types/Cargo.toml @@ -16,6 +16,7 @@ or-panic.workspace = true scale = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } serde-human-bytes.workspace = true +serde_with.workspace = true serde_jcs.workspace = true serde_json.workspace = true sha2.workspace = true diff --git a/dstack/dstack-types/src/mr_config.rs b/dstack/dstack-types/src/mr_config.rs index 846f4f746..996a68695 100644 --- a/dstack/dstack-types/src/mr_config.rs +++ b/dstack/dstack-types/src/mr_config.rs @@ -5,6 +5,7 @@ use or_panic::ResultOrPanic; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; +use serde_with::skip_serializing_none; use sha2::Sha256; use sha3::{Digest, Keccak256}; use std::{error::Error, fmt}; @@ -100,24 +101,26 @@ impl From for MrConfigDocumentError { } } +#[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MrConfigV3 { #[serde(default = "mr_config_v3_version")] pub version: u8, - #[serde(with = "hex_bytes")] - pub app_id: Vec, + /// Optional application identity pin. + #[serde(default, with = "hex_bytes")] + pub app_id: Option>, #[serde(with = "hex_bytes")] pub compose_hash: Vec, /// Hash of the raw application GPU policy. GPU launches populate it; /// non-GPU and historical v3 launch documents omit it. - #[serde(default, skip_serializing_if = "Option::is_none", with = "hex_bytes")] + #[serde(default, with = "hex_bytes")] pub gpu_policy_hash: Option>, pub key_provider: KeyProviderKind, #[serde(default, with = "hex_bytes")] - pub key_provider_id: Vec, + pub key_provider_id: Option>, #[serde(default, with = "hex_bytes")] - pub instance_id: Vec, + pub instance_id: Option>, } impl MrConfigV3 { @@ -131,12 +134,12 @@ impl MrConfigV3 { ) -> Self { Self { version: mr_config_v3_version(), - app_id, + app_id: (!app_id.is_empty()).then_some(app_id), compose_hash, gpu_policy_hash, key_provider, - key_provider_id, - instance_id, + key_provider_id: (!key_provider_id.is_empty()).then_some(key_provider_id), + instance_id: (!instance_id.is_empty()).then_some(instance_id), } } @@ -203,7 +206,7 @@ mod tests { vec![0x44; 20], ); let mut changed = config.clone(); - changed.app_id[0] ^= 0xff; + changed.app_id.as_mut().expect("app_id is set")[0] ^= 0xff; assert_ne!(config.to_snp_host_data(), changed.to_snp_host_data()); assert_eq!(config.to_snp_host_data().len(), 32); @@ -235,6 +238,17 @@ mod tests { Ok(()) } + #[test] + fn mr_config_v3_defaults_missing_app_id_to_empty() -> Result<(), Box> { + let config = MrConfigV3::from_document( + r#"{"compose_hash":"2222222222222222222222222222222222222222222222222222222222222222","key_provider":"none"}"#, + )?; + + assert!(config.app_id.is_none()); + assert!(!config.to_canonical_json().contains("app_id")); + Ok(()) + } + #[test] fn mr_config_v3_generates_jcs_but_hashes_document_bytes() -> Result<(), Box> { let config = MrConfigV3::new( diff --git a/dstack/dstack-util/src/system_setup/config_id_verifier.rs b/dstack/dstack-util/src/system_setup/config_id_verifier.rs index 72cf28755..02c905e57 100644 --- a/dstack/dstack-util/src/system_setup/config_id_verifier.rs +++ b/dstack/dstack-util/src/system_setup/config_id_verifier.rs @@ -168,17 +168,23 @@ fn verify_mr_config_v3_document( bail!("Invalid mr_config gpu_policy_hash"); } } - if mr_config.app_id.as_slice() != local.app_id { - bail!("Invalid mr_config app_id"); + if let Some(app_id) = mr_config.app_id.as_deref() { + if app_id != local.app_id { + bail!("Invalid mr_config app_id"); + } } - if mr_config.instance_id.as_slice() != local.instance_id { - bail!("Invalid mr_config instance_id"); + if let Some(instance_id) = mr_config.instance_id.as_deref() { + if instance_id != local.instance_id { + bail!("Invalid mr_config instance_id"); + } } if mr_config.key_provider != local.key_provider { bail!("Invalid mr_config key_provider"); } - if mr_config.key_provider_id.as_slice() != local.key_provider_id { - bail!("Invalid mr_config key_provider_id"); + if let Some(key_provider_id) = mr_config.key_provider_id.as_deref() { + if key_provider_id != local.key_provider_id { + bail!("Invalid mr_config key_provider_id"); + } } Ok(mr_config) } @@ -256,6 +262,35 @@ mod tests { } } + #[test] + fn mr_config_v3_skips_app_id_check_when_field_is_missing() -> Result<()> { + let compose_hash = [0x22u8; 32]; + let gpu_policy_hash = [0x55u8; 32]; + let app_id = [0x11u8; 20]; + let instance_id = [0x44u8; 20]; + let key_provider_id = [0x33u8; 32]; + let document = MrConfigV3::new( + Vec::new(), + compose_hash.to_vec(), + Some(gpu_policy_hash.to_vec()), + KeyProviderKind::Kms, + key_provider_id.to_vec(), + instance_id.to_vec(), + ) + .to_canonical_json(); + let local = LocalMrConfigValues { + compose_hash: &compose_hash, + gpu_policy_hash: &gpu_policy_hash, + app_id: &app_id, + instance_id: &instance_id, + key_provider: KeyProviderKind::Kms, + key_provider_id: &key_provider_id, + }; + + verify_mr_config_v3_document(&document, local)?; + Ok(()) + } + #[test] fn mr_config_v3_document_must_match_expected_gpu_policy_hash() { let compose_hash = [0x22u8; 32]; diff --git a/dstack/kms/src/main_service/amd_attest.rs b/dstack/kms/src/main_service/amd_attest.rs index 59387c895..a9a943131 100644 --- a/dstack/kms/src/main_service/amd_attest.rs +++ b/dstack/kms/src/main_service/amd_attest.rs @@ -113,9 +113,9 @@ fn build_amd_snp_boot_info_with_tcb_status( mr_aggregated, os_image_hash: os_image_hash.to_vec(), mr_system, - app_id: mr_config.app_id.clone(), + app_id: mr_config.app_id.clone().unwrap_or_default(), compose_hash: mr_config.compose_hash.clone(), - instance_id: mr_config.instance_id.clone(), + instance_id: mr_config.instance_id.clone().unwrap_or_default(), device_id: verified_chip_id.to_vec(), key_provider_info, tcb_status: tcb_status.to_string(), @@ -182,7 +182,7 @@ fn parse_measurement_input_from_vm_config(vm_config: &str) -> Result Result> { serde_json::to_vec(&KeyProviderInfo::new( mr_config.key_provider_name().to_string(), - hex::encode(&mr_config.key_provider_id), + hex::encode(mr_config.key_provider_id.as_deref().unwrap_or_default()), )) .context("failed to serialize key provider info") } diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 690b7c7af..47ebe3698 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -1083,27 +1083,12 @@ impl App { let manifest = work_dir.manifest().context("Failed to read manifest")?; let cfg = &self.config; let compose_hash = sha256_file(shared_dir.join(APP_COMPOSE))?; - let platform = cfg.cvm.resolved_platform(); let app_compose = work_dir .app_compose() .context("Failed to get app compose")?; - let use_mr_config_v3 = !manifest.no_tee - && (platform == crate::config::CvmPlatform::AmdSevSnp - || (platform == crate::config::CvmPlatform::Tdx - && cfg.cvm.use_mrconfigid - && !app_compose.key_provider_id.is_empty())); - let mr_config = if use_mr_config_v3 { - Some( - work_dir - .prepare_mr_config_v3( - &app_compose, - manifest.gpus.as_ref().is_some_and(GpuConfig::has_gpus), - ) - .context("Failed to prepare mr_config")?, - ) - } else { - None - }; + let mr_config = work_dir + .prepare_mr_config(&manifest, &cfg.cvm, &app_compose) + .context("Failed to prepare mr_config")?; let sys_config_str = make_sys_config( cfg, &manifest, @@ -1502,6 +1487,7 @@ pub(crate) fn needs_swtpm( #[cfg(test)] mod tests { + use super::mr_config::{mr_config_version, MrConfigVersion}; use super::*; use crate::config::{ load_config_figment, CvmPlatform, Networking, NetworkingMode, TdxAttestationVariantConfig, @@ -1742,6 +1728,47 @@ mod tests { } } + #[test] + fn selects_mr_config_version_for_each_tee_mode() -> Result<()> { + let manifest = test_manifest(2048); + assert_eq!( + mr_config_version(&manifest, CvmPlatform::AmdSevSnp, false, false)?, + Some(MrConfigVersion::V3) + ); + assert_eq!( + mr_config_version(&manifest, CvmPlatform::Tdx, false, false)?, + None + ); + assert_eq!( + mr_config_version(&manifest, CvmPlatform::Tdx, true, false)?, + Some(MrConfigVersion::V1) + ); + assert_eq!( + mr_config_version(&manifest, CvmPlatform::Tdx, true, true)?, + Some(MrConfigVersion::V3) + ); + assert_eq!( + mr_config_version(&manifest, CvmPlatform::Tdx, false, true) + .err() + .map(|error| error.to_string()), + Some("key provider ID requires MrConfigV3, but use_mrconfigid is disabled".to_string()) + ); + + let mut no_tee = manifest.clone(); + no_tee.no_tee = true; + assert_eq!( + mr_config_version(&no_tee, CvmPlatform::AmdSevSnp, true, true)?, + None + ); + + no_tee.simulated_tee = Some(dstack_types::TeeVariant::DstackAmdSevSnp); + assert_eq!( + mr_config_version(&no_tee, CvmPlatform::Tdx, false, false)?, + Some(MrConfigVersion::V3) + ); + Ok(()) + } + fn dummy_tdx_measurement_document() -> TdxOsImageMeasurementDocument { let measurement = TdxOsImageMeasurement { image: TdxImageMeasurement { @@ -2112,7 +2139,7 @@ mod tests { sys_config["nvidia_attestation_proxy_url"], "http://10.0.2.2:8090" ); - assert_eq!(parsed_mr_config.app_id, vec![0x11; 20]); + assert_eq!(parsed_mr_config.app_id, Some(vec![0x11; 20])); assert_eq!(parsed_mr_config.compose_hash, vec![0x22; 32]); assert_eq!(parsed_mr_config.gpu_policy_hash, None); assert_eq!(vm_config["mr_config"], sys_config["mr_config"]); diff --git a/dstack/vmm/src/app/mr_config.rs b/dstack/vmm/src/app/mr_config.rs index 434d8a1d1..d607255d0 100644 --- a/dstack/vmm/src/app/mr_config.rs +++ b/dstack/vmm/src/app/mr_config.rs @@ -11,7 +11,37 @@ use dstack_types::{gpu_policy_hash, AppCompose}; use fs_err as fs; use sha2::{Digest, Sha256}; -use super::VmWorkDir; +use super::{GpuConfig, Manifest, VmWorkDir}; +use crate::config::{CvmConfig, CvmPlatform}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum MrConfigVersion { + V1, + V3, +} + +pub(super) fn mr_config_version( + manifest: &Manifest, + platform: CvmPlatform, + use_mrconfigid: bool, + has_key_provider_id: bool, +) -> Result> { + if manifest.simulated_tee == Some(dstack_types::TeeVariant::DstackAmdSevSnp) { + return Ok(Some(MrConfigVersion::V3)); + } + if manifest.no_tee { + return Ok(None); + } + match platform { + CvmPlatform::AmdSevSnp => Ok(Some(MrConfigVersion::V3)), + CvmPlatform::Tdx if has_key_provider_id && !use_mrconfigid => { + bail!("key provider ID requires MrConfigV3, but use_mrconfigid is disabled") + } + CvmPlatform::Tdx if !use_mrconfigid => Ok(None), + CvmPlatform::Tdx if has_key_provider_id => Ok(Some(MrConfigVersion::V3)), + CvmPlatform::Tdx => Ok(Some(MrConfigVersion::V1)), + } +} pub(super) fn tdx_mr_config_id(workdir: &VmWorkDir, app_compose: &AppCompose) -> Result { if let Some(document) = workdir @@ -60,7 +90,30 @@ pub(super) fn snp_host_data(workdir: &VmWorkDir) -> Result { } impl VmWorkDir { - pub fn prepare_mr_config_v3(&self, app_compose: &AppCompose, has_gpus: bool) -> Result { + pub(crate) fn prepare_mr_config( + &self, + manifest: &Manifest, + config: &CvmConfig, + app_compose: &AppCompose, + ) -> Result> { + let version = mr_config_version( + manifest, + config.resolved_platform(), + config.use_mrconfigid, + !app_compose.key_provider_id.is_empty(), + )?; + match version { + Some(MrConfigVersion::V3) => self + .prepare_mr_config_v3( + app_compose, + manifest.gpus.as_ref().is_some_and(GpuConfig::has_gpus), + ) + .map(Some), + Some(MrConfigVersion::V1) | None => Ok(None), + } + } + + fn prepare_mr_config_v3(&self, app_compose: &AppCompose, has_gpus: bool) -> Result { let compose_hash = self .app_compose_hash() .context("failed to get compose hash")?; diff --git a/dstack/vmm/src/one_shot.rs b/dstack/vmm/src/one_shot.rs index ecbf9b83f..f3698681a 100644 --- a/dstack/vmm/src/one_shot.rs +++ b/dstack/vmm/src/one_shot.rs @@ -242,24 +242,9 @@ Compose file content (first 200 chars): let app_compose = vm_work_dir .app_compose() .context("Failed to get app compose")?; - let platform = config.cvm.resolved_platform(); - let use_mr_config_v3 = !manifest.no_tee - && (platform == crate::config::CvmPlatform::AmdSevSnp - || (platform == crate::config::CvmPlatform::Tdx - && config.cvm.use_mrconfigid - && !app_compose.key_provider_id.is_empty())); - let mr_config = if use_mr_config_v3 { - Some( - vm_work_dir - .prepare_mr_config_v3( - &app_compose, - manifest.gpus.as_ref().is_some_and(|gpus| gpus.has_gpus()), - ) - .context("Failed to prepare mr_config")?, - ) - } else { - None - }; + let mr_config = vm_work_dir + .prepare_mr_config(&manifest, &config.cvm, &app_compose) + .context("Failed to prepare mr_config")?; let sys_config_str = make_sys_config( &config, &manifest,