97 lines
4.3 KiB
Bash
97 lines
4.3 KiB
Bash
|
|
#!/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
|