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
{{ message }}
Repository navigation
[phase-2] Policy enforcement inside TEE — Cedar evaluation runs in the enclave, not at API boundary #124
Phase 1 enforces Cedar policy at the API layer — the gateway process evaluates policy before forwarding calls to upstream MCP servers. In Phase 1, a compromised host OS can still bypass this (it can intercept the gateway process).
Phase 2 moves the Cedar evaluation inside the TEE boundary so policy decisions are hardware-protected. The Cedar engine must run inside the enclave; the policy bundle must be measured into the TEE attestation report; the enforcement result must be signed with the TEE key.
Architecture change
Phase 1 (current):
Agent → [cMCP API layer → Cedar eval → forward] → Upstream MCP server
Phase 2 (target):
Agent → [cMCP API layer] → TEE enclave: [Cedar eval + session state + audit chain] → [forward or deny] → Upstream MCP server
The TEE boundary is at the Cedar evaluation step. The enclave holds:
The Cedar policy bundle (loaded and measured at startup)
The session state (sensitivity tags, call history)
The audit chain (hash-chained, TEE-signed)
Acceptance criteria
Cedar evaluation runs inside the enclave — the policy bundle is measured into the TEE attestation report
policy.bundle_hash in the TRACE Trust Record is the hash measured at enclave load time (tamper-evident)
policy.enforcement_mode = "enforce" means a deny inside the enclave cannot be bypassed by the host
All Phase 1 tests continue to pass
Conformance tests POLICY-TEE-001 through POLICY-TEE-005 added and passing
TRACE Trust Record policy.bundle_hash matches the value measured at startup
Notes
This is the critical path item for the GTC Berlin demo. Without it, the demo shows software-only governance (useful, but not the hardware-proof story). Target: end of Q3 2026.
moving Cedar evaluation inside the TEE closes the operator-trust gap for policy decisions. the receipt layer composes cleanly: the Cedar evaluation produces a ALLOW/DENY verdict inside the enclave, and the enclave-attested signature over the verdict is the evidence that the policy ran in a trusted environment.
the per-action receipt (action_ref = SHA-256(JCS({agent_id, action_type, scope, timestamp_ms}))) carries the same action identity as the Cedar evaluation input. a verifier checks: does the TEE attestation cover a Cedar evaluation that authorized this action_ref? the receipt and the TEE attestation compose as independent evidence layers — neither substitutes for the other.
one thing worth specifying in the Phase 2 design: what's the TEE attestation binding to? the Cedar policy snapshot digest, the evaluation input, and the verdict all need to be in the attested claim set — otherwise the TEE proves the enclave ran but not what policy it ran or what it decided. the receipt captures the 'what was decided' side; the attestation should capture 'the enclave ran this exact policy on this exact input.'
this maps to the summit framing: software enforcement cannot prove itself to someone who doesn't trust the operator. TEE + receipt closes that ceiling — the enclave attests the policy, the receipt attests the action.
Been researching this properly before proposing anything. comply54/vault gave me a head start on the general "what needs to be measured together" thinking, though the actual hardware here (SEV-SNP/TDX) is new territory for me specifically.
Picking up on @arian-gogani'''s open question above: what does the attestation actually need to bind to?
The gap, concretely:policy.bundle_hash in the TRACE Claim today is computed by ordinary Python in PolicyStore/bundle.py, at bundle load time. Nothing hardware-anchors that specific value: the SNP/TDX measurement field only covers what was loaded at VM launch, before the policy bundle is ever read. A compromised gateway process could report any hash it wants there; boot-time attestation would not notice, because policy loading happens entirely after that measurement was taken.
Proposed direction: cMCP already has the right building block for this. tee/base.py'''s nonce composition already went through one iteration of exactly this problem, for the audit chain (make_nonce to make_audit_bound_nonce, committing audit_chain_root into report_data). The natural extension is a third composition that also commits the Cedar policy bundle hash, with a fresh attestation report requested whenever PolicyEvaluator loads or hot-reloads a bundle (not just once at startup). Since report_data is a fixed 64 bytes and we'''re already using both halves (key thumbprint plus audit root), the clean way to add a third binding without losing room is to hash them together before embedding: SHA256(policy_bundle_hash || audit_chain_root) in the second half, same technique audit_root_commitment() already uses.
This deliberately does not propose baking the policy into the measured boot image. That would give the strongest possible guarantee, but kills PolicyStore'''s existing hot-reload capability, which seems like a real cost.
The part I'''m least sure about: does this fully answer your question, or were you also expecting the verdict itself (not just the policy identity) to be bound per-decision? My read is that the existing audit-chain-root commitment already covers "what was decided across the session" cumulatively, so a policy-bound nonce alongside it might close the loop without needing true per-decision attestation (which would add a hardware attestation call on every single tool call, likely too slow). But that'''s my inference, not something I want to assume.
Wanted to get this in front of you before writing any implementation, given how central this is to the Berlin timeline. Happy to prototype the mechanics (confirming a fresh mid-session attestation call actually works against the real provider interfaces) in parallel if useful, without committing to the final design yet.
Follow-up after digging further into the existing measurement code, since it directly sharpens the proposal above, and after prototyping the fix to check it actually holds up.
What's already solved:tee/measurement.py (issue #432, RFC #439 P2) already does most of what my earlier comment was reaching for, and it is hardware-validated, not theoretical. gateway_measurement() combines three things into one digest: code_digest() (every installed package, via pip's own RECORD file hashes, so transitive dependencies are covered too), policy_digest() (the actual Cedar policy bundle bytes), and config_digest() (effective config, secrets excluded). That combined digest gets extended into a TPM NV index using TPM_NT_EXTEND (a one-way, non-resettable-from-locality-0 operation, chosen specifically because application PCRs like 16 and 23 can be reset by a local attacker), then certified via two TPM2_NV_Certify calls bracketing the extend, giving a verifier a signed before/after pair to check after == H(before || digest) against. Validated on a real Azure Trusted Launch vTPM per the module docstring.
So the "what to measure" question is already answered, correctly, and proven on hardware: code plus policy plus config, as one digest.
The actual gap: this only runs for the tpm provider. _measure_gateway in startup.py returns early for anything else. SEV-SNP, TDX, and Azure-CVM have no equivalent, they are still relying purely on the boot-time measurement, which is the exact gap this issue is about.
This changes my earlier proposal. Rather than inventing a new commitment scheme (hashing policy hash and audit-chain root together into the nonce, as I suggested above), the more precise and precedented target is wiring the same, already-correct gateway_measurement() digest into SEV-SNP's and TDX's attestation reports. Those platforms do not have TPM's persistent extend-index hardware, so the binding mechanism has to be different (their report_data nonce, refreshed whenever the policy bundle reloads, is still the right tool for that), but what gets committed should be the existing, already-validated digest rather than something new.
Prototyped this end to end (software-only, no real SEV-SNP/TDX hardware available, so this proves the contract, not the hardware wiring):
A make_measurement_bound_nonce(tee_public_key, measurement_digest) helper, same 64-byte layout base.py already uses for make_audit_bound_nonce: jwk_thumbprint(pubkey) (32) || measurement_digest (32). gateway_measurement().digest is already a raw 32-byte SHA-256, so it drops into the second half with no reshaping, same as audit_root_commitment()'s output does today.
Verified against a real (non-editable) install that gateway_measurement() is deterministic across repeat calls, and that a single-character change in the Cedar bundle changes the policy component (and only the policy component, holding path and code fixed) while code and config stay identical, confirming the digest is genuinely content-addressed per component.
Round-tripped through SoftwareOnlyProvider: composed the nonce, got a report, and had an independent verifier recompute gateway_measurement() from scratch and confirm both halves of report_data (key binding and measurement) match.
Simulated a mid-session policy reload (swapped the Cedar file, matching what PolicyEvaluator._maybe_reload() does on a hash change) and called get_attestation_report() a second time in the same process. No exception, no architectural blocker. The new report's report_data reflects the new measurement, and does not match the pre-reload digest.
Confirmed the freshness property that actually matters for this issue: a report built from the stale (pre-reload) measurement is correctly rejected when a verifier recomputes the measurement fresh and compares. That is the property SEV-SNP/TDX binding needs, since unlike TPM's extend index, their report_data has no built-in append-only history, so verifier-side recompute-and-compare, refreshed on every reload, has to carry that job.
None of this touches the audit-chain-root question from my earlier comment, that is a separate concern (session activity, not gateway identity) and gateway_measurement() does not cover it.
Wanted to get this in front of you as one update rather than in pieces.
@kingztech2019 apologies for the long silence on this. Nineteen days on a comment that did prototyping work is not a good look on my side, and the analysis deserved a faster answer.
You are right, and I checked rather than took it. At mainb88871f1, _measure_gateway returns early at startup.py:160 on tee_provider.provider_name() != "tpm". The docstring justifies that with "SEV-SNP and TDX commit their own binding through the report's fields", and the thing that reasoning misses is exactly what you identified: those fields commit the launch measurement. It is boot-time and it does not move when PolicyEvaluator._maybe_reload() swaps the Cedar bundle. So on SEV-SNP, TDX and Azure-CVM the policy actually in force right now is committed to nothing.
And your revised proposal is better than your first one, for the reason you gave. Reusing the already-validated gateway_measurement() digest rather than inventing a policy-plus-audit-root commitment means there is one definition of what the gateway is, proven on hardware, instead of two that can drift. Matching make_audit_bound_nonce's existing 64-byte layout keeps it boring, which is what you want in a nonce composition. Changing your own proposal after reading the existing code is the right instinct and worth saying out loud.
Split out as #552 so it is not gated behind this issue. #124 is Cedar evaluation running inside the enclave, which is a much larger thing; measurement binding on SEV-SNP and TDX is separable, valuable on its own, and shippable now. Please open the PR against #552.
Scope there is what you prototyped, plus one thing to be explicit about: land the contract with the software-only proof you already have. Real SEV-SNP and TDX validation is a separate step and should not block it. The freshness property, where a stale pre-reload report is rejected on recompute, is the one I would most want a test around, because it is the property that carries the job the TPM extend index does for free.
Audit-chain-root binding stays out, as you said. Different concern.
Thanks for the thorough follow-through, and no apology needed, the analysis was worth the wait.
Will open the PR against #552 with the scope as written: the nonce composer, wired into SEV-SNP and TDX's report_data refreshed on reload, landed with the software-only proof, freshness test included. Finishing the sentinel PR sequence on integrations#77 first (in the order you asked for there), then moving to this.
Went and checked the current state before proposing anything else here.
Most of this is already shipped.STATUS.md lists "MCP interception + Cedar policy evaluation inside the TEE" as Shipped. The architecture explains why: this isn't an SGX-style model where the Cedar interpreter gets carved into a separate sub-enclave. It's whole-VM confidential computing — the gateway process, Cedar engine included, runs inside the SEV-SNP/TDX/Azure-CVM boundary by default, and attestation.provider: auto refuses to start on unattested hardware unless CMCP_DEV_MODE=1. So "Cedar evaluation runs inside the enclave" is true by construction for those three providers, not something left to build.
The concrete gap: POLICY-TEE-001 through POLICY-TEE-005 don't exist. Checked the whole tree, no match anywhere. That's the one item on this issue's own acceptance-criteria list that's genuinely still open, and it's authoring conformance tests for an already-shipped property rather than new architecture.
The rest of the checklist (policy.bundle_hash measured at load time, tamper-evident, matching the TRACE record) looks covered by what already landed via #432/#552/#563.
Happy to write the five conformance tests if that's the right read of what's actually left here — want to confirm the shape you'd want them to prove before I start, since "Cedar runs inside the enclave" could reasonably be tested a few different ways (measurement presence, attested-report binding, a live tamper-and-reject probe) and I'd rather not guess.
Narrowing the next step to the offered POLICY-TEE-001 through POLICY-TEE-005 conformance work. The whole-VM architecture and reported binding changes do not replace executable acceptance evidence. Please propose the five case-to-requirement mappings, marking which need live hardware, before claiming this tracker complete.
Proposing the five case-to-requirement mappings, checked directly against a fresh clone (HEAD 5b63642) rather than against my own earlier summaries in this thread.
One naming note first: tests/conformance/README.md already uses POLICY-001..005 for Group 2 (Cedar allow/deny behavior), and tests/unit/ separately reuses POLICY-001..004/008 in docstrings for hash-pinning and catalog validation — three different meanings already share the bare POLICY- prefix. POLICY-TEE-NNN avoids colliding with any of them, and I've followed test_audit_conformance.py's pattern (the one group whose README IDs are actually wired to matching test docstrings) rather than test_gateway_conformance.py's looser internal grouping.
POLICY-TEE-001 — Measurement composition.gateway_measurement() combines code+policy+config into one digest, and changing only the Cedar bundle moves only the policy component. Already covered by test_gateway_measurement.py::test_gateway_measurement_changes_with_any_component and test_measurement_report_binding.py::test_one_character_of_cedar_moves_only_the_policy_component. Software-only — pure digest computation, no hardware involved.
POLICY-TEE-002 — Binding into attestation evidence, per provider. The digest reaches report_data for sev-snp/tdx/azure-cvm-sev-snp (report_binding.py) or the NV-extend+certify pair for tpm (measurement.py) — scoped per provider, not claimed uniformly. One caveat worth stating plainly in the case itself: STATUS.md notes tpm's pair isn't yet carried in ordinary TRACE claims or refreshed on reload, so tpm doesn't reach the same end-to-end bar the other three do. Covered by test_measurement_report_binding.py::test_all_three_providers_put_the_whole_nonce_into_report_data and the NV-extend tests in test_gateway_measurement.py. Software-only for the wiring; real SEV-SNP/TDX/Azure-CVM silicon has never exercised this specific mechanism (report_binding.py:47 says so itself — out of scope for #552 on purpose). TPM's NV-extend/certify path does have a real-hardware run on record (docs/testing/hardware-validation.md, 2026-08-01).
POLICY-TEE-003 — Freshness on reload. A policy hot-reload produces an updated measurement and report; a stale pre-reload report is rejected on recompute; re-attestation fires on every reload even when the digest is unchanged. Covered by test_measurement_report_binding.py::test_reload_produces_a_report_reflecting_the_new_measurement, ::test_a_stale_pre_reload_report_is_rejected_on_recompute, ::test_every_reload_re_attests_even_when_the_measurement_is_unchanged. Software-only.
POLICY-TEE-004 — Verifier-side tamper detection.verify_trace_claim's step 7c rejects a report committing a stale/wrong measurement with MEASUREMENT_NOT_BOUND, fatal even in software-only mode (per attestation.md §3.3.2). Covered by test_measurement_report_binding.py::test_verifier_accepts_a_report_committing_the_expected_measurement and ::test_a_swapped_policy_bundle_no_longer_matches_the_report. Software-only — the verifier logic doesn't need real hardware; what it verifies (an actual signed report) is a separate, already-validated concern.
POLICY-TEE-005 — policy.bundle_hash / measurement relationship, stated precisely. This is the one where I want to flag something rather than paper over it: policy_bundle.hash (TRACE, Appendix B checklist step 7) and gateway_measurement's policy component (step 7c) are two separately-computed values, not one hash used twice — §3.3.2 checks the raw 32-byte measurement digest directly, step 7 checks the canonical JCS bundle hash separately. Both derive from the same on-disk bundle bytes. policy_bundle.hash's own tamper-evidence is already proven (experiments/claim1-policy-hash-binding + test_claim1_hash_binding.py — one-character tamper, avalanche, PolicyHashMismatch, TRACE signature breaks), and gateway_measurement's policy component has its own separate avalanche test (test_one_character_of_cedar_moves_only_the_policy_component, POLICY-TEE-001 above) — but nothing today tests both together against the same tamper in one case, which is what this ID would actually add: assert a single one-character policy change moves both digests, so a verifier checking either one independently still catches it. They are not byte-identical, and this case shouldn't imply they are. If the acceptance criterion's "matches the value measured at startup" meant literal byte-identity, that's not what's implemented and would mean undoing #552's deliberate separation of the two checks — if it meant "both anchored to the same source, both independently tamper-evident," that's the property this case would newly formalize. Would like your read on which was intended before I write it. Software-only regardless.
On "policy.enforcement_mode = "enforce" means a deny inside the enclave cannot be bypassed by the host" — I didn't fold this into the five above on purpose. It isn't a new mechanism to test; it's an emergent hardware property (memory encryption), and per threat-model.md's own A1 adversary section it doesn't hold uniformly: true for sev-snp/tdx/azure-cvm-sev-snp, explicitly not true for tpm on bare metal ("A TPM does not encrypt the memory of a running process... an operator with root can read or modify the gateway's memory while it runs, including flipping a Cedar decision, without the TPM detecting it"). No test in this repo can re-prove memory encryption — the closest existing evidence is the already-real-hardware-validated base attestation verifiers (docs/testing/hardware-validation.md, both 2026-07-27 entries). Do you want this documented as a sixth case citing the threat model and that validation record, or is it out of scope for the five-case budget?
"All Phase 1 tests continue to pass" isn't listed above since CI already covers it on every PR — not a new POLICY-TEE-NNN case.
@kingztech2019, proceed with the five bounded cases. For POLICY-TEE-005, test the two constructions against the same policy mutation, without asserting that their digests are equal. Include unchanged-policy controls and show each verification check independently rejects its mismatch.
For POLICY-TEE-002, distinguish startup measurement binding from ordinary session-claim evidence. The documented per-session report uses the same nonce slot for the audit root, so startup wiring alone does not establish both bindings in a session claim.
Document provider-specific hardware requirements separately. Existing attestation validation and memory encryption do not by themselves demonstrate that this deployment prevents host bypass. Keep that acceptance criterion open, alongside the TPM limitations; do not count a documentation citation as a passing sixth conformance case.
@kingztech2019 two changes on main since the 24 September go-ahead bear on POLICY-TEE-002. #709 (0ff0d12, 30 September) made verify_tpm_measurement reject a quote whose qualifying_data or pcrDigest does not match the claim. Before it, a quote with the wrong qualifying_data still came back verified, so the TPM rows now have two independent rejections to pair negative cases with. The report-data binding set is sev-snp, azure-cvm-sev-snp and tdx (tee/report_binding.py:74-77).
No POLICY-TEE case is on main yet. If you are still taking the five, a draft PR is enough to start review. If not, say so and the issue goes back to the open pool.
Activity
arian-gogani commentedon Jun 24, 2026
moving Cedar evaluation inside the TEE closes the operator-trust gap for policy decisions. the receipt layer composes cleanly: the Cedar evaluation produces a ALLOW/DENY verdict inside the enclave, and the enclave-attested signature over the verdict is the evidence that the policy ran in a trusted environment.
the per-action receipt (
action_ref = SHA-256(JCS({agent_id, action_type, scope, timestamp_ms}))) carries the same action identity as the Cedar evaluation input. a verifier checks: does the TEE attestation cover a Cedar evaluation that authorized thisaction_ref? the receipt and the TEE attestation compose as independent evidence layers — neither substitutes for the other.one thing worth specifying in the Phase 2 design: what's the TEE attestation binding to? the Cedar policy snapshot digest, the evaluation input, and the verdict all need to be in the attested claim set — otherwise the TEE proves the enclave ran but not what policy it ran or what it decided. the receipt captures the 'what was decided' side; the attestation should capture 'the enclave ran this exact policy on this exact input.'
this maps to the summit framing: software enforcement cannot prove itself to someone who doesn't trust the operator. TEE + receipt closes that ceiling — the enclave attests the policy, the receipt attests the action.
kingztech2019 commentedon Aug 4, 2026
Been researching this properly before proposing anything. comply54/vault gave me a head start on the general "what needs to be measured together" thinking, though the actual hardware here (SEV-SNP/TDX) is new territory for me specifically.
Picking up on @arian-gogani'''s open question above: what does the attestation actually need to bind to?
The gap, concretely:
policy.bundle_hashin the TRACE Claim today is computed by ordinary Python inPolicyStore/bundle.py, at bundle load time. Nothing hardware-anchors that specific value: the SNP/TDXmeasurementfield only covers what was loaded at VM launch, before the policy bundle is ever read. A compromised gateway process could report any hash it wants there; boot-time attestation would not notice, because policy loading happens entirely after that measurement was taken.Proposed direction: cMCP already has the right building block for this.
tee/base.py'''s nonce composition already went through one iteration of exactly this problem, for the audit chain (make_noncetomake_audit_bound_nonce, committingaudit_chain_rootintoreport_data). The natural extension is a third composition that also commits the Cedar policy bundle hash, with a fresh attestation report requested wheneverPolicyEvaluatorloads or hot-reloads a bundle (not just once at startup). Sincereport_datais a fixed 64 bytes and we'''re already using both halves (key thumbprint plus audit root), the clean way to add a third binding without losing room is to hash them together before embedding:SHA256(policy_bundle_hash || audit_chain_root)in the second half, same techniqueaudit_root_commitment()already uses.This deliberately does not propose baking the policy into the measured boot image. That would give the strongest possible guarantee, but kills
PolicyStore'''s existing hot-reload capability, which seems like a real cost.The part I'''m least sure about: does this fully answer your question, or were you also expecting the verdict itself (not just the policy identity) to be bound per-decision? My read is that the existing audit-chain-root commitment already covers "what was decided across the session" cumulatively, so a policy-bound nonce alongside it might close the loop without needing true per-decision attestation (which would add a hardware attestation call on every single tool call, likely too slow). But that'''s my inference, not something I want to assume.
Wanted to get this in front of you before writing any implementation, given how central this is to the Berlin timeline. Happy to prototype the mechanics (confirming a fresh mid-session attestation call actually works against the real provider interfaces) in parallel if useful, without committing to the final design yet.
kingztech2019 commentedon Aug 4, 2026
Follow-up after digging further into the existing measurement code, since it directly sharpens the proposal above, and after prototyping the fix to check it actually holds up.
What's already solved:
tee/measurement.py(issue #432, RFC #439 P2) already does most of what my earlier comment was reaching for, and it is hardware-validated, not theoretical.gateway_measurement()combines three things into one digest:code_digest()(every installed package, via pip's own RECORD file hashes, so transitive dependencies are covered too),policy_digest()(the actual Cedar policy bundle bytes), andconfig_digest()(effective config, secrets excluded). That combined digest gets extended into a TPM NV index usingTPM_NT_EXTEND(a one-way, non-resettable-from-locality-0 operation, chosen specifically because application PCRs like 16 and 23 can be reset by a local attacker), then certified via twoTPM2_NV_Certifycalls bracketing the extend, giving a verifier a signed before/after pair to checkafter == H(before || digest)against. Validated on a real Azure Trusted Launch vTPM per the module docstring.So the "what to measure" question is already answered, correctly, and proven on hardware: code plus policy plus config, as one digest.
The actual gap: this only runs for the
tpmprovider._measure_gatewayin startup.py returns early for anything else. SEV-SNP, TDX, and Azure-CVM have no equivalent, they are still relying purely on the boot-time measurement, which is the exact gap this issue is about.This changes my earlier proposal. Rather than inventing a new commitment scheme (hashing policy hash and audit-chain root together into the nonce, as I suggested above), the more precise and precedented target is wiring the same, already-correct
gateway_measurement()digest into SEV-SNP's and TDX's attestation reports. Those platforms do not have TPM's persistent extend-index hardware, so the binding mechanism has to be different (their report_data nonce, refreshed whenever the policy bundle reloads, is still the right tool for that), but what gets committed should be the existing, already-validated digest rather than something new.Prototyped this end to end (software-only, no real SEV-SNP/TDX hardware available, so this proves the contract, not the hardware wiring):
make_measurement_bound_nonce(tee_public_key, measurement_digest)helper, same 64-byte layoutbase.pyalready uses formake_audit_bound_nonce:jwk_thumbprint(pubkey) (32) || measurement_digest (32).gateway_measurement().digestis already a raw 32-byte SHA-256, so it drops into the second half with no reshaping, same asaudit_root_commitment()'s output does today.gateway_measurement()is deterministic across repeat calls, and that a single-character change in the Cedar bundle changes the policy component (and only the policy component, holding path and code fixed) while code and config stay identical, confirming the digest is genuinely content-addressed per component.SoftwareOnlyProvider: composed the nonce, got a report, and had an independent verifier recomputegateway_measurement()from scratch and confirm both halves ofreport_data(key binding and measurement) match.PolicyEvaluator._maybe_reload()does on a hash change) and calledget_attestation_report()a second time in the same process. No exception, no architectural blocker. The new report'sreport_datareflects the new measurement, and does not match the pre-reload digest.None of this touches the audit-chain-root question from my earlier comment, that is a separate concern (session activity, not gateway identity) and
gateway_measurement()does not cover it.Wanted to get this in front of you as one update rather than in pieces.
imran-siddique commentedon Aug 23, 2026
@kingztech2019 apologies for the long silence on this. Nineteen days on a comment that did prototyping work is not a good look on my side, and the analysis deserved a faster answer.
You are right, and I checked rather than took it. At
mainb88871f1,_measure_gatewayreturns early atstartup.py:160ontee_provider.provider_name() != "tpm". The docstring justifies that with "SEV-SNP and TDX commit their own binding through the report's fields", and the thing that reasoning misses is exactly what you identified: those fields commit the launch measurement. It is boot-time and it does not move whenPolicyEvaluator._maybe_reload()swaps the Cedar bundle. So on SEV-SNP, TDX and Azure-CVM the policy actually in force right now is committed to nothing.And your revised proposal is better than your first one, for the reason you gave. Reusing the already-validated
gateway_measurement()digest rather than inventing a policy-plus-audit-root commitment means there is one definition of what the gateway is, proven on hardware, instead of two that can drift. Matchingmake_audit_bound_nonce's existing 64-byte layout keeps it boring, which is what you want in a nonce composition. Changing your own proposal after reading the existing code is the right instinct and worth saying out loud.Split out as #552 so it is not gated behind this issue. #124 is Cedar evaluation running inside the enclave, which is a much larger thing; measurement binding on SEV-SNP and TDX is separable, valuable on its own, and shippable now. Please open the PR against #552.
Scope there is what you prototyped, plus one thing to be explicit about: land the contract with the software-only proof you already have. Real SEV-SNP and TDX validation is a separate step and should not block it. The freshness property, where a stale pre-reload report is rejected on recompute, is the one I would most want a test around, because it is the property that carries the job the TPM extend index does for free.
Audit-chain-root binding stays out, as you said. Different concern.
#124 stays open as the enclave-Cedar tracker.
kingztech2019 commentedon Aug 24, 2026
Thanks for the thorough follow-through, and no apology needed, the analysis was worth the wait.
Will open the PR against #552 with the scope as written: the nonce composer, wired into SEV-SNP and TDX's
report_datarefreshed on reload, landed with the software-only proof, freshness test included. Finishing the sentinel PR sequence on integrations#77 first (in the order you asked for there), then moving to this.kingztech2019 commentedon Aug 26, 2026
Went and checked the current state before proposing anything else here.
Most of this is already shipped.
STATUS.mdlists "MCP interception + Cedar policy evaluation inside the TEE" as Shipped. The architecture explains why: this isn't an SGX-style model where the Cedar interpreter gets carved into a separate sub-enclave. It's whole-VM confidential computing — the gateway process, Cedar engine included, runs inside the SEV-SNP/TDX/Azure-CVM boundary by default, andattestation.provider: autorefuses to start on unattested hardware unlessCMCP_DEV_MODE=1. So "Cedar evaluation runs inside the enclave" is true by construction for those three providers, not something left to build.The concrete gap:
POLICY-TEE-001throughPOLICY-TEE-005don't exist. Checked the whole tree, no match anywhere. That's the one item on this issue's own acceptance-criteria list that's genuinely still open, and it's authoring conformance tests for an already-shipped property rather than new architecture.The rest of the checklist (
policy.bundle_hashmeasured at load time, tamper-evident, matching the TRACE record) looks covered by what already landed via #432/#552/#563.Happy to write the five conformance tests if that's the right read of what's actually left here — want to confirm the shape you'd want them to prove before I start, since "Cedar runs inside the enclave" could reasonably be tested a few different ways (measurement presence, attested-report binding, a live tamper-and-reject probe) and I'd rather not guess.
imran-siddique commentedon Sep 7, 2026
Narrowing the next step to the offered POLICY-TEE-001 through POLICY-TEE-005 conformance work. The whole-VM architecture and reported binding changes do not replace executable acceptance evidence. Please propose the five case-to-requirement mappings, marking which need live hardware, before claiming this tracker complete.
kingztech2019 commentedon Sep 7, 2026
Proposing the five case-to-requirement mappings, checked directly against a fresh clone (HEAD
5b63642) rather than against my own earlier summaries in this thread.One naming note first:
tests/conformance/README.mdalready usesPOLICY-001..005for Group 2 (Cedar allow/deny behavior), andtests/unit/separately reusesPOLICY-001..004/008in docstrings for hash-pinning and catalog validation — three different meanings already share the barePOLICY-prefix.POLICY-TEE-NNNavoids colliding with any of them, and I've followedtest_audit_conformance.py's pattern (the one group whose README IDs are actually wired to matching test docstrings) rather thantest_gateway_conformance.py's looser internal grouping.POLICY-TEE-001 — Measurement composition.
gateway_measurement()combines code+policy+config into one digest, and changing only the Cedar bundle moves only the policy component. Already covered bytest_gateway_measurement.py::test_gateway_measurement_changes_with_any_componentandtest_measurement_report_binding.py::test_one_character_of_cedar_moves_only_the_policy_component. Software-only — pure digest computation, no hardware involved.POLICY-TEE-002 — Binding into attestation evidence, per provider. The digest reaches
report_dataforsev-snp/tdx/azure-cvm-sev-snp(report_binding.py) or the NV-extend+certify pair fortpm(measurement.py) — scoped per provider, not claimed uniformly. One caveat worth stating plainly in the case itself:STATUS.mdnotestpm's pair isn't yet carried in ordinary TRACE claims or refreshed on reload, sotpmdoesn't reach the same end-to-end bar the other three do. Covered bytest_measurement_report_binding.py::test_all_three_providers_put_the_whole_nonce_into_report_dataand the NV-extend tests intest_gateway_measurement.py. Software-only for the wiring; real SEV-SNP/TDX/Azure-CVM silicon has never exercised this specific mechanism (report_binding.py:47says so itself — out of scope for #552 on purpose). TPM's NV-extend/certify path does have a real-hardware run on record (docs/testing/hardware-validation.md, 2026-08-01).POLICY-TEE-003 — Freshness on reload. A policy hot-reload produces an updated measurement and report; a stale pre-reload report is rejected on recompute; re-attestation fires on every reload even when the digest is unchanged. Covered by
test_measurement_report_binding.py::test_reload_produces_a_report_reflecting_the_new_measurement,::test_a_stale_pre_reload_report_is_rejected_on_recompute,::test_every_reload_re_attests_even_when_the_measurement_is_unchanged. Software-only.POLICY-TEE-004 — Verifier-side tamper detection.
verify_trace_claim's step 7c rejects a report committing a stale/wrong measurement withMEASUREMENT_NOT_BOUND, fatal even in software-only mode (per attestation.md §3.3.2). Covered bytest_measurement_report_binding.py::test_verifier_accepts_a_report_committing_the_expected_measurementand::test_a_swapped_policy_bundle_no_longer_matches_the_report. Software-only — the verifier logic doesn't need real hardware; what it verifies (an actual signed report) is a separate, already-validated concern.POLICY-TEE-005 —
policy.bundle_hash/ measurement relationship, stated precisely. This is the one where I want to flag something rather than paper over it:policy_bundle.hash(TRACE, Appendix B checklist step 7) andgateway_measurement's policy component (step 7c) are two separately-computed values, not one hash used twice — §3.3.2 checks the raw 32-byte measurement digest directly, step 7 checks the canonical JCS bundle hash separately. Both derive from the same on-disk bundle bytes.policy_bundle.hash's own tamper-evidence is already proven (experiments/claim1-policy-hash-binding+test_claim1_hash_binding.py— one-character tamper, avalanche,PolicyHashMismatch, TRACE signature breaks), andgateway_measurement's policy component has its own separate avalanche test (test_one_character_of_cedar_moves_only_the_policy_component, POLICY-TEE-001 above) — but nothing today tests both together against the same tamper in one case, which is what this ID would actually add: assert a single one-character policy change moves both digests, so a verifier checking either one independently still catches it. They are not byte-identical, and this case shouldn't imply they are. If the acceptance criterion's "matches the value measured at startup" meant literal byte-identity, that's not what's implemented and would mean undoing #552's deliberate separation of the two checks — if it meant "both anchored to the same source, both independently tamper-evident," that's the property this case would newly formalize. Would like your read on which was intended before I write it. Software-only regardless.On "
policy.enforcement_mode = "enforce"means a deny inside the enclave cannot be bypassed by the host" — I didn't fold this into the five above on purpose. It isn't a new mechanism to test; it's an emergent hardware property (memory encryption), and perthreat-model.md's own A1 adversary section it doesn't hold uniformly: true forsev-snp/tdx/azure-cvm-sev-snp, explicitly not true fortpmon bare metal ("A TPM does not encrypt the memory of a running process... an operator with root can read or modify the gateway's memory while it runs, including flipping a Cedar decision, without the TPM detecting it"). No test in this repo can re-prove memory encryption — the closest existing evidence is the already-real-hardware-validated base attestation verifiers (docs/testing/hardware-validation.md, both 2026-07-27 entries). Do you want this documented as a sixth case citing the threat model and that validation record, or is it out of scope for the five-case budget?"All Phase 1 tests continue to pass" isn't listed above since CI already covers it on every PR — not a new POLICY-TEE-NNN case.
imran-siddique commentedon Sep 24, 2026
@kingztech2019, proceed with the five bounded cases. For POLICY-TEE-005, test the two constructions against the same policy mutation, without asserting that their digests are equal. Include unchanged-policy controls and show each verification check independently rejects its mismatch.
For POLICY-TEE-002, distinguish startup measurement binding from ordinary session-claim evidence. The documented per-session report uses the same nonce slot for the audit root, so startup wiring alone does not establish both bindings in a session claim.
Document provider-specific hardware requirements separately. Existing attestation validation and memory encryption do not by themselves demonstrate that this deployment prevents host bypass. Keep that acceptance criterion open, alongside the TPM limitations; do not count a documentation citation as a passing sixth conformance case.
imran-siddique commentedon Oct 15, 2026
@kingztech2019 two changes on main since the 24 September go-ahead bear on POLICY-TEE-002. #709 (0ff0d12, 30 September) made
verify_tpm_measurementreject a quote whosequalifying_dataorpcrDigestdoes not match the claim. Before it, a quote with the wrongqualifying_datastill came back verified, so the TPM rows now have two independent rejections to pair negative cases with. The report-data binding set issev-snp,azure-cvm-sev-snpandtdx(tee/report_binding.py:74-77).No POLICY-TEE case is on main yet. If you are still taking the five, a draft PR is enough to start review. If not, say so and the issue goes back to the open pool.