The sha256 only proves a download is intact; a compromised host could swap the tarball + its checksum. Add minisign signatures, which prove authenticity (the host can't forge them without the offline secret key). Ships INACTIVE behind a REPLACE_ME placeholder, so installs work until a real key is generated; then it's REQUIRED. - make_release.sh: signs the tarball when LP_MINISIGN_SECKEY is set -> <tarball>.minisig. - libreportal.pub: the public key (placeholder), ships in the tarball and is installed to the ROOT-OWNED footprint (/usr/local/lib/libreportal/libreportal.pub) by init.sh -> the manager can't swap it to accept forged updates. footprint_version -> 2. - install.sh: LP_MINISIGN_PUBKEY constant; once non-placeholder, downloads + verifies the .minisig (minisign -P) and REFUSES on invalid/missing (auto-installs minisign if needed). --no-verify-signature is a dev-only escape hatch. - fetch.sh (update path): verifies against the footprint .pub (minisign -p), refuses on invalid/missing. - docs/DEVELOPMENT.md: keygen (minisign -G), paste pubkey into libreportal.pub + install.sh, keep the secret key offline, sign builds via LP_MINISIGN_SECKEY, bump footprint_version on key rotation. Verified end-to-end with a real throwaway key: good signature accepted; tampered, wrong-key, and missing-signature all refused; placeholder skips (sha256 still enforced). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
127 lines
6.9 KiB
Bash
127 lines
6.9 KiB
Bash
#!/bin/bash
|
|
#
|
|
# LibrePortal source-fetch helpers for the RUNTIME paths (update / reinstall /
|
|
# reset). They place a fresh copy of the code at $script_dir (the install tree,
|
|
# which holds ONLY code — configs/logs live in the separate system tree, so the
|
|
# install tree can be replaced wholesale).
|
|
#
|
|
# Three modes, keyed off CFG_INSTALL_MODE:
|
|
# release download a versioned, checksum-verified tarball over HTTPS (default)
|
|
# git clone (handled by the existing git_* helpers / initGIT)
|
|
# local copy from a local folder (existing copyFilesFromLocal)
|
|
#
|
|
# init.sh's initGIT does the FRESH-install fetch inline (it is self-contained for
|
|
# the bare /root reinstall copy); these functions are the sourced runtime twins.
|
|
|
|
# Release host + channel: env wins (testing), then config, then the public default.
|
|
lpReleaseBaseUrl() { echo "${LP_RELEASE_BASE_URL:-${CFG_RELEASE_BASE_URL:-https://get.libreportal.org}}"; }
|
|
lpReleaseChannel() { echo "${LP_RELEASE_CHANNEL:-${CFG_RELEASE_CHANNEL:-stable}}"; }
|
|
|
|
# Downloader + checksum tool detection (curl|wget, sha256sum|shasum).
|
|
_lpFetchTool() { command -v curl >/dev/null 2>&1 && { echo curl; return; }; command -v wget >/dev/null 2>&1 && { echo wget; return; }; echo ""; }
|
|
_lpDownload() { # _lpDownload <url> <outfile|->
|
|
local tool; tool="$(_lpFetchTool)"
|
|
case "$tool" in
|
|
curl) [[ "$2" == "-" ]] && curl -fsSL "$1" || curl -fsSL "$1" -o "$2" ;;
|
|
wget) [[ "$2" == "-" ]] && wget -qO- "$1" || wget -qO "$2" "$1" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
_lpSha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1; fi; }
|
|
_lpJsonStr() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/'; }
|
|
_lpJsonNum() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*[0-9]+" | head -1 | grep -oE '[0-9]+$'; }
|
|
|
|
# Root-owned-footprint version: the one INSTALLED on this box (marker written by
|
|
# init.sh) vs the one the channel's latest release ships (manifest). When the
|
|
# latter is greater, a plain manager-run update can't apply it (it can't rewrite
|
|
# the root-owned helpers/wrapper/unit) — a root re-install is required.
|
|
lpInstalledFootprintVersion() { local v; v=$(cat /usr/local/lib/libreportal/.footprint_version 2>/dev/null | tr -d ' \t\n\r'); echo "${v:-0}"; }
|
|
lpReleaseLatestFootprint() {
|
|
local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1
|
|
local f; f=$(_lpJsonNum "$m" footprint_version); echo "${f:-0}"
|
|
}
|
|
|
|
# Resolve the latest published version of the configured channel (empty on failure).
|
|
lpReleaseLatestVersion() {
|
|
local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1
|
|
_lpJsonStr "$m" version
|
|
}
|
|
|
|
# Download + verify + extract a release into $script_dir. Arg: optional version
|
|
# (else the channel's latest). Returns non-zero (and leaves $script_dir untouched)
|
|
# on any download/checksum failure — callers must not proceed on failure.
|
|
lpFetchRelease() {
|
|
local want_ver="${1:-}" base channel manifest tarname want_sha tmp tar got
|
|
base="$(lpReleaseBaseUrl)"; channel="$(lpReleaseChannel)"
|
|
[[ -n "$(_lpFetchTool)" ]] || { isError "lpFetchRelease: need curl or wget"; return 1; }
|
|
|
|
if [[ -z "$want_ver" ]]; then
|
|
manifest="$(_lpDownload "$base/$channel/latest.json" - 2>/dev/null)" || { isError "lpFetchRelease: cannot fetch channel manifest"; return 1; }
|
|
want_ver="$(_lpJsonStr "$manifest" version)"
|
|
tarname="$(_lpJsonStr "$manifest" url)"
|
|
want_sha="$(_lpJsonStr "$manifest" sha256)"
|
|
fi
|
|
[[ -n "$want_ver" ]] || { isError "lpFetchRelease: could not determine version"; return 1; }
|
|
[[ -n "$tarname" ]] || tarname="libreportal-${want_ver}.tar.gz"
|
|
|
|
tmp="$(mktemp -d)"; tar="$tmp/$tarname"
|
|
if ! _lpDownload "$base/$channel/$tarname" "$tar"; then isError "lpFetchRelease: download failed ($tarname)"; rm -rf "$tmp"; return 1; fi
|
|
if [[ -z "$want_sha" ]]; then
|
|
_lpDownload "$base/$channel/$tarname.sha256" "$tar.sha256" 2>/dev/null && want_sha="$(cut -d' ' -f1 < "$tar.sha256")"
|
|
fi
|
|
[[ -n "$want_sha" ]] || { isError "lpFetchRelease: no checksum available — refusing"; rm -rf "$tmp"; return 1; }
|
|
got="$(_lpSha256 "$tar")"
|
|
if [[ "$got" != "$want_sha" ]]; then isError "lpFetchRelease: CHECKSUM MISMATCH ($tarname) — refusing"; rm -rf "$tmp"; return 1; fi
|
|
|
|
# Signature: once the root-owned public key is real (not the REPLACE_ME
|
|
# placeholder), a valid minisign signature is REQUIRED. The key lives in the
|
|
# footprint so the manager can't swap it to accept a forged update.
|
|
local pub="/usr/local/lib/libreportal/libreportal.pub"
|
|
if [[ -f "$pub" ]] && ! grep -q REPLACE_ME "$pub" 2>/dev/null; then
|
|
if ! command -v minisign >/dev/null 2>&1; then
|
|
isError "lpFetchRelease: minisign required to verify the release but not installed"; rm -rf "$tmp"; return 1
|
|
fi
|
|
if ! _lpDownload "$base/$channel/$tarname.minisig" "$tar.minisig"; then
|
|
isError "lpFetchRelease: release signature (.minisig) missing — refusing"; rm -rf "$tmp"; return 1
|
|
fi
|
|
if ! minisign -Vm "$tar" -p "$pub" -x "$tar.minisig" >/dev/null 2>&1; then
|
|
isError "lpFetchRelease: SIGNATURE INVALID ($tarname) — refusing"; rm -rf "$tmp"; return 1
|
|
fi
|
|
isNotice "Release signature verified."
|
|
fi
|
|
|
|
# Replace the install tree (code only; configs/logs are in the system tree).
|
|
runInstallOp rm -rf "$script_dir"
|
|
runInstallOp mkdir -p "$script_dir"
|
|
if ! runInstallOp tar xzf "$tar" -C "$script_dir" --strip-components=1; then
|
|
isError "lpFetchRelease: extract failed"; rm -rf "$tmp"; return 1
|
|
fi
|
|
rm -rf "$tmp"
|
|
isSuccessful "Fetched LibrePortal $want_ver ($channel) and verified its checksum."
|
|
}
|
|
|
|
# Mode-aware runtime fetch. git/local stay with their existing helpers; this adds
|
|
# the release path. Callers (updater/reinstall/reset) decide pre/post (config
|
|
# backup, redeploy) around it.
|
|
lpFetchSource() {
|
|
case "${CFG_INSTALL_MODE:-release}" in
|
|
release) lpFetchRelease "$@" ;;
|
|
local) declare -f copyFilesFromLocal >/dev/null 2>&1 && copyFilesFromLocal ;;
|
|
git) declare -f gitFolderResetAndBackup >/dev/null 2>&1 && gitFolderResetAndBackup ;;
|
|
*) isError "lpFetchSource: unknown CFG_INSTALL_MODE '${CFG_INSTALL_MODE}'"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Semver-ish compare: lpVersionGt A B → true if A > B (numeric dotted compare,
|
|
# trailing non-numeric ignored). Used by the updater + the badge generator.
|
|
lpVersionGt() {
|
|
local a="${1%%-*}" b="${2%%-*}" i x y
|
|
local -a A B; IFS='.' read -r -a A <<< "$a"; IFS='.' read -r -a B <<< "$b"
|
|
for ((i=0; i<${#A[@]} || i<${#B[@]}; i++)); do
|
|
x=$((10#${A[i]:-0} + 0)) 2>/dev/null; y=$((10#${B[i]:-0} + 0)) 2>/dev/null
|
|
(( x > y )) && return 0
|
|
(( x < y )) && return 1
|
|
done
|
|
return 1
|
|
}
|