montana/Node/External-Audit/scripts/verify-state.sh
2026-05-21 03:44:38 +03:00

97 lines
4.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Аудитор: скачивает events.jsonl, replay → state, сверяет с публичным state.json.
# Если совпадает — данные не подделаны. Если нет — мы врём.
set -u
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
EXPECTED_PUBKEY="d9a8bf07871d35c8e85f7de4a9b62896c330ba0987732468515c7bda8bb4adde"
echo "=== Montana — single-source-of-truth verification ==="
echo
echo "[1] Скачиваем events.jsonl..."
curl -sS --max-time 30 https://montana.quest/vpn/events.jsonl > "$TMP/events.jsonl"
N=$(wc -l < "$TMP/events.jsonl")
echo " $N events"
echo "[2] Скачиваем заявленный state.json..."
curl -sS --max-time 10 https://montana.quest/vpn/state.json > "$TMP/state-claimed.json"
HEAD=$(python3 -c "import json;print(json.load(open('$TMP/state-claimed.json'))['head_seq'])")
echo " head_seq=$HEAD"
echo "[3] Проверяем каждое event подписано $EXPECTED_PUBKEY..."
python3 - <<PY
import json, base64, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex("$EXPECTED_PUBKEY"))
bad = 0
expected_seq = 1
for i, line in enumerate(open("$TMP/events.jsonl"), 1):
e = json.loads(line)
if e['seq'] != expected_seq:
print(f' ✗ gap at seq {expected_seq}, got {e["seq"]}'); bad += 1
expected_seq = e['seq'] + 1
sig = e.pop('sig', '')
if not sig.startswith('ed25519:'): bad += 1; continue
canonical = json.dumps(e, sort_keys=True, separators=(',', ':'))
try:
pub.verify(base64.b64decode(sig[len('ed25519:'):]), canonical.encode())
except InvalidSignature:
print(f' ✗ bad sig at seq {e["seq"]}'); bad += 1
if bad == 0:
print(f' ✓ все $N events подписаны валидно, seq непрерывен')
sys.exit(0)
print(f' ✗ {bad} нарушений')
sys.exit(1)
PY
[ $? -ne 0 ] && echo -e "${RED}✗ VERIFICATION FAILED${NC}" && exit 1
echo "[4] Replay events → state (только до claimed head_seq)..."
python3 - <<PY
import json, hashlib, sys
claimed = json.load(open("$TMP/state-claimed.json"))
HEAD = claimed['head_seq']
events = [json.loads(l) for l in open("$TMP/events.jsonl") if l.strip()]
events.sort(key=lambda e: e['seq'])
events = [e for e in events if e['seq'] <= HEAD]
nodes = {}
seen = set()
head = (0, None)
for e in events:
head = (e['seq'], e['ts'])
t, d = e['type'], e['data']
if t == 'node_register':
nodes[d['alias']] = {'alias':d['alias'],'ip':d.get('ip'),'country':d.get('country'),'label':d.get('label',d['alias']),'online':True,'registered_at':e['ts'],'last_state_change':e['ts']}
elif t == 'node_deregister': nodes.pop(d['alias'], None)
elif t == 'node_online':
if d['alias'] in nodes: nodes[d['alias']]['online']=True; nodes[d['alias']]['last_state_change']=e['ts']
elif t == 'node_offline':
if d['alias'] in nodes: nodes[d['alias']]['online']=False; nodes[d['alias']]['last_state_change']=e['ts']
elif t == 'unique_user': seen.add(d['ip_hash'])
merkle = hashlib.sha256('\n'.join(sorted(seen)).encode()).hexdigest() if seen else hashlib.sha256(b'').hexdigest()
claimed = json.load(open("$TMP/state-claimed.json"))
ok = True
if claimed['head_seq'] != head[0]: print(f' ✗ head_seq mismatch: replay={head[0]} vs claimed={claimed["head_seq"]}'); ok=False
if claimed['unique_users'] != len(seen): print(f' ✗ unique_users mismatch: replay={len(seen)} vs claimed={claimed["unique_users"]}'); ok=False
if claimed['unique_users_merkle'] != merkle: print(f' ✗ merkle mismatch'); ok=False
if claimed['nodes'].keys() != nodes.keys(): print(f' ✗ nodes set mismatch'); ok=False
for a in nodes:
if claimed['nodes'].get(a,{}).get('online') != nodes[a]['online']:
print(f' ✗ node {a} online mismatch'); ok=False
if ok:
print(f' ✓ replay matches claimed state: head_seq={head[0]}, nodes={len(nodes)}, unique_users={len(seen)}')
else:
sys.exit(1)
PY
RC=$?
echo
if [ $RC -eq 0 ]; then
echo -e "${GREEN}=== ВСЕ ПРОВЕРКИ ЗЕЛЁНЫЕ ===${NC}"
else
echo -e "${RED}=== ПРОВЕРКА ПРОВАЛИЛАСЬ — данные не сходятся ===${NC}"
fi
exit $RC