@@ -11,10 +11,14 @@ use std::io::Write;
1111
1212use ez_hash:: { Hasher , Sha256 , Sha384 } ;
1313
14- /// The event type for dstack runtime events.
14+ /// The event type for dstack runtime events (v1) .
1515/// This code is not defined in the TCG specification.
1616/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf
1717pub const DSTACK_RUNTIME_EVENT_TYPE : u32 = 0x08000001 ;
18+ /// The event type for dstack runtime events (v2, JSON canonical content).
19+ /// V2 events use JCS (RFC 8785) canonical JSON as the digest input, enabling
20+ /// relying parties to define fine-grained trust policies on individual event claims.
21+ pub const DSTACK_RUNTIME_EVENT_TYPE_V2 : u32 = 0x08000002 ;
1822/// The path to the userspace TDX event log file.
1923pub const RUNTIME_EVENT_LOG_FILE : & str = "/run/log/dstack/runtime_events.log" ;
2024
@@ -26,11 +30,18 @@ pub struct RuntimeEvent {
2630 /// Event payload
2731 #[ serde( with = "base64" ) ]
2832 pub payload : Vec < u8 > ,
33+ /// Event log version (0 or absent = v1, 2 = v2 JSON canonical)
34+ #[ serde( default , skip_serializing) ]
35+ pub version : u32 ,
2936}
3037
3138impl RuntimeEvent {
32- pub fn new ( event : String , payload : Vec < u8 > ) -> Self {
33- Self { event, payload }
39+ pub fn new ( event : String , payload : Vec < u8 > , version : u32 ) -> Self {
40+ Self {
41+ event,
42+ payload,
43+ version,
44+ }
3445 }
3546
3647 pub fn read_all ( ) -> Result < Vec < RuntimeEvent > > {
@@ -97,7 +108,18 @@ impl RuntimeEvent {
97108 }
98109
99110 /// Compute the digest of the event.
111+ ///
112+ /// For v1 (version 0 or 1): `SHA(event_type_le || ":" || event_name || ":" || payload)`
113+ /// For v2 (version 2): `SHA(canonical_json({"event":"...","event_type":134217730,"payload":"hex..."}))`
100114 pub fn digest < H : Hasher > ( & self ) -> H :: Output {
115+ if self . is_v2 ( ) {
116+ self . digest_v2 :: < H > ( )
117+ } else {
118+ self . digest_v1 :: < H > ( )
119+ }
120+ }
121+
122+ fn digest_v1 < H : Hasher > ( & self ) -> H :: Output {
101123 H :: hash ( [
102124 & DSTACK_RUNTIME_EVENT_TYPE . to_ne_bytes ( ) [ ..] ,
103125 b":" ,
@@ -107,11 +129,46 @@ impl RuntimeEvent {
107129 ] )
108130 }
109131
132+ /// Compute the v2 digest using JCS (RFC 8785) canonical JSON.
133+ ///
134+ /// The canonical JSON has keys sorted alphabetically:
135+ /// `{"event":"<name>","event_type":134217730,"payload":"<hex>"}`
136+ fn digest_v2 < H : Hasher > ( & self ) -> H :: Output {
137+ let canonical =
138+ canonical_event_json ( & self . event , DSTACK_RUNTIME_EVENT_TYPE_V2 , & self . payload ) ;
139+ H :: hash ( [ canonical. as_bytes ( ) ] )
140+ }
141+
142+ pub fn is_v2 ( & self ) -> bool {
143+ self . version == 2
144+ }
145+
110146 pub fn cc_event_type ( & self ) -> u32 {
111- DSTACK_RUNTIME_EVENT_TYPE
147+ if self . is_v2 ( ) {
148+ DSTACK_RUNTIME_EVENT_TYPE_V2
149+ } else {
150+ DSTACK_RUNTIME_EVENT_TYPE
151+ }
112152 }
113153}
114154
155+ /// Construct JCS (RFC 8785) canonical JSON for a runtime event.
156+ ///
157+ /// Keys are sorted alphabetically: `event`, `event_type`, `payload`.
158+ /// The payload is hex-encoded for human readability.
159+ ///
160+ /// Output: `{"event":"<name>","event_type":<type>,"payload":"<hex>"}`
161+ pub fn canonical_event_json ( event : & str , event_type : u32 , payload : & [ u8 ] ) -> String {
162+ // Per JCS, strings must use minimal JSON escaping.
163+ // We use serde_json to correctly escape the event name.
164+ let escaped_event = serde_json:: to_string ( event) . expect ( "failed to serialize event name" ) ;
165+ let hex_payload = hex:: encode ( payload) ;
166+ format ! (
167+ r#"{{"event":{},"event_type":{},"payload":"{}"}}"# ,
168+ escaped_event, event_type, hex_payload
169+ )
170+ }
171+
115172/// Replay event logs
116173pub fn replay_events < H : Hasher > ( eventlog : & [ RuntimeEvent ] , to_event : Option < & str > ) -> H :: Output {
117174 let mut mr = H :: zeros ( ) ;
@@ -125,3 +182,99 @@ pub fn replay_events<H: Hasher>(eventlog: &[RuntimeEvent], to_event: Option<&str
125182 }
126183 mr
127184}
185+
186+ #[ cfg( test) ]
187+ mod tests {
188+ use super :: * ;
189+
190+ #[ test]
191+ fn v1_digest_unchanged ( ) {
192+ let event = RuntimeEvent :: new ( "app-id" . to_string ( ) , vec ! [ 0xde , 0xad , 0xbe , 0xef ] , 1 ) ;
193+ let digest = event. digest :: < Sha384 > ( ) ;
194+ let expected = Sha384 :: hash ( [
195+ & DSTACK_RUNTIME_EVENT_TYPE . to_ne_bytes ( ) [ ..] ,
196+ b":" ,
197+ b"app-id" ,
198+ b":" ,
199+ & [ 0xde , 0xad , 0xbe , 0xef ] ,
200+ ] ) ;
201+ assert_eq ! ( digest, expected, "v1 digest must be backward compatible" ) ;
202+ }
203+
204+ #[ test]
205+ fn v2_digest_is_canonical_json_hash ( ) {
206+ let event = RuntimeEvent :: new ( "compose-hash" . to_string ( ) , vec ! [ 0xab , 0xcd ] , 2 ) ;
207+ let canonical =
208+ canonical_event_json ( & event. event , DSTACK_RUNTIME_EVENT_TYPE_V2 , & event. payload ) ;
209+ assert_eq ! (
210+ canonical,
211+ r#"{"event":"compose-hash","event_type":134217730,"payload":"abcd"}"#
212+ ) ;
213+ let digest = event. digest :: < Sha384 > ( ) ;
214+ let expected = Sha384 :: hash ( [ canonical. as_bytes ( ) ] ) ;
215+ assert_eq ! ( digest, expected) ;
216+ }
217+
218+ #[ test]
219+ fn v2_digest_differs_from_v1 ( ) {
220+ let v1 = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ 1 , 2 , 3 ] , 1 ) ;
221+ let v2 = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ 1 , 2 , 3 ] , 2 ) ;
222+ assert_ne ! (
223+ v1. digest:: <Sha384 >( ) ,
224+ v2. digest:: <Sha384 >( ) ,
225+ "v1 and v2 digests must differ"
226+ ) ;
227+ }
228+
229+ #[ test]
230+ fn v1_event_type ( ) {
231+ let event = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ ] , 1 ) ;
232+ assert_eq ! ( event. cc_event_type( ) , DSTACK_RUNTIME_EVENT_TYPE ) ;
233+ }
234+
235+ #[ test]
236+ fn v2_event_type ( ) {
237+ let event = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ ] , 2 ) ;
238+ assert_eq ! ( event. cc_event_type( ) , DSTACK_RUNTIME_EVENT_TYPE_V2 ) ;
239+ }
240+
241+ #[ test]
242+ fn deserialize_v1_without_version_field ( ) {
243+ let json = r#"{"event":"app-id","payload":"AQID"}"# ;
244+ let event: RuntimeEvent = serde_json:: from_str ( json) . unwrap ( ) ;
245+ assert_eq ! ( event. version, 0 ) ;
246+ assert ! ( !event. is_v2( ) ) ;
247+ assert_eq ! ( event. cc_event_type( ) , DSTACK_RUNTIME_EVENT_TYPE ) ;
248+ }
249+
250+ #[ test]
251+ fn serialize_omits_version ( ) {
252+ let v1 = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ 1 ] , 1 ) ;
253+ let v2 = RuntimeEvent :: new ( "test" . to_string ( ) , vec ! [ 1 ] , 2 ) ;
254+ let json_v1 = serde_json:: to_string ( & v1) . unwrap ( ) ;
255+ let json_v2 = serde_json:: to_string ( & v2) . unwrap ( ) ;
256+ assert ! (
257+ !json_v1. contains( "version" ) ,
258+ "version should never be serialized"
259+ ) ;
260+ assert ! (
261+ !json_v2. contains( "version" ) ,
262+ "version should never be serialized"
263+ ) ;
264+ }
265+
266+ #[ test]
267+ fn canonical_json_escapes_special_chars ( ) {
268+ let canonical = canonical_event_json (
269+ "event\" with\\ special\n chars" ,
270+ DSTACK_RUNTIME_EVENT_TYPE_V2 ,
271+ & [ 0xff ] ,
272+ ) ;
273+ // Verify it's valid JSON
274+ let parsed: serde_json:: Value = serde_json:: from_str ( & canonical) . unwrap ( ) ;
275+ assert_eq ! (
276+ parsed[ "event" ] . as_str( ) . unwrap( ) ,
277+ "event\" with\\ special\n chars"
278+ ) ;
279+ }
280+ }
0 commit comments