Add event log v2 format with JSON canonical digest#646
Add event log v2 format with JSON canonical digest#646
Conversation
Introduce a new event log version (v2) that uses JCS canonical JSON as the digest input instead of the binary concatenation format. This enables relying parties to define fine-grained trust policies on individual event claims (e.g., check compose-hash directly) without needing to match the full RTMR3 value. V2 uses a new event_type (0x08000002) so verifiers can distinguish the format without access to app-compose.json. Controlled per-app via the `event_log_version` field in app-compose.json (default 1 for backward compatibility).
Replace u32 version fields with an EventLogVersion enum to get exhaustive match checking — adding a v3 in the future will force all match sites to be updated at compile time. Also removes version field from event log serialization as it's unnecessary (verifier distinguishes v1/v2 via event_type).
from_u32 now returns Option<Self>, rejecting unknown values like 0 or 42 rather than silently degrading to V1. A silent downgrade could produce incorrect RTMR measurements — a security-relevant bug.
- Replace expect() with manual JSON escaping to satisfy clippy::expect_used - Add #[codec(skip)] on RuntimeEvent.version to preserve scale binary compat with existing attestation fixtures - Pass EventLogVersion from caller in Attestation::quote_with_app_id - Read event_log_version from app-compose.json in extend-rtmr CLI instead of hardcoded default - from_u32 now returns Option, rejecting unknown version values
Replace manual format string with BTreeMap serialization to guarantee alphabetical key ordering per JCS RFC 8785. This prevents bugs if fields are added in the future — BTreeMap handles ordering automatically.
Replace manual BTreeMap + serde_json with serde_jcs::to_string which handles all JCS requirements (key ordering, number formatting, string escaping) per RFC 8785 specification.
The version field must survive serde serialization because the verify path in AttestationV1 uses RuntimeEvent directly from StackEvidence (serde-encoded). Without serializing version, all deserialized events default to V1, causing RTMR3 mismatch for V2 events. - Change from skip_serializing to always serialize version - Old data without version field still defaults to V1 via #[serde(default)] - Scale encoding still skips version via #[codec(skip)] for binary compat
Previously v1 and v2 used different event_type values (0x08000001 vs 0x08000002) to let verifiers distinguish formats. Now that version is serialized on RuntimeEvent and TdxEvent via serde, a single event_type (0x08000001) suffices — version is carried explicitly. Changes: - Drop DSTACK_RUNTIME_EVENT_TYPE_V2 constant - cc_event_type() always returns DSTACK_RUNTIME_EVENT_TYPE - Add version field to TdxEvent (serde default V1, #[codec(skip)] for scale compat) - V2 canonical JSON now includes explicit "version": 2 field for self-describing content - Serde skips version when V1 to keep existing JSON outputs unchanged Scale backward compat: legacy V0 attestations contain only v1 events, so #[codec(skip)] defaulting to V1 on decode is correct.
Covers key aspects of JCS (RFC 8785) that the digest depends on: - Bytewise-exact output for known inputs - Alphabetical key ordering - Short-form escapes (\b \f \n \r \t) vs \uXXXX for other control chars - Non-ASCII Unicode emitted verbatim (not \uXXXX escaped) - Lowercase hex payload - Empty string / empty payload edge cases - Idempotency across 100 invocations (guards against HashMap randomization) - No whitespace, valid JSON structure
Allow relying parties to opt-in to receiving the digest pre-image (hash_input) for each event, enabling digest verification and content inspection without knowing the dstack event schema. Proto changes: - RawQuoteArgs.include_hash_inputs: new bool field (default false) Runtime changes: - RuntimeEvent::hash_input() returns the bytes that get hashed: - V1: binary concatenation event_type_le || ":" || name || ":" || payload - V2: UTF-8 bytes of the JCS canonical JSON - TdxEvent gains optional hash_input: Option<String> (hex-encoded) - TdxEvent::fill_hash_input() populates it for runtime events - When include_hash_inputs=true, guest-agent fills hash_input before returning the event log (in GetQuote) or serializing the attestation (in Attest) Compatibility: - Default off: existing clients see no change - scale codec: hash_input is #[codec(skip)], derivable from other fields - serde: hash_input uses skip_serializing_if Option::is_none — only appears in JSON when populated
- Update v1::tests::msgpack_roundtrip_preserves_attestation to initialize the newly-added `version`/`hash_input` fields on `TdxEvent` and the `version` field on `RuntimeEvent`. - Move the `cc_eventlog::EventLogVersion` import in attestation.rs behind `#[cfg(feature = "quote")]` since it is only referenced in that gated `impl` block; without the gate, the default `cargo clippy -- -D warnings` run fails with an unused-imports error.
There was a problem hiding this comment.
Pull request overview
Adds a configurable “v2” runtime event digest format based on RFC 8785 (JCS) canonical JSON, and threads the chosen event-log version plus an RPC opt-in for returning per-event digest preimages (“hash inputs”) through guest-agent, simulator, and callers.
Changes:
- Introduces
EventLogVersion+AppCompose.event_log_version(default v1) and threads it through runtime event emission. - Implements v2 runtime event digest preimage as JCS-canonical JSON and adds optional
hash_inputexposure for relying parties. - Extends guest-agent RPC (
RawQuoteArgs) withinclude_hash_inputsand updates downstream clients/simulator accordingly.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ra-tls/src/cert.rs | Adapts to new quote_with_app_id signature by passing a default event log version. |
| kms/src/main_service/upgrade_authority.rs | Updates RawQuoteArgs construction to include include_hash_inputs. |
| guest-agent/src/rpc_service.rs | Threads include_hash_inputs into quote/attest paths and passes event_log_version into event emission. |
| guest-agent/src/backend.rs | Extends backend trait to support include_hash_inputs and versioned runtime event emission. |
| guest-agent/rpc/proto/agent_rpc.proto | Adds include_hash_inputs to RawQuoteArgs and documents behavior. |
| guest-agent-simulator/src/simulator.rs | Adds include_hash_inputs support for simulated quote/attest responses, populating hash inputs when requested. |
| guest-agent-simulator/src/main.rs | Updates simulator backend implementations/tests for new backend trait signatures. |
| guest-agent-simulator/Cargo.toml | Adds dstack-types dependency for EventLogVersion. |
| gateway/src/gen_debug_key.rs | Updates quote request construction for new RawQuoteArgs field. |
| gateway/src/distributed_certbot.rs | Updates quote/attest request construction for new RawQuoteArgs field. |
| dstack-util/src/system_setup.rs | Threads event_log_version from app-compose through multiple RTMR3 emit_runtime_event calls. |
| dstack-util/src/main.rs | Reads event_log_version from host-shared app-compose.json for extend command emission. |
| dstack-types/src/lib.rs | Defines EventLogVersion and adds event_log_version to AppCompose with defaulting. |
| dstack-attest/src/v1.rs | Adds mutable access to TDX event log to populate per-event hash inputs on demand. |
| dstack-attest/src/lib.rs | Updates emit_runtime_event API to accept an explicit event log version. |
| dstack-attest/src/attestation.rs | Adds optional inclusion of per-event hash inputs in event-log JSON and supports event-log version in quote_with_app_id. |
| cc-eventlog/src/tdx.rs | Extends TdxEvent with version + optional hash_input, plus helpers/tests. |
| cc-eventlog/src/tcg.rs | Ensures new TdxEvent fields are initialized for converted TCG events. |
| cc-eventlog/src/runtime_events.rs | Implements v2 canonical JSON digest input, versioned hashing, and extensive tests. |
| cc-eventlog/src/lib.rs | Re-exports EventLogVersion and canonical JSON helper(s). |
| cc-eventlog/Cargo.toml | Adds dstack-types + serde_jcs dependencies. |
| Cargo.lock | Locks new dependencies (serde_jcs, ryu-js, etc.). |
| .gitignore | Ignores .claude/worktrees/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- pin v1 digest event_type encoding to little-endian (to_le_bytes)
so the on-wire format is platform-independent. x86 (dstack's only
target) produces identical bytes, so existing RTMR3 values and
verifiers are unaffected. the 2024 spec called out to_ne_bytes
explicitly, but a stable wire format should not depend on the
host platform.
- fix the EventLogVersion::V2 doc comment in dstack-types: it listed
event_type 134217730 (0x08000002), but v1 and v2 share the same
event_type 134217729 (0x08000001). the version is distinguished by
the EventLogVersion field / json version key, not event_type.
- replace canonical_event_json_v2's .unwrap_or_default() with
.or_panic(). silently returning "" on serialization failure would
produce sha384("") as the digest, which is both wrong and hard to
debug. the input is a well-formed json!{} literal so serialization
cannot actually fail; or_panic makes that invariant explicit without
tripping the expect_used clippy lint.
There was a problem hiding this comment.
Pull request overview
This PR introduces an event log v2 digest format based on JCS (RFC 8785) canonical JSON, adds event_log_version to app-compose.json (defaulting to v1 for backward compatibility), and threads the selected version through runtime event emission and attestation/quote surfaces. It also adds an RPC opt-in (include_hash_inputs) to include the exact digest pre-image for runtime events in returned event logs/attestations.
Changes:
- Add
EventLogVersion+AppCompose.event_log_versionand propagate it to allemit_runtime_event(..., version)call sites. - Implement v2 runtime event hashing via canonical JSON and extend event log structures to carry per-event version + optional
hash_input. - Add
include_hash_inputstoRawQuoteArgsand plumb it through guest-agent, simulator, gateway, and kms clients.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ra-tls/src/cert.rs | Updates attestation quote call signature to pass an event log version. |
| kms/src/main_service/upgrade_authority.rs | Populates new include_hash_inputs flag in attest RPC call. |
| guest-agent/src/rpc_service.rs | Threads include_hash_inputs through quote/attest RPC handlers; threads event_log_version into event emission; updates tests. |
| guest-agent/src/backend.rs | Extends backend trait to support include_hash_inputs + EventLogVersion; real backend fills optional hash inputs. |
| guest-agent/rpc/proto/agent_rpc.proto | Adds include_hash_inputs to RawQuoteArgs and documents behavior. |
| guest-agent-simulator/src/simulator.rs | Supports include_hash_inputs for simulated quote/attest responses by populating hash_input. |
| guest-agent-simulator/src/main.rs | Updates simulator backend trait implementation and tests for new params. |
| guest-agent-simulator/Cargo.toml | Adds dstack-types dependency for EventLogVersion. |
| gateway/src/gen_debug_key.rs | Updates quote request to include include_hash_inputs. |
| gateway/src/distributed_certbot.rs | Updates quote/attest requests to include include_hash_inputs. |
| dstack-util/src/system_setup.rs | Threads event_log_version through all runtime event emissions during boot/setup. |
| dstack-util/src/main.rs | CLI extend command now reads event_log_version from shared app-compose and uses it for emit_runtime_event. |
| dstack-types/src/lib.rs | Introduces EventLogVersion enum and adds event_log_version field to AppCompose with default. |
| dstack-attest/src/v1.rs | Adds mutable access to TDX event log in platform evidence; updates tests for new fields. |
| dstack-attest/src/lib.rs | Updates emit_runtime_event signature to accept an event log version. |
| dstack-attest/src/attestation.rs | Adds optional hash-input inclusion for event log JSON; adds fill_event_hash_inputs; threads version into quote_with_app_id. |
| cc-eventlog/src/tdx.rs | Extends TdxEvent with version + optional hash_input; implements fill_hash_input; adjusts runtime event conversion. |
| cc-eventlog/src/tcg.rs | Initializes new TdxEvent fields when converting from TCG events. |
| cc-eventlog/src/runtime_events.rs | Adds v2 canonical JSON hash input + EventLogVersion handling for runtime event digests; adds tests. |
| cc-eventlog/src/lib.rs | Re-exports EventLogVersion and v2 helpers/constants. |
| cc-eventlog/Cargo.toml | Adds dependencies for JCS canonicalization (serde_jcs) and shared types. |
| Cargo.lock | Locks new dependencies (serde_jcs, ryu-js, etc.) and updated crate deps. |
| .gitignore | Ignores .claude/worktrees/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The previous proto comment described v2 hash_input as "canonical JSON content", which would suggest the field holds a raw JSON string. hash_input is actually hex-encoded bytes of the digest pre-image, so clients must hex-decode first before interpreting the payload (UTF-8 JSON for v2, the v1 binary concat for v1). Spell out the encoding and the verification recipe so relying parties don't guess wrong.
…oto comment Agent-Logs-Url: https://github.com/Dstack-TEE/dstack/sessions/042b74ec-8b33-465d-bd29-2871b0cfd72c Co-authored-by: kvinwang <6442159+kvinwang@users.noreply.github.com>
The v2 canonical JSON previously included a "version":2 field to make
the hashed content self-describing. Per review feedback, version is
now carried out-of-band via RuntimeEvent.version (scale/serde) only,
and the hashed content is a clean {event, event_type, payload} JSON.
v2 canonical JSON is now:
{"event":"<name>","event_type":134217729,"payload":"<hex>"}
Updates:
- canonical_event_json_v2: drop "version" key
- doc comments in runtime_events.rs / tdx.rs / dstack-types/lib.rs
updated to reflect the new v2 format
- tests: expected canonical strings, the hash_input v2 assertion, and
the key-order test no longer reference "version"
Note: RuntimeEvent serde still emits "version":2 when writing events
to /run/log/dstack/runtime_events.log (serde_roundtrip_preserves_version
test covers that) — that is the on-disk format, not the hashed content.
Root cause of a live RTMR3-mismatch observed during v2 CVM testing: `Attestation::into_versioned()` unconditionally wrapped as `VersionedAttestation::V0`, which SCALE-encodes the payload. But `RuntimeEvent::version` and `TdxEvent::version` carry `#[codec(skip)]` (added for V0 binary compat), so the `version` field is dropped on the wire. The KMS-side decoder then defaults each event to V1, replays with the V1 digest algorithm, and fails against the guest's V2-extended RTMR3. Fix: when any runtime event reports a non-V1 version, force V1 msgpack encoding (where `version` is a normal serde field and round-trips). Callers with only V1 events still get V0/SCALE, preserving wire-format compatibility with existing consumers. `from_bytes` already detects the encoding by leading byte (msgpack map prefix -> V1, else SCALE -> V0), so receivers don't need changes. Verified end-to-end: deployed a CVM with `event_log_version: 2`, boot now progresses past `get_app_key` without an RTMR3 mismatch; KMS replay sees `version=V2` and matches the quoted RTMR3.
Guard the conditional wire-format dispatch from regressions: - V1-only events must stay on the V0/SCALE path (backward compat). - Any V2 event must force the V1 msgpack path so the `version` field round-trips through the cert/RPC boundary.
Change the hashed v2 canonical JSON from
{"event":"...","event_type":134217729,"payload":"hex..."}
to
{"name":"...","type":134217729,"content":"hex..."}
Generic names match the wire-level intent better for third-party
consumers ("event_type"/"payload" are dstack-schema terms; generic
content descriptors are easier to align with policy languages like
ITA's CEL expressions).
Note: JCS key ordering changes — content < name < type — so the
on-wire byte sequence (and therefore v2 digests) are different.
This is a breaking change to the v2 digest format; safe because v2
is not yet released.
- vmm-cli.py compose: add --event-log-version {1,2} flag.
Omitting the flag keeps the field out of app-compose.json, so
existing compose hashes are unchanged when the flag isn't used.
- Web UI CreateVmDialog: add a dropdown under Networking with
V1 (legacy binary) / V2 (JCS canonical JSON); v2 is only written
into the generated app-compose when explicitly selected.
- useVmManager: thread event_log_version through VmFormState and
the clone-config path so re-creating a VM preserves the selection.
Change the v2 canonical JSON from
{"content":"<hex>","name":"...","type":134217729}
to
{"name":"...","payload":"<hex>","type":134217729}
`payload` is the more common name in event/messaging protocols
(CloudEvents, MQTT, JWT) and — unlike `data` — has no risk of being
read as an OPA/Rego root-namespace reference. It also matches the
existing `event_payload` field name in `TdxEvent`, so dstack-internal
serde/scale shapes and the policy-facing canonical form now speak
the same vocabulary.
Note: JCS key ordering becomes name < payload < type. On-wire bytes
(and therefore v2 digests) change — safe since v2 is not yet released.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in v2 runtime event-log digest format (JCS/RFC 8785 canonical JSON) and threads an event_log_version setting from compose config through guest event emission and attestation/quote RPCs, including an include_hash_inputs flag so relying parties can independently verify per-event digests.
Changes:
- Introduce
EventLogVersionandAppCompose.event_log_version(default V1) and plumb it into runtime event emission (RTMR3 extends). - Add
include_hash_inputsto GetQuote/Attest RPCs and populate per-runtime-eventhash_input(hex-encoded digest pre-image) when requested. - Expose event-log version selection in VMM UI + CLI compose generation.
Reviewed changes
Copilot reviewed 24 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vmm/ui/src/composables/useVmManager.ts | Add form state + compose serialization for event_log_version (omit when V1). |
| vmm/ui/src/components/CreateVmDialog.ts | Add UI dropdown to select V1 vs V2 event log format. |
| vmm/src/vmm-cli.py | Add --event-log-version and write it into app-compose.json. |
| ra-tls/src/cert.rs | Update RA-TLS quote callsite for new quote_with_app_id signature. |
| kms/src/main_service/upgrade_authority.rs | Update attest request to include include_hash_inputs flag. |
| guest-agent/src/rpc_service.rs | Thread include_hash_inputs into quote/attest; pass event_log_version into emit_event. |
| guest-agent/src/backend.rs | Extend backend trait to support include_hash_inputs + event-log versioned emission. |
| guest-agent/rpc/proto/agent_rpc.proto | Add include_hash_inputs field + clarified semantics in comments. |
| guest-agent-simulator/src/simulator.rs | Support include_hash_inputs in simulated quote/attest responses. |
| guest-agent-simulator/src/main.rs | Update simulator backend trait implementation + tests for new signatures. |
| guest-agent-simulator/Cargo.toml | Add dstack-types dependency for EventLogVersion. |
| gateway/src/gen_debug_key.rs | Populate include_hash_inputs in quote requests. |
| gateway/src/distributed_certbot.rs | Populate include_hash_inputs in quote/attest requests. |
| dstack-util/src/system_setup.rs | Pass event_log_version into all runtime event emissions during boot setup. |
| dstack-util/src/main.rs | Make extend command pick event-log version from shared app-compose.json. |
| dstack-types/src/lib.rs | Introduce EventLogVersion and add event_log_version to AppCompose. |
| dstack-attest/src/v1.rs | Expose mutable access to TDX event log for filling hash_input in simulator/tests. |
| dstack-attest/src/lib.rs | Version-aware emit_runtime_event API. |
| dstack-attest/src/attestation.rs | Add include_hash_inputs plumbing for JSON event log; version-aware wire encoding selection. |
| cc-eventlog/src/tdx.rs | Add version + optional hash_input to TdxEvent; compute digest input on demand. |
| cc-eventlog/src/tcg.rs | Initialize new TdxEvent fields when converting from TCG events. |
| cc-eventlog/src/runtime_events.rs | Add v2 canonical JSON digest input + EventLogVersion in RuntimeEvent. |
| cc-eventlog/src/lib.rs | Re-export EventLogVersion and v2 helpers/constants. |
| cc-eventlog/Cargo.toml | Add deps for JCS + version type. |
| Cargo.lock | Lockfile updates for new crates. |
| .gitignore | Ignore .claude/worktrees/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app_compose["swap_size"] = swap_bytes | ||
| else: | ||
| app_compose.pop("swap_size", None) | ||
| if args.event_log_version is not None: |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
event_log_versionfield toapp-compose.json(default 1, backward compatible)event_type0x08000001; version is carried out-of-band viaRuntimeEvent::version(scale/serde), not inside the hashed contentSHA384({"name":"<event-name>","payload":"<hex>","type":134217729})(keys are JCS-sorted:name<payload<type)include_hash_inputsopt-in onGetQuote/AttestRPCs so relying parties can receive the per-event digest pre-image (hex-encoded) for independent verificationvmm-cli.py compose --event-log-versionand a dropdown in the VMM Web UIThis enables relying parties (e.g., Intel ITA) to define fine-grained trust policies on individual event claims (e.g.,
input.event.name == "compose-hash") instead of matching the full RTMR3 value, which varies across VM instances due to other measured events.Enabling v2 for an app
Via vmm-cli
Omitting
--event-log-versionkeeps the field out ofapp-compose.json, so existing compose hashes for v1 apps are unchanged.Via VMM Web UI
"Create VM" dialog → "Event log format" dropdown (under Networking). Defaults to V1; pick V2 to opt in.
Consuming v2 attestation
Once the CVM is booted, call
GetQuote/Attestwithinclude_hash_inputs=trueso relying parties can see the canonical JSON pre-image of each event:Each runtime event in the returned
event_logcarries ahash_inputfield whose value is a hex-encoded string. Clients verify digests independently with:Wire format notes
event_type.to_le_bytes() || ":" || event_name || ":" || payload(little-endian is now pinned; previouslyto_ne_bytes(), identical on x86){"name":"<event-name>","payload":"<hex>","type":134217729}. Field names are generic (no keyword collisions in Rego / CEL) and match common messaging/event-protocol conventions (CloudEvents, MQTT, JWT).include_hash_inputs=truereturns each runtime event's pre-image as a hex-encoded string so clients can independently verifysha384(hex_decode(hash_input)) == event.digestWire-format compatibility safety-net
Attestation::into_versioned()conditionally upgrades to V1 msgpack when any runtime event reports a non-V1 version (SCALE V0 skips theversionfield for legacy binary compat, which would otherwise drop v2 information on the wire). V1-only attestations keep the V0/SCALE encoding, so existing consumers are unaffected.from_bytesdetects the encoding by leading byte, so the receiver side needs no changes.Test plan
cargo check --all-featurespassescargo clippypasses (with-D warnings -D clippy::expect_used -D clippy::unwrap_used)cargo fmt --check --allpassescargo test -p cc-eventlog— 25 tests including v1 backward compat, v2 digest, canonical JSON escaping/sorting/idempotency, mixed v1/v2 replay, scale round-tripcargo test -p dstack-attest— 8 tests including msgpack round-trip with new fields and V0/V1 dispatch coveragevmm-cli.py compose --event-log-version 2writes the field; omitting the flag keeps existing compose hashes stableevent_log_version: 2; boot completes (done), KMS verifier replays RTMR3 without mismatch, and guest-reported digests match the new canonical JSON bytewise