Skip to content

prototype(gate_store,cmcp_gate): support binary secrets, enforce canonical base64url, and wrap malformed store responses in GateStoreUnavailable #606

Closed

Description

@drk-ram

What is wrong

At main (4c83b1d), prototype/gate_store.py and prototype/cmcp_gate.py have three security and fail-closed boundary bugs:

  1. RemoteGateStore crashes on non-UTF-8 binary secrets and accepts secrets shorter than 16 bytes:

    • GateStoreServer.__init__ (prototype/gate_store.py:337-343) accepts secret: bytes and enforces if len(secret) < 16: raise ValueError("store secret must be at least 16 bytes").
    • RemoteGateStore.__init__ (prototype/gate_store.py:381-384) takes secret: bytes and immediately calls self._secret = secret.decode(), which raises UnicodeDecodeError on any random 16+-byte binary key (os.urandom(32)), and does not enforce isinstance(secret, bytes) or len(secret) >= 16.
  2. RemoteGateStore leaks ValueError (binascii.Error), TypeError, and IndexError on malformed server responses instead of raising GateStoreUnavailable, and _unb64 / _recv accept non-canonical input:

    • GateStoreUnavailable (prototype/gate_store.py:42-46) is deliberately a ConnectionError and not a ValueError, because "cMCP turns a ValueError from the gate into a refusal receipt, which would need the same unreachable store."
    • However, in RemoteGateStore.admission, bump_admission, receipt_head, and receipts (prototype/gate_store.py:415-451), calling _unb64(found[0]), _int(...), _str(...), or unpacking existing, head = self._call(...) on a malformed store response raises binascii.Error (a ValueError subclass!), TypeError, or ValueError outside _call's try block, violating the GateStoreUnavailable contract.
    • gate_store._unb64 (lines 262-265) accepts padded and non-canonical base64url strings instead of enforcing canonical unpadded base64url, and _recv (lines 281-285) does not reject duplicate JSON keys or catch RecursionError.
  3. CMCPTraceGate does not validate gateway_key, gateway_issuer, session_id, call_id, or action types at entry:

    • CMCPTraceGate.__init__ (prototype/cmcp_gate.py:40-66) does not validate isinstance(context, v.VerificationContext), isinstance(gateway_key, Ed25519PrivateKey), gateway_issuer as a valid Identifier, or callable(clock).
    • challenge, admit, begin, recheck, and refusal do not validate session_id / call_id as Identifier strings or isinstance(action, dict) before hashing action with v.canonical_digest(action) or rfc8785.dumps(action) (which accepts lists, strings, or integers as top-level JSON values).

Reproduction on 4c83b1d:

from prototype import gate_store

# 1. Binary 16-byte secret raises UnicodeDecodeError:
gate_store.RemoteGateStore("127.0.0.1", 9999, b"\x80" * 16)

# 2. 5-byte secret is accepted by RemoteGateStore:
gate_store.RemoteGateStore("127.0.0.1", 9999, b"short")

# 3. Padded base64url is accepted by _unb64:
assert gate_store._unb64("AAAA====") == b"\x00\x00\x00"

Spec section or file

  • prototype/gate_store.py (GateStoreServer, RemoteGateStore, _unb64, _recv)
  • prototype/cmcp_gate.py (CMCPTraceGate)

Expected behavior

  1. Both GateStoreServer and RemoteGateStore validate isinstance(secret, bytes) and len(secret) >= 16 and support arbitrary binary bytes secrets across the wire (or UTF-8 strings / base64url without crashing on binary bytes).
  2. gate_store._unb64 enforces canonical unpadded base64url, _recv rejects duplicate JSON keys and catches RecursionError, and RemoteGateStore wraps any malformed response structure/field error in GateStoreUnavailable so ValueError / TypeError never leaks into cMCP's refusal-receipt path.
  3. CMCPTraceGate validates context, gateway_key, gateway_issuer, clock, session_id, call_id, and isinstance(action, dict) with ProfileError.

Impact

RemoteGateStore cannot be initialized with random binary secrets (os.urandom(32)), and a corrupted or truncated store response raises ValueError (binascii.Error) or TypeError instead of GateStoreUnavailable, causing cMCP to attempt writing a refusal receipt to the broken store instead of failing closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions