montana/Node/External-Audit/scripts/verify-state.sh

97 lines
4.3 KiB
Bash
Raw Normal View History

2026-05-21 03:44:38 +03:00
#!/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