diff --git a/src/cmcp_gateway/audit/trace_claim.py b/src/cmcp_gateway/audit/trace_claim.py index f0a096d0..5537eea5 100644 --- a/src/cmcp_gateway/audit/trace_claim.py +++ b/src/cmcp_gateway/audit/trace_claim.py @@ -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] diff --git a/src/cmcp_gateway/startup.py b/src/cmcp_gateway/startup.py index 4c2be11b..a75ddfe1 100644 --- a/src/cmcp_gateway/startup.py +++ b/src/cmcp_gateway/startup.py @@ -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: diff --git a/tests/unit/test_trace_claim.py b/tests/unit/test_trace_claim.py index 32ddfccd..c7ad360d 100644 --- a/tests/unit/test_trace_claim.py +++ b/tests/unit/test_trace_claim.py @@ -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)