Skip to content

Commit 2c7b7ea

Browse files
committed
feat(vmm): support tdx acpi table requirement
1 parent 3da9a08 commit 2c7b7ea

8 files changed

Lines changed: 240 additions & 18 deletions

File tree

dstack-types/src/lib.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,18 @@ pub struct Requirements {
126126
/// an explicit empty list means no platform is allowed.
127127
#[serde(skip_serializing_if = "Option::is_none")]
128128
pub platforms: Option<Vec<String>>,
129+
/// TDX-only ACPI table measurement requirement. When set, this overrides
130+
/// the VMM-side TDX lite attestation policy: `true` requires legacy mode
131+
/// with ACPI tables measured, while `false` requires lite mode.
132+
#[serde(skip_serializing_if = "Option::is_none")]
133+
pub tdx_measure_acpi_tables: Option<bool>,
129134
}
130135

131136
impl Requirements {
132137
pub fn is_empty(&self) -> bool {
133-
self.os_version.is_none() && self.platforms.is_none()
138+
self.os_version.is_none()
139+
&& self.platforms.is_none()
140+
&& self.tdx_measure_acpi_tables.is_none()
134141
}
135142
}
136143

@@ -381,7 +388,8 @@ mod app_compose_tests {
381388
"runner": "docker-compose",
382389
"requirements": {
383390
"os_version": ">=0.6.1",
384-
"platforms": ["dstack-gcp-tdx", "dstack-tdx"]
391+
"platforms": ["dstack-gcp-tdx", "dstack-tdx"],
392+
"tdx_measure_acpi_tables": true
385393
}
386394
}))
387395
.unwrap();
@@ -391,6 +399,7 @@ mod app_compose_tests {
391399
requirements.platforms,
392400
Some(vec!["dstack-gcp-tdx".to_string(), "dstack-tdx".to_string()])
393401
);
402+
assert_eq!(requirements.tdx_measure_acpi_tables, Some(true));
394403

395404
let err = serde_json::from_value::<AppCompose>(serde_json::json!({
396405
"manifest_version": "3",
@@ -429,6 +438,19 @@ mod app_compose_tests {
429438
let requirements = explicit_empty.requirements.as_ref().unwrap();
430439
assert_eq!(requirements.platforms, Some(vec![]));
431440
assert!(!requirements.is_empty());
441+
442+
let acpi_tables: AppCompose = serde_json::from_value(serde_json::json!({
443+
"manifest_version": "3",
444+
"name": "test",
445+
"runner": "docker-compose",
446+
"requirements": {
447+
"tdx_measure_acpi_tables": false
448+
}
449+
}))
450+
.unwrap();
451+
let requirements = acpi_tables.requirements.as_ref().unwrap();
452+
assert_eq!(requirements.tdx_measure_acpi_tables, Some(false));
453+
assert!(!requirements.is_empty());
432454
}
433455
}
434456

dstack-util/src/system_setup.rs

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ fn verify_manifest_version(app_compose: &AppCompose) -> Result<u32> {
774774
Ok(manifest_version)
775775
}
776776

777-
fn verify_app_compose_policy(app_compose: &AppCompose) -> Result<()> {
777+
fn verify_app_compose_policy(app_compose: &AppCompose, sys_config: &SysConfig) -> Result<()> {
778778
verify_manifest_feature_requirements(app_compose)?;
779779
let Some(requirements) = app_compose.requirements.as_ref() else {
780780
return Ok(());
@@ -789,9 +789,18 @@ fn verify_app_compose_policy(app_compose: &AppCompose) -> Result<()> {
789789
bail!("Unsupported attestation platform: requirements.platforms is empty");
790790
}
791791
let current_platform =
792-
AttestationMode::detect().context("Failed to detect current attestation platform")?;
792+
AttestationMode::detect().context("failed to detect current attestation platform")?;
793793
verify_platform_requirements(app_compose, current_platform)?;
794794
}
795+
if requirements.tdx_measure_acpi_tables.is_some() {
796+
let current_platform =
797+
AttestationMode::detect().context("failed to detect current attestation platform")?;
798+
verify_tdx_measure_acpi_tables_requirement(
799+
app_compose,
800+
&sys_config.vm_config,
801+
current_platform,
802+
)?;
803+
}
795804
Ok(())
796805
}
797806

@@ -867,6 +876,41 @@ fn format_requirement_platforms(platforms: &[String]) -> String {
867876
platforms.join(", ")
868877
}
869878

879+
fn verify_tdx_measure_acpi_tables_requirement(
880+
app_compose: &AppCompose,
881+
vm_config: &str,
882+
current_platform: AttestationMode,
883+
) -> Result<()> {
884+
let Some(measure_acpi_tables) = app_compose
885+
.requirements
886+
.as_ref()
887+
.and_then(|requirements| requirements.tdx_measure_acpi_tables)
888+
else {
889+
return Ok(());
890+
};
891+
if current_platform != AttestationMode::DstackTdx {
892+
return Ok(());
893+
}
894+
let vm_config: dstack_types::VmConfig = serde_json::from_str(vm_config)
895+
.context("failed to parse vm_config for requirements.tdx_measure_acpi_tables")?;
896+
let uses_lite = vm_config.tdx_attestation_variant.is_lite();
897+
if measure_acpi_tables && uses_lite {
898+
bail!(
899+
"unsupported TDX attestation mode: requirements.tdx_measure_acpi_tables=true requires ACPI table measurement"
900+
);
901+
}
902+
if !measure_acpi_tables && !uses_lite {
903+
bail!(
904+
"unsupported TDX attestation mode: requirements.tdx_measure_acpi_tables=false requires TDX lite attestation"
905+
);
906+
}
907+
info!(
908+
"tdx ACPI table measurement requirement satisfied: measure_acpi_tables={}",
909+
measure_acpi_tables
910+
);
911+
Ok(())
912+
}
913+
870914
fn read_current_os_version() -> Result<String> {
871915
const OS_RELEASE_PATHS: &[&str] = &["/etc/os-release", "/usr/lib/os-release"];
872916
for path in OS_RELEASE_PATHS {
@@ -930,7 +974,7 @@ pub async fn cmd_sys_setup(args: SetupArgs) -> Result<()> {
930974
}
931975

932976
async fn do_sys_setup(stage0: Stage0<'_>) -> Result<()> {
933-
verify_app_compose_policy(&stage0.shared.app_compose)
977+
verify_app_compose_policy(&stage0.shared.app_compose, &stage0.shared.sys_config)
934978
.context("Failed to verify app-compose policy")?;
935979
if stage0.shared.app_compose.secure_time {
936980
info!("Waiting for the system time to be synchronized");
@@ -2198,6 +2242,70 @@ fn test_platform_requirements_reject_invalid_platform_value() {
21982242
.contains("Invalid requirements.platforms[0]"));
21992243
}
22002244

2245+
#[test]
2246+
fn test_tdx_measure_acpi_tables_requirement_matches_vm_config() {
2247+
let app_compose: AppCompose = serde_json::from_value(serde_json::json!({
2248+
"manifest_version": "3",
2249+
"name": "test",
2250+
"runner": "docker-compose",
2251+
"requirements": {
2252+
"tdx_measure_acpi_tables": true
2253+
}
2254+
}))
2255+
.unwrap();
2256+
verify_tdx_measure_acpi_tables_requirement(&app_compose, r#"{}"#, AttestationMode::DstackTdx)
2257+
.unwrap();
2258+
let err = verify_tdx_measure_acpi_tables_requirement(
2259+
&app_compose,
2260+
r#"{"tdx_attestation_variant":"lite"}"#,
2261+
AttestationMode::DstackTdx,
2262+
)
2263+
.unwrap_err();
2264+
assert!(err.to_string().contains("tdx_measure_acpi_tables=true"));
2265+
2266+
let app_compose: AppCompose = serde_json::from_value(serde_json::json!({
2267+
"manifest_version": "3",
2268+
"name": "test",
2269+
"runner": "docker-compose",
2270+
"requirements": {
2271+
"tdx_measure_acpi_tables": false
2272+
}
2273+
}))
2274+
.unwrap();
2275+
verify_tdx_measure_acpi_tables_requirement(
2276+
&app_compose,
2277+
r#"{"tdx_attestation_variant":"lite"}"#,
2278+
AttestationMode::DstackTdx,
2279+
)
2280+
.unwrap();
2281+
let err = verify_tdx_measure_acpi_tables_requirement(
2282+
&app_compose,
2283+
r#"{}"#,
2284+
AttestationMode::DstackTdx,
2285+
)
2286+
.unwrap_err();
2287+
assert!(err.to_string().contains("tdx_measure_acpi_tables=false"));
2288+
}
2289+
2290+
#[test]
2291+
fn test_tdx_measure_acpi_tables_requirement_ignored_on_non_tdx() {
2292+
let app_compose: AppCompose = serde_json::from_value(serde_json::json!({
2293+
"manifest_version": "3",
2294+
"name": "test",
2295+
"runner": "docker-compose",
2296+
"requirements": {
2297+
"tdx_measure_acpi_tables": true
2298+
}
2299+
}))
2300+
.unwrap();
2301+
verify_tdx_measure_acpi_tables_requirement(
2302+
&app_compose,
2303+
r#"{"tdx_attestation_variant":"lite"}"#,
2304+
AttestationMode::DstackAmdSevSnp,
2305+
)
2306+
.unwrap();
2307+
}
2308+
22012309
#[test]
22022310
fn test_os_release_value_parses_quoted_version_id() {
22032311
let content = r#"

sdk/go/dstack/compose_hash.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ const (
3939

4040
// Requirements represents guest-side requirements.
4141
type Requirements struct {
42-
OsVersion string `json:"os_version,omitempty"`
43-
Platforms *[]RequirementPlatform `json:"platforms,omitempty"`
42+
OsVersion string `json:"os_version,omitempty"`
43+
Platforms *[]RequirementPlatform `json:"platforms,omitempty"`
44+
TdxMeasureAcpiTables *bool `json:"tdx_measure_acpi_tables,omitempty"`
4445
}
4546

4647
// AppCompose represents the application composition structure

sdk/js/src/get-compose-hash.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface DockerConfig extends SortableObject {
5151
export interface Requirements extends SortableObject {
5252
os_version?: string;
5353
platforms?: RequirementPlatform[];
54+
tdx_measure_acpi_tables?: boolean;
5455
}
5556

5657
export interface AppCompose extends SortableObject {

sdk/python/src/dstack_sdk/get_compose_hash.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ def __init__(
5353
self,
5454
os_version: Optional[str] = None,
5555
platforms: Optional[List[str]] = None,
56+
tdx_measure_acpi_tables: Optional[bool] = None,
5657
) -> None:
5758
"""Initialize a new ``Requirements`` instance."""
5859
self.os_version = os_version
5960
self.platforms = platforms
61+
self.tdx_measure_acpi_tables = tdx_measure_acpi_tables
6062

6163
def to_dict(self) -> Dict[str, Any]:
6264
"""Return a dictionary representation excluding ``None`` fields."""
@@ -65,6 +67,8 @@ def to_dict(self) -> Dict[str, Any]:
6567
result["os_version"] = self.os_version
6668
if self.platforms is not None:
6769
result["platforms"] = self.platforms
70+
if self.tdx_measure_acpi_tables is not None:
71+
result["tdx_measure_acpi_tables"] = self.tdx_measure_acpi_tables
6872
return result
6973

7074

0 commit comments

Comments
 (0)