mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-30 02:49:32 +02:00
314 lines
11 KiB
Python
314 lines
11 KiB
Python
"""S13B Gate Identity Surface Narrowing.
|
|
|
|
Tests:
|
|
- ordinary member gate_messages response strips node_id/public_key/public_key_algo/signature/sequence
|
|
- privileged gate/audit view retains identity fields
|
|
- GET /api/mesh/infonet/messages?gate=... uses narrowed member view for ordinary gate members
|
|
- GET /api/mesh/infonet/event/{event_id} uses narrowed member view for ordinary gate members
|
|
- non-gate public event redaction remains unchanged
|
|
- do not overclaim operator privacy; this sprint is only member-facing API narrowing
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
# ── Identity fields that must NOT appear in member view ──────────────────
|
|
|
|
_IDENTITY_FIELDS = {"node_id", "public_key", "public_key_algo", "signature", "sequence"}
|
|
|
|
# ── Content fields that MUST appear in both views ────────────────────────
|
|
|
|
_CONTENT_FIELDS = {"event_id", "event_type", "timestamp", "protocol_version"}
|
|
_PAYLOAD_FIELDS = {
|
|
"gate",
|
|
"ciphertext",
|
|
"format",
|
|
"nonce",
|
|
"sender_ref",
|
|
"gate_envelope",
|
|
"envelope_hash",
|
|
"reply_to",
|
|
}
|
|
|
|
|
|
def _sample_raw_gate_event() -> dict:
|
|
"""A raw gate_message event as it would be stored internally."""
|
|
return {
|
|
"event_id": "evt-abc-123",
|
|
"event_type": "gate_message",
|
|
"timestamp": 1700000000,
|
|
"node_id": "node-secret-id",
|
|
"sequence": 42,
|
|
"signature": "deadbeef",
|
|
"public_key": "c2VjcmV0",
|
|
"public_key_algo": "Ed25519",
|
|
"protocol_version": "0.9.6",
|
|
"payload": {
|
|
"gate": "test-gate",
|
|
"ciphertext": "encrypted-blob",
|
|
"format": "mls_v1",
|
|
"nonce": "random-nonce",
|
|
"sender_ref": "anon-handle-xyz",
|
|
"gate_envelope": "envelope-data",
|
|
"envelope_hash": "envelope-hash",
|
|
"reply_to": "evt-parent-456",
|
|
},
|
|
}
|
|
|
|
|
|
# ── _strip_gate_identity_member tests ────────────────────────────────────
|
|
|
|
|
|
def test_member_view_strips_identity_fields():
|
|
"""Ordinary member view must NOT expose identity fields."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_member(raw)
|
|
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result, f"member view must not contain top-level '{field}'"
|
|
|
|
|
|
def test_member_view_preserves_content_fields():
|
|
"""Ordinary member view must preserve all content fields."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_member(raw)
|
|
|
|
for field in _CONTENT_FIELDS:
|
|
assert field in result, f"member view must contain '{field}'"
|
|
assert result["event_id"] == "evt-abc-123"
|
|
assert result["event_type"] == "gate_message"
|
|
assert result["protocol_version"] == "0.9.6"
|
|
|
|
|
|
def test_member_view_preserves_payload_fields():
|
|
"""Ordinary member view must preserve the default safe payload fields."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_member(raw)
|
|
payload = result["payload"]
|
|
|
|
for field in _PAYLOAD_FIELDS:
|
|
assert field in payload, f"member view payload must contain '{field}'"
|
|
assert payload["sender_ref"] == "anon-handle-xyz"
|
|
assert payload["ciphertext"] == "encrypted-blob"
|
|
assert payload["reply_to"] == ""
|
|
assert payload["gate_envelope"] == "envelope-data"
|
|
assert payload["envelope_hash"] == "envelope-hash"
|
|
|
|
|
|
def test_member_view_preserves_envelope_material_for_member_decrypt():
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_member(raw, envelope_policy="envelope_recovery")
|
|
|
|
assert result["payload"]["gate_envelope"] == "envelope-data"
|
|
assert result["payload"]["envelope_hash"] == "envelope-hash"
|
|
|
|
|
|
def test_member_view_no_identity_in_payload():
|
|
"""Identity fields must not leak into the payload either."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_member(raw)
|
|
payload = result["payload"]
|
|
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in payload, f"payload must not contain '{field}'"
|
|
|
|
|
|
# ── _strip_gate_identity_privileged tests ────────────────────────────────
|
|
|
|
|
|
def test_privileged_view_retains_identity_fields():
|
|
"""Privileged/audit view must retain all identity fields."""
|
|
from routers.mesh_public import _strip_gate_identity_privileged
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_privileged(raw)
|
|
|
|
assert result["node_id"] == "node-secret-id"
|
|
assert result["public_key"] == "c2VjcmV0"
|
|
assert result["public_key_algo"] == "Ed25519"
|
|
assert result["signature"] == "deadbeef"
|
|
assert result["sequence"] == 42
|
|
|
|
|
|
def test_privileged_view_preserves_content_fields():
|
|
"""Privileged view must also preserve all content fields."""
|
|
from routers.mesh_public import _strip_gate_identity_privileged
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_privileged(raw)
|
|
|
|
for field in _CONTENT_FIELDS:
|
|
assert field in result, f"privileged view must contain '{field}'"
|
|
assert result["event_id"] == "evt-abc-123"
|
|
assert result["protocol_version"] == "0.9.6"
|
|
|
|
|
|
def test_privileged_view_preserves_payload_fields():
|
|
"""Privileged view must preserve all payload fields."""
|
|
from routers.mesh_public import _strip_gate_identity_privileged
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity_privileged(raw)
|
|
payload = result["payload"]
|
|
|
|
for field in _PAYLOAD_FIELDS:
|
|
assert field in payload, f"privileged view payload must contain '{field}'"
|
|
assert payload["sender_ref"] == "anon-handle-xyz"
|
|
assert payload["gate_envelope"] == "envelope-data"
|
|
assert payload["envelope_hash"] == "envelope-hash"
|
|
|
|
|
|
# ── _strip_gate_for_access routing tests ─────────────────────────────────
|
|
|
|
|
|
def test_strip_for_access_member_strips_identity():
|
|
"""_strip_gate_for_access with 'member' must use the narrowed view."""
|
|
from routers.mesh_public import _strip_gate_for_access
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_for_access(raw, "member")
|
|
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result, f"member access must not expose '{field}'"
|
|
assert result["payload"]["sender_ref"] == "anon-handle-xyz"
|
|
|
|
|
|
def test_strip_for_access_privileged_retains_identity():
|
|
"""_strip_gate_for_access with 'privileged' must use the full view."""
|
|
from routers.mesh_public import _strip_gate_for_access
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_for_access(raw, "privileged")
|
|
|
|
assert result["node_id"] == "node-secret-id"
|
|
assert result["public_key"] == "c2VjcmV0"
|
|
assert result["signature"] == "deadbeef"
|
|
assert result["sequence"] == 42
|
|
|
|
|
|
# ── main.py sync verification ────────────────────────────────────────────
|
|
|
|
|
|
def test_main_member_view_strips_identity():
|
|
"""main.py _strip_gate_identity_member must match router behavior."""
|
|
import main
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = main._strip_gate_identity_member(raw)
|
|
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result, f"main.py member view must not contain '{field}'"
|
|
assert result["payload"]["sender_ref"] == "anon-handle-xyz"
|
|
|
|
|
|
def test_main_privileged_view_retains_identity():
|
|
"""main.py _strip_gate_identity_privileged must match router behavior."""
|
|
import main
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = main._strip_gate_identity_privileged(raw)
|
|
|
|
assert result["node_id"] == "node-secret-id"
|
|
assert result["public_key"] == "c2VjcmV0"
|
|
assert result["signature"] == "deadbeef"
|
|
assert result["sequence"] == 42
|
|
|
|
|
|
def test_main_strip_for_access_routes_correctly():
|
|
"""main.py _strip_gate_for_access must route member vs privileged correctly."""
|
|
import main
|
|
|
|
raw = _sample_raw_gate_event()
|
|
|
|
member = main._strip_gate_for_access(raw, "member")
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in member
|
|
|
|
privileged = main._strip_gate_for_access(raw, "privileged")
|
|
assert privileged["node_id"] == "node-secret-id"
|
|
|
|
|
|
# ── Legacy alias defaults to member view ─────────────────────────────────
|
|
|
|
|
|
def test_legacy_strip_gate_identity_uses_member_view():
|
|
"""_strip_gate_identity (legacy alias) must default to member (narrowed) view."""
|
|
from routers.mesh_public import _strip_gate_identity
|
|
|
|
raw = _sample_raw_gate_event()
|
|
result = _strip_gate_identity(raw)
|
|
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result, f"legacy alias must not expose '{field}'"
|
|
assert result["payload"]["sender_ref"] == "anon-handle-xyz"
|
|
|
|
|
|
# ── Non-gate public event redaction unchanged ────────────────────────────
|
|
|
|
|
|
def test_redact_public_event_unchanged():
|
|
"""_redact_public_event must not be affected by gate identity changes."""
|
|
from routers.mesh_public import _redact_public_event
|
|
|
|
public_event = {
|
|
"event_id": "pub-001",
|
|
"event_type": "status_update",
|
|
"timestamp": 1700000000,
|
|
"node_id": "node-public",
|
|
"sequence": 1,
|
|
"signature": "sig-public",
|
|
"public_key": "pub-key",
|
|
"public_key_algo": "Ed25519",
|
|
"protocol_version": "0.9.6",
|
|
"payload": {"message": "hello"},
|
|
}
|
|
result = _redact_public_event(public_event)
|
|
# Public redaction is a different path; it should not strip identity fields
|
|
# the same way gate member redaction does. Just verify it returns a dict.
|
|
assert isinstance(result, dict)
|
|
assert result.get("event_id") == "pub-001"
|
|
|
|
|
|
# ── Edge cases ───────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_member_view_handles_empty_event():
|
|
"""Member view must handle empty/malformed events gracefully."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
result = _strip_gate_identity_member({})
|
|
assert result["event_type"] == "gate_message"
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result
|
|
|
|
|
|
def test_member_view_handles_none_event():
|
|
"""Member view must handle None gracefully."""
|
|
from routers.mesh_public import _strip_gate_identity_member
|
|
|
|
result = _strip_gate_identity_member(None)
|
|
assert result["event_type"] == "gate_message"
|
|
for field in _IDENTITY_FIELDS:
|
|
assert field not in result
|
|
|
|
|
|
def test_privileged_view_handles_empty_event():
|
|
"""Privileged view must handle empty events gracefully."""
|
|
from routers.mesh_public import _strip_gate_identity_privileged
|
|
|
|
result = _strip_gate_identity_privileged({})
|
|
assert result["event_type"] == "gate_message"
|
|
# Identity fields should be present but empty/zero
|
|
assert result["node_id"] == ""
|
|
assert result["sequence"] == 0
|