Harden stamp/verify/hash_report so unknown and content-shaped args fail instead of being ignored; publish additionalProperties=false and cover with schema/runtime tests. Also keep streamable-http host allowlist.
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
"""In-memory MCP Client against hash-only tools."""
|
|
|
|
import base64
|
|
|
|
import pytest
|
|
from mcp import Client
|
|
|
|
from verae_mcp_timestamping_server.guard import FORBIDDEN_CONTENT_KEYS
|
|
from verae_mcp_timestamping_server.ledger import SHA256_EMPTY, MemoryLedger
|
|
from verae_mcp_timestamping_server.server import build_mcp
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
def _error_text(result) -> str:
|
|
parts = []
|
|
for block in result.content or []:
|
|
text = getattr(block, "text", None)
|
|
if text:
|
|
parts.append(text)
|
|
return "\n".join(parts)
|
|
|
|
|
|
async def test_stamp_tool_returns_receipts():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp, raise_exceptions=True) as client:
|
|
result = await client.call_tool(
|
|
"stamp", {"sha256": SHA256_EMPTY, "scope": "local"}
|
|
)
|
|
assert result.is_error is not True
|
|
data = result.structured_content
|
|
assert data is not None
|
|
receipts = data["receipts"]
|
|
assert receipts[0]["hash"] == SHA256_EMPTY
|
|
assert receipts[0]["scope"] == "local"
|
|
|
|
|
|
async def test_stamp_and_hash_report_schemas_forbid_content_bytes():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp, raise_exceptions=True) as client:
|
|
listed = await client.list_tools()
|
|
by_name = {t.name: t for t in listed.tools}
|
|
for name in ("stamp", "hash_report", "verify"):
|
|
tool = by_name[name]
|
|
props = tool.input_schema["properties"]
|
|
for key in FORBIDDEN_CONTENT_KEYS:
|
|
assert key not in props, f"{name} schema still lists {key}"
|
|
assert tool.input_schema.get("additionalProperties") is False
|
|
assert "sha256" in by_name["stamp"].input_schema["properties"]
|
|
assert "sha256" in by_name["hash_report"].input_schema["properties"]
|
|
|
|
|
|
async def test_stamp_rejects_content_bytes_at_runtime():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp) as client:
|
|
r1 = await client.call_tool(
|
|
"stamp",
|
|
{
|
|
"sha256": SHA256_EMPTY,
|
|
"scope": "local",
|
|
"content": "this is file bytes",
|
|
},
|
|
)
|
|
assert r1.is_error is True
|
|
assert "content/bytes paths are rejected" in _error_text(r1)
|
|
|
|
r2 = await client.call_tool(
|
|
"stamp",
|
|
{
|
|
"sha256": SHA256_EMPTY,
|
|
"bytes": base64.b64encode(b"payload").decode(),
|
|
},
|
|
)
|
|
assert r2.is_error is True
|
|
assert "content/bytes paths are rejected" in _error_text(r2)
|
|
|
|
r3 = await client.call_tool(
|
|
"hash_report",
|
|
{"sha256": SHA256_EMPTY, "file": "/tmp/secret.bin"},
|
|
)
|
|
assert r3.is_error is True
|
|
assert "content/bytes paths are rejected" in _error_text(r3)
|
|
|
|
|
|
async def test_stamp_rejects_unknown_extra_keys():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp) as client:
|
|
result = await client.call_tool(
|
|
"stamp",
|
|
{"sha256": SHA256_EMPTY, "scope": "local", "not_a_real_field": 1},
|
|
)
|
|
assert result.is_error is True
|
|
assert "Extra inputs are not permitted" in _error_text(result)
|
|
|
|
|
|
async def test_stamp_rejects_non_digest_masquerading_as_sha256():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp) as client:
|
|
long_b64 = base64.b64encode(b"x" * 200).decode()
|
|
multiline = "line1\nline2\nline3"
|
|
for bad in ("not-a-hash", "", long_b64, multiline, "a" * 128):
|
|
result = await client.call_tool(
|
|
"stamp", {"sha256": bad, "scope": "local"}
|
|
)
|
|
assert result.is_error is True, bad
|
|
assert "sha256" in _error_text(result).lower()
|
|
|
|
|
|
async def test_verify_and_hash_report_tools():
|
|
mcp = build_mcp(MemoryLedger())
|
|
async with Client(mcp, raise_exceptions=True) as client:
|
|
stamped = await client.call_tool(
|
|
"stamp", {"sha256": SHA256_EMPTY, "scope": "local"}
|
|
)
|
|
receipts = stamped.structured_content["receipts"]
|
|
receipt_id = receipts[0]["receipt_id"]
|
|
|
|
verified = await client.call_tool(
|
|
"verify", {"receipt_id": receipt_id, "want_scope": "local"}
|
|
)
|
|
assert verified.structured_content["valid"] is True
|
|
|
|
report = await client.call_tool("hash_report", {"sha256": SHA256_EMPTY})
|
|
items = report.structured_content["receipts"]
|
|
assert items[0]["receipt_id"] == receipt_id
|