LibrePortal/scripts/release/make_release.sh
librelad b28268a61f feat(system): "Verified" integrity check against the signed release manifest
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>
2026-05-28 19:41:22 +01:00

121 lines
5.5 KiB
Bash
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"