Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 69a5e6c

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

14 files changed

+153-63Lines changed: 153 additions & 63 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/security/security-best-practices.md‎

Copy file name to clipboardExpand all lines: docs/security/security-best-practices.md
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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.
Collapse file

‎dstack/cc-eventlog/src/tcg.rs‎

Copy file name to clipboardExpand all lines: dstack/cc-eventlog/src/tcg.rs
+16Lines 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::*;
Collapse file

‎dstack/cc-eventlog/src/tdx.rs‎

Copy file name to clipboardExpand all lines: dstack/cc-eventlog/src/tdx.rs
+12-20Lines 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-
}
Collapse file

‎dstack/dstack-attest/src/attestation.rs‎

Copy file name to clipboardExpand all lines: dstack/dstack-attest/src/attestation.rs
+4-16Lines 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

Collapse file

‎dstack/dstack-attest/src/lib.rs‎

Copy file name to clipboardExpand all lines: dstack/dstack-attest/src/lib.rs
+52-6Lines 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
Collapse file

‎dstack/dstack-types/src/lib.rs‎

Copy file name to clipboardExpand all lines: dstack/dstack-types/src/lib.rs
+26-1Lines 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!({
Collapse file

‎dstack/dstack-util/src/system_setup.rs‎

Copy file name to clipboardExpand all lines: dstack/dstack-util/src/system_setup.rs
+2-4Lines 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")?;
Collapse file

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

Copy file name to clipboardExpand all lines: dstack/guest-agent-simulator/src/simulator.rs
+1-5Lines 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()?,
Collapse file

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

Copy file name to clipboardExpand all lines: dstack/guest-agent/rpc/proto/agent_rpc.proto
+3-3Lines 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;
Collapse file

‎dstack/guest-agent/src/backend.rs‎

Copy file name to clipboardExpand all lines: dstack/guest-agent/src/backend.rs
+22-7Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.