From 108808027fc79c61fea5ba68869e7e7c44ff6f48 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Wed, 22 Apr 2026 16:57:33 -0400 Subject: [PATCH 1/2] [security] MCP Python hardening: remove private key logging, wire require_badge SEC-MCP-001: Replace private key output with fingerprint-only hint in stderr SEC-MCP-002: Wire require_badge from GuardConfig into EvaluateConfig RPC request --- capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py | 1 + capiscio_mcp/connect.py | 24 +++++++++++++--------- capiscio_mcp/guard.py | 1 + 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py b/capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py index 1fa4496..b6ab3d9 100644 --- a/capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py +++ b/capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py @@ -69,6 +69,7 @@ class EvaluateConfig: min_trust_level: int = 0 accept_level_zero: bool = False allowed_tools: List[str] = field(default_factory=list) + require_badge: bool = False @dataclass diff --git a/capiscio_mcp/connect.py b/capiscio_mcp/connect.py index 147be3b..96f2e0e 100644 --- a/capiscio_mcp/connect.py +++ b/capiscio_mcp/connect.py @@ -92,28 +92,32 @@ def _load_private_key_pem(pem_text: str) -> tuple[Ed25519PrivateKey, str, str, s def _log_key_capture_hint(server_id: str, private_key_pem: str) -> None: """Write a one-time hint to stderr telling the user how to persist key material. - Uses ``print(..., file=sys.stderr)`` instead of the logger so the private - key never enters log aggregation pipelines. The hint is only emitted on - first-run key generation. + Only the key fingerprint is emitted — never the private key itself. + The hint is only emitted on first-run key generation. """ - import sys as _sys # local import — only needed for this hint + import hashlib as _hashlib + import sys as _sys + + # Compute fingerprint from public key (first 8 hex chars of SHA-256) + key = load_pem_private_key(private_key_pem.encode(), password=None) + pub_raw = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + fingerprint = _hashlib.sha256(pub_raw).hexdigest()[:16] - escaped_pem = private_key_pem.replace("\n", "\\n") hint = ( "\n" " ╔══════════════════════════════════════════════════════════════╗\n" " ║ New server identity generated — save key for persistence ║\n" " ╚══════════════════════════════════════════════════════════════╝\n" "\n" + f" Key fingerprint: {fingerprint}\n" + "\n" " If this server runs in an ephemeral environment (containers,\n" " serverless, CI) the identity will be lost on restart unless\n" " you persist the private key.\n" "\n" - " Add to your secrets manager / .env:\n" - "\n" - f' CAPISCIO_SERVER_PRIVATE_KEY_PEM="{escaped_pem}"\n' - "\n" - " The DID will be re-derived automatically on startup.\n" + " Set the CAPISCIO_SERVER_PRIVATE_KEY_PEM environment variable\n" + " to the contents of your key file. The DID will be re-derived\n" + " automatically on startup.\n" ) print(hint, file=_sys.stderr, flush=True) diff --git a/capiscio_mcp/guard.py b/capiscio_mcp/guard.py index 63e0c2a..0b32679 100644 --- a/capiscio_mcp/guard.py +++ b/capiscio_mcp/guard.py @@ -285,6 +285,7 @@ async def evaluate_tool_access( min_trust_level=effective_config.min_trust_level, accept_level_zero=effective_config.accept_level_zero, allowed_tools=effective_config.allowed_tools or [], + require_badge=effective_config.require_badge, ), ) From a7c481b50a388bdfd1bab92589ef35f2350047dd Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Wed, 22 Apr 2026 21:51:28 -0400 Subject: [PATCH 2/2] fix: wrap fingerprint in try/except, defer require_badge wiring - _log_key_capture_hint: wrap fingerprint computation in try/except (best-effort hint) - Fix comment: "first 16 hex chars" not "8" - Remove require_badge from EvaluateConfig constructor (proto field not yet defined) Wiring deferred until proto is updated in capiscio-core --- capiscio_mcp/connect.py | 13 +++++++++---- capiscio_mcp/guard.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/capiscio_mcp/connect.py b/capiscio_mcp/connect.py index 96f2e0e..895d0c2 100644 --- a/capiscio_mcp/connect.py +++ b/capiscio_mcp/connect.py @@ -98,10 +98,15 @@ def _log_key_capture_hint(server_id: str, private_key_pem: str) -> None: import hashlib as _hashlib import sys as _sys - # Compute fingerprint from public key (first 8 hex chars of SHA-256) - key = load_pem_private_key(private_key_pem.encode(), password=None) - pub_raw = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) - fingerprint = _hashlib.sha256(pub_raw).hexdigest()[:16] + # Compute fingerprint from public key (first 16 hex chars of SHA-256). + # Best-effort: never let fingerprint computation block identity setup. + fingerprint = "" + try: + key = load_pem_private_key(private_key_pem.encode(), password=None) + pub_raw = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + fingerprint = _hashlib.sha256(pub_raw).hexdigest()[:16] + except Exception: + pass hint = ( "\n" diff --git a/capiscio_mcp/guard.py b/capiscio_mcp/guard.py index 0b32679..f72299c 100644 --- a/capiscio_mcp/guard.py +++ b/capiscio_mcp/guard.py @@ -285,7 +285,7 @@ async def evaluate_tool_access( min_trust_level=effective_config.min_trust_level, accept_level_zero=effective_config.accept_level_zero, allowed_tools=effective_config.allowed_tools or [], - require_badge=effective_config.require_badge, + # TODO: wire require_badge once the proto field is added in capiscio-core ), )