Adds per-file integrity attestation on top of the existing signed-tarball release flow. make_release now generates a SHA256SUMS manifest over the shipped tree and (when a key is configured) signs it, riding both inside the release tarball so they land in the install tree with no extra download. lpVerifyInstall (scripts/source/verify.sh) re-hashes the install tree against that manifest and verifies the manifest's minisign signature against the root-owned footprint pubkey, yielding states: verified / modified / tampered / unsigned / unverifiable / development. webuiSystemVerify writes verify_status.json (throttled daily, force on demand, also after each update apply), surfaced as an Integrity line + "Verify now" button on the Admin → Overview Updates card and a row in the update details panel. `libreportal verify` exposes the same check on the CLI. Honest framing: this is a self-check (run by the software it verifies), so red fires only for genuine modified/tampered states; the badge tooltip points to out-of-band `minisign -Vm` for an independent guarantee. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
121 lines
5.5 KiB
Bash
121 lines
5.5 KiB
Bash
#!/bin/bash
|
||
#
|
||
# Build a versioned, checksum-verified LibrePortal release artifact.
|
||
#
|
||
# The stable install fetches a release tarball over plain HTTPS (no git, no auth),
|
||
# so it's reproducible and version-pinned. This builds that artifact from the
|
||
# COMMITTED tree via `git archive`, which honours the `export-ignore` rules in
|
||
# .gitattributes — so dev-only trees (scripts/unused, site, .claude, …) never ship.
|
||
#
|
||
# No infrastructure needed: output lands in dist/<channel>/ laid out exactly like
|
||
# the hosting will serve it, so you can point an install at it with:
|
||
# ( cd dist && python3 -m http.server 8000 )
|
||
# LP_RELEASE_BASE_URL=http://localhost:8000 ./install.sh ...
|
||
#
|
||
# Usage: scripts/release/make_release.sh [channel] [git-ref]
|
||
# channel stable (default) | edge
|
||
# git-ref HEAD (default) | a tag/commit to build from
|
||
set -euo pipefail
|
||
|
||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
cd "$REPO_ROOT"
|
||
|
||
CHANNEL="${1:-stable}"
|
||
REF="${2:-HEAD}"
|
||
|
||
VERSION="$(tr -d ' \t\n\r' < VERSION 2>/dev/null || true)"
|
||
[[ -n "$VERSION" ]] || { echo "make_release: VERSION file is empty or missing" >&2; exit 1; }
|
||
|
||
# Root-owned-footprint version (helpers/wrapper/unit/sudoers). Published in the
|
||
# manifest so the updater can detect when an update needs a root re-install.
|
||
FOOTPRINT_VERSION="$(grep -oE '^footprint_version=[0-9]+' init.sh | head -1 | cut -d= -f2)"
|
||
[[ -n "$FOOTPRINT_VERSION" ]] || FOOTPRINT_VERSION=0
|
||
|
||
# Guard: a release must never ship stale source arrays. Regenerate them; if that
|
||
# changes anything not committed, the tree was stale — abort and tell the dev to
|
||
# commit, so `git archive` (committed files only) can't bake in a mismatch. Only
|
||
# enforced when building HEAD (an explicit old tag/ref is taken as-is).
|
||
if [[ "$REF" == "HEAD" && -x scripts/source/files/generate_arrays.sh ]]; then
|
||
scripts/source/files/generate_arrays.sh run >/dev/null 2>&1 || true
|
||
if ! git diff --quiet -- scripts/source/files/arrays 2>/dev/null; then
|
||
echo "make_release: source arrays were stale — regenerated them now." >&2
|
||
echo " Commit the changes under scripts/source/files/arrays/ and re-run." >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
TARBALL="libreportal-${VERSION}.tar.gz"
|
||
PREFIX="libreportal-${VERSION}/" # top-level dir inside the tarball
|
||
OUT="$REPO_ROOT/dist/$CHANNEL"
|
||
mkdir -p "$OUT"
|
||
|
||
echo "Building $TARBALL (channel=$CHANNEL, ref=$REF) ..."
|
||
|
||
# Build into a staging tree (not a streamed tarball) so we can drop a per-file
|
||
# integrity manifest INSIDE the release. `git archive --format=tar` still honours
|
||
# .gitattributes export-ignore, so dev-only trees stay out of the staging tree.
|
||
STAGE="$(mktemp -d)"
|
||
trap 'rm -rf "$STAGE"' EXIT
|
||
git archive --format=tar --prefix="$PREFIX" "$REF" | tar -x -C "$STAGE"
|
||
|
||
# SHA256SUMS: one `sha256sum`-format line per shipped file (relative ./paths,
|
||
# stable sort), excluding the manifest + its signature themselves. The running
|
||
# install re-hashes against this to prove its files match the signed release
|
||
# (see lpVerifyInstall in scripts/source/verify.sh). The manifest's own trust
|
||
# comes from SHA256SUMS.minisig below, not from being in the list.
|
||
(
|
||
cd "$STAGE/$PREFIX"
|
||
find . -type f ! -name SHA256SUMS ! -name SHA256SUMS.minisig -print0 \
|
||
| LC_ALL=C sort -z | xargs -0 sha256sum > SHA256SUMS
|
||
)
|
||
MANIFEST_FILES="$(wc -l < "$STAGE/$PREFIX/SHA256SUMS" | tr -d ' ')"
|
||
|
||
# Sign the manifest with the same offline minisign key used for the tarball, so
|
||
# the install can verify it against the root-owned public key. Unsigned builds
|
||
# still ship SHA256SUMS (drift detection works; the badge just reports it as
|
||
# unsigned). Keep LP_MINISIGN_SECKEY on the release machine only.
|
||
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 "$STAGE/$PREFIX/SHA256SUMS" -s "$LP_MINISIGN_SECKEY" -t "libreportal $VERSION manifest ($CHANNEL)" >/dev/null
|
||
MANIFEST_SIGNED=" ✓ SHA256SUMS.minisig (manifest signed, inside tarball)"
|
||
else
|
||
MANIFEST_SIGNED=" ℹ︎ SHA256SUMS unsigned (set LP_MINISIGN_SECKEY to sign)"
|
||
fi
|
||
|
||
# Pack the staging tree (manifest + signature included). --sort/--owner/--group
|
||
# keep the archive deterministic for a given commit.
|
||
tar --sort=name --owner=0 --group=0 --numeric-owner -czf "$OUT/$TARBALL" -C "$STAGE" "$PREFIX"
|
||
|
||
( cd "$OUT" && sha256sum "$TARBALL" > "$TARBALL.sha256" )
|
||
SHA="$(cut -d' ' -f1 < "$OUT/$TARBALL.sha256")"
|
||
|
||
cat > "$OUT/latest.json" <<EOF
|
||
{
|
||
"version": "$VERSION",
|
||
"channel": "$CHANNEL",
|
||
"url": "$TARBALL",
|
||
"sha256": "$SHA",
|
||
"footprint_version": $FOOTPRINT_VERSION,
|
||
"notes": ""
|
||
}
|
||
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"
|
||
echo " ✓ SHA256SUMS ($MANIFEST_FILES files, inside tarball)"
|
||
echo "$MANIFEST_SIGNED"
|