mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-09 02:35:37 +02:00
543 lines
18 KiB
Python
543 lines
18 KiB
Python
"""S2 Gate Envelope Authenticity — prove gate_envelope is cryptographically bound.
|
|
|
|
Tests verify:
|
|
- Tampered gate_envelope is rejected when envelope_hash is present
|
|
- Stripped gate_envelope is rejected when envelope_hash is present
|
|
- Envelopes without envelope_hash are rejected rather than trusted as legacy
|
|
- Route-level: ingest rejects mismatched envelope_hash + gate_envelope
|
|
- compose_encrypted_gate_message produces envelope_hash
|
|
"""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
|
|
import pytest
|
|
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _enable_runtime_recovery_envelopes(monkeypatch):
|
|
from services.config import get_settings
|
|
|
|
monkeypatch.setenv("MESH_GATE_RECOVERY_ENVELOPE_ENABLE", "true")
|
|
monkeypatch.setenv("MESH_GATE_RECOVERY_ENVELOPE_ENABLE_ACKNOWLEDGE", "true")
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _encrypt_envelope(gate_id: str, plaintext: str) -> str:
|
|
"""Encrypt a gate envelope using the real gate secret path."""
|
|
from services.mesh.mesh_gate_mls import _gate_envelope_encrypt
|
|
|
|
return _gate_envelope_encrypt(gate_id, plaintext)
|
|
|
|
|
|
def _decrypt_envelope(gate_id: str, token: str) -> str | None:
|
|
"""Decrypt a gate envelope using the real waterfall."""
|
|
from services.mesh.mesh_gate_mls import _gate_envelope_decrypt
|
|
|
|
return _gate_envelope_decrypt(gate_id, token)
|
|
|
|
|
|
def _compute_hash(envelope: str) -> str:
|
|
return hashlib.sha256(envelope.encode("ascii")).hexdigest()
|
|
|
|
|
|
def _install_test_gate(
|
|
gate_id: str,
|
|
*,
|
|
envelope_policy: str = "envelope_recovery",
|
|
gate_secret: str = "test-gate-secret-authenticity",
|
|
):
|
|
from services.mesh.mesh_reputation import gate_manager
|
|
|
|
original = gate_manager.gates.get(gate_id)
|
|
gate_manager.gates[gate_id] = {
|
|
"creator_node_id": "test",
|
|
"display_name": "Envelope Authenticity Test",
|
|
"description": "",
|
|
"rules": {},
|
|
"created_at": 0,
|
|
"message_count": 0,
|
|
"fixed": False,
|
|
"sort_order": 1000,
|
|
"gate_secret": gate_secret,
|
|
"envelope_policy": envelope_policy,
|
|
"legacy_envelope_fallback": False,
|
|
}
|
|
return original
|
|
|
|
|
|
def _restore_test_gate(gate_id: str, original: dict | None) -> None:
|
|
from services.mesh.mesh_reputation import gate_manager
|
|
|
|
if original is None:
|
|
gate_manager.gates.pop(gate_id, None)
|
|
else:
|
|
gate_manager.gates[gate_id] = original
|
|
|
|
|
|
# ── Decrypt-level: tampered envelope with envelope_hash ─────────────────
|
|
|
|
|
|
def test_tampered_gate_envelope_rejected_when_hash_present():
|
|
"""A tampered gate_envelope must fail when envelope_hash binds it."""
|
|
from services.mesh.mesh_gate_mls import decrypt_gate_message_for_local_identity
|
|
|
|
gate_id = "__test_gate_auth_tampered"
|
|
original = _install_test_gate(gate_id)
|
|
try:
|
|
real_envelope = _encrypt_envelope(gate_id, "real message")
|
|
envelope_hash = _compute_hash(real_envelope)
|
|
|
|
# Attacker replaces envelope with one containing different plaintext
|
|
tampered_envelope = _encrypt_envelope(gate_id, "INJECTED BY ATTACKER")
|
|
|
|
result = decrypt_gate_message_for_local_identity(
|
|
gate_id=gate_id,
|
|
epoch=1,
|
|
ciphertext="dummyct", # won't reach MLS path
|
|
nonce="dummynonce",
|
|
gate_envelope=tampered_envelope,
|
|
envelope_hash=envelope_hash,
|
|
recovery_envelope=True,
|
|
)
|
|
|
|
assert result["ok"] is False
|
|
assert "integrity" in result["detail"].lower()
|
|
finally:
|
|
_restore_test_gate(gate_id, original)
|
|
|
|
|
|
def test_stripped_gate_envelope_rejected_when_hash_present():
|
|
"""A stripped gate_envelope must fail when envelope_hash is present."""
|
|
from services.mesh.mesh_gate_mls import decrypt_gate_message_for_local_identity
|
|
|
|
gate_id = "__test_gate_auth_stripped"
|
|
original = _install_test_gate(gate_id)
|
|
try:
|
|
real_envelope = _encrypt_envelope(gate_id, "real message")
|
|
envelope_hash = _compute_hash(real_envelope)
|
|
|
|
result = decrypt_gate_message_for_local_identity(
|
|
gate_id=gate_id,
|
|
epoch=1,
|
|
ciphertext="dummyct",
|
|
nonce="dummynonce",
|
|
gate_envelope="", # stripped
|
|
envelope_hash=envelope_hash,
|
|
recovery_envelope=True,
|
|
)
|
|
|
|
assert result["ok"] is False
|
|
assert "missing" in result["detail"].lower()
|
|
finally:
|
|
_restore_test_gate(gate_id, original)
|
|
|
|
|
|
# ── Decrypt-level: unsigned envelope rejection ──────────────────────────
|
|
|
|
|
|
def test_gate_envelope_without_hash_is_rejected():
|
|
"""A gate_envelope without envelope_hash is unauthenticated and must not decrypt."""
|
|
from services.mesh.mesh_gate_mls import decrypt_gate_message_for_local_identity
|
|
|
|
gate_id = "__test_gate_auth_unsigned"
|
|
original = _install_test_gate(gate_id)
|
|
try:
|
|
envelope = _encrypt_envelope(gate_id, "unsigned envelope content")
|
|
|
|
result = decrypt_gate_message_for_local_identity(
|
|
gate_id=gate_id,
|
|
epoch=1,
|
|
ciphertext="dummyct",
|
|
nonce="dummynonce",
|
|
gate_envelope=envelope,
|
|
envelope_hash="",
|
|
recovery_envelope=True,
|
|
)
|
|
|
|
assert result["ok"] is False
|
|
assert "envelope_hash" in result["detail"]
|
|
finally:
|
|
_restore_test_gate(gate_id, original)
|
|
|
|
|
|
def test_valid_envelope_with_correct_hash_decrypts():
|
|
"""New-format messages with correct hash decrypt on explicit recovery reads."""
|
|
from services.mesh.mesh_gate_mls import decrypt_gate_message_for_local_identity
|
|
|
|
gate_id = "__test_gate_auth_valid"
|
|
original = _install_test_gate(gate_id)
|
|
try:
|
|
plaintext = "authenticated message"
|
|
envelope = _encrypt_envelope(gate_id, plaintext)
|
|
envelope_hash = _compute_hash(envelope)
|
|
|
|
result = decrypt_gate_message_for_local_identity(
|
|
gate_id=gate_id,
|
|
epoch=1,
|
|
ciphertext="dummyct",
|
|
nonce="dummynonce",
|
|
gate_envelope=envelope,
|
|
envelope_hash=envelope_hash,
|
|
recovery_envelope=True,
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["plaintext"] == plaintext
|
|
finally:
|
|
_restore_test_gate(gate_id, original)
|
|
|
|
|
|
def test_recovery_envelope_not_used_on_ordinary_reads():
|
|
"""envelope_recovery gates must not trust gate_envelope on ordinary reads."""
|
|
from services.mesh.mesh_gate_mls import decrypt_gate_message_for_local_identity
|
|
|
|
gate_id = "__test_gate_auth_ordinary_read"
|
|
original = _install_test_gate(gate_id, envelope_policy="envelope_recovery")
|
|
try:
|
|
envelope = _encrypt_envelope(gate_id, "recovery-only material")
|
|
envelope_hash = _compute_hash(envelope)
|
|
|
|
result = decrypt_gate_message_for_local_identity(
|
|
gate_id=gate_id,
|
|
epoch=1,
|
|
ciphertext="dummyct",
|
|
nonce="dummynonce",
|
|
gate_envelope=envelope,
|
|
envelope_hash=envelope_hash,
|
|
recovery_envelope=False,
|
|
)
|
|
|
|
assert result["ok"] is False
|
|
assert result["detail"] == "no active gate identity"
|
|
finally:
|
|
_restore_test_gate(gate_id, original)
|
|
|
|
|
|
# ── Route-level: ingest handler rejects tampered envelope ───────────────
|
|
|
|
|
|
def _build_gate_message_body(
|
|
gate_id: str,
|
|
*,
|
|
gate_envelope: str = "",
|
|
envelope_hash: str = "",
|
|
) -> dict:
|
|
"""Build a minimal gate_message body for the ingest handler."""
|
|
return {
|
|
"sender_id": "!sb_test1234567890",
|
|
"ciphertext": "dGVzdA==",
|
|
"nonce": "dGVzdG5vbmNl",
|
|
"sender_ref": "testref1234",
|
|
"format": "mls1",
|
|
"public_key": "",
|
|
"public_key_algo": "Ed25519",
|
|
"signature": "deadbeef",
|
|
"sequence": 1,
|
|
"protocol_version": "infonet/2",
|
|
"transport_lock": "private_strong",
|
|
"gate_envelope": gate_envelope,
|
|
"envelope_hash": envelope_hash,
|
|
}
|
|
|
|
|
|
def test_ingest_rejects_mismatched_envelope_at_route(monkeypatch):
|
|
"""The gate_message ingest handler must reject tampered envelopes.
|
|
|
|
We monkeypatch signature verification to pass so we can reach the
|
|
envelope binding check.
|
|
"""
|
|
import main
|
|
|
|
# Skip signature and integrity checks to reach the envelope binding check
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
|
|
gate_id = "infonet"
|
|
real_envelope = _encrypt_envelope(gate_id, "real content")
|
|
envelope_hash = _compute_hash(real_envelope)
|
|
tampered_envelope = _encrypt_envelope(gate_id, "ATTACKER CONTENT")
|
|
|
|
body = _build_gate_message_body(
|
|
gate_id,
|
|
gate_envelope=tampered_envelope,
|
|
envelope_hash=envelope_hash,
|
|
)
|
|
|
|
from starlette.requests import Request
|
|
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = main._submit_gate_message_envelope(request, gate_id, body)
|
|
|
|
assert result["ok"] is False
|
|
assert "does not match" in result["detail"]
|
|
|
|
|
|
def test_ingest_rejects_stripped_envelope_at_route(monkeypatch):
|
|
"""The ingest handler must reject when envelope_hash is present but envelope is stripped."""
|
|
import main
|
|
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
|
|
gate_id = "infonet"
|
|
real_envelope = _encrypt_envelope(gate_id, "real content")
|
|
envelope_hash = _compute_hash(real_envelope)
|
|
|
|
body = _build_gate_message_body(
|
|
gate_id,
|
|
gate_envelope="", # stripped
|
|
envelope_hash=envelope_hash,
|
|
)
|
|
|
|
from starlette.requests import Request
|
|
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = main._submit_gate_message_envelope(request, gate_id, body)
|
|
|
|
assert result["ok"] is False
|
|
assert "required" in result["detail"].lower()
|
|
|
|
|
|
def test_ingest_rejects_unsigned_envelope_at_route(monkeypatch):
|
|
"""The ingest handler must reject a durable envelope unless its hash is signed."""
|
|
import main
|
|
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
|
|
gate_id = "infonet"
|
|
envelope = _encrypt_envelope(gate_id, "unsigned content")
|
|
body = _build_gate_message_body(
|
|
gate_id,
|
|
gate_envelope=envelope,
|
|
envelope_hash="",
|
|
)
|
|
|
|
from starlette.requests import Request
|
|
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = main._submit_gate_message_envelope(request, gate_id, body)
|
|
|
|
assert result["ok"] is False
|
|
assert "envelope_hash" in result["detail"]
|
|
|
|
|
|
def test_ingest_accepts_legacy_message_without_hash(monkeypatch):
|
|
"""MLS-only legacy messages without envelope material remain accepted at ingest."""
|
|
import main
|
|
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
monkeypatch.setattr(main, "_resolve_envelope_policy", lambda _gate_id: "envelope_disabled")
|
|
# Gate access and cooldown
|
|
from services.mesh.mesh_reputation import gate_manager
|
|
monkeypatch.setattr(gate_manager, "can_enter", lambda *a, **kw: (True, "ok"))
|
|
monkeypatch.setattr(main, "_check_gate_post_cooldown", lambda *a: (True, "ok"))
|
|
monkeypatch.setattr(main, "_record_gate_post_cooldown", lambda *a: None)
|
|
monkeypatch.setattr(gate_manager, "record_message", lambda *a: None)
|
|
|
|
# Mock sequence advancement and gate_store
|
|
from services.mesh import mesh_hashchain
|
|
monkeypatch.setattr(
|
|
mesh_hashchain.infonet,
|
|
"validate_and_set_sequence",
|
|
lambda node_id, seq: (True, "ok"),
|
|
)
|
|
monkeypatch.setattr(
|
|
mesh_hashchain.gate_store,
|
|
"append",
|
|
lambda gate_id, event: {**event, "event_id": "test-ev-1"},
|
|
)
|
|
from services.mesh.mesh_reputation import reputation_ledger
|
|
monkeypatch.setattr(reputation_ledger, "register_node", lambda *a: None)
|
|
|
|
gate_id = "infonet"
|
|
body = _build_gate_message_body(
|
|
gate_id,
|
|
gate_envelope="",
|
|
envelope_hash="",
|
|
)
|
|
|
|
from starlette.requests import Request
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = main._submit_gate_message_envelope(request, gate_id, body)
|
|
assert result["ok"] is True
|
|
|
|
|
|
def test_ingest_rejects_envelope_always_message_without_envelope(monkeypatch):
|
|
"""envelope_always gates must never store MLS-only messages."""
|
|
import main
|
|
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
monkeypatch.setattr(main, "_resolve_envelope_policy", lambda _gate_id: "envelope_always")
|
|
|
|
gate_id = "infonet"
|
|
body = _build_gate_message_body(gate_id, gate_envelope="", envelope_hash="")
|
|
|
|
from starlette.requests import Request
|
|
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = main._submit_gate_message_envelope(request, gate_id, body)
|
|
assert result == {"ok": False, "detail": "gate_envelope_required"}
|
|
|
|
|
|
# ── mesh_public.py router: same behavior ────────────────────────────────
|
|
|
|
|
|
def test_router_ingest_rejects_mismatched_envelope(monkeypatch):
|
|
"""The mesh_public router handler must also reject tampered envelopes."""
|
|
import main
|
|
from routers import mesh_public
|
|
|
|
monkeypatch.setattr(main, "_verify_gate_message_signed_write", lambda **kw: (True, "ok", kw.get("reply_to", "")))
|
|
|
|
gate_id = "infonet"
|
|
real_envelope = _encrypt_envelope(gate_id, "real content")
|
|
envelope_hash = _compute_hash(real_envelope)
|
|
tampered_envelope = _encrypt_envelope(gate_id, "ATTACKER CONTENT")
|
|
|
|
body = _build_gate_message_body(
|
|
gate_id,
|
|
gate_envelope=tampered_envelope,
|
|
envelope_hash=envelope_hash,
|
|
)
|
|
|
|
from starlette.requests import Request
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"headers": [(b"content-type", b"application/json")],
|
|
"client": ("test", 12345),
|
|
"method": "POST",
|
|
"path": f"/api/mesh/gate/{gate_id}/message",
|
|
}
|
|
)
|
|
|
|
result = mesh_public._submit_gate_message_envelope(request, gate_id, body)
|
|
|
|
assert result["ok"] is False
|
|
assert "does not match" in result["detail"]
|
|
|
|
|
|
# ── Normalization: envelope_hash survives ───────────────────────────────
|
|
|
|
|
|
def test_normalize_preserves_envelope_hash():
|
|
"""envelope_hash must survive normalization so it reaches the signature."""
|
|
from services.mesh.mesh_protocol import normalize_payload
|
|
|
|
payload = {
|
|
"gate": "infonet",
|
|
"ciphertext": "ct",
|
|
"nonce": "n",
|
|
"sender_ref": "sr",
|
|
"format": "mls1",
|
|
"envelope_hash": "abc123",
|
|
}
|
|
normalized = normalize_payload("gate_message", payload)
|
|
assert normalized["envelope_hash"] == "abc123"
|
|
|
|
|
|
def test_normalize_omits_envelope_hash_when_empty():
|
|
"""Empty envelope_hash must not appear in normalized payload."""
|
|
from services.mesh.mesh_protocol import normalize_payload
|
|
|
|
payload = {
|
|
"gate": "infonet",
|
|
"ciphertext": "ct",
|
|
"nonce": "n",
|
|
"sender_ref": "sr",
|
|
"format": "mls1",
|
|
}
|
|
normalized = normalize_payload("gate_message", payload)
|
|
assert "envelope_hash" not in normalized
|
|
|
|
|
|
# ── build_signature_payload: envelope_hash is NOT stripped ──────────────
|
|
|
|
|
|
def test_envelope_hash_included_in_signature_payload():
|
|
"""envelope_hash must be included in the signature payload (not stripped)."""
|
|
from services.mesh.mesh_crypto import build_signature_payload
|
|
|
|
payload_with_hash = {
|
|
"gate": "infonet",
|
|
"ciphertext": "ct",
|
|
"nonce": "n",
|
|
"sender_ref": "sr",
|
|
"format": "mls1",
|
|
"envelope_hash": "abc123",
|
|
}
|
|
payload_without_hash = {
|
|
"gate": "infonet",
|
|
"ciphertext": "ct",
|
|
"nonce": "n",
|
|
"sender_ref": "sr",
|
|
"format": "mls1",
|
|
}
|
|
|
|
sig_with = build_signature_payload(
|
|
event_type="gate_message",
|
|
node_id="!sb_test",
|
|
sequence=1,
|
|
payload=payload_with_hash,
|
|
)
|
|
sig_without = build_signature_payload(
|
|
event_type="gate_message",
|
|
node_id="!sb_test",
|
|
sequence=1,
|
|
payload=payload_without_hash,
|
|
)
|
|
|
|
# The signature payloads must differ when envelope_hash is present
|
|
assert sig_with != sig_without
|
|
assert "abc123" in sig_with
|
|
assert "abc123" not in sig_without
|