Skip to content

validate_v2_preimages is documented as binding RTMR replay to displayed fields, but inspects the event list that is not replayed #1294

Description

@kvinwang

Label: DESIGN. The API shape is wrong, not a live decision. The demonstration below is of the shape, not of an exploit.

Base: origin/next @ 030fbb2183.

What the chain claims

cc-eventlog/src/tdx.rs:170-175, the doc comment on validate_v2_preimages:

Validate the externally supplied digest preimage of every V2 runtime event.
The preimage must be present, valid hex, hash to the advertised digest, and equal the canonical representation reconstructed from the public event fields. This binds RTMR replay and displayed fields to the same bytes.

What it enforces

validate_v2_preimages is called on q.event_log / q.tdx_quote.event_log — dstack-attest/src/attestation.rs:2389-2399.

The RTMR3 replay uses a different field: self.runtime_events — dstack-attest/src/attestation.rs:2521 (verify_tdx) and :1807 (verify_tdx_quote_with_events).

These are two independent fields on the wire:

  • SCALE/V0: Attestation { quote: AttestationQuote::DstackTdx(TdxQuote { event_log }), runtime_events, .. } — dstack-attest/src/attestation.rs:1449-1465.
  • msgpack/V1: PlatformEvidence::Tdx { quote, event_log } (dstack-attest/src/v1.rs:66-70) versus StackEvidence::Dstack { report_data, runtime_events, config } (dstack-attest/src/v1.rs:199-206).

Nothing checks that they agree. from_msgpack (v1.rs:~305) and VersionedAttestation::from_bytes (attestation.rs:742-770) do not; neither does verify_with_time.

So the function documented as binding replay to displayed fields is applied to the list that is not replayed, and the list that is replayed carries no supplied digest for the function to check.

Demonstrated

Scratch crate outside the repo, path-depending on ra-tls and cc-eventlog, driving the checked-in sdk/simulator/attestation.bin:

platform.event_log entries   = 33
stack.runtime_events entries = 9
app_id unmodified                        = 5bb4ff9a3837357f19dc176407a5709c62eb6c56
app_id with platform.event_log emptied   = 5bb4ff9a3837357f19dc176407a5709c62eb6c56
app_id with stack.runtime_events emptied = "" (empty = true)

Emptying the list validate_v2_preimages inspects changes nothing about the decoded identity; emptying the other one empties it.

Reachability

  • Who can trigger it: anyone who can submit an attestation blob — POST /verify with attestation (verifier/src/main.rs:95, no auth fairing), a SignCertRequest CSR, or an RA-TLS certificate extension.
  • What credential it needs: none for POST /verify.
  • Who controls frequency: the submitter.

What it costs, concretely

The event log a consumer displays or exports and the event log a verifier replays can disagree, and the stated safety property is attached to the wrong one.

Today the blast radius is small and I want to be accurate about that: VerificationResponse does not echo the log (verifier/src/types.rs:80-100), and ct_monitor derives both lists from one blob via Attestation::from_tdx_quote (ct_monitor/src/main.rs:173, attestation.rs:2137-2161), so its two copies are consistent by construction. The exposure is to any consumer that reads platform.event_log out of an attestation blob it also verified — which the type invites, because both fields are public.

The one place platform.event_log does feed a check is TDX-lite ACPI (verifier/src/verification.rs:1059-1072), and that is safe: the reported digests are compared against recomputed ones and then discarded, and RTMR0 is rebuilt from the recomputed values (verification.rs:1074-1081).

Steelman

validate_v2_preimages is doing real work where it is: it is what lets a relying party reading the serialized TdxEvent list check a V2 event's digest field, and it rejects a non-canonical preimage that happens to hash to the advertised digest — pinned by rejects_noncanonical_v2_preimage_with_matching_digest (cc-eventlog/src/tdx.rs:397-404). Carrying two representations is a compatibility artefact rather than a design choice: RuntimeEvent is the semantic type and TdxEvent is the TCG-shaped one, and both were already on the wire when the V1 msgpack schema was written, so the schema recorded both.

It is also worth saying that the replay itself is sound: TdxEvent::digest() recomputes runtime-event digests from (name, payload, version) rather than trusting the supplied field (cc-eventlog/src/tdx.rs:121-126), and replay_events hashes those. There is no forgeable digest in the replay path — which is exactly why validate_v2_preimages has nothing to do there.

Improvement directions

  1. Cheap, closes it, wire-compatible. At decode time, derive runtime_events from platform.event_log's imr == 3 entries instead of accepting it as an independent field — which is precisely what Attestation::from_tdx_quote already does (attestation.rs:2137-2161). One field leaves the trusted surface; existing encoders keep working because they emit both consistently. Cost: one read-side change plus a test that a divergent pair no longer decodes two ways.
  2. Cheaper, weaker. Add an equality check in verify_with_time and reject an attestation whose two lists disagree on their imr == 3 entries. Keeps the wire format exactly; costs one comparison per verification. Risk: any historical encoder that legitimately stripped one list differently would start failing — into_stripped (attestation.rs:804-830, v1.rs:20-62) strips event_log but not runtime_events, so this option needs that asymmetry resolved first, which is why (1) is cleaner.
  3. Minimum. Correct the doc comment at cc-eventlog/src/tdx.rs:170-175 to say what the function actually validates, so the next reader does not inherit the claim.

(1) is the one worth doing; (3) should happen regardless.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions