From 31e39cd705e33d9a744ca475ad9a06f1dc34d902 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Thu, 18 Jun 2026 21:45:34 -0700 Subject: [PATCH] security: downgrade TPM attestation to software-only on SHA-1 fallback SHA-1 PCR measurements are cryptographically broken. When the TPM SHA-256 bank is unavailable and the provider falls back to SHA-1, the resulting measurement must not present as hardware-attested in the TRACE Claim. Set platform to software-only and emit a warning. Signed-off-by: Imran Siddique Co-Authored-By: Claude Sonnet 4.6 --- src/cmcp_runtime/tee/tpm.py | 23 +++++++++++-- tests/unit/test_tee_providers.py | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/cmcp_runtime/tee/tpm.py b/src/cmcp_runtime/tee/tpm.py index aea19c9..871ed14 100644 --- a/src/cmcp_runtime/tee/tpm.py +++ b/src/cmcp_runtime/tee/tpm.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib +import logging import subprocess # nosec B404 import sys from datetime import UTC, datetime @@ -11,6 +12,8 @@ from cmcp_runtime.tee.base import AttestationReport, TEEProvider +logger = logging.getLogger(__name__) + if TYPE_CHECKING: pass @@ -74,6 +77,11 @@ def _report_via_tss2(self, nonce: bytes) -> AttestationReport: except Exception: # noqa: BLE001 # Fall back to SHA-1 measurement_note = "sha1-bank-fallback" + logger.warning( + "TPM SHA-1 fallback: SHA-256 PCR bank unavailable. " + "Downgrading attestation to software-only. " + "TRACE Claim will not present as hardware-attested." + ) pcr_sel = TPML_PCR_SELECTION.parse("sha1:0,1,2,3,4,5,6,7") _, _, digests = ectx.pcr_read(pcr_sel) raw_pcrs = [] @@ -105,8 +113,11 @@ def _report_via_tss2(self, nonce: bytes) -> AttestationReport: except Exception: # noqa: BLE001 raw_evidence = None + effective_provider = ( + "software-only" if measurement_note == "sha1-bank-fallback" else self.provider_name() + ) return AttestationReport( - provider=self.provider_name(), + provider=effective_provider, measurement=measurement, report_data=nonce.hex(), raw_evidence=raw_evidence, @@ -145,6 +156,11 @@ def _report_via_subprocess(self, nonce: bytes) -> AttestationReport: f"{result.returncode}: {result.stderr.strip()}" ) measurement_note: str | None = "sha1-bank-fallback" + logger.warning( + "TPM SHA-1 fallback: SHA-256 PCR bank unavailable. " + "Downgrading attestation to software-only. " + "TRACE Claim will not present as hardware-attested." + ) else: measurement_note = None @@ -157,8 +173,11 @@ def _report_via_subprocess(self, nonce: bytes) -> AttestationReport: concatenated = b"".join(pcr_values[:8]) measurement = "sha256:" + hashlib.sha256(concatenated).hexdigest() + effective_provider = ( + "software-only" if measurement_note == "sha1-bank-fallback" else self.provider_name() + ) return AttestationReport( - provider=self.provider_name(), + provider=effective_provider, measurement=measurement, report_data=nonce.hex(), raw_evidence=None, diff --git a/tests/unit/test_tee_providers.py b/tests/unit/test_tee_providers.py index 0687611..63c6160 100644 --- a/tests/unit/test_tee_providers.py +++ b/tests/unit/test_tee_providers.py @@ -205,3 +205,59 @@ def test_tpm_get_report_raises_when_no_tss2(monkeypatch: pytest.MonkeyPatch) -> def test_tpm_detect_does_not_raise_on_exception() -> None: with patch.object(Path, "exists", side_effect=PermissionError("no access")): assert TPMProvider().detect() is False + + +def test_tpm_sha1_fallback_subprocess_produces_software_only_provider( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """SHA-1 fallback via subprocess must downgrade provider to software-only.""" + monkeypatch.setattr("cmcp_runtime.tee.tpm._TSS2_AVAILABLE", False) + + sha256_fail = MagicMock(spec=subprocess.CompletedProcess) + sha256_fail.returncode = 1 + sha256_fail.stderr = "algorithm not supported" + sha256_fail.stdout = "" + + sha1_pcr_output = "\n".join( + [ + "sha1:", + *[f" {i} : 0x" + ("ab" * 20) for i in range(8)], + ] + ) + sha1_ok = MagicMock(spec=subprocess.CompletedProcess) + sha1_ok.returncode = 0 + sha1_ok.stderr = "" + sha1_ok.stdout = sha1_pcr_output + + monkeypatch.setattr(subprocess, "run", MagicMock(side_effect=[sha256_fail, sha1_ok])) + + report = TPMProvider().get_attestation_report(b"\x00" * 32) + + assert report.provider == "software-only" + assert report.measurement_note == "sha1-bank-fallback" + assert report.measurement.startswith("sha256:") + + +def test_tpm_sha256_success_subprocess_keeps_tpm_provider( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """When SHA-256 PCR bank is available, provider must remain 'tpm'.""" + monkeypatch.setattr("cmcp_runtime.tee.tpm._TSS2_AVAILABLE", False) + + sha256_pcr_output = "\n".join( + [ + "sha256:", + *[f" {i} : 0x" + ("cd" * 32) for i in range(8)], + ] + ) + sha256_ok = MagicMock(spec=subprocess.CompletedProcess) + sha256_ok.returncode = 0 + sha256_ok.stderr = "" + sha256_ok.stdout = sha256_pcr_output + + monkeypatch.setattr(subprocess, "run", MagicMock(return_value=sha256_ok)) + + report = TPMProvider().get_attestation_report(b"\x00" * 32) + + assert report.provider == "tpm" + assert report.measurement_note is None