Update brutal_test.py
Browse files- brutal_test.py +160 -112
brutal_test.py
CHANGED
|
@@ -1,125 +1,173 @@
|
|
| 1 |
-
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import rft_flightrecorder as fr
|
| 3 |
|
| 4 |
-
def read_lines(p):
|
| 5 |
-
with open(p, "r", encoding="utf-8") as f:
|
| 6 |
-
return [ln.rstrip("\n") for ln in f if ln.strip()]
|
| 7 |
|
| 8 |
-
def
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
if not cond:
|
| 15 |
-
raise AssertionError(msg)
|
| 16 |
|
| 17 |
-
def assert_false(cond, msg):
|
| 18 |
-
if cond:
|
| 19 |
-
raise AssertionError(msg)
|
| 20 |
|
| 21 |
def main():
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
print("TEMP:", td)
|
| 25 |
|
| 26 |
-
|
|
|
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
|
| 33 |
-
# append signed events
|
| 34 |
-
for i in range(1, 6):
|
| 35 |
-
ev, m = fr.append_event(
|
| 36 |
-
log_path=log,
|
| 37 |
-
session_id=sid,
|
| 38 |
-
event_type="note",
|
| 39 |
-
payload_text=json.dumps({"i": i, "msg": "ok"}, ensure_ascii=False),
|
| 40 |
-
parent_event_hash="",
|
| 41 |
-
sign_event=True,
|
| 42 |
-
sk_hex=sk,
|
| 43 |
-
model_id="audit-demo",
|
| 44 |
-
run_mode="deterministic",
|
| 45 |
-
)
|
| 46 |
-
assert_true(ev is not None, f"append failed at {i}: {m}")
|
| 47 |
-
|
| 48 |
-
status, ok, report = fr.verify_session(log, sid, pk, require_signatures=True)
|
| 49 |
-
print("verify signed:", status)
|
| 50 |
-
assert_true(ok, "Signed session should verify PASS")
|
| 51 |
-
|
| 52 |
-
anchor, msg = fr.finalise_session(log, sid, True, sk, "audit-demo", "deterministic")
|
| 53 |
-
print("finalise:", msg)
|
| 54 |
-
assert_true(anchor is not None, "Finalise failed")
|
| 55 |
-
|
| 56 |
-
status, ok, report = fr.verify_session(log, sid, pk, require_signatures=True)
|
| 57 |
-
print("verify after finalise:", status)
|
| 58 |
-
assert_true(ok, "Session should still verify after finalise")
|
| 59 |
-
|
| 60 |
-
bundle, msg = fr.export_session_bundle(log, sid)
|
| 61 |
-
print("export:", msg)
|
| 62 |
-
assert_true(bundle and os.path.exists(bundle), "Bundle not created")
|
| 63 |
-
|
| 64 |
-
# import verify
|
| 65 |
-
status, ok, report, stored = fr.import_bundle_verify(bundle, pk_hex=pk, require_signatures=False, store_into_log=False, log_path=log)
|
| 66 |
-
print("import verify:", status)
|
| 67 |
-
assert_true(ok, "Imported bundle should verify PASS")
|
| 68 |
-
|
| 69 |
-
# --- FAIL: tamper payload ---
|
| 70 |
-
lines = read_lines(log)
|
| 71 |
-
assert_true(len(lines) > 3, "Not enough log lines")
|
| 72 |
-
|
| 73 |
-
tampered = lines[:]
|
| 74 |
-
obj = json.loads(tampered[2])
|
| 75 |
-
obj["payload"]["msg"] = "hacked"
|
| 76 |
-
tampered[2] = json.dumps(obj, ensure_ascii=False)
|
| 77 |
-
tamper_log = os.path.join(td, "tamper_payload.jsonl")
|
| 78 |
-
write_lines(tamper_log, tampered)
|
| 79 |
-
|
| 80 |
-
status, ok, report = fr.verify_session(tamper_log, sid, pk, require_signatures=True)
|
| 81 |
-
print("tamper payload verify:", status)
|
| 82 |
-
assert_false(ok, "Tampered payload must FAIL")
|
| 83 |
-
|
| 84 |
-
# --- FAIL: reorder lines ---
|
| 85 |
-
reordered = lines[:]
|
| 86 |
-
if len(reordered) >= 4:
|
| 87 |
-
reordered[2], reordered[3] = reordered[3], reordered[2]
|
| 88 |
-
reorder_log = os.path.join(td, "tamper_reorder.jsonl")
|
| 89 |
-
write_lines(reorder_log, reordered)
|
| 90 |
-
|
| 91 |
-
status, ok, report = fr.verify_session(reorder_log, sid, pk, require_signatures=True)
|
| 92 |
-
print("reorder verify:", status)
|
| 93 |
-
assert_false(ok, "Reordered events must FAIL")
|
| 94 |
-
|
| 95 |
-
# --- FAIL: delete a line ---
|
| 96 |
-
deleted = lines[:]
|
| 97 |
-
deleted.pop(2)
|
| 98 |
-
del_log = os.path.join(td, "tamper_delete.jsonl")
|
| 99 |
-
write_lines(del_log, deleted)
|
| 100 |
-
|
| 101 |
-
status, ok, report = fr.verify_session(del_log, sid, pk, require_signatures=True)
|
| 102 |
-
print("delete verify:", status)
|
| 103 |
-
assert_false(ok, "Deleted event must FAIL")
|
| 104 |
-
|
| 105 |
-
# --- FAIL: wrong public key ---
|
| 106 |
-
sk2, pk2 = fr.gen_keys()
|
| 107 |
-
status, ok, report = fr.verify_session(log, sid, pk2, require_signatures=True)
|
| 108 |
-
print("wrong pk verify:", status)
|
| 109 |
-
assert_false(ok, "Wrong public key must FAIL")
|
| 110 |
-
|
| 111 |
-
# --- FAIL: require signatures but unsigned event exists ---
|
| 112 |
-
# create new session with mixed signed/unsigned
|
| 113 |
-
sid2, _ = fr.start_session(log, "audit-demo", "deterministic", "mixed sigs", False, sk)
|
| 114 |
-
ev, _ = fr.append_event(log, sid2, "note", '{"x":1}', "", False, sk, "audit-demo", "deterministic") # unsigned
|
| 115 |
-
status, ok, report = fr.verify_session(log, sid2, pk, require_signatures=True)
|
| 116 |
-
print("require sigs w/ unsigned:", status)
|
| 117 |
-
assert_false(ok, "Require signatures must fail if any event unsigned")
|
| 118 |
-
|
| 119 |
-
print("\nALL BRUTAL TESTS PASSED (meaning PASS/FAIL behaved correctly).")
|
| 120 |
-
shutil.rmtree(td, ignore_errors=True)
|
| 121 |
-
# leave bundle in cwd for inspection
|
| 122 |
-
print("Bundle left in cwd:", bundle)
|
| 123 |
|
| 124 |
if __name__ == "__main__":
|
| 125 |
main()
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import threading
|
| 4 |
+
import tempfile
|
| 5 |
+
import zipfile
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
import rft_flightrecorder as fr
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
def _append_with_retry(*, tries: int = 40, sleep_s: float = 0.01, **kwargs):
|
| 12 |
+
last = ""
|
| 13 |
+
for _ in range(tries):
|
| 14 |
+
ev, msg = fr.append_event(**kwargs)
|
| 15 |
+
if ev is not None:
|
| 16 |
+
return ev, msg
|
| 17 |
+
last = msg or ""
|
| 18 |
+
if "busy" in last.lower() or "lock" in last.lower():
|
| 19 |
+
time.sleep(sleep_s)
|
| 20 |
+
continue
|
| 21 |
+
break
|
| 22 |
+
return None, last
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def two_tab_spam_test(log_path: str, threads: int = 6, events_per_thread: int = 80):
|
| 26 |
+
sk_hex, _pk_hex = fr.gen_keys()
|
| 27 |
+
sid, msg = fr.start_session(log_path, "brutal-test", "deterministic", "spam test", False, sk_hex)
|
| 28 |
+
assert sid, f"Failed to start session: {msg}"
|
| 29 |
+
|
| 30 |
+
errors = []
|
| 31 |
+
errors_lock = threading.Lock()
|
| 32 |
+
|
| 33 |
+
def worker(tid: int):
|
| 34 |
+
for i in range(events_per_thread):
|
| 35 |
+
payload = json.dumps({"thread": tid, "i": i}, ensure_ascii=False)
|
| 36 |
+
ev, m = _append_with_retry(
|
| 37 |
+
log_path=log_path,
|
| 38 |
+
session_id=sid,
|
| 39 |
+
event_type="note",
|
| 40 |
+
payload_text=payload,
|
| 41 |
+
parent_event_hash="",
|
| 42 |
+
sign_event=False,
|
| 43 |
+
sk_hex=sk_hex,
|
| 44 |
+
model_id="brutal-test",
|
| 45 |
+
run_mode="deterministic",
|
| 46 |
+
)
|
| 47 |
+
if not ev:
|
| 48 |
+
with errors_lock:
|
| 49 |
+
errors.append(m)
|
| 50 |
+
|
| 51 |
+
ts = [threading.Thread(target=worker, args=(t,)) for t in range(threads)]
|
| 52 |
+
for t in ts:
|
| 53 |
+
t.start()
|
| 54 |
+
for t in ts:
|
| 55 |
+
t.join()
|
| 56 |
+
|
| 57 |
+
assert not errors, f"Append errors occurred (first 5): {errors[:5]}"
|
| 58 |
+
|
| 59 |
+
status, ok, report = fr.verify_session(log_path, sid, pk_hex="", require_signatures=False)
|
| 60 |
+
assert ok, f"Session failed verification after spam.\n{status}\n{report}"
|
| 61 |
+
|
| 62 |
+
all_events, _corrupt = fr.read_jsonl(log_path)
|
| 63 |
+
evs = fr.events_for_session(all_events, sid)
|
| 64 |
+
expected_before = 1 + (threads * events_per_thread)
|
| 65 |
+
assert len(evs) == expected_before, f"Expected {expected_before} events before finalise, got {len(evs)}"
|
| 66 |
+
|
| 67 |
+
anchor, m = fr.finalise_session(log_path, sid, False, sk_hex, "brutal-test", "deterministic")
|
| 68 |
+
assert anchor, f"Finalise failed: {m}"
|
| 69 |
+
|
| 70 |
+
ev, m = fr.append_event(
|
| 71 |
+
log_path=log_path,
|
| 72 |
+
session_id=sid,
|
| 73 |
+
event_type="note",
|
| 74 |
+
payload_text='{"post_end": true}',
|
| 75 |
+
parent_event_hash="",
|
| 76 |
+
sign_event=False,
|
| 77 |
+
sk_hex=sk_hex,
|
| 78 |
+
model_id="brutal-test",
|
| 79 |
+
run_mode="deterministic",
|
| 80 |
+
)
|
| 81 |
+
assert ev is None and "end" in (m or "").lower(), f"Expected refused append after end, got: {m}"
|
| 82 |
+
|
| 83 |
+
status, ok, report = fr.verify_session(log_path, sid, pk_hex="", require_signatures=False)
|
| 84 |
+
assert ok, f"Session failed verification after finalise.\n{status}\n{report}"
|
| 85 |
+
|
| 86 |
+
all_events, _corrupt = fr.read_jsonl(log_path)
|
| 87 |
+
evs = fr.events_for_session(all_events, sid)
|
| 88 |
+
expected_after = expected_before + 1 # session_end
|
| 89 |
+
assert len(evs) == expected_after, f"Expected {expected_after} events after finalise, got {len(evs)}"
|
| 90 |
+
|
| 91 |
+
return sid
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def tamper_zip_test(log_path: str):
|
| 95 |
+
sk_hex, _pk_hex = fr.gen_keys()
|
| 96 |
+
sid, msg = fr.start_session(log_path, "brutal-test", "deterministic", "tamper test", False, sk_hex)
|
| 97 |
+
assert sid, f"Failed to start session: {msg}"
|
| 98 |
+
|
| 99 |
+
for i in range(25):
|
| 100 |
+
ev, m = _append_with_retry(
|
| 101 |
+
log_path=log_path,
|
| 102 |
+
session_id=sid,
|
| 103 |
+
event_type="note",
|
| 104 |
+
payload_text=json.dumps({"i": i}),
|
| 105 |
+
parent_event_hash="",
|
| 106 |
+
sign_event=False,
|
| 107 |
+
sk_hex=sk_hex,
|
| 108 |
+
model_id="brutal-test",
|
| 109 |
+
run_mode="deterministic",
|
| 110 |
+
)
|
| 111 |
+
assert ev, f"Append failed during tamper test: {m}"
|
| 112 |
+
|
| 113 |
+
anchor, m = fr.finalise_session(log_path, sid, False, sk_hex, "brutal-test", "deterministic")
|
| 114 |
+
assert anchor, f"Finalise failed during tamper test: {m}"
|
| 115 |
+
|
| 116 |
+
zip_name, msg = fr.export_session_bundle(log_path, sid)
|
| 117 |
+
assert zip_name and os.path.exists(zip_name), f"Export failed: {msg}"
|
| 118 |
+
|
| 119 |
+
with tempfile.TemporaryDirectory() as td:
|
| 120 |
+
with zipfile.ZipFile(zip_name, "r") as z:
|
| 121 |
+
z.extractall(td)
|
| 122 |
+
|
| 123 |
+
events_file = None
|
| 124 |
+
for fn in os.listdir(td):
|
| 125 |
+
if fn.endswith("_events.jsonl"):
|
| 126 |
+
events_file = os.path.join(td, fn)
|
| 127 |
+
break
|
| 128 |
+
assert events_file, "No events jsonl found inside exported zip"
|
| 129 |
+
|
| 130 |
+
with open(events_file, "r", encoding="utf-8") as f:
|
| 131 |
+
lines = f.read().splitlines()
|
| 132 |
+
assert len(lines) > 5, "Not enough events in bundle to tamper"
|
| 133 |
+
|
| 134 |
+
obj = json.loads(lines[4])
|
| 135 |
+
obj.setdefault("payload", {})
|
| 136 |
+
if isinstance(obj["payload"], dict):
|
| 137 |
+
obj["payload"]["tampered"] = True
|
| 138 |
+
lines[4] = json.dumps(obj, ensure_ascii=False)
|
| 139 |
+
|
| 140 |
+
with open(events_file, "w", encoding="utf-8") as f:
|
| 141 |
+
f.write("\n".join(lines) + "\n")
|
| 142 |
+
|
| 143 |
+
tampered_zip = os.path.join(td, "tampered.zip")
|
| 144 |
+
with zipfile.ZipFile(tampered_zip, "w", compression=zipfile.ZIP_DEFLATED) as z:
|
| 145 |
+
z.write(events_file, arcname=os.path.basename(events_file))
|
| 146 |
+
|
| 147 |
+
status, ok, report, _ = fr.import_bundle_verify(
|
| 148 |
+
bundle_path=tampered_zip,
|
| 149 |
+
pk_hex="",
|
| 150 |
+
require_signatures=False,
|
| 151 |
+
store_into_log=False,
|
| 152 |
+
log_path=log_path,
|
| 153 |
+
)
|
| 154 |
+
assert not ok, f"Tampered bundle incorrectly verified PASS.\n{status}\n{report}"
|
| 155 |
|
| 156 |
+
return True
|
|
|
|
|
|
|
| 157 |
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
def main():
|
| 160 |
+
with tempfile.TemporaryDirectory() as td:
|
| 161 |
+
log_path = os.path.join(td, "flightlog.jsonl")
|
|
|
|
| 162 |
|
| 163 |
+
sid1 = two_tab_spam_test(log_path, threads=8, events_per_thread=60)
|
| 164 |
+
print(f"[PASS] two_tab_spam_test: session_id={sid1}")
|
| 165 |
|
| 166 |
+
ok = tamper_zip_test(log_path)
|
| 167 |
+
print(f"[PASS] tamper_zip_test: {ok}")
|
| 168 |
+
|
| 169 |
+
print("[ALL PASS]")
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
if __name__ == "__main__":
|
| 173 |
main()
|