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>
113 lines
4.9 KiB
Bash
113 lines
4.9 KiB
Bash
#!/bin/bash
|
|
#
|
|
# Integrity verification of the installed LibrePortal code tree.
|
|
#
|
|
# Re-hashes $script_dir against the signed SHA256SUMS manifest that ships INSIDE
|
|
# every release tarball (built by scripts/release/make_release.sh) and checks
|
|
# that manifest's minisign signature against the ROOT-OWNED public key in the
|
|
# footprint (/usr/local/lib/libreportal/libreportal.pub). A pass proves the
|
|
# installed files still match the release we published and signed.
|
|
#
|
|
# Honest scope: this runs BY the software it verifies, so it catches accidental
|
|
# drift, partial tampering, and "is this a real signed release" — but it is NOT
|
|
# tamper-proof on its own (a wholesale code swap could also fake the result).
|
|
# The hard guarantee is out-of-band: the user running `minisign -Vm <tarball>`.
|
|
#
|
|
# Results land in LP_VERIFY_* globals so the generator + CLI can format them:
|
|
# LP_VERIFY_STATE verified|modified|tampered|unsigned|unverifiable|development
|
|
# LP_VERIFY_SIGNED true|false (a real, non-placeholder key signed it)
|
|
# LP_VERIFY_SIG_VALID true|false|null (null = not applicable / couldn't check)
|
|
# LP_VERIFY_TOTAL files listed in the manifest
|
|
# LP_VERIFY_OK / _MODIFIED / _MISSING
|
|
# LP_VERIFY_SAMPLE up to 5 offending relative paths, newline-separated
|
|
# LP_VERIFY_ERROR human message, or empty
|
|
|
|
lpVerifyPubKeyPath() { echo "/usr/local/lib/libreportal/libreportal.pub"; }
|
|
|
|
lpVerifyInstall() {
|
|
LP_VERIFY_STATE="development"
|
|
LP_VERIFY_SIGNED="false"
|
|
LP_VERIFY_SIG_VALID="null"
|
|
LP_VERIFY_TOTAL=0; LP_VERIFY_OK=0; LP_VERIFY_MODIFIED=0; LP_VERIFY_MISSING=0
|
|
LP_VERIFY_SAMPLE=""
|
|
LP_VERIFY_ERROR=""
|
|
|
|
local mode="${CFG_INSTALL_MODE:-release}"
|
|
local root="${script_dir%/}"
|
|
local manifest="$root/SHA256SUMS"
|
|
local sig="$root/SHA256SUMS.minisig"
|
|
local pub; pub="$(lpVerifyPubKeyPath)"
|
|
|
|
# No manifest — a git/local dev install, or a build without one. There's
|
|
# nothing to verify against, so report a neutral "development build" rather
|
|
# than implying something is wrong.
|
|
if [[ "$mode" != "release" || ! -f "$manifest" ]]; then
|
|
LP_VERIFY_STATE="development"
|
|
return 0
|
|
fi
|
|
|
|
LP_VERIFY_TOTAL=$(wc -l < "$manifest" 2>/dev/null | tr -d ' ')
|
|
[[ -n "$LP_VERIFY_TOTAL" ]] || LP_VERIFY_TOTAL=0
|
|
|
|
# Manifest signature. The pubkey is in the root-owned footprint so the
|
|
# manager can't swap it to bless a forged manifest. A REPLACE_ME placeholder
|
|
# means signing isn't activated for this build → treat the manifest as
|
|
# unsigned (drift detection still works, we just can't vouch for it).
|
|
if [[ -f "$pub" ]] && ! grep -q REPLACE_ME "$pub" 2>/dev/null; then
|
|
LP_VERIFY_SIGNED="true"
|
|
if ! command -v minisign >/dev/null 2>&1; then
|
|
LP_VERIFY_STATE="unverifiable"
|
|
LP_VERIFY_ERROR="minisign is not installed, so the signed manifest can't be checked."
|
|
return 0
|
|
fi
|
|
if [[ ! -f "$sig" ]]; then
|
|
LP_VERIFY_STATE="tampered"
|
|
LP_VERIFY_SIG_VALID="false"
|
|
LP_VERIFY_ERROR="The release manifest signature (SHA256SUMS.minisig) is missing."
|
|
return 0
|
|
fi
|
|
if ! minisign -Vm "$manifest" -p "$pub" -x "$sig" >/dev/null 2>&1; then
|
|
LP_VERIFY_STATE="tampered"
|
|
LP_VERIFY_SIG_VALID="false"
|
|
LP_VERIFY_ERROR="The release manifest signature is invalid."
|
|
return 0
|
|
fi
|
|
LP_VERIFY_SIG_VALID="true"
|
|
fi
|
|
|
|
# Re-hash every listed file. `--check --quiet` prints only failures; each is
|
|
# either "<path>: FAILED" (content differs) or "<path>: FAILED open or read"
|
|
# (missing/unreadable). Lines prefixed "sha256sum:" are diagnostics/summary
|
|
# — skip them so a missing file isn't double-counted. Run from $root so the
|
|
# manifest's ./relative paths resolve.
|
|
local check_out line path sample_count=0
|
|
check_out="$(cd "$root" && sha256sum --check --quiet "$manifest" 2>&1)"
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" || "$line" == sha256sum:* ]] && continue
|
|
if [[ "$line" == *": FAILED open or read" ]]; then
|
|
LP_VERIFY_MISSING=$((LP_VERIFY_MISSING + 1)); path="${line%: FAILED open or read}"
|
|
elif [[ "$line" == *": FAILED" ]]; then
|
|
LP_VERIFY_MODIFIED=$((LP_VERIFY_MODIFIED + 1)); path="${line%: FAILED}"
|
|
else
|
|
continue
|
|
fi
|
|
if (( sample_count < 5 )); then
|
|
LP_VERIFY_SAMPLE+="${LP_VERIFY_SAMPLE:+$'\n'}${path#./}"
|
|
sample_count=$((sample_count + 1))
|
|
fi
|
|
done <<< "$check_out"
|
|
|
|
local bad=$((LP_VERIFY_MODIFIED + LP_VERIFY_MISSING))
|
|
LP_VERIFY_OK=$((LP_VERIFY_TOTAL - bad))
|
|
(( LP_VERIFY_OK < 0 )) && LP_VERIFY_OK=0
|
|
|
|
if (( bad > 0 )); then
|
|
LP_VERIFY_STATE="modified"
|
|
elif [[ "$LP_VERIFY_SIGNED" == "true" ]]; then
|
|
LP_VERIFY_STATE="verified"
|
|
else
|
|
LP_VERIFY_STATE="unsigned"
|
|
fi
|
|
return 0
|
|
}
|