#!/usr/bin/env sh # SaiCore Linux installer (Go agent) — signed supply chain. # # curl -fsSL https://panel.saicore.ru/install.sh | \ # sudo SAICORE_URL=https://panel.saicore.ru SAICORE_PAIRING=токен sh # # Что делает: # 1. Скачивает ПОДПИСАННЫЙ Ed25519-манифест version.json + .sig # 2. Верифицирует подпись pinned-ключом (SAICORE_RELEASE_PUBKEY hex) # 3. Скачивает бинарь, верифицирует SHA-256 из manifest # 4. Устанавливает systemd-сервис с hardened sandbox # 5. Self-update делает САМ АГЕНТ (с той же Ed25519-проверкой), # НЕ systemd-таймер, «обходящий» подпись. # # Для security-аудита: этот скрипт только fetches + verifies; единственный # trust-root — публичный ключ ниже. set -eu # =================== root of trust =================== # Ed25519 public key релизов SaiCore (32 байта hex, 64 символа). # Чтобы сменить — нужен НОВЫЙ релизный приватный ключ. До этого ни один # сервер не будет принимать компроматные бинари, даже если control-plane # захвачен. SAICORE_RELEASE_PUBKEY="${SAICORE_RELEASE_PUBKEY:-330a7df2290579255d1f99f23cb150547ae70f85b4ba3fd2aed53e9c94b8415f}" # =================== env =================== : "${SAICORE_URL:?SAICORE_URL is required (e.g. https://panel.saicore.ru)}" : "${SAICORE_PAIRING:?SAICORE_PAIRING is required}" INSTALL_DIR="/usr/local/bin" CONFIG_DIR="/etc/saicore" CACHE_DIR="/var/lib/saicore" LOG_DIR="/var/log/saicore" SERVICE_NAME="saicore-agent" SERVICE="/etc/systemd/system/${SERVICE_NAME}.service" LOG_PREFIX="[SaiCore]" BIN="$INSTALL_DIR/saicore-agent" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT log() { printf "%s %s\n" "$LOG_PREFIX" "$*"; } fail() { printf "%s [!] %s\n" "$LOG_PREFIX" "$*" >&2; exit 1; } # ---------- 1. sanity ---------- [ "$(id -u)" = "0" ] || fail "нужны права root (запусти через sudo)." [ -d /run/systemd/system ] || fail "система без systemd не поддерживается." # ---------- 2. detect OS/arch ---------- . /etc/os-release 2>/dev/null || true OS_ID="${ID:-unknown}"; OS_VER="${VERSION_ID:-}" ARCH="$(uname -m)" case "$ARCH" in x86_64|amd64) GOARCH=amd64 ;; aarch64|arm64) GOARCH=arm64 ;; *) fail "архитектура $ARCH не поддерживается (нужно amd64 или arm64)" ;; esac log "OS: $OS_ID $OS_VER ($ARCH -> $GOARCH)" # ---------- 3. deps ---------- install_pkgs() { if command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq "$@" >/dev/null elif command -v dnf >/dev/null 2>&1; then dnf install -y -q "$@" >/dev/null elif command -v yum >/dev/null 2>&1; then yum install -y -q "$@" >/dev/null elif command -v apk >/dev/null 2>&1; then apk add --no-progress --quiet "$@" else fail "не удалось определить пакетный менеджер — установи iptables/curl/python3 вручную." fi } command -v iptables >/dev/null 2>&1 || install_pkgs iptables command -v curl >/dev/null 2>&1 || install_pkgs curl ca-certificates command -v ipset >/dev/null 2>&1 || install_pkgs ipset || true command -v python3 >/dev/null 2>&1 || install_pkgs python3 # ---------- 4. fetch + verify manifest (Ed25519) ---------- log "Загружаю signed release manifest..." curl -fsSL "$SAICORE_URL/dl/version.json" -o "$TMP/version.json" || fail "не смог скачать version.json" curl -fsSL "$SAICORE_URL/dl/version.json.sig" -o "$TMP/version.json.sig" || fail "не смог скачать version.json.sig" # Маленький inline-verifier на Python3 (std-lib hashlib + pure-python ed25519). # Используем минималистичный pure-Python Ed25519 (RFC 8032) чтобы не требовать # 'cryptography' пакет. Если на системе установлен cryptography — используем его. log "Проверяю Ed25519-подпись манифеста..." SAICORE_RELEASE_PUBKEY="$SAICORE_RELEASE_PUBKEY" python3 - "$TMP/version.json" "$TMP/version.json.sig" <<'PYVERIFY' || fail "подпись НЕ прошла проверку — отказ от установки" import os, sys, base64, binascii pub_hex = os.environ["SAICORE_RELEASE_PUBKEY"].strip() pub = binascii.unhexlify(pub_hex) assert len(pub) == 32, f"pubkey len={len(pub)}" with open(sys.argv[1], "rb") as f: msg = f.read() with open(sys.argv[2], "rb") as f: sig = base64.b64decode(f.read().strip()) assert len(sig) == 64, f"sig len={len(sig)}" # Попытка 1: cryptography (fast, cached на многих дистрах) try: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey Ed25519PublicKey.from_public_bytes(pub).verify(sig, msg) print("ok: verified via cryptography") sys.exit(0) except ModuleNotFoundError: pass except Exception as e: print(f"verify failed: {e}", file=sys.stderr); sys.exit(2) # Попытка 2: pure-python (RFC 8032). Медленно, но работает везде. # Короткая, аудитопригодная имплементация. p = 2**255 - 19 def inv(x): return pow(x, p-2, p) d = -121665 * inv(121666) % p def edwards_add(P, Q): x1,y1=P; x2,y2=Q t = d*x1*x2*y1*y2 % p x3 = (x1*y2 + x2*y1) * inv(1 + t) % p y3 = (y1*y2 + x1*x2) * inv(1 - t) % p return (x3, y3) def scalarmult(P, e): if e==0: return (0,1) Q = scalarmult(P, e//2); Q = edwards_add(Q, Q) if e & 1: Q = edwards_add(Q, P) return Q By = 4*inv(5) % p Bx_sq = (By*By - 1) * inv(d*By*By + 1) % p Bx = pow(Bx_sq, (p+3)//8, p) if Bx*Bx % p != Bx_sq % p: Bx = Bx * pow(2, (p-1)//4, p) % p if Bx & 1: Bx = p - Bx B = (Bx, By) def xrecover(y): xx = (y*y-1) * inv(d*y*y+1) % p x = pow(xx, (p+3)//8, p) if x*x % p != xx % p: x = x * pow(2, (p-1)//4, p) % p if x & 1: x = p - x return x def decodepoint(s): y = int.from_bytes(s, 'little') & ((1<<255)-1) x = xrecover(y) if x & 1 != (s[31] >> 7) & 1: x = p - x return (x, y) import hashlib def H(m): return hashlib.sha512(m).digest() def Hint(m): return int.from_bytes(H(m), 'little') q = 2**252 + 27742317777372353535851937790883648493 def verify(pub, msg, sig): A = decodepoint(pub) R = decodepoint(sig[:32]) S = int.from_bytes(sig[32:], 'little') h = Hint(sig[:32] + pub + msg) % q P1 = scalarmult(B, S) P2 = edwards_add(R, scalarmult(A, h)) return P1 == P2 if not verify(pub, msg, sig): print("pure-python verify failed", file=sys.stderr); sys.exit(2) print("ok: verified via pure-python ed25519") PYVERIFY # ---------- 5. разбор manifest: берём expected SHA256 ---------- WANT_SHA="$(python3 -c ' import json,sys with open(sys.argv[1]) as f: m=json.load(f) a=m["artifacts"].get("linux-'"$GOARCH"'") if not a or not a.get("sha256"): sys.exit(1) print(a["sha256"])' "$TMP/version.json")" || fail "в manifest нет sha256 для linux-$GOARCH" log "Expected SHA-256: ${WANT_SHA}" # ---------- 6. скачать бинарь + verify SHA256 ---------- BIN_URL="$SAICORE_URL/dl/saicore-agent-linux-$GOARCH" log "Скачиваю $BIN_URL ..." curl -fsSL "$BIN_URL" -o "$TMP/agent" || fail "не смог скачать бинарь" GOT_SHA="$(sha256sum "$TMP/agent" | awk '{print $1}')" if [ "$GOT_SHA" != "$WANT_SHA" ]; then fail "SHA-256 MISMATCH! want=$WANT_SHA got=$GOT_SHA — отказ." fi log "✓ SHA-256 подтверждён ($GOT_SHA)" # ---------- 7. установка ---------- mkdir -p "$CONFIG_DIR" "$CACHE_DIR" "$LOG_DIR" chmod 750 "$CONFIG_DIR" install -m 0755 "$TMP/agent" "$BIN" log "Размер: $(du -h "$BIN" | awk '{print $1}')" # ---------- 8. registration ---------- log "Регистрируюсь на $SAICORE_URL ..." SAICORE_URL="$SAICORE_URL" SAICORE_PAIRING="$SAICORE_PAIRING" "$BIN" -register \ || fail "регистрация не прошла — проверь URL и pairing token" # ---------- 9. systemd unit (hardened) ---------- log "Ставлю systemd unit (sandbox)..." cat > "$SERVICE" </dev/null 2>&1 systemctl restart "$SERVICE_NAME" # ---------- 10. verify service ---------- log "Проверяю, что агент запущен..." sleep 2 if systemctl is-active --quiet "$SERVICE_NAME"; then log "✓ агент запущен. journalctl -u $SERVICE_NAME -f" else log "[!] не стартовал:" journalctl -u "$SERVICE_NAME" -n 40 --no-pager || true exit 1 fi # ---------- ВАЖНО: self-update делает сам агент ---------- # НЕ ставим systemd-таймер с curl+chmod+x+mv — это обходит подпись. # Встроенный updater (internal/updater/updater.go) раз в 6 часов тянет # version.json + .sig, верифицирует Ed25519, скачивает бинарь, проверяет # SHA-256, атомарно заменяет — и всё это ПОД КОНТРОЛЕМ pinned-ключа. # Если вдруг захочется выключить self-update: # systemctl edit $SERVICE_NAME --drop-in=nopatch # и дописать ExecStart=$BIN -json -no-update log "✓ Готово. Бинарь: $BIN, конфиг: $CONFIG_DIR/agent.json, audit: $LOG_DIR/updates.log" log " Self-update активен (signed), pubkey pinned: ${SAICORE_RELEASE_PUBKEY:0:16}..." log " Удалить: curl -fsSL $SAICORE_URL/uninstall.sh | sudo sh"