Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/security/security-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,23 @@ services:
image: nginx@sha256:eee5eae48e79b2e75178328c7c585b89d676eaae616f03f9a1813aaed820745a
network_mode: host
```
## Runtime event-log V2 policies

Event-log V2 exposes canonical digest pre-images so a relying party can check
individual claims such as `compose-hash`. Verifying
`sha384(preimage) == digest` proves only that those bytes participate in the
quoted RTMR/PCR extension chain. It does **not** prove that trusted dstack boot
code originated the event name: privileged code inside the CVM can append
additional measured events after boot.

Policies that trust a named V2 event must therefore also validate ordering and
the boot boundary. In particular, select the expected claim before
`boot-mr-done`/`system-ready`, reject duplicate trusted claim names, and replay
the complete quoted chain. Never accept an arbitrary later event solely because
its digest matches its supplied pre-image.

V2 is a coordinated upgrade. Upgrade every KMS, gateway, verifier, and other
relying party before enabling `event_log_version: 2`; older verifiers interpret
runtime events as V1 and reject the quote. Older guest images may ignore the
compose field and emit V1 events, so confirm that the selected image advertises
V2 support before relying on per-event claims.
7 changes: 7 additions & 0 deletions dstack/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ regorus = { version = "0.10.1", default-features = false, features = ["full-opa"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
safe-write = "0.1.2"
rustix = { version = "0.38", features = ["fs"] }
nix = "0.29.0"
sd-notify = "0.4.5"
listenfd = "1.0"
Expand Down
3 changes: 3 additions & 0 deletions dstack/cc-eventlog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ license.workspace = true
[dependencies]
anyhow.workspace = true
digest = "0.10.7"
dstack-types.workspace = true
ez-hash.workspace = true
fs-err.workspace = true
hex.workspace = true
or-panic.workspace = true
scale.workspace = true
serde.workspace = true
serde-human-bytes.workspace = true
serde_jcs = "0.2.0"
serde_json = { workspace = true, features = ["alloc"] }
sha2.workspace = true

Expand Down
69 changes: 67 additions & 2 deletions dstack/cc-eventlog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
//
// SPDX-License-Identifier: Apache-2.0

pub use runtime_events::{replay_events, RuntimeEvent};
pub use dstack_types::EventLogVersion;
pub use runtime_events::{
canonical_event_json_v2, replay_events, RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE,
};
pub use tdx::TdxEvent;

mod codecs;
mod runtime_events;
mod tcg;
pub mod tcg;
pub mod tdx;
pub mod tpm;

#[cfg(test)]
mod tests {
use super::*;
use dstack_types::EventLogVersion;
use ez_hash::{Hasher, Sha384};
use tdx::TdxEvent;

#[test]
fn parse_ccel() {
Expand All @@ -24,4 +30,63 @@ mod tests {
let json = serde_json::to_string_pretty(&tdx_event_logs).unwrap();
insta::assert_snapshot!(json);
}

#[test]
fn encode_runtime_events_roundtrip() {
// Synthesize a CCEL: sample boot-time bytes + encoded runtime events.
// Decode with the same TcgEventLog parser and verify:
// 1. all boot-time events are preserved
// 2. runtime events appear with the expected pcrIndex / event_type / digest
// 3. sha384(event_data) == digest (the property we document)
let boot_raw = include_bytes!("../samples/ccel.bin");
let valid = tcg::ccel_content_len(boot_raw).unwrap();

let runtime_v1: TdxEvent = RuntimeEvent::new(
"app-id".to_string(),
vec![0xaa, 0xbb, 0xcc],
EventLogVersion::V1,
)
.into();
let runtime_v2: TdxEvent = RuntimeEvent::new(
"compose-hash".to_string(),
vec![0xde, 0xad, 0xbe, 0xef],
EventLogVersion::V2,
)
.into();
let runtime_events = vec![runtime_v1.clone(), runtime_v2.clone()];

let mut merged = boot_raw[..valid].to_vec();
merged.extend_from_slice(&tcg::encode_runtime_events_as_tcg(&runtime_events));
merged.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());

let parsed = tcg::TcgEventLog::decode(&mut merged.as_slice()).unwrap();
let boot_count = tcg::TcgEventLog::decode(&mut boot_raw.as_slice())
.unwrap()
.event_logs
.len();
assert_eq!(parsed.event_logs.len(), boot_count + 2);

// Convert and verify runtime tail matches what we put in.
let converted = parsed.to_cc_event_log().unwrap();
let tail = &converted[converted.len() - 2..];
for (orig, got) in runtime_events.iter().zip(tail.iter()) {
assert_eq!(got.imr, orig.imr);
assert_eq!(got.event_type, orig.event_type);
assert_eq!(got.digest, orig.digest());
// The event_payload carried in TdxEvent after TCG round-trip is the
// preimage bytes we stored as TCG event data.
let runtime = orig.to_runtime_event().unwrap();
assert_eq!(got.event_payload, runtime.preimage());
assert_eq!(
got.preimage.as_deref(),
Some(hex::encode(&got.event_payload).as_str())
);
// Decoded CCEL records do not retain the structured dstack event
// tuple, so digest() must use the authoritative stored digest.
assert_eq!(got.digest(), got.digest);
// Property: sha384(event_data) == digest
let h = Sha384::hash([got.event_payload.as_slice()]);
assert_eq!(h.as_slice(), got.digest.as_slice());
}
}
}
Loading
Loading