diff --git a/dstack/cc-eventlog/src/tdx.rs b/dstack/cc-eventlog/src/tdx.rs index 98682ea7e..d4bd94e49 100644 --- a/dstack/cc-eventlog/src/tdx.rs +++ b/dstack/cc-eventlog/src/tdx.rs @@ -85,7 +85,7 @@ impl TdxEvent { Self { imr: self.imr, event_type: self.event_type, - digest: Vec::new(), + digest: self.digest.clone(), event: self.event.clone(), event_payload: self.event_payload.clone(), version: self.version, @@ -322,6 +322,17 @@ mod tests { assert_eq!(actual.as_slice(), &tdx.digest); } + #[test] + fn stripped_v2_runtime_event_preserves_digest_binding() { + let mut event = v2_event(); + event.fill_preimage(); + + let stripped = event.stripped(); + + assert_eq!(stripped.digest, event.digest); + validate_v2_preimages(&[stripped]).expect("stripped V2 event remains verifiable"); + } + #[test] fn fill_preimage_skips_non_runtime_events() { let mut boot_event = TdxEvent::new(0, 0x1, "EV_POST_CODE".to_string(), vec![1, 2, 3]); diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index 7486fa0ea..e55d63827 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -2414,7 +2414,11 @@ impl Attestation { /// msgpack wire format so the `version` field is preserved (SCALE /// V0 skips it for legacy binary compat). Otherwise default to V0 for /// backward compat with callers that expect the SCALE format. - pub fn into_versioned(self) -> VersionedAttestation { + pub fn into_versioned(mut self) -> VersionedAttestation { + // V2 event digests cannot be reconstructed from the serialized event + // fields alone. Populate their canonical preimages before the legacy + // quote is projected into either wire schema. + self.fill_event_preimages(); let has_v2 = self .runtime_events .iter() @@ -2990,6 +2994,17 @@ mod tests { #[test] fn into_versioned_upgrades_to_v1_when_any_event_is_v2() { let mut att = dummy_tdx_attestation([8u8; 64]); + let AttestationQuote::DstackTdx(tdx_quote) = &mut att.quote else { + panic!("expected TDX attestation"); + }; + tdx_quote.event_log.push( + cc_eventlog::RuntimeEvent::new( + "compose-hash".into(), + vec![4, 5, 6], + cc_eventlog::EventLogVersion::V2, + ) + .into(), + ); att.runtime_events.push(cc_eventlog::RuntimeEvent::new( "app-id".into(), vec![1, 2, 3], @@ -3000,11 +3015,22 @@ mod tests { vec![4, 5, 6], cc_eventlog::EventLogVersion::V2, )); - let versioned = att.into_versioned(); + // RA-TLS certificates use the stripped representation. Its runtime + // events must retain the advertised digest paired with each preimage. + let encoded = att.into_versioned().into_stripped().to_bytes().unwrap(); + let VersionedAttestation::V1 { attestation } = + VersionedAttestation::from_bytes(&encoded).unwrap() + else { + panic!("presence of a V2 event must force the V1 msgpack wire format"); + }; + let PlatformEvidence::Tdx { event_log, .. } = attestation.platform else { + panic!("expected TDX platform evidence"); + }; assert!( - matches!(versioned, VersionedAttestation::V1 { .. }), - "presence of a V2 event must force the V1 msgpack wire format to preserve `version`" + event_log[0].preimage.is_some(), + "serialized V2 TDX events must carry their canonical digest preimage" ); + cc_eventlog::tdx::validate_v2_preimages(&event_log).unwrap(); } fn v1_event(event: String, payload: Vec) -> RuntimeEvent { RuntimeEvent::new(event, payload, EventLogVersion::V1)