Verify TDX lite ACPI tables against the declared VM shape - #1051
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR strengthens TDX lite attestation by optionally verifying RTMR0’s ACPI-related measurements using in-process ACPI blob regeneration (from vm_config) instead of replaying the guest-reported digests, closing a self-consistency hole in the lite path while remaining opt-in for compatibility.
Changes:
- Add a verifier-side config gate (
verify_tdx_lite_acpi_tables, defaultfalse) that recomputes and verifies TDX lite ACPI digests and reportsacpi_tables_verified=truewhen enabled. - Introduce
dstack_mr::tdx::expected_rtmr0_acpi_hashes()to regenerate the three RTMR0 ACPI blob hashes from VM shape inputs. - Wire the flag through both
dstack-verifieranddstack-kmsconfigs and document the behavior in verifier docs and fixtures.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| dstack/verifier/src/verification.rs | Adds the opt-in ACPI digest recomputation + mismatch reporting and sets details.acpi_tables_verified when enabled. |
| dstack/verifier/src/main.rs | Exposes verify_tdx_lite_acpi_tables in verifier config and wires it into CvmVerifier. |
| dstack/verifier/README.md | Documents the new config option and explains the TDX lite ACPI verification behavior and rationale. |
| dstack/verifier/fixtures/tdx-lite.README.md | Updates fixture expectations and shows how enabling the flag changes ACPI tables verified. |
| dstack/verifier/dstack-verifier.toml | Adds the new verifier config key with commentary and default false. |
| dstack/kms/src/main_service.rs | Wires the KMS image config flag into CvmVerifier. |
| dstack/kms/src/config.rs | Adds the new verify_tdx_lite_acpi_tables field to KMS image configuration. |
| dstack/kms/kms.toml | Documents and defaults the new KMS config key to false. |
| dstack/dstack-types/src/lib.rs | Updates VmConfig::tdx_measurement docs to reflect gated lite-path ACPI verification. |
| dstack/dstack-mr/src/tdx.rs | Factors out machine construction and adds expected_rtmr0_acpi_hashes() for verifier use. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
kvinwang
force-pushed
the
feat/verifier-lite-acpi
branch
from
August 13, 2026 12:23
9b8a5b6 to
f2ab3d5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #1052 for the "QEMU newer than the newest profile" case; correct without it, just less forgiving of a QEMU upgrade.
Problem
RTMR0 covers three blobs QEMU hands to OVMF:
acpi-loader,acpi-rsdp,acpi-tables. The full-image TDX path regenerates them and compares the resulting RTMR0 against the quote. The lite path cannot download the image, so it did something weaker (verify_os_image_hash_for_dstack_tdx_lite): it pulled those three digests out of the guest's event log and fed them straight back into the expected RTMR0 it rebuilds.That is self-consistent but not a check. An untrusted host that pushes modified ACPI tables through fw_cfg gets OVMF to measure them faithfully, the guest reports the resulting digests faithfully, and the verifier rebuilds an expected RTMR0 from those same digests — so it matches by construction.
acpi-tablescarries the DSDT, i.e. AML the guest kernel executes, plus a table-loader command stream that tells OVMF where to patch guest memory. The rest of RTMR0 (TD HOB, CFV constants, SecureBoot variables, boot events) is pinned bymeasurement.tdx.cborandvm_config; these three entries were the hole, anddocs/security/security-model.mdhad a section explaining why it was left open.The tables could not go into
measurement.tdx.cbor: they depend on the deployment topology (vCPU count, RAM size, NIC/GPU/NVSwitch count, hugepages, QEMU version), which is unknown at image build time. Precomputing them host-side and shipping them invm_configwould be circular — the host would be handing the verifier the answer.Fix
#1050 removed the reason the lite path skipped this: ACPI generation is now pure Rust, in-process, and needs no image bytes at all — only the VM shape, every field of which
vm_configalready carries. So the verifier derives the expected digests itself.dstack_mr::tdx::expected_rtmr0_acpi_hashes(vm_config, ovmf_variant)regenerates the three blobs and returns their SHA-384s, reusing theMachineconstruction that MRTD candidate selection already does (extracted asmachine_from_vm_config).The expected values stay derived from low-dimensional inputs the host cannot lie about for free: the same
vm_configfields also drive the TD HOB measurement in RTMR0 and the kernel measurement in RTMR1, so misreporting the topology to fit forged tables breaks the other measurements.Why mandatory, and why both failure modes are fatal
The first version of this PR put the check behind a config gate and reported the outcome in
acpi_tables_verified. That does not close the hole, for a reason worth spelling out:swtpmandqemu_versionare host-declaredvm_configfields that no other measurement independently constrains.swtpm = truemakes the generator bail; a bogus version makes it generate for the wrong profile. So under any design where "could not generate" or "did not match" still yieldsis_valid = true, an attacker ships forged ACPI tables and declaresswtpm = trueto land in the unverified branch. The check would be optional at the attacker's discretion, and nothing in-tree consumesacpi_tables_verifiedtoday (KMSboot_infodoes not carry it).So there is no knob and no soft outcome:
swtpm = true, QEMU older than 8.0) → verification fails; there is nothing to compare against.Consequence to be explicit about: CVMs using the TPM key provider (
swtpm = true,KeyProviderKind::Tpm) can no longer be verified on the lite path. They already could not be verified on the full-image path —Machine::build_tables()has rejected swtpm sincerefactor(attestation): reject swtpm measurement— so this makes the two paths agree rather than introducing a new gap. Supporting swtpm inqemu-acpi(TPM2 table, the PPI AML, and a second fixture matrix) is a separate piece of work.Verification
The lite fixture (
verifier/fixtures/tdx-lite-attestation.json) was captured from a real CVM, so its RTMR0 ACPI digests are whatever QEMU 8.2.2 actually produced on that host. Recomputing them in-process reproduces them exactly — the generator agrees with hardware, not just with itself:Tests:
verifies_tdx_lite_fixture_without_image_download— the real fixture passes and now reportsacpi_tables_verified(previously assertedfalse).tdx_lite_acpi_hashes_depend_on_the_reported_vm_shape— recomputed digests match the captured CVM, and adding one vCPU makes them stop matching, so the comparison has teeth.tdx_lite_acpi_hash_mismatch_names_the_table— a mismatch namesacpi-tables.cargo test -p dstack-verifier -p dstack-mr -p dstack-kms --all-featurespasses;cargo fmt/cargo clippyclean.Docs updated rather than left stale:
docs/security/security-model.md's "Why TDX lite mode does not validate ACPI table contents" is replaced by the fail-closed rationale (the BadAML sandbox stays documented as defense in depth), plus the verifier README and the lite fixture README.