You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Classification: DESIGN. All claims argued from code — no GCP hardware was available, and nothing
here is demonstrated.
On GCP TDX the OS-image identity rests on exactly two comparisons: PCR0 against a hardcoded constant,
and one 32-byte UKI Authenticode digest. The same quote carries MRTD, RTMR0, RTMR1 and RTMR2, and
none of them is compared against anything — which is mostly a platform constraint, and I want to say
so up front. What is a dstack choice, and is the part worth deciding on, is that the quoted PCR
selection omits PCR7 (Secure Boot policy) and PCR12 (kernel command line / credentials), both of
which AWS NitroTPM — the other TPM platform in this tree — binds.
expected_uki_hash is GcpOsImageMeasurement::uki_authenticode_sha256
(dstack/dstack-types/src/lib.rs:1618-1621) — the document's only field.
What is not compared.
MRTD, RTMR0, RTMR1, RTMR2. All four are in the GCP quote. The GCP arm of the OS-image dispatch
(verification.rs:812-814) computes no expected value for any of them, and decode_mr_gcp_tpm
(dstack/dstack-attest/src/attestation.rs:1855-1879) builds mr_aggregated from PCR0, PCR2 and
the replayed runtime PCR only — the TDX registers are not in the MR either. Compare TDX-legacy
(verification.rs:1390-1417) and TDX-lite (:1089-1091), which compare all four.
PCR7 and PCR12. The quoted selection is a constant in dstack's own code: dstack_pcr_policy() = sha256 [0, 2, 14], dstack/tpm-attest/src/lib.rs:33-35. AWS NitroTPM
binds PCR4, PCR7 and PCR12 (AWS_NITRO_TPM_BOOT_PCRS, dstack-attest/src/attestation.rs:1273,
compared at verification.rs:1209-1215).
dstack.rootfs_hash. SEV-SNP (dstack-mr/src/sev.rs:118,760,876) and TDX
(dstack-mr/src/tdx.rs:331) both require the measured cmdline to carry a well-formed dstack.rootfs_hash. There is no such call on the GCP path.
PCR0 as a firmware pin.EXPECTED_PCR0 is a single constant in source — not config, not an
allowlist, not versioned.
What GCP does well, for balance. Its runtime-event lane is the best in the tree: the guest
extends both RTMR3 and TPM PCR14 (dstack-attest/src/lib.rs:189-201), and the verifier replays and
compares both (attestation.rs:1808-1815 and :1090-1101), plus binds the vTPM quote to the TDX
quote via qualified_data == sha256(tdx_quote) (:1086-1089). Nothing in this issue is about the
runtime lane.
2. The steelman
(1) is close to forced. On GCP the TD is launched by Google's firmware and the payload is booted
through UEFI, so MRTD and RTMR0–2 describe a boot chain dstack neither builds nor publishes an
expected value for. There is no measurement.gcp.cbor field that could carry an expected MRTD,
because dstack does not produce GCP's TDVF. Routing image identity through the vTPM instead is a
reasonable response to that constraint, and the UKI Authenticode digest is a genuinely good anchor:
a UKI bundles kernel, cmdline and initrd into one PE image, so 32 bytes really do cover all three.
(3) follows from (1): with the cmdline inside the UKI, a separate dstack.rootfs_hash check adds
little.
(2) is the weakest link in the steelman. PCR7 and PCR12 are ordinary TPM PCRs available on the
same vTPM that already supplies PCR0, PCR2 and PCR14. The likely history is "we bound what the UKI
digest and the runtime lane needed", not a decision that Secure Boot state and external command-line
material are out of scope — especially since the sibling platform binds them.
(4) EXPECTED_PCR0 as a constant is a pragmatic pin on the GCP OVMF build that was current when
the path was written, and a constant fails closed, which is the right direction.
3. What it costs
One lane instead of two. TDX-legacy has two independent image checks — recompute every MR from
the downloaded image and replay the event log. GCP has one. If the vTPM lane is wrong for any
reason, the TDX registers sitting in the same quote will not catch it. This is not hypothetical as a
class: PR #1238 found that the UKI digest was being read from an event log that need never have been
quoted, and documents that POST /verify and the KMS path failed closed only "for an unrelated
reason … an accident in a different function written for a different purpose". #1238 fixes which
event log is read; it does not add a second lane.
Secure Boot state is not in the image identity on GCP but is on AWS. Whatever requireTcbUpToDate
and allowedOsImages mean on AWS, they mean strictly less on GCP in this respect.
Positional extraction.pcr2_events.get(2) (:1263) identifies "Event 28" by index rather than
by event type or description, with a GCP-OVMF-specific comment at :1254-1255. Any firmware change
that adds or reorders a PCR-2 event silently moves what is compared.
Agility. A GCP OVMF roll changes PCR0, EXPECTED_PCR0 stops matching, and every GCP verification
fails at once. The only remedy is a dstack release. That is an availability cost, not a security one,
but it is the kind of thing that gets worked around under pressure.
4. Improvement direction
Cheapest first.
Free / cheap — write down what GCP binds and what it does not, in verifier/README.md and docs/security/security-model.md, including that MRTD/RTMR0–2 are present in the quote and not
compared, and why. Today a reader of the GCP path reasonably assumes it behaves like the TDX path
because it is the same quote format. (This overlaps the result-contract issue filed alongside
this one.)
Cheap, no measurement change — make EXPECTED_PCR0 a configured allowlist rather than a
source constant, so a GCP firmware roll is an operator action rather than a release, and so more
than one GCP OVMF build can be accepted during a transition. Keeps the fail-closed default.
Deployment event — add PCR7 and PCR12 to dstack_pcr_policy() and to the GCP image-identity
comparison, mirroring AWS_NITRO_TPM_BOOT_PCRS. This changes the quoted selection, so guests and
verifiers must move together; if the new PCRs enter mr_aggregated then every allowlisted GCP
measurement has to be re-issued. Worth deciding deliberately: binding them without putting them
in mr_aggregated (a separate equality check against a value in measurement.gcp.cbor, the way
AWS's boot_pcr_digest works) gets most of the benefit with a much smaller migration.
Probably not worth it — modelling GCP's TDVF to produce expected MRTD/RTMR0–2. It would mean
tracking Google's firmware builds, which is the problem (2) exists to avoid. Recording this as
considered-and-rejected is more useful than leaving it implicit.
(1) + (2): the host / cloud operator, within whatever the vTPM lane does not already cover.
Credential: host control of the VM shape. Frequency: host-controlled. I have not demonstrated any
concrete substitution — a UKI change is caught by the Authenticode digest, and I did not identify
something that changes PCR7 or PCR12 without also changing PCR2. Treat the reachability as
unestablished; the finding is the missing defence, not a demonstrated bypass.
(4) EXPECTED_PCR0: nobody triggers it; it is an availability and agility cost borne on Google's
release schedule.
A full per-platform parity matrix backing this issue — with a file:line or an explicit "nothing",
annotated [platform] or [dstack], in every cell — is in .agent/TRUST-CHAIN-platform-parity.md.
Summary
Classification: DESIGN. All claims argued from code — no GCP hardware was available, and nothing
here is demonstrated.
On GCP TDX the OS-image identity rests on exactly two comparisons: PCR0 against a hardcoded constant,
and one 32-byte UKI Authenticode digest. The same quote carries MRTD, RTMR0, RTMR1 and RTMR2, and
none of them is compared against anything — which is mostly a platform constraint, and I want to say
so up front. What is a dstack choice, and is the part worth deciding on, is that the quoted PCR
selection omits PCR7 (Secure Boot policy) and PCR12 (kernel command line / credentials), both of
which AWS NitroTPM — the other TPM platform in this tree — binds.
Tree:
origin/next@030fbb2183.1. What the design currently is
verify_os_image_hash_for_gcp_tdx,dstack/verifier/src/verification.rs:1219-1278:expected_uki_hashisGcpOsImageMeasurement::uki_authenticode_sha256(
dstack/dstack-types/src/lib.rs:1618-1621) — the document's only field.What is not compared.
(
verification.rs:812-814) computes no expected value for any of them, anddecode_mr_gcp_tpm(
dstack/dstack-attest/src/attestation.rs:1855-1879) buildsmr_aggregatedfrom PCR0, PCR2 andthe replayed runtime PCR only — the TDX registers are not in the MR either. Compare TDX-legacy
(
verification.rs:1390-1417) and TDX-lite (:1089-1091), which compare all four.dstack_pcr_policy()= sha256[0, 2, 14],dstack/tpm-attest/src/lib.rs:33-35. AWS NitroTPMbinds PCR4, PCR7 and PCR12 (
AWS_NITRO_TPM_BOOT_PCRS,dstack-attest/src/attestation.rs:1273,compared at
verification.rs:1209-1215).dstack.rootfs_hash. SEV-SNP (dstack-mr/src/sev.rs:118,760,876) and TDX(
dstack-mr/src/tdx.rs:331) both require the measured cmdline to carry a well-formeddstack.rootfs_hash. There is no such call on the GCP path.PCR0 as a firmware pin.
EXPECTED_PCR0is a single constant in source — not config, not anallowlist, not versioned.
What GCP does well, for balance. Its runtime-event lane is the best in the tree: the guest
extends both RTMR3 and TPM PCR14 (
dstack-attest/src/lib.rs:189-201), and the verifier replays andcompares both (
attestation.rs:1808-1815and:1090-1101), plus binds the vTPM quote to the TDXquote via
qualified_data == sha256(tdx_quote)(:1086-1089). Nothing in this issue is about theruntime lane.
2. The steelman
(1) is close to forced. On GCP the TD is launched by Google's firmware and the payload is booted
through UEFI, so MRTD and RTMR0–2 describe a boot chain dstack neither builds nor publishes an
expected value for. There is no
measurement.gcp.cborfield that could carry an expected MRTD,because dstack does not produce GCP's TDVF. Routing image identity through the vTPM instead is a
reasonable response to that constraint, and the UKI Authenticode digest is a genuinely good anchor:
a UKI bundles kernel, cmdline and initrd into one PE image, so 32 bytes really do cover all three.
(3) follows from (1): with the cmdline inside the UKI, a separate
dstack.rootfs_hashcheck addslittle.
(2) is the weakest link in the steelman. PCR7 and PCR12 are ordinary TPM PCRs available on the
same vTPM that already supplies PCR0, PCR2 and PCR14. The likely history is "we bound what the UKI
digest and the runtime lane needed", not a decision that Secure Boot state and external command-line
material are out of scope — especially since the sibling platform binds them.
(4)
EXPECTED_PCR0as a constant is a pragmatic pin on the GCP OVMF build that was current whenthe path was written, and a constant fails closed, which is the right direction.
3. What it costs
One lane instead of two. TDX-legacy has two independent image checks — recompute every MR from
the downloaded image and replay the event log. GCP has one. If the vTPM lane is wrong for any
reason, the TDX registers sitting in the same quote will not catch it. This is not hypothetical as a
class: PR #1238 found that the UKI digest was being read from an event log that need never have been
quoted, and documents that
POST /verifyand the KMS path failed closed only "for an unrelatedreason … an accident in a different function written for a different purpose". #1238 fixes which
event log is read; it does not add a second lane.
Secure Boot state is not in the image identity on GCP but is on AWS. Whatever
requireTcbUpToDateand
allowedOsImagesmean on AWS, they mean strictly less on GCP in this respect.Positional extraction.
pcr2_events.get(2)(:1263) identifies "Event 28" by index rather thanby event type or description, with a GCP-OVMF-specific comment at
:1254-1255. Any firmware changethat adds or reorders a PCR-2 event silently moves what is compared.
Agility. A GCP OVMF roll changes PCR0,
EXPECTED_PCR0stops matching, and every GCP verificationfails at once. The only remedy is a dstack release. That is an availability cost, not a security one,
but it is the kind of thing that gets worked around under pressure.
4. Improvement direction
Cheapest first.
verifier/README.mdanddocs/security/security-model.md, including that MRTD/RTMR0–2 are present in the quote and notcompared, and why. Today a reader of the GCP path reasonably assumes it behaves like the TDX path
because it is the same quote format. (This overlaps the result-contract issue filed alongside
this one.)
EXPECTED_PCR0a configured allowlist rather than asource constant, so a GCP firmware roll is an operator action rather than a release, and so more
than one GCP OVMF build can be accepted during a transition. Keeps the fail-closed default.
pcr2_events.get(2). fix(tpm-qvl): bound collateral fetching from untrusted URLs, and bind the GCP image identity to an attested PCR #1238 already changes this call site to read the attested subset; pickingthe event by identity rather than index is a natural addition rather than a conflict.
dstack_pcr_policy()and to the GCP image-identitycomparison, mirroring
AWS_NITRO_TPM_BOOT_PCRS. This changes the quoted selection, so guests andverifiers must move together; if the new PCRs enter
mr_aggregatedthen every allowlisted GCPmeasurement has to be re-issued. Worth deciding deliberately: binding them without putting them
in
mr_aggregated(a separate equality check against a value inmeasurement.gcp.cbor, the wayAWS's
boot_pcr_digestworks) gets most of the benefit with a much smaller migration.tracking Google's firmware builds, which is the problem (2) exists to avoid. Recording this as
considered-and-rejected is more useful than leaving it implicit.
5. Dedup
NOVEL. Adjacent, not overlapping:
not change what is compared, and explicitly refutes the stronger "reject an event log with events
for an unattested PCR" as something that would break every real GCP quote.
os_image_is_dev,os_image_hashanchoring) and PR feat(verifier): say which trust anchor os_image_hash_verified came from #1266 cover what theresponse says about the image, not what is compared to produce it.
6. Reachability
Credential: host control of the VM shape. Frequency: host-controlled. I have not demonstrated any
concrete substitution — a UKI change is caught by the Authenticode digest, and I did not identify
something that changes PCR7 or PCR12 without also changing PCR2. Treat the reachability as
unestablished; the finding is the missing defence, not a demonstrated bypass.
EXPECTED_PCR0: nobody triggers it; it is an availability and agility cost borne on Google'srelease schedule.
A full per-platform parity matrix backing this issue — with a
file:lineor an explicit "nothing",annotated
[platform]or[dstack], in every cell — is in.agent/TRUST-CHAIN-platform-parity.md.