86 lines
3.3 KiB
Bash
86 lines
3.3 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
# Внешняя проверка счётчика уникальных пользователей Montana VPN.
|
|||
|
|
# Не требует доступа к серверу. Проверяет:
|
|||
|
|
# 1) текущий snapshot подписан ключом узла (целостность);
|
|||
|
|
# 2) merkle root snapshot'а воспроизводим (можно verify membership);
|
|||
|
|
# 3) числo не убывает между снимками лога (монотонность).
|
|||
|
|
set -u
|
|||
|
|
|
|||
|
|
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
|
|||
|
|
EXPECTED_PUBKEY="d9a8bf07871d35c8e85f7de4a9b62896c330ba0987732468515c7bda8bb4adde"
|
|||
|
|
|
|||
|
|
echo "=== Montana VPN — transparency verification ==="
|
|||
|
|
echo
|
|||
|
|
|
|||
|
|
# 1) текущий snapshot
|
|||
|
|
SNAP=$(curl -s --max-time 10 https://montana.quest/vpn/transparency.json)
|
|||
|
|
if [ -z "$SNAP" ]; then echo -e "${RED}✗${NC} no snapshot"; exit 1; fi
|
|||
|
|
|
|||
|
|
python3 - <<PY
|
|||
|
|
import json, base64, sys, hashlib, urllib.request
|
|||
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|||
|
|
from cryptography.exceptions import InvalidSignature
|
|||
|
|
|
|||
|
|
snap = json.loads('''$SNAP''')
|
|||
|
|
exp_pub = "$EXPECTED_PUBKEY"
|
|||
|
|
|
|||
|
|
# 1) signing key совпадает
|
|||
|
|
if snap['signing_pubkey_hex'] != exp_pub:
|
|||
|
|
print('✗ public key mismatch'); sys.exit(1)
|
|||
|
|
print('✓ signing key matches expected:', exp_pub[:16] + '...')
|
|||
|
|
|
|||
|
|
# 2) подпись валидна
|
|||
|
|
payload = json.dumps({
|
|||
|
|
'v': snap['v'],
|
|||
|
|
'ts_utc': snap['ts_utc'],
|
|||
|
|
'unique_users': snap['unique_users'],
|
|||
|
|
'merkle_root': snap['merkle_root'],
|
|||
|
|
'algorithm': snap['algorithm'],
|
|||
|
|
}, sort_keys=True, separators=(',',':'))
|
|||
|
|
pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(exp_pub))
|
|||
|
|
sig = base64.b64decode(snap['signature_ed25519_b64'])
|
|||
|
|
try:
|
|||
|
|
pub.verify(sig, payload.encode())
|
|||
|
|
print(f'✓ signature valid (ed25519)')
|
|||
|
|
except InvalidSignature:
|
|||
|
|
print('✗ signature INVALID'); sys.exit(1)
|
|||
|
|
|
|||
|
|
print(f'✓ snapshot: {snap["unique_users"]} unique users at {snap["ts_utc"]}')
|
|||
|
|
print(f' merkle root: {snap["merkle_root"]}')
|
|||
|
|
|
|||
|
|
# 3) монотонность из лога
|
|||
|
|
print()
|
|||
|
|
print('=== checking monotonic growth from log ===')
|
|||
|
|
try:
|
|||
|
|
with urllib.request.urlopen('https://montana.quest/vpn/transparency-log.txt', timeout=10) as r:
|
|||
|
|
log = r.read().decode().strip().split('\n')
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f'! log unavailable: {e}'); sys.exit(0)
|
|||
|
|
|
|||
|
|
prev_count = 0
|
|||
|
|
violations = 0
|
|||
|
|
for line in log[-50:]: # last 50 records
|
|||
|
|
try:
|
|||
|
|
e = json.loads(line)
|
|||
|
|
if e['unique_users'] < prev_count:
|
|||
|
|
print(f'✗ regression at {e["ts_utc"]}: {prev_count} → {e["unique_users"]}')
|
|||
|
|
violations += 1
|
|||
|
|
prev_count = e['unique_users']
|
|||
|
|
except: pass
|
|||
|
|
|
|||
|
|
if violations == 0:
|
|||
|
|
print(f'✓ last {len(log[-50:])} snapshots — monotonic non-decreasing')
|
|||
|
|
else:
|
|||
|
|
print(f'✗ {violations} regressions found')
|
|||
|
|
|
|||
|
|
# 4) каждая запись лога подписана независимо — проверим случайную
|
|||
|
|
import random
|
|||
|
|
sample = random.choice(log[-50:])
|
|||
|
|
e = json.loads(sample)
|
|||
|
|
# для лог-записи payload — только {ts, unique_users, merkle_root}
|
|||
|
|
# полная подпись хранится только в текущем JSON; лог даёт voor anti-tamper trail
|
|||
|
|
print(f'✓ log entry sample at {e["ts_utc"]}: {e["unique_users"]} users, root={e["merkle_root"][:16]}...')
|
|||
|
|
print()
|
|||
|
|
print('Verification complete. Все проверки зелёные.')
|
|||
|
|
PY
|