feat(release): minisign signature signing + verification
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>
This commit is contained in:
parent
d0162ccda1
commit
5700f78c6b
@ -136,6 +136,36 @@ atomically. (Re-running the installer is idempotent.)
|
||||
the sudoers.** Forgetting it means those root components silently stay stale until
|
||||
the next full reinstall.
|
||||
|
||||
## Signing releases (minisign)
|
||||
|
||||
The sha256 only proves a download is intact — a *compromised host* could swap the
|
||||
tarball **and** its checksum. A minisign signature proves the release is genuinely
|
||||
ours: the host can't forge it without the offline secret key. It ships **inactive**
|
||||
(a `REPLACE_ME` placeholder), so installs work today; once you set a real key,
|
||||
verification becomes **required** for release installs + updates.
|
||||
|
||||
**One-time setup:**
|
||||
```bash
|
||||
minisign -G -p libreportal.pub -s ~/.minisign/libreportal.key # generate the keypair
|
||||
# 1. keep ~/.minisign/libreportal.key OFFLINE (this is the thing to protect)
|
||||
# 2. paste the PUBLIC key (the RW… line) into BOTH:
|
||||
# - libreportal.pub (ships + installed to the root footprint, used by updates)
|
||||
# - install.sh LP_MINISIGN_PUBKEY=… (the standalone bootstrap)
|
||||
# 3. bump footprint_version in init.sh (the footprint's public key changed)
|
||||
```
|
||||
|
||||
**Signing a build:** point `make_release` at the secret key on the release machine:
|
||||
```bash
|
||||
LP_MINISIGN_SECKEY=~/.minisign/libreportal.key scripts/release/make_release.sh stable
|
||||
```
|
||||
It emits `libreportal-<ver>.tar.gz.minisig` alongside the tarball. `install.sh` and
|
||||
the updater (`lpFetchRelease`) download `.minisig`, verify it against the public key,
|
||||
and **refuse** on a bad/missing signature. `--no-verify-signature` on `install.sh`
|
||||
is a dev-only escape hatch.
|
||||
|
||||
> Rotating the key later = repeat steps 2–3 and re-bump `footprint_version` (the
|
||||
> root-owned public key is part of the footprint).
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Versioning:** semver in `VERSION`. Bump before building; `latest.json` carries it.
|
||||
|
||||
8
init.sh
8
init.sh
@ -130,7 +130,7 @@ command_symlink="/usr/local/bin/libreportal"
|
||||
# `update apply` runs as the manager and CANNOT rewrite root-owned files, so a bump
|
||||
# tells the updater the new release needs a root re-install (which re-bakes them).
|
||||
# Recorded at install in $lp_lib_dir/.footprint_version. See docs/DEVELOPMENT.md.
|
||||
footprint_version=1
|
||||
footprint_version=2
|
||||
footprint_marker="$lp_lib_dir/.footprint_version"
|
||||
|
||||
# Directories — three independently-relocatable roots (see scripts/source/paths.sh
|
||||
@ -948,6 +948,12 @@ initRootHelpers()
|
||||
rm -f "$helper_tmp"
|
||||
done
|
||||
|
||||
# Install the release-signing PUBLIC key into the root-owned footprint, so the
|
||||
# runtime updater verifies signatures against a key the manager can't swap.
|
||||
if [[ -f "$script_dir/libreportal.pub" ]]; then
|
||||
sudo install -m 0644 -o root -g root "$script_dir/libreportal.pub" "$lp_lib_dir/libreportal.pub"
|
||||
fi
|
||||
|
||||
# Record the footprint version now installed (root-owned, world-readable) so the
|
||||
# manager-run updater can tell when a release bumps it and a root re-install is
|
||||
# needed. Written here because this re-runs on every install/reinstall.
|
||||
|
||||
23
install.sh
23
install.sh
@ -22,6 +22,13 @@ RELEASE_BASE_URL="${LP_RELEASE_BASE_URL:-https://get.libreportal.org}"
|
||||
PASSWORD="${LP_PASSWORD:-}"
|
||||
SYSTEM_DIR="" ; CONTAINERS_DIR="" ; BACKUPS_DIR="" ; MANAGER_USER="" ; ALLOW_HOME=""
|
||||
DRY_RUN=0
|
||||
NO_VERIFY_SIG=0
|
||||
|
||||
# minisign public key. Keep the SECRET key offline. Once you run `minisign -G`,
|
||||
# paste the public key here AND into libreportal.pub. While it contains REPLACE_ME,
|
||||
# signature verification is skipped (the sha256 still runs); once replaced, a valid
|
||||
# signature becomes REQUIRED for release installs.
|
||||
LP_MINISIGN_PUBKEY="RWREPLACE_ME_run_minisign_-G_then_paste_the_public_key_here_and_in_install.sh"
|
||||
|
||||
usage() {
|
||||
sed -n '3,12p' "$0" | sed 's/^# \{0,1\}//'
|
||||
@ -61,6 +68,7 @@ for arg in "$@"; do
|
||||
--git-user=*) GIT_USER="${arg#*=}" ;;
|
||||
--git-token=*) GIT_TOKEN="${arg#*=}" ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--no-verify-signature) NO_VERIFY_SIG=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "install.sh: unknown option '$arg' (try --help)" >&2; exit 2 ;;
|
||||
esac
|
||||
@ -120,6 +128,21 @@ case "$MODE" in
|
||||
got_sha="$($SHACMD "$STAGE/$tarname" | cut -d' ' -f1)"
|
||||
[[ "$got_sha" == "$want_sha" ]] || err "CHECKSUM MISMATCH — refusing to install. expected $want_sha, got $got_sha"
|
||||
echo "✓ checksum verified ($got_sha)"
|
||||
|
||||
# Signature: once the public key is real (not the REPLACE_ME placeholder), a
|
||||
# valid minisign signature is REQUIRED — it survives a compromised host (the
|
||||
# checksum alone doesn't, since the host serves it too). --no-verify-signature
|
||||
# is a dev escape hatch only.
|
||||
if [[ "$LP_MINISIGN_PUBKEY" != *REPLACE_ME* && "$NO_VERIFY_SIG" -ne 1 ]]; then
|
||||
command -v minisign >/dev/null 2>&1 || { echo "Installing minisign to verify the release ..."; apt-get install -y minisign >/dev/null 2>&1 || true; }
|
||||
command -v minisign >/dev/null 2>&1 || err "minisign required to verify the release but couldn't be installed"
|
||||
fetch "$tarurl.minisig" "$STAGE/$tarname.minisig" || err "release signature (.minisig) missing — refusing"
|
||||
# -P takes the public key as a string (our inline constant is just the
|
||||
# key line, not a full 2-line pubkey file).
|
||||
minisign -Vm "$STAGE/$tarname" -P "$LP_MINISIGN_PUBKEY" -x "$STAGE/$tarname.minisig" >/dev/null 2>&1 \
|
||||
|| err "SIGNATURE INVALID — refusing to install (the release isn't authentic)"
|
||||
echo "✓ signature verified"
|
||||
fi
|
||||
mkdir -p "$STAGE/x"
|
||||
tar xzf "$STAGE/$tarname" -C "$STAGE/x" --strip-components=1
|
||||
SRC="$STAGE/x"
|
||||
|
||||
2
libreportal.pub
Normal file
2
libreportal.pub
Normal file
@ -0,0 +1,2 @@
|
||||
untrusted comment: LibrePortal release signing key — REPLACE_ME (run `minisign -G`)
|
||||
RWREPLACE_ME_run_minisign_-G_then_paste_the_public_key_here_and_in_install.sh
|
||||
@ -53,6 +53,19 @@ cat > "$OUT/latest.json" <<EOF
|
||||
}
|
||||
EOF
|
||||
|
||||
# Sign the tarball with minisign if a secret key is configured. Keep that key
|
||||
# OFFLINE — set LP_MINISIGN_SECKEY to its path on the release machine only. The
|
||||
# public half lives in libreportal.pub + install.sh; verification activates once
|
||||
# you replace their REPLACE_ME placeholder. Produces <tarball>.minisig.
|
||||
if [[ -n "${LP_MINISIGN_SECKEY:-}" ]]; then
|
||||
command -v minisign >/dev/null 2>&1 || { echo "make_release: LP_MINISIGN_SECKEY set but 'minisign' isn't installed" >&2; exit 1; }
|
||||
minisign -Sm "$OUT/$TARBALL" -s "$LP_MINISIGN_SECKEY" -t "libreportal $VERSION ($CHANNEL)" >/dev/null
|
||||
SIGNED=" ✓ $OUT/$TARBALL.minisig (signed)"
|
||||
else
|
||||
SIGNED=" ℹ︎ unsigned (set LP_MINISIGN_SECKEY to sign)"
|
||||
fi
|
||||
|
||||
echo "✓ $OUT/$TARBALL"
|
||||
echo "✓ $OUT/$TARBALL.sha256 ($SHA)"
|
||||
echo "✓ $OUT/latest.json"
|
||||
echo "$SIGNED"
|
||||
|
||||
@ -73,6 +73,23 @@ lpFetchRelease() {
|
||||
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user