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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Raised the `agent-manifest` floor to `>=0.6.1` and made an unappraisable manifest fail closed with a diagnostic message. `verify_agent_manifest_binding` runs the SDK verifier over a peer-supplied manifest, and before 0.6.1 a manifest declaring `ML-DSA-65` or `hybrid-Ed25519-ML-DSA-65` crashed the SDK with an uncaught `RuntimeError` on any install without the optional `[pq]` extra, so this path answered a crash instead of a rejection. The SDK now returns `UNVERIFIABLE`, which cMCP already rejects; the `UNVERIFIABLE` message now distinguishes "could not be verified" (missing trusted key or unavailable algorithm) from "verification failed" (a bad signature) and surfaces the verifier's own reason, because those call for different operator responses.

### Changed

- Attestation crypto now delegates to agent-manifest's shared verification library (`agent-manifest>=0.5`) instead of cMCP's own copies: the SEV-SNP report signature (`verify_snp_signature`) and the VCEK/PCK certificate-chain verification (the generic `verify_cert_chain`) are shared across the org rather than duplicated. cMCP keeps its own DCAP quote parser, `*VerificationResult` shapes, TR-field semantics, and report_data/qualifying-data bindings; behavior is unchanged (all tests pass unchanged). cMCP's TPM verifier stays local (Phase-1 parse-only: no AK signature/chain to share).
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ classifiers = [
requires-python = ">=3.11"
dependencies = [
"agentrust-trace>=0.4",
"agent-manifest>=0.5",
# 0.6.1 is the floor, not merely a compatible version: verify_manifest()
# is called on a peer-supplied manifest below, and before 0.6.1 a manifest
# declaring ML-DSA-65 or hybrid crashed the verifier with an uncaught
# RuntimeError on any install without the optional [pq] extra.
"agent-manifest>=0.6.1",
"cryptography>=42.0",
"pyyaml>=6.0",
"httpx>=0.27",
Expand Down
10 changes: 9 additions & 1 deletion src/cmcp_runtime/agent_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,15 @@ def _raise_for_sdk_result(result: Any, *, require_runtime_artifacts: bool) -> No
if result.result == agent_manifest_sdk.OverallResult.SIGNATURE_MISSING:
raise ConfigError("Agent Manifest signature block is missing")
if result.result == agent_manifest_sdk.OverallResult.UNVERIFIABLE:
raise ConfigError("Agent Manifest signature verification failed")
# UNVERIFIABLE is not a failed signature: the verifier could not appraise
# the signature at all, because it lacks the trusted key or (since
# agent-manifest 0.6.1) the algorithm. Both reject, but an operator needs
# to know which, so surface the verifier's own reason when it gives one.
reason = "; ".join(getattr(result, "warnings", None) or [])
detail = f": {reason}" if reason else ""
raise ConfigError(
f"Agent Manifest signature could not be verified{detail}"
)
if result.result == agent_manifest_sdk.OverallResult.INCOMPATIBLE_VERSION:
raise ConfigError("Agent Manifest version is not supported by the SDK verifier")
if result.result == agent_manifest_sdk.OverallResult.INCOMPLETE:
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/test_agent_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,25 @@ def fake_signing_pre_image(manifest_arg):
)

assert cmcp_agent_manifest.signing_pre_image(manifest) == b"sdk-pre-image"


def test_post_quantum_manifest_without_pq_extra_fails_closed_cleanly() -> None:
# A peer can present a manifest declaring any registered algorithm. Before
# agent-manifest 0.6.1 an ML-DSA-65 declaration crashed the verifier with an
# uncaught RuntimeError on installs without the optional [pq] extra, so this
# path answered with a crash rather than a rejection. It must fail closed as
# a ConfigError, and the message must say the signature could not be checked
# rather than that it failed, since those need different operator responses.
priv, pub, key_id = _keypair()
manifest = _signed_manifest(priv, key_id)
manifest["crypto_profile"] = "post-quantum"
manifest["signature"]["algorithm"] = "ML-DSA-65"

with pytest.raises(ConfigError, match="could not be verified"):
verify_agent_manifest_binding(
manifest,
{key_id: pub},
authenticated_subject=AGENT_ID,
policy_bundle_hash=POLICY_HASH,
tool_catalog_hash=CATALOG_HASH,
)