diff --git a/README.md b/README.md index 3ca693b..87556ec 100644 --- a/README.md +++ b/README.md @@ -115,15 +115,15 @@ Agent -> cMCP Runtime -> Cedar Policy Engine (TEE) -> Tool | `sev-snp` | AMD SEV-SNP (Azure DCasv5, AWS C6a Nitro) | High | AMD KDS | | `tdx` | Intel TDX (Azure DCedsv5, GCP C3) | High | Intel PCS | | `gpu-cc` _(v0.2)_ | NVIDIA H100/H200/Blackwell (CC mode) | High | NVIDIA Remote Attestation Service (NRAS) | -| `opaque` _(opt-in)_ | OPAQUE Confidential Runtime | n/a _(not yet implemented)_ | Placeholder: `detect()` returns `False`, so it is never auto-selected until implemented | +| `opaque` _(opt-in)_ | OPAQUE Confidential Runtime | n/a _(not yet implemented)_ | Placeholder: excluded from auto-detect; selecting it explicitly raises a not-implemented error | -Provider auto-detect probe order: `tpm -> sev-snp -> tdx -> opaque` — the first provider whose `detect()` succeeds is selected. `opaque` is a not-yet-implemented placeholder and is never auto-selected. If no hardware provider is detected, the gateway starts only under `CMCP_DEV_MODE=1` (a non-attested software-only fallback) and otherwise refuses to start. +Provider auto-detect probe order: `tpm -> sev-snp -> tdx` — the first provider whose `detect()` succeeds is selected. `opaque` is a not-yet-implemented placeholder: it is excluded from auto-detect, and selecting it explicitly raises `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` rather than falling through silently. If no hardware provider is detected, the gateway starts only under `CMCP_DEV_MODE=1` (a non-attested software-only fallback) and otherwise refuses to start. ```python from cmcp_runtime.config import TEEProvider # Auto-detect (default) -# attestation.provider: auto -> tpm -> sev-snp -> tdx -> opaque +# attestation.provider: auto -> tpm -> sev-snp -> tdx # (software-only is used only under CMCP_DEV_MODE=1) # Explicit hardware selection diff --git a/STATUS.md b/STATUS.md index 1b15152..d9cbad2 100644 --- a/STATUS.md +++ b/STATUS.md @@ -8,7 +8,7 @@ picture is stated once. Developer Preview: interfaces may change before v1.0. | Setting | Default | |---|---| -| `attestation.provider` | `auto` (probe order `tpm -> sev-snp -> tdx -> opaque`) | +| `attestation.provider` | `auto` (probe order `tpm -> sev-snp -> tdx`) | | `attestation.enforcement_mode` | `enforcing` | | `attestation.staleness_policy` | `fail_closed` | | `attestation.validity_seconds` | `86400` | @@ -25,7 +25,7 @@ picture is stated once. Developer Preview: interfaces may change before v1.0. | Offline verification (`cmcp_verify`) | Shipped | No operator trust required when the verifier independently checks the attestation report. | | Agent Manifest identity binding | Shipped | Optional; trust in the issuer key is an out-of-band PKI concern. | | Attestation verifiers: `tpm`, `sev-snp`, `tdx` | Partial | Report parsing + certificate-chain verification against real vendor roots; report-signature paths validated with synthetic vectors. End-to-end validation against a real hardware quote on a confidential VM is pending — do not describe as fully hardware-attested until then. | -| `opaque` provider | Not implemented | Opt-in placeholder; `detect()` returns `False`, so it is never auto-selected until implemented. | +| `opaque` provider | Not implemented | Opt-in placeholder; excluded from auto-detect. Selecting it explicitly raises `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` rather than falling through silently. | | `gpu-cc` (NVIDIA H100/H200/Blackwell, via NRAS) | Planned (v0.2) | | | Transparency-log anchoring for TRACE Claims | v0.2 | Write and lookup. | | Server-side (provider) attestation | Not yet (Phase 2) | Phase 1 attests the gateway boundary only. | diff --git a/docs/SPEC.md b/docs/SPEC.md index bec0db6..bf1e2b1 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -258,11 +258,12 @@ Across the four problems and 13 shapes, Phase 1 covers 11 outright and partially | tdx | Intel TDX (Azure DCedsv5, GCP C3) | High | | opaque | OPAQUE Managed Runtime (opt-in; not yet implemented) | n/a | -Auto-detection probe order: `tpm -> sev-snp -> tdx -> opaque`. The first provider whose -`detect()` succeeds is selected. `opaque` is a placeholder whose `detect()` currently returns -`False`, so it is never auto-selected until it is implemented. If no hardware provider is -detected, the gateway starts only under `CMCP_DEV_MODE=1` (a non-attested software-only -fallback) and otherwise refuses to start. Default `enforcement_mode` is `enforcing`. +Auto-detection probe order: `tpm -> sev-snp -> tdx`. The first provider whose `detect()` +succeeds is selected. `opaque` is a not-yet-implemented placeholder: it is excluded from +auto-detect, and selecting it explicitly raises `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` rather +than falling through silently. If no hardware provider is detected, the gateway starts only +under `CMCP_DEV_MODE=1` (a non-attested software-only fallback) and otherwise refuses to +start. Default `enforcement_mode` is `enforcing`. --- diff --git a/docs/spec/attestation.md b/docs/spec/attestation.md index 5d0fe82..ef47b78 100644 --- a/docs/spec/attestation.md +++ b/docs/spec/attestation.md @@ -19,9 +19,11 @@ The cMCP Runtime produces TRACE Claims: signed, hardware-attested proof artifact At runtime startup, the process probes for TEE providers in the following fixed order. The first provider whose conditions are satisfied is selected. Only one provider is active per runtime instance. ``` -probe_order = ["tpm", "sev-snp", "tdx", "opaque"] +probe_order = ["tpm", "sev-snp", "tdx"] ``` +The `opaque` (OPAQUE managed-runtime) provider is a recognized but not-yet-implemented placeholder. It is intentionally excluded from `probe_order`, so it is never auto-selected. Selecting it explicitly (`attestation.provider: opaque`) raises `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` rather than reporting itself as "not detected". + The detection loop: ``` @@ -97,7 +99,12 @@ The full TD report and quote are stored in `attestation_report.raw_evidence` for #### OPAQUE (Highest Assurance) -Detection conditions: +> **Not yet implemented.** This subsection describes the intended design. The current +> `OpaqueProvider` is a placeholder: it is excluded from auto-detect and raises +> `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` when selected explicitly. The conditions below +> are the planned detection behavior, not shipped behavior. + +Detection conditions (planned): - The environment variable `OPAQUE_RUNTIME_ENDPOINT` is set and non-empty. What goes in `attestation_report.measurement`: diff --git a/docs/spec/error-codes.md b/docs/spec/error-codes.md index 8592d22..a4dfefb 100644 --- a/docs/spec/error-codes.md +++ b/docs/spec/error-codes.md @@ -8,6 +8,7 @@ This is the normative registry for all error codes used across the cMCP Runtime. |---|---|---|---|---| | `ATTESTATION_REPORT_UNAVAILABLE` | 503 | FATAL | TEE provider did not return an attestation report within timeout | [failure-modes.md FM-1](failure-modes.md) | | `ATTESTATION_PROVIDER_UNSUPPORTED` | 500 | FATAL | No supported TEE provider detected and `CMCP_DEV_MODE` is not set | [attestation.md §1.1](attestation.md) | +| `ATTESTATION_PROVIDER_NOT_IMPLEMENTED` | 501 | FATAL | A recognized provider was explicitly selected but is not yet implemented (e.g. `opaque`) | [attestation.md §1.1](attestation.md) | | `POLICY_HASH_MISMATCH` | 500 | FATAL | Measured policy bundle hash does not match deployment manifest | [failure-modes.md FM-4](failure-modes.md) | | `CATALOG_HASH_MISMATCH` | 500 | FATAL | Measured catalog hash does not match deployment manifest | [attestation.md §5](attestation.md) | | `AGENT_MANIFEST_BINDING_FAILED` | 500 | FATAL | Signed Agent Manifest signature, authenticated subject, policy hash, or catalog hash did not match the runtime session inputs | [session-policy.md](session-policy.md) | diff --git a/src/cmcp_runtime/errors.py b/src/cmcp_runtime/errors.py index fd37b13..f9b7439 100644 --- a/src/cmcp_runtime/errors.py +++ b/src/cmcp_runtime/errors.py @@ -19,6 +19,19 @@ class AttestationProviderUnsupported(CMCPError): http_status = 500 +class AttestationProviderNotImplemented(AttestationProviderUnsupported): + """A recognized provider was explicitly selected but is not yet implemented. + + Distinct from AttestationProviderUnsupported (hardware simply not present): + this signals a known placeholder provider (e.g. ``opaque``) so an operator who + selects it gets an explicit error instead of a silent fall-through. Subclasses + AttestationProviderUnsupported so the gateway still refuses to start. + """ + + code = "ATTESTATION_PROVIDER_NOT_IMPLEMENTED" + http_status = 501 + + class PolicyHashMismatch(CMCPError): code = "POLICY_HASH_MISMATCH" http_status = 500 diff --git a/src/cmcp_runtime/tee/detect.py b/src/cmcp_runtime/tee/detect.py index d0cb626..ee2ec30 100644 --- a/src/cmcp_runtime/tee/detect.py +++ b/src/cmcp_runtime/tee/detect.py @@ -6,13 +6,18 @@ from cmcp_runtime.config import Config from cmcp_runtime.config import TEEProvider as TEEProviderEnum -from cmcp_runtime.errors import AttestationProviderUnsupported +from cmcp_runtime.errors import ( + AttestationProviderNotImplemented, + AttestationProviderUnsupported, +) from cmcp_runtime.tee.base import SoftwareOnlyProvider, TEEProvider logger = logging.getLogger(__name__) -# Detection probe order from docs/spec/attestation.md §1.1 -_PROBE_ORDER: list[str] = ["tpm", "sev-snp", "tdx", "opaque"] +# Detection probe order from docs/spec/attestation.md §1.1. The `opaque` provider is +# intentionally excluded: it is a not-yet-implemented placeholder, so it is never +# auto-selected. Selecting it explicitly raises AttestationProviderNotImplemented. +_PROBE_ORDER: list[str] = ["tpm", "sev-snp", "tdx"] def _get_provider_impl(name: str, config: Config | None = None) -> TEEProvider | None: @@ -50,7 +55,7 @@ def detect_provider(config: Config) -> TEEProvider: Detect and return the active TEE provider. Follows the probe order from docs/spec/attestation.md §1.1: - tpm -> sev-snp -> tdx -> opaque + tpm -> sev-snp -> tdx If no hardware provider is found: - CMCP_DEV_MODE=1: returns SoftwareOnlyProvider with a WARN log @@ -74,7 +79,15 @@ def detect_provider(config: Config) -> TEEProvider: ) return SoftwareOnlyProvider() impl = _get_provider_impl(name, config) - if impl is None or not impl.detect(): + if impl is None: + raise AttestationProviderUnsupported( + f"Requested provider '{name}' not available on this host", + detail="Check that the TEE hardware is present and accessible", + ) + # A placeholder provider (e.g. opaque) raises AttestationProviderNotImplemented + # from detect(); let that explicit error propagate rather than collapsing it into + # a generic "not available on this host". + if not impl.detect(): raise AttestationProviderUnsupported( f"Requested provider '{name}' not available on this host", detail="Check that the TEE hardware is present and accessible", @@ -85,7 +98,15 @@ def detect_provider(config: Config) -> TEEProvider: # Auto-detection for name in _PROBE_ORDER: impl = _get_provider_impl(name, config) - if impl is not None and impl.detect(): + if impl is None: + continue + try: + available = impl.detect() + except AttestationProviderNotImplemented: + # Defensive: a not-yet-implemented provider must never be auto-selected. + logger.debug("Provider %s is not implemented; skipping in auto-detect", name) + continue + if available: logger.info("TEE provider: %s (auto-detected)", name) return impl diff --git a/src/cmcp_runtime/tee/opaque.py b/src/cmcp_runtime/tee/opaque.py index 2dd3534..ef51004 100644 --- a/src/cmcp_runtime/tee/opaque.py +++ b/src/cmcp_runtime/tee/opaque.py @@ -1,18 +1,35 @@ -"""Opaque Systems TEE provider stub: not yet implemented.""" +"""OPAQUE managed-runtime TEE provider: not yet implemented. + +Selecting this provider raises AttestationProviderNotImplemented rather than +silently reporting "not detected". It is excluded from the auto-detect probe order. +""" from __future__ import annotations +from cmcp_runtime.errors import AttestationProviderNotImplemented from cmcp_runtime.tee.base import AttestationReport, TEEProvider +_NOT_IMPLEMENTED_MSG = ( + "The OPAQUE managed-runtime attestation provider is not yet implemented. " + "Select tpm, sev-snp, or tdx, or set CMCP_DEV_MODE=1 for software-only development." +) + class OpaqueProvider(TEEProvider): - """Placeholder for the Opaque Systems TEE provider (not yet implemented).""" + """Placeholder for the OPAQUE managed-runtime provider (not yet implemented). + + Unlike a hardware provider that is simply absent, ``detect`` and + ``get_attestation_report`` raise AttestationProviderNotImplemented rather than + reporting "not detected", so explicitly selecting this provider yields a clear + error instead of a silent fall-through. It is intentionally excluded from the + auto-detect probe order (see ``tee/detect.py``). + """ def provider_name(self) -> str: return "opaque" def detect(self) -> bool: - return False + raise AttestationProviderNotImplemented(_NOT_IMPLEMENTED_MSG) def get_attestation_report(self, nonce: bytes) -> AttestationReport: - raise NotImplementedError("Opaque provider not yet implemented") + raise AttestationProviderNotImplemented(_NOT_IMPLEMENTED_MSG) diff --git a/tests/unit/test_tee.py b/tests/unit/test_tee.py index 6071a69..3139da3 100644 --- a/tests/unit/test_tee.py +++ b/tests/unit/test_tee.py @@ -9,7 +9,10 @@ from cmcp_runtime.config import Config from cmcp_runtime.config import TEEProvider as TEEProviderEnum -from cmcp_runtime.errors import AttestationProviderUnsupported +from cmcp_runtime.errors import ( + AttestationProviderNotImplemented, + AttestationProviderUnsupported, +) from cmcp_runtime.tee.base import SoftwareOnlyProvider, jwk_thumbprint, make_nonce from cmcp_runtime.tee.detect import detect_provider @@ -138,6 +141,21 @@ def test_detect_explicit_software_only_with_dev_mode(dev_config): assert isinstance(provider, SoftwareOnlyProvider) +def test_detect_explicit_opaque_raises_not_implemented(dev_config): + """Explicitly selecting the opaque provider raises an explicit not-implemented error, + not a silent fall-through or a generic 'unsupported'.""" + dev_config.attestation.provider = TEEProviderEnum.OPAQUE + with pytest.raises(AttestationProviderNotImplemented): + detect_provider(dev_config) + + +def test_opaque_excluded_from_auto_probe_order(): + """The not-yet-implemented opaque provider must never be in the auto-detect order.""" + from cmcp_runtime.tee.detect import _PROBE_ORDER + + assert "opaque" not in _PROBE_ORDER + + # ── HW-001: AttestationReport provider validation ──────────────────────────── def test_attestation_report_unknown_provider_raises(): diff --git a/tests/unit/test_tee_providers.py b/tests/unit/test_tee_providers.py index 63c6160..fcb11a3 100644 --- a/tests/unit/test_tee_providers.py +++ b/tests/unit/test_tee_providers.py @@ -11,6 +11,7 @@ import pytest +from cmcp_runtime.errors import AttestationProviderNotImplemented from cmcp_runtime.tee.opaque import OpaqueProvider from cmcp_runtime.tee.sev_snp import SEVSNPProvider, _SnpAttestationReport from cmcp_runtime.tee.tdx import TDXProvider, _TdxReportReq @@ -18,12 +19,13 @@ # ── OpaqueProvider ───────────────────────────────────────────────────────────── -def test_opaque_detect_returns_false() -> None: - assert OpaqueProvider().detect() is False +def test_opaque_detect_raises_not_implemented() -> None: + with pytest.raises(AttestationProviderNotImplemented): + OpaqueProvider().detect() -def test_opaque_get_report_raises() -> None: - with pytest.raises(NotImplementedError): +def test_opaque_get_report_raises_not_implemented() -> None: + with pytest.raises(AttestationProviderNotImplemented): OpaqueProvider().get_attestation_report(b"\x00" * 32)