#!/usr/bin/env bash # Minimal production bootstrap served from https://install.siyaj.wb.sa/. set -Eeuo pipefail umask 077 CHANNEL="${SIYAJ_RELEASE_CHANNEL:-stable}" RELEASE_BASE_URL="${SIYAJ_RELEASE_BASE_URL:-https://releases.siyaj.wb.sa}" # Production publishing replaces this placeholder with the pinned release root. RELEASE_PUBLIC_KEY="${SIYAJ_RELEASE_PUBLIC_KEY:-LlU8qbb_49k7hVSjDh6jlFypCIjJYXO5cP9At6g_t3s}" TMP="$(mktemp -d -t siyaj-install.XXXXXXXX)" trap 'rm -rf "$TMP"' EXIT INT TERM fail(){ printf 'SIYAJ installer: %s\n' "$1" >&2; exit 1; } need(){ command -v "$1" >/dev/null 2>&1 || fail "required tool not found: $1"; } [ "$(id -u)" -eq 0 ] || fail "run with sudo" need curl; need python3; need openssl; need sha256sum; need base64; need tar [ "$(uname -s)" = Linux ] || fail "Ubuntu Server 22.04 or 24.04 is required" case "$(uname -m)" in x86_64|amd64) ;; *) fail "x86_64/amd64 is required";; esac case "$RELEASE_BASE_URL" in https://releases.siyaj.wb.sa|https://releases.siyaj.wb.sa/*) ;; *) [ "${SIYAJ_ALLOW_RELEASE_URL_OVERRIDE:-}" = 1 ] || fail "release URL override requires SIYAJ_ALLOW_RELEASE_URL_OVERRIDE=1" case "$RELEASE_BASE_URL" in https://*) ;; *) fail "release URL must use HTTPS";; esac SIYAJ_ALLOWED_RELEASE_HOST="$(python3 -c 'import sys,urllib.parse; print(urllib.parse.urlparse(sys.argv[1]).hostname or "")' "$RELEASE_BASE_URL")" [ -n "$SIYAJ_ALLOWED_RELEASE_HOST" ] || fail "release URL override has no hostname" export SIYAJ_ALLOWED_RELEASE_HOST esac case "$RELEASE_PUBLIC_KEY" in __SIYAJ_*) fail "release verification key is not configured";; esac MANIFEST_URL="${RELEASE_BASE_URL%/}/${CHANNEL}/manifest.json" SIGNATURE_URL="${RELEASE_BASE_URL%/}/${CHANNEL}/manifest.sig" curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-redirs 0 "$MANIFEST_URL" -o "$TMP/manifest.json" curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-redirs 0 "$SIGNATURE_URL" -o "$TMP/manifest.sig" python3 - "$TMP" "$RELEASE_PUBLIC_KEY" <<'PY' import base64,sys from pathlib import Path root=Path(sys.argv[1]); raw=base64.urlsafe_b64decode(sys.argv[2]+'==') if len(raw)!=32: raise SystemExit('invalid pinned release public key') prefix=bytes.fromhex('302a300506032b6570032100') (root/'release-public.der').write_bytes(prefix+raw) sig=base64.urlsafe_b64decode((root/'manifest.sig').read_text().strip()+'==') (root/'manifest.sig.bin').write_bytes(sig) PY openssl pkeyutl -verify -pubin -keyform DER -inkey "$TMP/release-public.der" -rawin -in "$TMP/manifest.json" -sigfile "$TMP/manifest.sig.bin" >/dev/null 2>&1 || fail "release manifest signature verification failed" readarray -t FIELDS < <(python3 - "$TMP/manifest.json" "$CHANNEL" <<'PY' import json,sys,urllib.parse,datetime m=json.load(open(sys.argv[1])); channel=sys.argv[2] if m.get('schema')!='siyaj_release_manifest_v1' or m.get('channel')!=channel: raise SystemExit('invalid release manifest') now=datetime.datetime.now(datetime.timezone.utc) def dt(v): return datetime.datetime.fromisoformat(v.replace('Z','+00:00')) try: published,expires=dt(m['published_at']),dt(m['expires_at']) except Exception: raise SystemExit('invalid release validity window') if published > now+datetime.timedelta(minutes=15) or expires <= now or expires <= published: raise SystemExit('expired or invalid release manifest') arts=[a for a in m.get('artifacts',[]) if a.get('os')=='linux' and a.get('architecture')=='amd64'] if len(arts)!=1: raise SystemExit('no unique linux/amd64 release') a=arts[0]; u=urllib.parse.urlparse(a['url']) if u.scheme!='https' or not u.hostname or u.username or u.fragment: raise SystemExit('invalid artifact URL') base=urllib.parse.urlparse(__import__('os').environ.get('SIYAJ_RELEASE_BASE_URL','https://releases.siyaj.wb.sa')) if u.hostname.lower() != (base.hostname or '').lower(): raise SystemExit('artifact host is not the approved release host') if '/' in a['name'] or '\\' in a['name'] or int(a['size'])<=0 or int(a['size'])>8*1024**3: raise SystemExit('invalid artifact metadata') print(a['url']); print(a['sha256']); print(a['size']); print(a['name']) PY ) || fail "release manifest validation failed" [ "${#FIELDS[@]}" -eq 4 ] || fail "release manifest is incomplete" ARTIFACT="$TMP/${FIELDS[3]}" curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-redirs 0 "${FIELDS[0]}" -o "$ARTIFACT" [ "$(stat -c %s "$ARTIFACT")" = "${FIELDS[2]}" ] || fail "release artifact size mismatch" printf '%s %s\n' "${FIELDS[1]}" "$ARTIFACT" | sha256sum -c - >/dev/null || fail "release artifact checksum mismatch" mkdir "$TMP/release" python3 - "$ARTIFACT" "$TMP/release" <<'PY' import os,sys,tarfile archive,dst=sys.argv[1:] with tarfile.open(archive,'r:gz') as tf: for m in tf.getmembers(): n=os.path.normpath(m.name) if os.path.isabs(n) or n=='..' or n.startswith('../') or m.issym() or m.islnk() or m.isdev(): raise SystemExit('unsafe release archive') tf.extractall(dst) PY ROOTS=("$TMP/release"/*) [ "${#ROOTS[@]}" -eq 1 ] && [ -x "${ROOTS[0]}/bin/siyajctl" ] || fail "release does not contain siyajctl" exec "${ROOTS[0]}/bin/siyajctl" install --source "${ROOTS[0]}" --release-manifest "$TMP/manifest.json" --manifest-signature "$TMP/manifest.sig" --release-public-key "$RELEASE_PUBLIC_KEY" "$@"