diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index 5fabb8176..14b1e036c 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -870,6 +870,40 @@ impl TdxAttestationExt for AttestationV1 { } impl AttestationV1 { + /// Convert a V1 dstack attestation back to the legacy SCALE schema. + /// + /// This is only lossless for the original dstack stack with V1 runtime + /// events. Pod payloads and newer event encodings must remain on the V1 + /// msgpack wire format. + pub fn try_into_legacy(self) -> Result { + let Self { + platform, stack, .. + } = self; + let StackEvidence::Dstack { + report_data, + runtime_events, + config, + } = stack + else { + bail!("dstack-pod attestation cannot be represented by the legacy schema"); + }; + if runtime_events + .iter() + .any(|event| !matches!(event.version, EventLogVersion::V1)) + { + bail!("non-V1 runtime events cannot be represented by the legacy schema"); + } + Ok(Attestation { + quote: platform_into_legacy_quote(platform), + runtime_events, + report_data: report_data + .try_into() + .map_err(|_| anyhow!("stack.report_data must be 64 bytes"))?, + config, + report: (), + }) + } + /// Decode the VM config from the external or embedded config. pub fn decode_vm_config<'a>(&'a self, config: &'a str) -> Result { decode_vm_config_with_fallback(config, self.stack.config()) @@ -2819,6 +2853,40 @@ mod tests { assert!(matches!(upgraded.stack, StackEvidence::Dstack { .. })); } + #[test] + fn v1_dstack_with_v1_events_converts_losslessly_to_legacy() { + let mut legacy = dummy_tdx_attestation([0x5a; 64]); + legacy.runtime_events.push(cc_eventlog::RuntimeEvent::new( + "legacy-event".into(), + vec![1, 2, 3], + cc_eventlog::EventLogVersion::V1, + )); + let converted = legacy.clone().into_v1().try_into_legacy().unwrap(); + assert_eq!(converted.report_data, legacy.report_data); + assert_eq!(converted.runtime_events.len(), 1); + assert!(matches!( + converted.into_versioned(), + VersionedAttestation::V0 { .. } + )); + } + + #[test] + fn v1_conversion_rejects_lossy_legacy_projection() { + let pod = dummy_tdx_attestation([0x5b; 64]) + .into_v1() + .into_dstack_pod("payload".into()); + assert!(pod.try_into_legacy().is_err()); + let mut v2 = dummy_tdx_attestation([0x5c; 64]).into_v1(); + if let StackEvidence::Dstack { runtime_events, .. } = &mut v2.stack { + runtime_events.push(cc_eventlog::RuntimeEvent::new( + "v2-event".into(), + vec![4, 5, 6], + cc_eventlog::EventLogVersion::V2, + )); + } + assert!(v2.try_into_legacy().is_err()); + } + #[test] fn versioned_v0_projects_to_v1() { let projected = dummy_tdx_attestation([5u8; 64]).into_versioned().into_v1(); diff --git a/dstack/guest-agent-simulator/src/main.rs b/dstack/guest-agent-simulator/src/main.rs index 30456f3a0..e8974f8b0 100644 --- a/dstack/guest-agent-simulator/src/main.rs +++ b/dstack/guest-agent-simulator/src/main.rs @@ -181,10 +181,11 @@ mod tests { } #[test] - fn simulator_attest_response_uses_supplied_report_data() { + fn simulator_attest_response_preserves_legacy_wire_format() { let platform = load_fixture_platform(); let report_data = [0x5a; 64]; let response = platform.attest_response(report_data).unwrap(); + assert_eq!(response.attestation.first(), Some(&0x00)); let patched = VersionedAttestation::from_bytes(&response.attestation) .unwrap() .into_v1(); diff --git a/dstack/guest-agent-simulator/src/simulator.rs b/dstack/guest-agent-simulator/src/simulator.rs index f7fc6a91b..c0288cc73 100644 --- a/dstack/guest-agent-simulator/src/simulator.rs +++ b/dstack/guest-agent-simulator/src/simulator.rs @@ -58,23 +58,24 @@ pub fn simulated_quote_response( } pub fn simulated_attest_response( - attestation: &VersionedAttestation, + source: &VersionedAttestation, report_data: [u8; 64], patch_report_data: bool, generator: Option<&TdxGenerator>, ) -> Result { - let mut attestation = prepare_attestation( - attestation, - report_data, - patch_report_data, - generator, - "attest", - )?; + let preserve_legacy = matches!(source, VersionedAttestation::V0 { .. }); + let mut attestation = + prepare_attestation(source, report_data, patch_report_data, generator, "attest")?; if let Some(event_log) = attestation.platform.tdx_event_log_mut() { cc_eventlog::tdx::fill_v2_preimages(event_log); } + let attestation = if preserve_legacy { + attestation.try_into_legacy()?.into_versioned() + } else { + VersionedAttestation::V1 { attestation } + }; Ok(AttestResponse { - attestation: VersionedAttestation::V1 { attestation }.to_bytes()?, + attestation: attestation.to_bytes()?, }) } @@ -83,19 +84,23 @@ pub fn simulated_info_attestation(attestation: &VersionedAttestation) -> Version } pub fn simulated_certificate_attestation( - attestation: &VersionedAttestation, + source: &VersionedAttestation, pubkey: &[u8], patch_report_data: bool, generator: Option<&TdxGenerator>, ) -> Result { + let preserve_legacy = matches!(source, VersionedAttestation::V0 { .. }); let report_data = QuoteContentType::RaTlsCert.to_report_data(pubkey); let attestation = prepare_attestation( - attestation, + source, report_data, patch_report_data, generator, "certificate_attestation", )?; + if preserve_legacy { + return Ok(attestation.try_into_legacy()?.into_versioned()); + } Ok(VersionedAttestation::V1 { attestation }) }