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
23 changes: 21 additions & 2 deletions src/cmcp_runtime/tee/tpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import hashlib
import logging
import subprocess # nosec B404
import sys
from datetime import UTC, datetime
Expand All @@ -11,6 +12,8 @@

from cmcp_runtime.tee.base import AttestationReport, TEEProvider

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
pass

Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/test_tee_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading