Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
654021a
Add event log v2 format with JSON canonical digest (RFC 8785)
kvinwang Apr 16, 2026
660e0e3
refactor: use EventLogVersion enum instead of raw u32
kvinwang Apr 16, 2026
32241ad
fix: reject unknown event_log_version values instead of silent fallback
kvinwang Apr 16, 2026
4cf928c
fix: address CI failures and PR review comments
kvinwang Apr 16, 2026
96ce9d4
test: add mixed v1/v2 replay and scale round-trip tests
kvinwang Apr 16, 2026
f553c11
chore: remove accidentally committed worktree
kvinwang Apr 16, 2026
2b31c5d
refactor: use BTreeMap for canonical JSON key ordering
kvinwang Apr 16, 2026
4712710
chore: gitignore worktrees directory
kvinwang Apr 16, 2026
9e761b8
refactor: use serde_jcs for RFC 8785 compliant canonical JSON
kvinwang Apr 16, 2026
3320eb2
fix: serialize version in RuntimeEvent for attestation roundtrip
kvinwang Apr 16, 2026
d88d2c6
refactor: use single event_type for both v1 and v2
kvinwang Apr 16, 2026
4dee4bb
test: add comprehensive canonical JSON tests for RFC 8785 compliance
kvinwang Apr 16, 2026
796683b
feat: add include_hash_inputs parameter to GetQuote and Attest RPCs
kvinwang Apr 16, 2026
374f3d3
fix: resolve CI failures from new v1 test and gated import
kvinwang Apr 16, 2026
270aaf8
fix: address copilot review on v1 digest encoding and v2 fallback
kvinwang Apr 16, 2026
f191603
docs: clarify include_hash_inputs returns hex-encoded bytes
kvinwang Apr 16, 2026
de1d4a0
fix: clarify include_hash_inputs only applies to runtime events in pr…
Copilot Apr 16, 2026
5b6b57d
refactor: drop version field from v2 canonical JSON
kvinwang Apr 16, 2026
ca0f577
fix: upgrade to V1 msgpack attestation when v2 events present
kvinwang Apr 16, 2026
90765a3
test: add into_versioned V0/V1 dispatch coverage
kvinwang Apr 16, 2026
55ce10f
refactor: rename v2 canonical JSON fields to name/type/content
kvinwang Apr 16, 2026
3e4b533
feat(vmm): expose event_log_version in vmm-cli and Web UI
kvinwang Apr 16, 2026
5a7d188
refactor: rename v2 canonical JSON content field back to payload
kvinwang Apr 16, 2026
be0eb0b
Potential fix for pull request finding
kvinwang Apr 16, 2026
d581179
feat(guest-agent): add CCEL binary event log to GetQuote response
kvinwang Apr 20, 2026
f7f0491
chore: remove verify_ccel_device example
kvinwang Apr 20, 2026
8d07851
Potential fix for pull request finding
kvinwang Apr 20, 2026
d3fd44d
Merge origin/master into feat/event-log-v2-canonical-json
Copilot Apr 22, 2026
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
2 changes: 2 additions & 0 deletions 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 cc-eventlog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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
Expand Down
5 changes: 4 additions & 1 deletion cc-eventlog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
//
// 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, replay_events, RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE_V2,
};
pub use tdx::TdxEvent;

mod codecs;
Expand Down
167 changes: 156 additions & 11 deletions cc-eventlog/src/runtime_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0

use anyhow::{Context, Result};
use dstack_types::EventLogVersion;
use fs_err as fs;
use scale::{Decode, Encode};
use serde::{Deserialize, Serialize};
Expand All @@ -11,10 +12,14 @@ use std::io::Write;

use ez_hash::{Hasher, Sha256, Sha384};

/// The event type for dstack runtime events.
/// The event type for dstack runtime events (v1).
/// This code is not defined in the TCG specification.
/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf
pub const DSTACK_RUNTIME_EVENT_TYPE: u32 = 0x08000001;
Comment thread
kvinwang marked this conversation as resolved.
Comment thread
kvinwang marked this conversation as resolved.
/// The event type for dstack runtime events (v2, JSON canonical content).
/// V2 events use JCS (RFC 8785) canonical JSON as the digest input, enabling
/// relying parties to define fine-grained trust policies on individual event claims.
pub const DSTACK_RUNTIME_EVENT_TYPE_V2: u32 = 0x08000002;
/// The path to the userspace TDX event log file.
pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/dstack/runtime_events.log";

Expand All @@ -26,11 +31,18 @@ pub struct RuntimeEvent {
/// Event payload
#[serde(with = "base64")]
pub payload: Vec<u8>,
/// Event log version
#[serde(default, skip_serializing)]
pub version: EventLogVersion,
}

impl RuntimeEvent {
pub fn new(event: String, payload: Vec<u8>) -> Self {
Self { event, payload }
pub fn new(event: String, payload: Vec<u8>, version: EventLogVersion) -> Self {
Self {
event,
payload,
version,
}
}

pub fn read_all() -> Result<Vec<RuntimeEvent>> {
Expand Down Expand Up @@ -97,21 +109,51 @@ impl RuntimeEvent {
}

/// Compute the digest of the event.
///
/// - V1: `SHA(event_type_le || ":" || event_name || ":" || payload)`
/// - V2: `SHA(canonical_json({"event":"...","event_type":134217730,"payload":"hex..."}))`
pub fn digest<H: Hasher>(&self) -> H::Output {
Comment thread
kvinwang marked this conversation as resolved.
H::hash([
&DSTACK_RUNTIME_EVENT_TYPE.to_ne_bytes()[..],
b":",
self.event.as_bytes(),
b":",
&self.payload,
])
match self.version {
EventLogVersion::V1 => H::hash([
&DSTACK_RUNTIME_EVENT_TYPE.to_ne_bytes()[..],
b":",
self.event.as_bytes(),
b":",
&self.payload,
]),
EventLogVersion::V2 => {
let canonical =
canonical_event_json(&self.event, DSTACK_RUNTIME_EVENT_TYPE_V2, &self.payload);
H::hash([canonical.as_bytes()])
}
}
}

pub fn cc_event_type(&self) -> u32 {
DSTACK_RUNTIME_EVENT_TYPE
match self.version {
EventLogVersion::V1 => DSTACK_RUNTIME_EVENT_TYPE,
EventLogVersion::V2 => DSTACK_RUNTIME_EVENT_TYPE_V2,
}
}
}

/// Construct JCS (RFC 8785) canonical JSON for a runtime event.
///
/// Keys are sorted alphabetically: `event`, `event_type`, `payload`.
/// The payload is hex-encoded for human readability.
///
/// Output: `{"event":"<name>","event_type":<type>,"payload":"<hex>"}`
pub fn canonical_event_json(event: &str, event_type: u32, payload: &[u8]) -> String {
// Per JCS, strings must use minimal JSON escaping.
// We use serde_json to correctly escape the event name.
let escaped_event = serde_json::to_string(event).expect("failed to serialize event name");
let hex_payload = hex::encode(payload);
format!(
r#"{{"event":{},"event_type":{},"payload":"{}"}}"#,
escaped_event, event_type, hex_payload
Comment thread
kvinwang marked this conversation as resolved.
Outdated
)
}
Comment thread
kvinwang marked this conversation as resolved.

/// Replay event logs
pub fn replay_events<H: Hasher>(eventlog: &[RuntimeEvent], to_event: Option<&str>) -> H::Output {
let mut mr = H::zeros();
Expand All @@ -125,3 +167,106 @@ pub fn replay_events<H: Hasher>(eventlog: &[RuntimeEvent], to_event: Option<&str
}
mr
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn v1_digest_unchanged() {
let event = RuntimeEvent::new(
"app-id".to_string(),
vec![0xde, 0xad, 0xbe, 0xef],
EventLogVersion::V1,
);
let digest = event.digest::<Sha384>();
let expected = Sha384::hash([
&DSTACK_RUNTIME_EVENT_TYPE.to_ne_bytes()[..],
b":",
b"app-id",
b":",
&[0xde, 0xad, 0xbe, 0xef],
]);
assert_eq!(digest, expected, "v1 digest must be backward compatible");
}

#[test]
fn v2_digest_is_canonical_json_hash() {
let event = RuntimeEvent::new(
"compose-hash".to_string(),
vec![0xab, 0xcd],
EventLogVersion::V2,
);
let canonical =
canonical_event_json(&event.event, DSTACK_RUNTIME_EVENT_TYPE_V2, &event.payload);
assert_eq!(
canonical,
r#"{"event":"compose-hash","event_type":134217730,"payload":"abcd"}"#
);
let digest = event.digest::<Sha384>();
let expected = Sha384::hash([canonical.as_bytes()]);
assert_eq!(digest, expected);
}

#[test]
fn v2_digest_differs_from_v1() {
let v1 = RuntimeEvent::new("test".to_string(), vec![1, 2, 3], EventLogVersion::V1);
let v2 = RuntimeEvent::new("test".to_string(), vec![1, 2, 3], EventLogVersion::V2);
assert_ne!(
v1.digest::<Sha384>(),
v2.digest::<Sha384>(),
"v1 and v2 digests must differ"
);
}

#[test]
fn v1_event_type() {
let event = RuntimeEvent::new("test".to_string(), vec![], EventLogVersion::V1);
assert_eq!(event.cc_event_type(), DSTACK_RUNTIME_EVENT_TYPE);
}

#[test]
fn v2_event_type() {
let event = RuntimeEvent::new("test".to_string(), vec![], EventLogVersion::V2);
assert_eq!(event.cc_event_type(), DSTACK_RUNTIME_EVENT_TYPE_V2);
}

#[test]
fn deserialize_v1_without_version_field() {
let json = r#"{"event":"app-id","payload":"AQID"}"#;
let event: RuntimeEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.version, EventLogVersion::V1);
assert_eq!(event.cc_event_type(), DSTACK_RUNTIME_EVENT_TYPE);
}

#[test]
fn serialize_omits_version() {
let v1 = RuntimeEvent::new("test".to_string(), vec![1], EventLogVersion::V1);
let v2 = RuntimeEvent::new("test".to_string(), vec![1], EventLogVersion::V2);
let json_v1 = serde_json::to_string(&v1).unwrap();
let json_v2 = serde_json::to_string(&v2).unwrap();
assert!(
!json_v1.contains("version"),
"version should never be serialized"
);
assert!(
!json_v2.contains("version"),
"version should never be serialized"
);
}

#[test]
fn canonical_json_escapes_special_chars() {
let canonical = canonical_event_json(
"event\"with\\special\nchars",
DSTACK_RUNTIME_EVENT_TYPE_V2,
&[0xff],
);
// Verify it's valid JSON
let parsed: serde_json::Value = serde_json::from_str(&canonical).unwrap();
assert_eq!(
parsed["event"].as_str().unwrap(),
"event\"with\\special\nchars"
);
}
}
25 changes: 20 additions & 5 deletions cc-eventlog/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use scale::{Decode, Encode};
use serde::{Deserialize, Serialize};

use crate::{
runtime_events::{RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE},
runtime_events::{RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE, DSTACK_RUNTIME_EVENT_TYPE_V2},
tcg::TcgEventLog,
};

Expand All @@ -16,7 +16,9 @@ use crate::{
/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format
/// which is one-based.
///
/// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`.
/// As for RTMR3:
/// - V1 (event_type 0x08000001): digest = `sha384(event_type_le || ":" || event || ":" || payload)`
/// - V2 (event_type 0x08000002): digest = `sha384(canonical_json({"event":"...","event_type":134217730,"payload":"hex..."}))`
#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)]
pub struct TdxEvent {
/// IMR index, starts from 0
Expand Down Expand Up @@ -75,22 +77,35 @@ impl TdxEvent {

pub fn is_runtime_event(&self) -> bool {
self.event_type == DSTACK_RUNTIME_EVENT_TYPE
|| self.event_type == DSTACK_RUNTIME_EVENT_TYPE_V2
}

pub fn to_runtime_event(&self) -> Option<RuntimeEvent> {
self.is_runtime_event().then_some(RuntimeEvent {
if !self.is_runtime_event() {
return None;
}
use dstack_types::EventLogVersion;
let version = if self.event_type == DSTACK_RUNTIME_EVENT_TYPE_V2 {
EventLogVersion::V2
} else {
EventLogVersion::V1
};
Some(RuntimeEvent {
event: self.event.clone(),
payload: self.event_payload.clone(),
version,
})
}
}

impl From<RuntimeEvent> for TdxEvent {
fn from(value: RuntimeEvent) -> Self {
let event_type = value.cc_event_type();
let digest = value.sha384_digest().to_vec();
TdxEvent {
imr: 3,
event_type: DSTACK_RUNTIME_EVENT_TYPE,
digest: value.sha384_digest().to_vec(),
event_type,
digest,
event: value.event,
event_payload: value.payload,
}
Expand Down
8 changes: 6 additions & 2 deletions dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub const TDX_QUOTE_REPORT_DATA_RANGE: std::ops::Range<usize> = 568..632;
use std::{borrow::Cow, time::SystemTime};

use anyhow::{anyhow, bail, Context, Result};
use cc_eventlog::{RuntimeEvent, TdxEvent};
use cc_eventlog::{EventLogVersion, RuntimeEvent, TdxEvent};
use dcap_qvl::{
quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15},
verify::VerifiedReport as TdxVerifiedReport,
Expand Down Expand Up @@ -1036,7 +1036,11 @@ impl Attestation {
let runtime_events = if mode.is_composable() {
RuntimeEvent::read_all().context("Failed to read runtime events")?
} else if let Some(app_id) = app_id {
vec![RuntimeEvent::new("app-id".to_string(), app_id.to_vec())]
vec![RuntimeEvent::new(
"app-id".to_string(),
app_id.to_vec(),
EventLogVersion::V1,
Comment thread
kvinwang marked this conversation as resolved.
Outdated
)]
} else {
vec![]
};
Expand Down
10 changes: 7 additions & 3 deletions dstack-attest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0

use anyhow::Context;
use cc_eventlog::RuntimeEvent;
use cc_eventlog::{EventLogVersion, RuntimeEvent};

pub use cc_eventlog as ccel;
pub use tdx_attest as tdx;
Expand All @@ -14,8 +14,12 @@ pub mod attestation;
mod v1;

/// Emit a runtime event that extends RTMR3 and logs the event.
pub fn emit_runtime_event(event: &str, payload: &[u8]) -> anyhow::Result<()> {
let event = RuntimeEvent::new(event.to_string(), payload.to_vec());
pub fn emit_runtime_event(
event: &str,
payload: &[u8],
version: EventLogVersion,
) -> anyhow::Result<()> {
let event = RuntimeEvent::new(event.to_string(), payload.to_vec(), version);

let mode = AttestationMode::detect()?;

Expand Down
Loading
Loading