Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion experiments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Each experiment imports directly from `cmcp_runtime`. Run from the repo root aft
| [claim4-trace-claim-nonce](claim4-trace-claim-nonce/) | Claim 4 — TRACE Claim nonce binding | 6 properties: nonce determinism, session/instance binding, replay prevention, sig tamper, selective disclosure |
| [claim5-temporal-adjacency](claim5-temporal-adjacency/) | Claim 5 — Temporal adjacency provenance | Zero false negatives by construction; provenance disclaimer in every summary; denied calls in graph |
| [claim6-cross-org-attestation](claim6-cross-org-attestation/) | Claim 6 — Cross-org attestation chains | Dual-TEE protocol: independent keys, session linkage, independent verify, binary swap detection |
| [claim-hw-attestation](claim-hw-attestation/) | Hardware attestation (real TEE) | Requires a confidential VM; SKIPs without one. Real report + nonce binding + end-to-end claim verification |

## Running

Expand All @@ -27,9 +28,12 @@ python experiments/claim3-rug-pull-detection/run.py
python experiments/claim4-trace-claim-nonce/run.py
python experiments/claim5-temporal-adjacency/run.py
python experiments/claim6-cross-org-attestation/run.py

# Requires a confidential VM; SKIPs cleanly (exit 0) on hosts without a TEE.
python experiments/claim-hw-attestation/run.py
```

All experiments run in software-only mode. No hardware TEE is required. TRACE Claims produced in software-only mode carry `attestation_assurance: none` and must not be used for compliance purposes.
The `claim1`–`claim6` experiments run in software-only mode. No hardware TEE is required. TRACE Claims produced in software-only mode carry `attestation_assurance: none` and must not be used for compliance purposes. The `claim-hw-attestation` experiment is the exception: it exercises a real hardware report and verification, and only produces results on a confidential VM (see [claim-hw-attestation/README.md](claim-hw-attestation/README.md)).

## CI tests

Expand Down
93 changes: 93 additions & 0 deletions experiments/claim-hw-attestation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Hardware TEE attestation experiment

This is the one experiment that needs real confidential-computing hardware. Every
other experiment in `experiments/` runs in software-only mode and produces TRACE
Claims with `attestation_assurance: none`. This one exercises the real path:
a genuine attestation report from the TEE, bound to the gateway key, wrapped in a
signed TRACE Claim, and checked by the verifier.

`run.py` is safe to run anywhere. On a host with no TEE it prints `SKIP` and exits
0, so it never breaks CI or a laptop. The properties below can only be observed on
real hardware.

## What it checks

| # | Property | Hardware-only |
|---|----------|---------------|
| P1 | A real provider is detected (sev-snp / tdx / tpm), not software-only | yes |
| P2 | `report.report_data` equals the nonce the gateway supplied | yes |
| P3 | The measurement is a real value, not the dev-mode placeholder | yes |
| P4 | A different nonce yields different `report_data` (freshness) | yes |
| P5 | The provider-specific verifier accepts the raw hardware evidence | yes |
| P6 | A full TRACE Claim verifies end to end (schema, signature, key binding) | yes |

## Hardware you need

Any one of:

| Platform | Azure | GCP |
|----------|-------|-----|
| AMD SEV-SNP | `Standard_DC2as_v5` (DCasv5) | `n2d-standard-4` + `--confidential-compute-type=SEV_SNP` |
| Intel TDX | `Standard_DC2es_v6` (DCesv6) | `c3-standard-4` + `--confidential-compute-type=TDX` |
| TPM 2.0 | any Trusted Launch VM | any VM with vTPM |

The repo's deploy scripts provision these directly:

```bash
# Azure (SEV-SNP by default; pass tdx to switch)
scripts/deploy-azure.sh

# GCP
scripts/deploy-gcp.sh
```

See `docs/tutorials/deploy-azure.md` and `docs/tutorials/deploy-gcp.md` for the
full walkthrough.

## Run it on the VM

```bash
# 1. SSH into the confidential VM provisioned above.
# 2. Install cMCP.
pip install -e .

# 3. (optional) Pin the provider explicitly instead of auto-detect.
export CMCP_DEV_MODE= # must be UNSET so software-only fallback is disabled
# either rely on auto-detection, or set provider in cmcp-config.yaml:
# attestation:
# provider: sev-snp # or tdx / tpm
# expected_measurement: "sha256:<golden>" # optional, enables HW-002 check

# 4. Run the experiment.
python experiments/claim-hw-attestation/run.py
```

Expected output on a SEV-SNP VM (abridged):

```
P1 Hardware provider detected
Provider: sev-snp
PASS: a hardware TEE provider is active (not software-only)
P2 Report binds the gateway-supplied nonce
report_data == nonce -- report is bound to this gateway key
...
P6 Full TRACE Claim verifies end to end
PASS: schema + signature verify; claim is bound to the TEE key
```

## What is NOT covered yet (needs the vendor services, not just hardware)

P5 verifies report format, measurement, and the nonce binding. It does **not** yet
verify the cert chain that proves the report was signed by genuine TEE silicon.
Those appear under `unverified_fields` until the following are wired into
`cmcp_verify`:

- **AMD SEV-SNP:** VCEK/VLEK cert-chain validation via AMD KDS
(`src/cmcp_verify/sev_snp.py`)
- **Intel TDX:** DCAP quote signature + TCB status
(`src/cmcp_verify/tdx.py`)
- **TPM:** EK certificate chain to the manufacturer CA
(`src/cmcp_verify/tpm.py`)

These require the vendor endpoints (and, for development, real reports to test
against), so they are the last step and are tracked separately from this runner.
281 changes: 281 additions & 0 deletions experiments/claim-hw-attestation/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
"""
Hardware TEE attestation experiment (requires a confidential VM).

Every other experiment in this directory runs in software-only mode and produces
TRACE Claims with attestation_assurance = none. This one exercises the *real*
hardware path end to end: it asks the detected TEE provider for a genuine
attestation report, binds the gateway key into it, builds a signed TRACE Claim,
and runs the verifier over both the claim and the raw hardware evidence.

It is safe to run anywhere: on a host with no confidential-computing hardware it
detects that and exits 0 with a SKIP message, so it does not fail in CI or on a
laptop. The hardware properties can only be demonstrated on a real SEV-SNP, TDX,
or TPM-equipped host -- see README.md for the Azure / GCP deploy steps.

Properties demonstrated (hardware only):

P1 A real TEE provider is detected (sev-snp / tdx / tpm), not software-only.
P2 The attestation report binds the gateway-supplied nonce: report.report_data
equals the nonce we passed in.
P3 The measurement is a real hardware value, not the software-only placeholder.
P4 A fresh report with a different nonce yields different report_data
(freshness / instance binding).
P5 The provider-specific verifier accepts the raw hardware evidence (format,
measurement, and report_data checks). Cert-chain appraisal (AMD KDS /
Intel DCAP / TPM EK) is reported separately -- it requires the vendor
services and is listed under unverified_fields until those are wired.
P6 A full TRACE Claim built from the real report verifies end to end: schema,
Ed25519 signature, and TEE key binding (report_data[:32] == JWK thumbprint).

Running:
pip install -e .
python experiments/claim-hw-attestation/run.py
"""
from __future__ import annotations

import base64
import hashlib
import json
import sys

from cmcp_runtime.audit.keys import SigningKey
from cmcp_runtime.audit.trace_claim import (
AttestationReportInfo,
CallGraphSummary,
CallSummary,
PolicyBundleInfo,
ToolCatalogInfo,
generate_trace_claim,
)
from cmcp_runtime.tee.base import AttestationReport, TEEProvider
from cmcp_verify.verify import ApprovedHashes, verify_trace_claim

_SW_ONLY_MEASUREMENT = "DEVELOPMENT_ONLY_NOT_FOR_PRODUCTION"
_ZERO_HASH = "sha256:" + "0" * 64


def _detect_hardware_provider() -> TEEProvider | None:
"""Return the first available hardware TEE provider, or None.

Mirrors the gateway probe order (tpm -> sev-snp -> tdx) but never falls back
to the software-only provider: this experiment is meaningless without real
hardware, so when nothing is detected we skip rather than simulate.
"""
candidates: list[TEEProvider] = []
try:
from cmcp_runtime.tee.tpm import TPMProvider

candidates.append(TPMProvider())
except ImportError:
pass
try:
from cmcp_runtime.tee.sev_snp import SEVSNPProvider

candidates.append(SEVSNPProvider())
except ImportError:
pass
try:
from cmcp_runtime.tee.tdx import TDXProvider

candidates.append(TDXProvider())
except ImportError:
pass

for provider in candidates:
try:
if provider.detect():
return provider
except Exception:
continue
return None


def _jwk_thumbprint(x_b64url: str) -> bytes:
"""RFC 7638 JWK thumbprint for an Ed25519 OKP key (members sorted: crv,kty,x).

This matches the gateway's nonce construction in cmcp_runtime.startup so the
verifier's TEE key-binding check (report_data[:32] == thumbprint) passes.
"""
members = f'{{"crv":"Ed25519","kty":"OKP","x":"{x_b64url}"}}'
return hashlib.sha256(members.encode()).digest()


def _gateway_nonce(signing_key: SigningKey, salt: bytes) -> bytes:
"""Reproduce the gateway nonce: JWK thumbprint (32) || salt (32)."""
x_b64 = base64.urlsafe_b64encode(signing_key.public_key_bytes).rstrip(b"=").decode()
return _jwk_thumbprint(x_b64) + salt


def _build_claim(report: AttestationReport, signing_key: SigningKey, session_id: str):
policy = PolicyBundleInfo(hash=_ZERO_HASH, enforcement_mode="enforcing", policy_version="1.0.0")
catalog = ToolCatalogInfo(hash=_ZERO_HASH)
summary = CallSummary(
tool_calls_total=1,
tool_calls_allowed=1,
tool_calls_denied=0,
tool_calls_faulted=0,
tools_invoked=["ehr.get_patient"],
session_max_sensitivity="hipaa_phi",
call_graph_summary=CallGraphSummary(compliance_domains_touched=["hipaa_phi"], cross_boundary_events=[]),
)
report_info = AttestationReportInfo(
provider=report.provider,
measurement=report.measurement,
report_data=report.report_data,
attestation_generated_at=report.attestation_generated_at.isoformat(),
attestation_validity_seconds=report.attestation_validity_seconds,
measurement_note=report.measurement_note,
raw_evidence=(base64.b64encode(report.raw_evidence).decode() if report.raw_evidence else None),
)
return generate_trace_claim(
session_id=session_id,
signing_key=signing_key,
attestation_report=report_info,
policy_bundle=policy,
tool_catalog=catalog,
call_summary=summary,
audit_chain_root=_ZERO_HASH,
audit_chain_tip=_ZERO_HASH,
audit_chain_length=1,
)


def _verify_raw_evidence(report: AttestationReport, signing_key: SigningKey, session_id: str):
"""Run the provider-specific verifier over the real hardware evidence."""
name = report.provider
if name == "sev-snp":
from cmcp_verify.sev_snp import verify_sev_snp_measurement

return verify_sev_snp_measurement(
measurement=report.measurement,
raw_evidence=report.raw_evidence,
report_data_hex=report.report_data,
)
if name == "tdx":
from cmcp_verify.tdx import verify_tdx_measurement

return verify_tdx_measurement(
measurement=report.measurement,
raw_evidence=report.raw_evidence,
report_data_hex=report.report_data,
)
if name == "tpm":
from cmcp_verify.tpm import verify_tpm_measurement

return verify_tpm_measurement(
measurement=report.measurement,
raw_evidence=report.raw_evidence,
tee_public_key_hex=signing_key.public_key_hex,
session_id=session_id,
)
return None


def _result(label: str, value: str) -> None:
print(f" {label}: {value}")


def main() -> int:
print()
print("Hardware TEE attestation | real attestation report + verification")
print("=" * 72)

provider = _detect_hardware_provider()
if provider is None:
print()
print("SKIP: no hardware TEE detected on this host.")
print("This experiment requires a confidential VM (AMD SEV-SNP, Intel TDX,")
print("or a TPM 2.0 device). See experiments/claim-hw-attestation/README.md")
print("for Azure / GCP deployment steps. Exiting 0 so CI and dev hosts pass.")
print()
return 0

signing_key = SigningKey()
session_id = "hw-experiment-session-001"

# --- P1: real provider ---
print()
print("P1 Hardware provider detected")
_result("Provider", provider.provider_name())
if provider.provider_name() not in ("sev-snp", "tdx", "tpm"):
print(" FAIL: detected provider is not a hardware provider")
return 1
print(" PASS: a hardware TEE provider is active (not software-only)")

# --- P2: nonce binding ---
print()
print("P2 Report binds the gateway-supplied nonce")
salt = b"\x11" * 32 # fixed salt so P4 can vary it; production uses secrets.token_bytes
nonce = _gateway_nonce(signing_key, salt)
report = provider.get_attestation_report(nonce)
_result("Nonce (hex)", nonce.hex())
_result("report_data", report.report_data)
if report.report_data != nonce.hex():
print(" FAIL: report_data does not equal the nonce we supplied")
return 1
print(" PASS: report_data == nonce -- report is bound to this gateway key")

# --- P3: real measurement ---
print()
print("P3 Measurement is a real hardware value")
_result("Measurement", report.measurement[:48] + ("..." if len(report.measurement) > 48 else ""))
_result("Raw evidence bytes", str(len(report.raw_evidence) if report.raw_evidence else 0))
if not report.measurement or report.measurement == _SW_ONLY_MEASUREMENT:
print(" FAIL: measurement is empty or the software-only placeholder")
return 1
print(" PASS: measurement is hardware-backed")

# --- P4: freshness / instance binding ---
print()
print("P4 A different nonce yields a different report")
nonce2 = _gateway_nonce(signing_key, b"\x22" * 32)
report2 = provider.get_attestation_report(nonce2)
_result("report_data #1", report.report_data)
_result("report_data #2", report2.report_data)
if report.report_data == report2.report_data:
print(" FAIL: two different nonces produced identical report_data")
return 1
print(" PASS: report_data tracks the nonce -- no replay across nonces")

# --- P5: verify raw hardware evidence ---
print()
print("P5 Provider-specific verification of the raw evidence")
raw_result = _verify_raw_evidence(report, signing_key, session_id)
if raw_result is None:
print(" FAIL: no verifier for this provider")
return 1
_result("Verified fields", ", ".join(raw_result.verified_fields) or "(none)")
_result("Unverified fields", ", ".join(raw_result.unverified_fields) or "(none)")
if raw_result.failure_reason:
_result("Failure reason", str(raw_result.failure_reason))
for k, v in raw_result.details.items():
_result(f"detail[{k}]", v)
print(" NOTE: cert-chain appraisal (AMD KDS / Intel DCAP / TPM EK) appears under")
print(" unverified_fields until the vendor services are wired in cmcp_verify.")

# --- P6: end-to-end TRACE Claim verification ---
print()
print("P6 Full TRACE Claim verifies end to end")
claim = _build_claim(report, signing_key, session_id)
claim_json = json.loads(claim.model_dump_json(exclude_none=True))
result = verify_trace_claim(
claim_json,
ApprovedHashes(policy_bundle_hash=_ZERO_HASH, tool_catalog_hash=_ZERO_HASH),
)
_result("Status", str(result.status))
_result("Verified fields", ", ".join(result.verified_fields) or "(none)")
_result("Unverified fields", ", ".join(result.unverified_fields) or "(none)")
for required in ("schema", "signature"):
if required not in result.verified_fields:
print(f" FAIL: expected '{required}' to verify")
return 1
print(" PASS: schema + signature verify; claim is bound to the TEE key")

print()
print("All hardware properties demonstrated on:", provider.provider_name())
print()
return 0


if __name__ == "__main__":
sys.exit(main())