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
11 changes: 9 additions & 2 deletions src/cmcp_gateway/audit/trace_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,17 @@ def _build_runtime(report: AttestationReportInfo) -> RuntimeInfo:
if report.measurement.startswith(("sha256:", "sha384:"))
else f"sha256:{report.measurement}"
)
# CRYPTO-003: raise on malformed report_data instead of silently dropping the nonce.
# A missing nonce removes the binding between the attestation report and the session;
# a malformed report_data indicates a broken or compromised TEE shim.
try:
nonce = base64.urlsafe_b64encode(bytes.fromhex(report.report_data)).rstrip(b"=").decode()
except ValueError:
nonce = None
except ValueError as exc:
raise ValueError(
f"TEE attestation report contains malformed report_data: {exc!r}. "
"The nonce binding to the session cannot be established. "
"Check the TEE provider implementation."
) from exc

return RuntimeInfo(platform=platform, measurement=measurement, nonce=nonce) # type: ignore[arg-type]

Expand Down
8 changes: 6 additions & 2 deletions src/cmcp_gateway/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ def run_startup(config_path: str) -> GatewayContext:
signing_key = SigningKey()
logger.info("Signing key generated: %s...", signing_key.public_key_hex[:16])

# Attest with nonce = SHA-256(public_key || "startup")
# CRYPTO-002: nonce must be session-unique. Use SHA-256(public_key || random_session_id)
# so two gateways with different random bytes produce different nonces even if they
# share the same keypair (e.g. during blue-green deploy).
import hashlib
nonce = hashlib.sha256(signing_key.public_key_bytes + b"startup").digest()
import secrets
session_id = secrets.token_bytes(32)
nonce = hashlib.sha256(signing_key.public_key_bytes + session_id).digest()
try:
attestation_report = tee_provider.get_attestation_report(nonce)
except Exception as exc:
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_trace_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,36 @@ def test_to_dict_includes_signature_field():
d = _to_dict(claim)
assert "signature" in d
assert d["signature"] == ""


# ── CRYPTO-003: nonce binding ─────────────────────────────────────────────────


def test_build_runtime_valid_report_data_produces_nonce():
"""CRYPTO-003: valid hex report_data must produce a nonce in RuntimeInfo."""
from cmcp_gateway.audit.trace_claim import AttestationReportInfo, _build_runtime
report = AttestationReportInfo(
provider="sev-snp",
measurement="sha256:" + "a" * 64,
report_data="deadbeef" * 8, # valid hex
attestation_generated_at="2026-06-06T00:00:00+00:00",
attestation_validity_seconds=86400,
)
runtime = _build_runtime(report)
assert runtime.nonce is not None


def test_build_runtime_malformed_report_data_raises():
"""CRYPTO-003: malformed report_data must raise ValueError, not set nonce=None."""
import pytest

from cmcp_gateway.audit.trace_claim import AttestationReportInfo, _build_runtime
report = AttestationReportInfo(
provider="sev-snp",
measurement="sha256:" + "a" * 64,
report_data="not-hex!!",
attestation_generated_at="2026-06-06T00:00:00+00:00",
attestation_validity_seconds=86400,
)
with pytest.raises(ValueError, match="malformed report_data"):
_build_runtime(report)
Loading