#!/bin/sh # install.sh — install Startux for the current user. No root needed. # # curl -fsSL https://startux.app/go | bash # # (The standalone startux.app repository pins this exact script to a Startux # commit and serves it as /go.) # # Downloads the release binary for your architecture, verifies its minisign # release signature plus the signed manifest's size and SHA-256, atomically # installs it to ~/.local/bin, and adds an app-menu entry. set -eu # Override for mirrors and hermetic installer tests, e.g.: # STARTUX_RELEASES_URL=https://example.invalid/releases sh install.sh STARTUX_RELEASES_URL="${STARTUX_RELEASES_URL:-https://gitgud.io/mike/startux/-/releases}" ASSET_URL="$STARTUX_RELEASES_URL/permalink/latest/downloads" # STARTUX_PRODUCTION_MINISIGN_PUBLIC_KEY_GOES_HERE. Mike replaces this empty # base64 payload during the owner-held offline key ceremony. There is # deliberately no environment override: until the key is embedded, the # production installer fails closed instead of trusting unsigned releases. STARTUX_PUBLIC_KEY='' BIN_NAME="startux" BIN_DIR="$HOME/.local/bin" APP_DIR="$HOME/.local/share/applications" ICON_DIR="$HOME/.local/share/icons/hicolor/scalable/apps" die() { printf 'install: error: %s\n' "$*" >&2; exit 1; } info() { printf 'install: %s\n' "$*"; } # --- architecture ---------------------------------------------------------- case "$(uname -m)" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) die "unsupported architecture: $(uname -m) (want x86_64 or aarch64)" ;; esac ASSET="startux-linux-$ARCH" # --- download tool ----------------------------------------------------------- case "$STARTUX_RELEASES_URL" in https://*) ;; *) die "release URL must use HTTPS: $STARTUX_RELEASES_URL" ;; esac command -v curl >/dev/null 2>&1 || die "need curl to download Startux over HTTPS" fetch() { curl --fail --silent --show-error --location \ --proto '=https' --proto-redir '=https' --tlsv1.2 \ --max-filesize "$3" --output "$2" "$1" } # --- checksum tool ----------------------------------------------------------- if command -v sha256sum >/dev/null 2>&1; then sha256_of() { sha256sum "$1" | cut -d ' ' -f 1; } elif command -v shasum >/dev/null 2>&1; then sha256_of() { shasum -a 256 "$1" | cut -d ' ' -f 1; } else die "need sha256sum (or shasum) to verify the download" fi command -v sync >/dev/null 2>&1 || die "need sync to install Startux atomically" [ -n "$STARTUX_PUBLIC_KEY" ] || die "release verification is not configured yet; the project owner must publish and embed the production signing key" # --- minisign verification -------------------------------------------------- # Prefer minisign itself. OpenSSL 3 is a cryptographically equivalent local # fallback: it validates both the Ed25519 message signature and minisign's # global signature over the trusted comment, including key-id/algorithm checks. verify_minisign_openssl() ( message="$1" signature_file="$2" work="$TMP/minisign-verify" rm -rf "$work" mkdir -m 700 "$work" || return 1 [ "$(wc -l <"$signature_file" | tr -d ' ')" = 4 ] || return 1 printf '%s\n' "$STARTUX_PUBLIC_KEY" | base64 -d >"$work/public.raw" 2>/dev/null || return 1 sed -n '2p' "$signature_file" | base64 -d >"$work/signature.raw" 2>/dev/null || return 1 sed -n '4p' "$signature_file" | base64 -d >"$work/global-signature.raw" 2>/dev/null || return 1 [ "$(wc -c <"$work/public.raw" | tr -d ' ')" = 42 ] || return 1 [ "$(wc -c <"$work/signature.raw" | tr -d ' ')" = 74 ] || return 1 [ "$(wc -c <"$work/global-signature.raw" | tr -d ' ')" = 64 ] || return 1 public_algorithm="$(od -An -tx1 -N2 "$work/public.raw" | tr -d ' \n')" signature_algorithm="$(od -An -tx1 -N2 "$work/signature.raw" | tr -d ' \n')" public_key_id="$(od -An -tx1 -j2 -N8 "$work/public.raw" | tr -d ' \n')" signature_key_id="$(od -An -tx1 -j2 -N8 "$work/signature.raw" | tr -d ' \n')" [ "$public_algorithm" = 4564 ] || return 1 case "$signature_algorithm" in 4564|4544) ;; *) return 1 ;; esac [ "$public_key_id" = "$signature_key_id" ] || return 1 trusted_comment="$(sed -n '3s/^trusted comment: //p' "$signature_file")" grep -q '^trusted comment: ' "$signature_file" || return 1 # ASN.1 SubjectPublicKeyInfo prefix for a raw Ed25519 public key. printf '\060\052\060\005\006\003\053\145\160\003\041\000' >"$work/public.der" dd if="$work/public.raw" of="$work/public.der" bs=1 skip=10 count=32 seek=12 conv=notrunc 2>/dev/null || return 1 dd if="$work/signature.raw" of="$work/message.sig" bs=1 skip=10 count=64 2>/dev/null || return 1 verify_input="$message" if [ "$signature_algorithm" = 4544 ]; then openssl dgst -blake2b512 -binary -out "$work/message.hash" "$message" >/dev/null 2>&1 || return 1 verify_input="$work/message.hash" fi openssl pkeyutl -verify -pubin -keyform DER -inkey "$work/public.der" \ -rawin -in "$verify_input" -sigfile "$work/message.sig" >/dev/null 2>&1 || return 1 cp "$work/message.sig" "$work/global-message" || return 1 printf '%s' "$trusted_comment" >>"$work/global-message" openssl pkeyutl -verify -pubin -keyform DER -inkey "$work/public.der" \ -rawin -in "$work/global-message" -sigfile "$work/global-signature.raw" >/dev/null 2>&1 || return 1 ) verify_minisign() { message="$1" signature_file="$2" if command -v minisign >/dev/null 2>&1; then minisign -Vm "$message" -x "$signature_file" -P "$STARTUX_PUBLIC_KEY" >/dev/null 2>&1 return fi command -v openssl >/dev/null 2>&1 || die "need minisign or OpenSSL 3 to verify the release signature" case "$(openssl version 2>/dev/null)" in "OpenSSL 3."*) ;; *) die "need minisign or OpenSSL 3 to verify the release signature" ;; esac command -v base64 >/dev/null 2>&1 || die "need base64 to verify the release signature" command -v od >/dev/null 2>&1 || die "need od to verify the release signature" verify_minisign_openssl "$message" "$signature_file" } manifest_artifact() { manifest="$1" wanted="$2" awk -v wanted="$wanted" ' $0 == " \"name\": \"" wanted "\"," { matches++ if ((getline osline) <= 0 || (getline archline) <= 0 || (getline sizeline) <= 0 || (getline shaline) <= 0) exit 40 if (osline != " \"os\": \"linux\",") exit 41 arch = archline; sub(/^ "arch": "/, "", arch); sub(/",$/, "", arch) size = sizeline; sub(/^ "size": /, "", size); sub(/,$/, "", size) sha = shaline; sub(/^ "sha256": "/, "", sha); sub(/"$/, "", sha) print arch "|" size "|" sha } END { if (matches != 1) exit 42 } ' "$manifest" } # atomic_replace stages in the destination directory, fsyncs the complete # bytes, and renames over the destination. The binary update additionally # retains one previous version without ever moving the live binary aside. atomic_replace() ( source_file="$1" target_file="$2" file_mode="$3" keep_previous="$4" target_dir="${target_file%/*}" target_name="${target_file##*/}" target_tmp="$target_dir/.$target_name.install.$$" previous_tmp="$target_dir/.$target_name.previous.$$" trap 'rm -f "$target_tmp" "$previous_tmp"' EXIT HUP INT TERM cp "$source_file" "$target_tmp" || die "could not stage $target_file" chmod "$file_mode" "$target_tmp" || die "could not set permissions on $target_file" cmp -s "$source_file" "$target_tmp" || die "staged bytes differ for $target_file" sync -f "$target_tmp" || die "could not flush $target_file to disk" if [ "$keep_previous" = yes ] && [ -f "$target_file" ]; then cp "$target_file" "$previous_tmp" || die "could not preserve the previous Startux binary" chmod "$file_mode" "$previous_tmp" || die "could not secure the previous Startux binary" sync -f "$previous_tmp" || die "could not flush the previous Startux binary" mv -f "$previous_tmp" "$target_file.previous" || die "could not save the previous Startux binary" fi mv -f "$target_tmp" "$target_file" || die "could not activate $target_file" sync -f "$target_dir" || die "could not flush the Startux installation directory" ) # --- download + verify --------------------------------------------------------- TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT info "downloading signed release manifest" fetch "$ASSET_URL/startux-release.json" "$TMP/startux-release.json" 1048576 || die "release manifest download failed" fetch "$ASSET_URL/startux-release.json.minisig" "$TMP/startux-release.json.minisig" 16384 || die "release manifest signature download failed" verify_minisign "$TMP/startux-release.json" "$TMP/startux-release.json.minisig" || die "release manifest signature is invalid" artifact="$(manifest_artifact "$TMP/startux-release.json" "$ASSET")" || die "signed release manifest has no unique valid entry for $ASSET" old_ifs="$IFS" IFS='|' read -r manifest_arch expected_size expected_sha <"$TMP/startux.svg" <<'STARTUX_ICON' Startux STARTUX_ICON # Exec uses the absolute install path because desktop sessions often omit # ~/.local/bin from PATH. Desktop Entry quoted arguments must escape these # four characters; percent is doubled so it cannot become a field code. # The single-quoted sed expression intentionally matches a literal dollar. # shellcheck disable=SC2016 desktop_exec="$(printf '%s' "$BIN_DIR/$BIN_NAME" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')" cat >"$TMP/startux.desktop" </dev/null 2>&1; then update-desktop-database "$APP_DIR" >/dev/null 2>&1 || true fi if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" >/dev/null 2>&1 || true fi # --- PATH hint ------------------------------------------------------------------- case ":$PATH:" in *":$BIN_DIR:"*) ;; *) printf '\nNOTE: %s is not in your PATH.\n' "$BIN_DIR" printf 'Add this line to ~/.profile (or your shell rc file), then log out and back in:\n' printf " export PATH=\"\$HOME/.local/bin:\$PATH\"\n" ;; esac printf '\nStartux installed. Start it with: %s\n' "$BIN_NAME"