Skip to content

Commit 69a5e6c

Browse files
committed
fix: address event log v2 follow-up review
1 parent 8aaa821 commit 69a5e6c

14 files changed

Lines changed: 153 additions & 63 deletions

File tree

docs/security/security-best-practices.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,9 @@ the boot boundary. In particular, select the expected claim before
176176
`boot-mr-done`/`system-ready`, reject duplicate trusted claim names, and replay
177177
the complete quoted chain. Never accept an arbitrary later event solely because
178178
its digest matches its supplied pre-image.
179+
180+
V2 is a coordinated upgrade. Upgrade every KMS, gateway, verifier, and other
181+
relying party before enabling `event_log_version: 2`; older verifiers interpret
182+
runtime events as V1 and reject the quote. Older guest images may ignore the
183+
compose field and emit V1 events, so confirm that the selected image advertises
184+
V2 support before relying on per-event claims.

dstack/cc-eventlog/src/tcg.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,22 @@ impl TryFrom<TcgEvent> for TdxEvent {
448448
}
449449
}
450450

451+
/// Build a merged TCG binary event log: raw ACPI CCEL (boot-time) followed by
452+
/// the given runtime events encoded as TCG_PCR_EVENT2 records.
453+
///
454+
/// Non-runtime entries in `events` are ignored; only events with
455+
/// `event_type == DSTACK_RUNTIME_EVENT_TYPE` are appended.
456+
pub fn build_ccel_event_log(events: &[TdxEvent]) -> Result<Vec<u8>> {
457+
let raw = read_ccel_raw()?;
458+
let end = ccel_content_len(&raw)?;
459+
let mut out = raw[..end].to_vec();
460+
out.extend_from_slice(&encode_runtime_events_as_tcg(events));
461+
// Append the 0xFFFFFFFF terminator so parsers know where the event
462+
// stream ends (the original ACPI table has trailing 0xFF padding).
463+
out.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
464+
Ok(out)
465+
}
466+
451467
#[cfg(test)]
452468
mod tests {
453469
use super::*;

dstack/cc-eventlog/src/tdx.rs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ pub struct TdxEvent {
5555

5656
/// Optional digest pre-image, hex-encoded.
5757
///
58-
/// The exact bytes hashed to produce `digest`. Only populated when
59-
/// explicitly requested (e.g., via RPC opt-in) so that relying parties can
60-
/// verify the digest computation or inspect v2 JSON content without
61-
/// knowing the dstack schema.
58+
/// The exact bytes hashed to produce `digest`. V2 events exposed through
59+
/// quote and attestation APIs always include it, allowing relying parties
60+
/// to verify `sha384(hex_decode(preimage)) == digest`.
6261
/// Never included in scale encoding (derivable from other fields).
6362
#[serde(default, skip_serializing_if = "Option::is_none")]
6463
#[codec(skip)]
@@ -167,6 +166,15 @@ impl From<RuntimeEvent> for TdxEvent {
167166
}
168167
}
169168

169+
/// Populate digest preimages for all V2 runtime events.
170+
pub fn fill_v2_preimages(events: &mut [TdxEvent]) {
171+
for event in events {
172+
if matches!(event.version, EventLogVersion::V2) {
173+
event.fill_preimage();
174+
}
175+
}
176+
}
177+
170178
pub fn is_tdx_acpi_data_event(event: &TdxEvent) -> bool {
171179
event.imr == 0
172180
&& event.event_type == TDX_ACPI_DATA_EVENT_TYPE
@@ -319,19 +327,3 @@ mod tests {
319327
assert!(!json.contains("preimage"));
320328
}
321329
}
322-
323-
/// Build a merged TCG binary event log: raw ACPI CCEL (boot-time) followed by
324-
/// the given runtime events encoded as TCG_PCR_EVENT2 records.
325-
///
326-
/// Non-runtime entries in `events` are ignored; only events with
327-
/// `event_type == DSTACK_RUNTIME_EVENT_TYPE` are appended.
328-
pub fn build_ccel_event_log(events: &[TdxEvent]) -> Result<Vec<u8>> {
329-
let raw = crate::tcg::read_ccel_raw()?;
330-
let end = crate::tcg::ccel_content_len(&raw)?;
331-
let mut out = raw[..end].to_vec();
332-
out.extend_from_slice(&crate::tcg::encode_runtime_events_as_tcg(events));
333-
// Append the 0xFFFFFFFF terminator so parsers know where the event
334-
// stream ends (the original ACPI table has trailing 0xFF padding).
335-
out.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
336-
Ok(out)
337-
}

dstack/dstack-attest/src/attestation.rs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -817,11 +817,7 @@ pub trait TdxAttestationExt {
817817
fn tdx_event_log_string(&self) -> Option<String> {
818818
self.tdx_event_log().map(|event_log| {
819819
let mut events: Vec<TdxEvent> = event_log.to_vec();
820-
for event in &mut events {
821-
if matches!(event.version, EventLogVersion::V2) {
822-
event.fill_preimage();
823-
}
824-
}
820+
cc_eventlog::tdx::fill_v2_preimages(&mut events);
825821
serde_json::to_string(&events).unwrap_or_default()
826822
})
827823
}
@@ -1449,11 +1445,7 @@ impl<T> Attestation<T> {
14491445
/// digest pre-images alongside events.
14501446
pub fn fill_event_preimages(&mut self) {
14511447
if let Some(q) = self.tdx_quote_mut() {
1452-
for event in &mut q.event_log {
1453-
if matches!(event.version, EventLogVersion::V2) {
1454-
event.fill_preimage();
1455-
}
1456-
}
1448+
cc_eventlog::tdx::fill_v2_preimages(&mut q.event_log);
14571449
}
14581450
}
14591451

@@ -1481,11 +1473,7 @@ impl<T> Attestation<T> {
14811473
stripped
14821474
})
14831475
.collect();
1484-
for event in &mut stripped {
1485-
if matches!(event.version, EventLogVersion::V2) {
1486-
event.fill_preimage();
1487-
}
1488-
}
1476+
cc_eventlog::tdx::fill_v2_preimages(&mut stripped);
14891477
serde_json::to_string(&stripped).unwrap_or_default()
14901478
})
14911479
}
@@ -1505,7 +1493,7 @@ impl<T> Attestation<T> {
15051493
let Some(q) = self.tdx_quote() else {
15061494
return Ok(Vec::new());
15071495
};
1508-
cc_eventlog::tdx::build_ccel_event_log(&q.event_log)
1496+
cc_eventlog::tcg::build_ccel_event_log(&q.event_log)
15091497
}
15101498
}
15111499

dstack/dstack-attest/src/lib.rs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,20 @@ fn runtime_event_lock() -> anyhow::Result<fs_err::File> {
4343
/// processes share it. This must be called before [`emit_runtime_event`].
4444
/// Repeating the same configuration is allowed; changing it is rejected.
4545
pub fn set_runtime_event_version(version: EventLogVersion) -> anyhow::Result<()> {
46+
let _lock = runtime_event_lock()?;
47+
set_runtime_event_version_file(RUNTIME_EVENT_VERSION_FILE, version)
48+
}
49+
50+
fn set_runtime_event_version_file(
51+
path: impl AsRef<std::path::Path>,
52+
version: EventLogVersion,
53+
) -> anyhow::Result<()> {
54+
let path = path.as_ref();
4655
let value = match version {
4756
EventLogVersion::V1 => "1",
4857
EventLogVersion::V2 => "2",
4958
};
50-
let _lock = runtime_event_lock()?;
51-
match fs_err::read_to_string(RUNTIME_EVENT_VERSION_FILE) {
59+
match fs_err::read_to_string(path) {
5260
Ok(configured) => {
5361
anyhow::ensure!(
5462
configured.trim() == value,
@@ -58,24 +66,62 @@ pub fn set_runtime_event_version(version: EventLogVersion) -> anyhow::Result<()>
5866
Ok(())
5967
}
6068
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
61-
safe_write::safe_write(RUNTIME_EVENT_VERSION_FILE, value.as_bytes())
69+
safe_write::safe_write(path, value.as_bytes())
6270
.context("failed to write runtime event version")
6371
}
6472
Err(err) => Err(err).context("failed to read runtime event version"),
6573
}
6674
}
6775

6876
fn runtime_event_version() -> anyhow::Result<EventLogVersion> {
69-
let value = fs_err::read_to_string(RUNTIME_EVENT_VERSION_FILE)
70-
.context("runtime event version is not configured")?;
77+
runtime_event_version_file(RUNTIME_EVENT_VERSION_FILE)
78+
}
79+
80+
fn runtime_event_version_file(
81+
path: impl AsRef<std::path::Path>,
82+
) -> anyhow::Result<EventLogVersion> {
83+
let value = fs_err::read_to_string(path).context(
84+
"runtime event version is not configured; complete dstack system setup before emitting events",
85+
)?;
7186
match value.trim() {
7287
"1" => Ok(EventLogVersion::V1),
7388
"2" => Ok(EventLogVersion::V2),
7489
value => anyhow::bail!("invalid runtime event version: {value}"),
7590
}
7691
}
7792

78-
/// Emit a dstack measured event using the legacy V1 digest format.
93+
#[cfg(test)]
94+
mod runtime_event_version_tests {
95+
use super::*;
96+
97+
fn temp_path(name: &str) -> std::path::PathBuf {
98+
std::env::temp_dir().join(format!("dstack-{name}-{}", std::process::id()))
99+
}
100+
101+
#[test]
102+
fn rejects_conflicting_runtime_event_version() {
103+
let path = temp_path("event-version-conflict");
104+
let _ = fs_err::remove_file(&path);
105+
set_runtime_event_version_file(&path, EventLogVersion::V1).unwrap();
106+
set_runtime_event_version_file(&path, EventLogVersion::V1).unwrap();
107+
let err = set_runtime_event_version_file(&path, EventLogVersion::V2).unwrap_err();
108+
assert!(err.to_string().contains("already set to 1"));
109+
let _ = fs_err::remove_file(path);
110+
}
111+
112+
#[test]
113+
fn reports_unconfigured_runtime_event_version() {
114+
let path = temp_path("event-version-missing");
115+
let _ = fs_err::remove_file(&path);
116+
let err = runtime_event_version_file(path).unwrap_err();
117+
assert!(err.to_string().contains("complete dstack system setup"));
118+
}
119+
}
120+
121+
/// Emit a dstack measured event using the system-configured digest format.
122+
///
123+
/// The event-log append and platform-register extension are serialized by a
124+
/// system-wide file lock so their ordering cannot diverge across processes.
79125
///
80126
/// - TDX-family: RTMR3
81127
/// - GCP TPM: SHA256 PCR14

dstack/dstack-types/src/lib.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ pub enum EventLogVersion {
8686
}
8787

8888
impl EventLogVersion {
89+
pub fn is_v1(&self) -> bool {
90+
matches!(self, Self::V1)
91+
}
92+
8993
pub fn from_u32(v: u32) -> Option<Self> {
9094
match v {
9195
1 => Some(EventLogVersion::V1),
@@ -153,7 +157,7 @@ pub struct AppCompose {
153157
pub storage_fs: Option<String>,
154158
#[serde(default, with = "human_size")]
155159
pub swap_size: u64,
156-
#[serde(default)]
160+
#[serde(default, skip_serializing_if = "EventLogVersion::is_v1")]
157161
pub event_log_version: EventLogVersion,
158162
/// Per-port policy consumed by the gateway (PROXY protocol opt-in,
159163
/// optional port whitelist).
@@ -571,6 +575,27 @@ mod app_compose_tests {
571575
assert_eq!(compose.manifest_version_u32(), Some(3));
572576
}
573577

578+
#[test]
579+
fn event_log_v1_is_omitted_but_v2_is_serialized() {
580+
#[derive(Serialize)]
581+
struct VersionField {
582+
#[serde(skip_serializing_if = "EventLogVersion::is_v1")]
583+
event_log_version: EventLogVersion,
584+
}
585+
586+
let v1 = serde_json::to_value(VersionField {
587+
event_log_version: EventLogVersion::V1,
588+
})
589+
.unwrap();
590+
assert!(v1.get("event_log_version").is_none());
591+
592+
let v2 = serde_json::to_value(VersionField {
593+
event_log_version: EventLogVersion::V2,
594+
})
595+
.unwrap();
596+
assert_eq!(v2["event_log_version"], 2);
597+
}
598+
574599
#[test]
575600
fn parses_supported_container_snapshotters() {
576601
let compose: AppCompose = serde_json::from_value(serde_json::json!({

dstack/dstack-util/src/system_setup.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,8 +2100,7 @@ impl<'a> Stage0<'a> {
21002100
.tls_client_key(cert_pair.key_pem)
21012101
.tls_ca_cert(tmp_ca.ca_cert.clone())
21022102
.attestation_verifier(attestation_verifier)
2103-
.cert_validator({
2104-
Box::new(move |cert| {
2103+
.cert_validator(Box::new(|cert| {
21052104
let Some(cert) = cert else {
21062105
bail!("Missing server cert");
21072106
};
@@ -2122,8 +2121,7 @@ impl<'a> Stage0<'a> {
21222121
}
21232122
}
21242123
Ok(())
2125-
})
2126-
})
2124+
}))
21272125
.build()
21282126
.into_client()
21292127
.context("Failed to create client")?;

dstack/guest-agent-simulator/src/simulator.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ pub fn simulated_attest_response(
5757
let mut attestation =
5858
maybe_patch_report_data(attestation, report_data, patch_report_data, "attest");
5959
if let Some(event_log) = attestation.platform.tdx_event_log_mut() {
60-
for event in event_log {
61-
if matches!(event.version, dstack_types::EventLogVersion::V2) {
62-
event.fill_preimage();
63-
}
64-
}
60+
cc_eventlog::tdx::fill_v2_preimages(event_log);
6561
}
6662
Ok(AttestResponse {
6763
attestation: VersionedAttestation::V1 { attestation }.to_bytes()?,

dstack/guest-agent/rpc/proto/agent_rpc.proto

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,6 @@ message TdxQuoteArgs {
170170
message RawQuoteArgs {
171171
// 64 bytes of report data
172172
bytes report_data = 1;
173-
// V2 runtime events always include their hex-encoded digest preimage in the
174-
// returned event log. Clients verify sha384(hex_decode(preimage)) == digest.
175173
}
176174

177175
message TdxQuoteResponse {
@@ -201,7 +199,9 @@ message GpuInfoResponse {
201199
message GetQuoteResponse {
202200
// TDX quote (empty on non-TDX platforms such as AMD SEV-SNP)
203201
bytes quote = 1;
204-
// Event log (empty on non-TDX platforms)
202+
// Event log (empty on non-TDX platforms). V2 runtime events always include
203+
// their hex-encoded digest preimage. Clients should verify
204+
// sha384(hex_decode(preimage)) == digest.
205205
string event_log = 2;
206206
// Report data
207207
bytes report_data = 3;

dstack/guest-agent/src/backend.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ use ra_tls::attestation::Attestation;
88
use ra_tls::attestation::{QuoteContentType, VersionedAttestation};
99
use tracing::warn;
1010

11+
fn event_log_ccel_or_empty(result: Result<Vec<u8>>) -> Vec<u8> {
12+
match result {
13+
Ok(event_log) => event_log,
14+
Err(err) => {
15+
warn!(error = ?err, "failed to build TDX CCEL event log");
16+
Vec::new()
17+
}
18+
}
19+
}
20+
1121
pub trait PlatformBackend: Send + Sync {
1222
fn attestation_for_info(&self) -> Result<VersionedAttestation>;
1323
fn certificate_attestation(&self, pubkey: &[u8]) -> Result<VersionedAttestation>;
@@ -36,13 +46,7 @@ impl PlatformBackend for RealPlatform {
3646
let attestation = Attestation::quote(&report_data).context("Failed to get quote")?;
3747
let tdx_quote = attestation.get_tdx_quote_bytes();
3848
let tdx_event_log = attestation.get_tdx_event_log_string();
39-
let event_log_ccel = match attestation.get_tdx_event_log_ccel() {
40-
Ok(event_log) => event_log,
41-
Err(err) => {
42-
warn!(error = ?err, "failed to build TDX CCEL event log");
43-
Vec::new()
44-
}
45-
};
49+
let event_log_ccel = event_log_ccel_or_empty(attestation.get_tdx_event_log_ccel());
4650
let versioned = if tdx_quote.is_some() {
4751
Vec::new()
4852
} else {
@@ -70,3 +74,14 @@ impl PlatformBackend for RealPlatform {
7074
})
7175
}
7276
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use super::*;
81+
82+
#[test]
83+
fn ccel_export_failure_is_fail_open() {
84+
let event_log = event_log_ccel_or_empty(Err(anyhow::anyhow!("CCEL unavailable")));
85+
assert!(event_log.is_empty());
86+
}
87+
}

0 commit comments

Comments
 (0)