Merge claude/1

This commit is contained in:
librelad 2026-05-28 19:41:22 +01:00
commit 271b489029
12 changed files with 432 additions and 14 deletions

View File

@ -53,6 +53,14 @@
.admin-status-dot.warn { background: #fbbd23; }
.admin-status-dot.none { background: rgba(var(--text-rgb), 0.25); }
/* Inline integrity readout (dot + label) inside a card line. */
.admin-integrity {
display: inline-flex;
align-items: center;
gap: 6px;
}
.admin-integrity .admin-status-dot { width: 8px; height: 8px; }
.admin-card-body {
display: flex;
flex-direction: column;

View File

@ -15,15 +15,16 @@ class AdminOverview {
const r = this.root();
if (r) r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading…</div></div>';
this.bindEvents();
const [upd, backup, ssh, disk, mem, info] = await Promise.all([
const [upd, verify, backup, ssh, disk, mem, info] = await Promise.all([
this.fetchJson('/data/system/update_status.json'),
this.fetchJson('/data/system/verify_status.json'),
this.fetchJson('/data/backup/generated/dashboard.json'),
this.fetchJson('/data/ssh/access.json'),
this.fetchJson('/data/system/disk_usage.json'),
this.fetchJson('/data/system/memory_usage.json'),
this.fetchJson('/data/system/system_info.json')
]);
this.d = { upd, backup, ssh, disk, mem, info };
this.d = { upd, verify, backup, ssh, disk, mem, info };
this.render();
}
@ -50,7 +51,18 @@ class AdminOverview {
const go = e.target.closest('[data-admin-go]');
if (go) { this.go(go.dataset.adminGo); return; }
if (e.target.closest('[data-admin-update]')) { this.runUpdate(); return; }
if (e.target.closest('[data-admin-verify]')) { this.runVerify(); return; }
});
// When a verify (or update) task finishes, re-read the integrity status
// and re-render so the badge reflects reality without a manual reload.
const onTask = (ev) => {
const cmd = ev?.detail?.command || ev?.detail?.task?.command || '';
if (/^libreportal (verify|update)\b/.test(cmd)) {
setTimeout(() => this.refreshVerify(), 1500);
}
};
window.addEventListener('taskCompleted', onTask);
window.addEventListener('taskUpdated', onTask);
}
go(where) {
@ -71,6 +83,44 @@ class AdminOverview {
catch (e) { this.notify(`Failed to start update: ${e.message || e}`, 'error'); }
}
async runVerify() {
if (!this.taskManager) { this.notify('Task system unavailable', 'error'); return; }
try {
await this.taskManager.createTask('libreportal verify', 'verify', null);
this.notify('Verifying installation…', 'info');
} catch (e) { this.notify(`Failed to start verification: ${e.message || e}`, 'error'); }
}
async refreshVerify() {
const verify = await this.fetchJson('/data/system/verify_status.json');
if (this.d) { this.d.verify = verify; this.render(); }
}
/* Map verify_status.json { kind (dot colour), label, note }. Red (warn)
only for genuine problems (modified/tampered); everything else neutral. */
verifyDisplay(v) {
switch (v && v.state) {
case 'verified': return { kind: 'ok', label: 'Verified', note: 'Files match the signed release' };
case 'modified': return { kind: 'warn', label: 'Modified', note: `${v.files_modified || 0} changed, ${v.files_missing || 0} missing` };
case 'tampered': return { kind: 'warn', label: 'Signature invalid', note: v.error || 'Manifest signature failed' };
case 'unsigned': return { kind: 'none', label: 'Unsigned build', note: 'Matches an unsigned manifest' };
case 'unverifiable': return { kind: 'none', label: 'Cant verify', note: v.error || 'minisign unavailable' };
case 'development': return { kind: 'none', label: 'Development build', note: 'No signed manifest to check' };
default: return null;
}
}
// Integrity readout line: a coloured dot + label, with an honest tooltip
// about the limits of a self-check.
integrityLine(disp) {
const tip = 'Confirms your installed files match the signed release manifest. '
+ 'This is a self-check — for an independent guarantee, verify the release with `minisign -Vm`.';
return `<div class="admin-card-line" title="${this.escape(tip)}">`
+ `<span>Integrity</span>`
+ `<strong><span class="admin-integrity"><span class="admin-status-dot ${disp.kind}"></span>${this.escape(disp.label)}</span></strong>`
+ `</div>`;
}
/* A status card: kind sets the dot colour (ok/warn/none). actionsHtml is
the footer (Manage link / button). */
card(title, kind, lines, actionsHtml) {
@ -94,18 +144,27 @@ class AdminOverview {
if (!root) return;
const d = this.d || {};
// Updates
// Updates (+ integrity)
const upd = d.upd || {};
const updAvail = upd.update_available === true;
const vDisp = this.verifyDisplay(d.verify);
const integrityBad = vDisp && vDisp.kind === 'warn';
let updBody = updAvail
? this.line('Status', 'Update available') + this.line('Current → latest', `${upd.current_version || '?'}${upd.latest_version || '?'}`)
: this.line('Status', 'Up to date') + this.line('Version', upd.current_version || '—');
if (vDisp) updBody += this.integrityLine(vDisp);
// Update now takes priority when one's available; otherwise offer Verify now.
const updActions = (updAvail && upd.can_update)
? `<button type="button" class="backup-primary-btn" data-admin-update>Update now</button>`
: `<button type="button" class="backup-secondary-btn" data-admin-verify>Verify now</button>`;
const updCard = this.card(
'Updates',
updAvail ? 'warn' : 'ok',
updAvail
? this.line('Status', 'Update available') + this.line('Current → latest', `${upd.current_version || '?'}${upd.latest_version || '?'}`)
: this.line('Status', 'Up to date') + this.line('Version', upd.current_version || '—'),
updAvail && upd.can_update
? `<button type="button" class="backup-primary-btn" data-admin-update>Update now</button>`
: `<span class="admin-card-ok">Nothing to do</span>`
(updAvail || integrityBad) ? 'warn' : 'ok',
updBody,
updActions
);
// Backups

View File

@ -29,9 +29,12 @@ class UpdateNotifier {
if (this.fetching) return this.fetching;
this.fetching = (async () => {
try {
const res = await fetch('/data/system/update_status.json', { cache: 'no-store' });
if (!res.ok) return null;
this.status = await res.json();
const [s, v] = await Promise.all([
fetch('/data/system/update_status.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : null).catch(() => null),
fetch('/data/system/verify_status.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : null).catch(() => null),
]);
if (s !== null) this.status = s; // keep last-good on a failed fetch
if (v !== null) this.verify = v;
return this.status;
} catch {
return null;
@ -221,6 +224,8 @@ class UpdateNotifier {
['Branch', s.branch || '—'],
['Last checked', this._formatTime(s.checked_at)],
];
const integrity = this._integrityLabel();
if (integrity) rows.splice(2, 0, ['Integrity', integrity]);
const statusLine = local
? 'This is a local installation — updates are managed manually on the host.'
@ -319,6 +324,18 @@ class UpdateNotifier {
else console.log(`[update] ${message}`);
}
_integrityLabel() {
switch (this.verify && this.verify.state) {
case 'verified': return 'Verified · matches signed release';
case 'modified': return `Modified (${this.verify.files_modified || 0} changed)`;
case 'tampered': return 'Signature invalid';
case 'unsigned': return 'Unsigned build';
case 'unverifiable': return 'Cant verify';
case 'development': return 'Development build';
default: return '';
}
}
_versionLabel() {
const s = this.status;
if (!s) return '';

View File

@ -0,0 +1,65 @@
#!/bin/bash
# Verify Commands Handler
# Re-checks the installed code against the signed release manifest and refreshes
# the integrity status the WebUI "Updates" card reads (verify_status.json).
cliHandleVerifyCommands()
{
local verify_type="$initial_command2"
case "$verify_type" in
""|"now"|"check")
# Run the integrity check, print a human summary, and (re)write
# verify_status.json so the WebUI badge reflects the result.
cliRunVerify
;;
"json"|"status")
# Non-interactive: just (re)write verify_status.json. This is what
# the WebUI "Verify now" button runs through the task pipeline.
webuiSystemVerify "force"
;;
*)
isNotice "Invalid verify command: ${RED}$verify_type${NC}"
cliShowVerifyHelp
;;
esac
}
# Run the check (via webuiSystemVerify, which also rewrites verify_status.json
# and leaves the LP_VERIFY_* globals set) and print a readable summary.
cliRunVerify()
{
isHeader "LibrePortal Integrity Check"
if ! declare -f webuiSystemVerify >/dev/null 2>&1; then
isError "Verification is unavailable on this install."
return 1
fi
webuiSystemVerify "force"
case "$LP_VERIFY_STATE" in
verified)
isSuccessful "Verified — all ${LP_VERIFY_TOTAL} files match the signed release."
;;
modified)
isError "Modified — ${LP_VERIFY_MODIFIED} changed, ${LP_VERIFY_MISSING} missing of ${LP_VERIFY_TOTAL} files."
if [[ -n "$LP_VERIFY_SAMPLE" ]]; then
isNotice "Affected files (sample):"
while IFS= read -r _p; do [[ -n "$_p" ]] && echo " - $_p"; done <<< "$LP_VERIFY_SAMPLE"
fi
;;
tampered)
isError "Manifest signature invalid — ${LP_VERIFY_ERROR:-the release manifest cannot be trusted}."
;;
unsigned)
isNotice "Files match the manifest, but it isn't signed yet (no production key) — can't fully vouch for it."
;;
unverifiable)
isNotice "${LP_VERIFY_ERROR:-The signed manifest could not be checked.}"
;;
*)
isNotice "Development build (${CFG_INSTALL_MODE:-git} install) — no signed manifest to verify against."
;;
esac
}

View File

@ -0,0 +1,18 @@
#!/bin/bash
# Verify Commands Header
# Shows available verify commands and help information
cliShowVerifyHelp()
{
echo ""
echo "Available Verify Commands:"
echo ""
echo " libreportal verify - Check installed files against the signed release manifest"
echo " libreportal verify json - Refresh the integrity status file only (no console output)"
echo ""
echo "Confirms your installed LibrePortal code still matches the release we signed."
echo "This is a self-check (it runs from the same install), so for an independent"
echo "guarantee verify the release tarball yourself with 'minisign -Vm'."
echo ""
}

View File

@ -50,7 +50,41 @@ OUT="$REPO_ROOT/dist/$CHANNEL"
mkdir -p "$OUT"
echo "Building $TARBALL (channel=$CHANNEL, ref=$REF) ..."
git archive --format=tar.gz --prefix="$PREFIX" -o "$OUT/$TARBALL" "$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")"
@ -82,3 +116,5 @@ echo "✓ $OUT/$TARBALL"
echo "$OUT/$TARBALL.sha256 ($SHA)"
echo "$OUT/latest.json"
echo "$SIGNED"
echo " ✓ SHA256SUMS ($MANIFEST_FILES files, inside tarball)"
echo "$MANIFEST_SIGNED"

View File

@ -44,6 +44,8 @@ cli_scripts=(
"cli/commands/update/cli_update_header.sh"
"cli/commands/validation/cli_validation_commands.sh"
"cli/commands/validation/cli_validation_header.sh"
"cli/commands/verify/cli_verify_commands.sh"
"cli/commands/verify/cli_verify_header.sh"
"cli/commands/webui/cli_webui_commands.sh"
"cli/commands/webui/cli_webui_header.sh"
"cli/task/cli_task_run.sh"

View File

@ -30,5 +30,6 @@ source_scripts=(
"source/files/arrays/files_webui.sh"
"source/files/arrays/function_manifest.sh"
"source/files/generate_function_manifest.sh"
"source/verify.sh"
)

View File

@ -266,8 +266,10 @@ declare -gA LP_FN_MAP=(
[cliHandleSystemCommands]="cli/commands/system/cli_system_commands.sh"
[cliHandleUpdateCommands]="cli/commands/update/cli_update_commands.sh"
[cliHandleValidationCommands]="cli/commands/validation/cli_validation_commands.sh"
[cliHandleVerifyCommands]="cli/commands/verify/cli_verify_commands.sh"
[cliHandleWebuiCommands]="cli/commands/webui/cli_webui_commands.sh"
[cliInitialize]="cli/cli_initialize.sh"
[cliRunVerify]="cli/commands/verify/cli_verify_commands.sh"
[cliShowAppHelp]="cli/commands/app/cli_app_header.sh"
[cliShowBackupHelp]="cli/commands/backup/cli_backup_header.sh"
[cliShowConfigHelp]="cli/commands/config/cli_config_header.sh"
@ -285,6 +287,7 @@ declare -gA LP_FN_MAP=(
[cliShowSystemHelp]="cli/commands/system/cli_system_header.sh"
[cliShowUpdateHelp]="cli/commands/update/cli_update_header.sh"
[cliShowValidationHelp]="cli/commands/validation/cli_validation_header.sh"
[cliShowVerifyHelp]="cli/commands/verify/cli_verify_header.sh"
[cliShowWebuiHelp]="cli/commands/webui/cli_webui_header.sh"
[cliTaskFollow]="cli/task/cli_task_run.sh"
[cliTaskRun]="cli/task/cli_task_run.sh"
@ -564,6 +567,8 @@ declare -gA LP_FN_MAP=(
[lpReleaseLatestFootprint]="source/fetch.sh"
[lpReleaseLatestVersion]="source/fetch.sh"
[_lpSha256]="source/fetch.sh"
[lpVerifyInstall]="source/verify.sh"
[lpVerifyPubKeyPath]="source/verify.sh"
[lpVersionGt]="source/fetch.sh"
[mainLoop]="crontab/task/crontab_task_processor.sh"
[mainMenu]="menu/menu_main.sh"
@ -664,6 +669,7 @@ declare -gA LP_FN_MAP=(
[prometheus_install_post_compose]="prometheus/scripts/prometheus_install_hooks.sh"
[prometheus_install_post_start]="prometheus/scripts/prometheus_install_hooks.sh"
[readTaskField]="crontab/task/crontab_task_processor.sh"
[reclaimDockerSpace]="cli/commands/system/cli_system_commands.sh"
[reconcileConfigFile]="config/core/variables/config_scan_variables.sh"
[reconcileContainersTopOwnership]="function/permission/libreportal_folders.sh"
[reconcileDockerOwnership]="function/permission/libreportal_folders.sh"
@ -884,6 +890,7 @@ declare -gA LP_FN_MAP=(
[webuiSystemMetrics]="webui/data/generators/system/webui_system_metrics.sh"
[webuiSystemUpdate]="webui/data/generators/system/webui_system_update.sh"
[webuiSystemUpdateCheck]="webui/data/generators/system/webui_system_update.sh"
[webuiSystemVerify]="webui/data/generators/system/webui_system_update.sh"
[webuiUpdateAppLog]="webui/data/utils/webui_app_log.sh"
[webuiUpdateAppStatus]="webui/data/generators/apps/webui_app_status.sh"
[webuiUpdateSystemConfig]="webui/data/generators/config/webui_update_config.sh"
@ -1155,8 +1162,10 @@ declare -gA LP_FN_ROOT=(
[cliHandleSystemCommands]="scripts"
[cliHandleUpdateCommands]="scripts"
[cliHandleValidationCommands]="scripts"
[cliHandleVerifyCommands]="scripts"
[cliHandleWebuiCommands]="scripts"
[cliInitialize]="scripts"
[cliRunVerify]="scripts"
[cliShowAppHelp]="scripts"
[cliShowBackupHelp]="scripts"
[cliShowConfigHelp]="scripts"
@ -1174,6 +1183,7 @@ declare -gA LP_FN_ROOT=(
[cliShowSystemHelp]="scripts"
[cliShowUpdateHelp]="scripts"
[cliShowValidationHelp]="scripts"
[cliShowVerifyHelp]="scripts"
[cliShowWebuiHelp]="scripts"
[cliTaskFollow]="scripts"
[cliTaskRun]="scripts"
@ -1453,6 +1463,8 @@ declare -gA LP_FN_ROOT=(
[lpReleaseLatestFootprint]="scripts"
[lpReleaseLatestVersion]="scripts"
[_lpSha256]="scripts"
[lpVerifyInstall]="scripts"
[lpVerifyPubKeyPath]="scripts"
[lpVersionGt]="scripts"
[mainLoop]="scripts"
[mainMenu]="scripts"
@ -1553,6 +1565,7 @@ declare -gA LP_FN_ROOT=(
[prometheus_install_post_compose]="containers"
[prometheus_install_post_start]="containers"
[readTaskField]="scripts"
[reclaimDockerSpace]="scripts"
[reconcileConfigFile]="scripts"
[reconcileContainersTopOwnership]="scripts"
[reconcileDockerOwnership]="scripts"
@ -1773,6 +1786,7 @@ declare -gA LP_FN_ROOT=(
[webuiSystemMetrics]="scripts"
[webuiSystemUpdate]="scripts"
[webuiSystemUpdateCheck]="scripts"
[webuiSystemVerify]="scripts"
[webuiUpdateAppLog]="scripts"
[webuiUpdateAppStatus]="scripts"
[webuiUpdateSystemConfig]="scripts"
@ -2064,8 +2078,10 @@ cliHandleSshCommands() { source "${install_scripts_dir}cli/commands/ssh/cli_ssh_
cliHandleSystemCommands() { source "${install_scripts_dir}cli/commands/system/cli_system_commands.sh"; cliHandleSystemCommands "$@"; }
cliHandleUpdateCommands() { source "${install_scripts_dir}cli/commands/update/cli_update_commands.sh"; cliHandleUpdateCommands "$@"; }
cliHandleValidationCommands() { source "${install_scripts_dir}cli/commands/validation/cli_validation_commands.sh"; cliHandleValidationCommands "$@"; }
cliHandleVerifyCommands() { source "${install_scripts_dir}cli/commands/verify/cli_verify_commands.sh"; cliHandleVerifyCommands "$@"; }
cliHandleWebuiCommands() { source "${install_scripts_dir}cli/commands/webui/cli_webui_commands.sh"; cliHandleWebuiCommands "$@"; }
cliInitialize() { source "${install_scripts_dir}cli/cli_initialize.sh"; cliInitialize "$@"; }
cliRunVerify() { source "${install_scripts_dir}cli/commands/verify/cli_verify_commands.sh"; cliRunVerify "$@"; }
cliShowAppHelp() { source "${install_scripts_dir}cli/commands/app/cli_app_header.sh"; cliShowAppHelp "$@"; }
cliShowBackupHelp() { source "${install_scripts_dir}cli/commands/backup/cli_backup_header.sh"; cliShowBackupHelp "$@"; }
cliShowConfigHelp() { source "${install_scripts_dir}cli/commands/config/cli_config_header.sh"; cliShowConfigHelp "$@"; }
@ -2083,6 +2099,7 @@ cliShowSshHelp() { source "${install_scripts_dir}cli/commands/ssh/cli_ssh_header
cliShowSystemHelp() { source "${install_scripts_dir}cli/commands/system/cli_system_header.sh"; cliShowSystemHelp "$@"; }
cliShowUpdateHelp() { source "${install_scripts_dir}cli/commands/update/cli_update_header.sh"; cliShowUpdateHelp "$@"; }
cliShowValidationHelp() { source "${install_scripts_dir}cli/commands/validation/cli_validation_header.sh"; cliShowValidationHelp "$@"; }
cliShowVerifyHelp() { source "${install_scripts_dir}cli/commands/verify/cli_verify_header.sh"; cliShowVerifyHelp "$@"; }
cliShowWebuiHelp() { source "${install_scripts_dir}cli/commands/webui/cli_webui_header.sh"; cliShowWebuiHelp "$@"; }
cliTaskFollow() { source "${install_scripts_dir}cli/task/cli_task_run.sh"; cliTaskFollow "$@"; }
cliTaskRun() { source "${install_scripts_dir}cli/task/cli_task_run.sh"; cliTaskRun "$@"; }
@ -2362,6 +2379,8 @@ lpReleaseChannel() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseCh
lpReleaseLatestFootprint() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestFootprint "$@"; }
lpReleaseLatestVersion() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestVersion "$@"; }
_lpSha256() { source "${install_scripts_dir}source/fetch.sh"; _lpSha256 "$@"; }
lpVerifyInstall() { source "${install_scripts_dir}source/verify.sh"; lpVerifyInstall "$@"; }
lpVerifyPubKeyPath() { source "${install_scripts_dir}source/verify.sh"; lpVerifyPubKeyPath "$@"; }
lpVersionGt() { source "${install_scripts_dir}source/fetch.sh"; lpVersionGt "$@"; }
mainLoop() { source "${install_scripts_dir}crontab/task/crontab_task_processor.sh"; mainLoop "$@"; }
mainMenu() { source "${install_scripts_dir}menu/menu_main.sh"; mainMenu "$@"; }
@ -2462,6 +2481,7 @@ processBcryptPassword() { source "${install_scripts_dir}config/password/bcrypt/p
prometheus_install_post_compose() { source "${install_containers_dir}prometheus/scripts/prometheus_install_hooks.sh"; prometheus_install_post_compose "$@"; }
prometheus_install_post_start() { source "${install_containers_dir}prometheus/scripts/prometheus_install_hooks.sh"; prometheus_install_post_start "$@"; }
readTaskField() { source "${install_scripts_dir}crontab/task/crontab_task_processor.sh"; readTaskField "$@"; }
reclaimDockerSpace() { source "${install_scripts_dir}cli/commands/system/cli_system_commands.sh"; reclaimDockerSpace "$@"; }
reconcileConfigFile() { source "${install_scripts_dir}config/core/variables/config_scan_variables.sh"; reconcileConfigFile "$@"; }
reconcileContainersTopOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileContainersTopOwnership "$@"; }
reconcileDockerOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileDockerOwnership "$@"; }
@ -2682,6 +2702,7 @@ webuiSystemMemory() { source "${install_scripts_dir}webui/data/generators/system
webuiSystemMetrics() { source "${install_scripts_dir}webui/data/generators/system/webui_system_metrics.sh"; webuiSystemMetrics "$@"; }
webuiSystemUpdate() { source "${install_scripts_dir}webui/data/generators/system/webui_system_update.sh"; webuiSystemUpdate "$@"; }
webuiSystemUpdateCheck() { source "${install_scripts_dir}webui/data/generators/system/webui_system_update.sh"; webuiSystemUpdateCheck "$@"; }
webuiSystemVerify() { source "${install_scripts_dir}webui/data/generators/system/webui_system_update.sh"; webuiSystemVerify "$@"; }
webuiUpdateAppLog() { source "${install_scripts_dir}webui/data/utils/webui_app_log.sh"; webuiUpdateAppLog "$@"; }
webuiUpdateAppStatus() { source "${install_scripts_dir}webui/data/generators/apps/webui_app_status.sh"; webuiUpdateAppStatus "$@"; }
webuiUpdateSystemConfig() { source "${install_scripts_dir}webui/data/generators/config/webui_update_config.sh"; webuiUpdateSystemConfig "$@"; }

112
scripts/source/verify.sh Normal file
View File

@ -0,0 +1,112 @@
#!/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
}

View File

@ -65,6 +65,7 @@ webuiRunUpdate()
dockerInstallApp "libreportal"
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated to v${lat}."
return 0
fi
@ -117,6 +118,7 @@ webuiRunUpdate()
# out-of-date flag.
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated."
}

View File

@ -9,6 +9,7 @@ webuiSystemUpdate() {
webuiSystemMemory
webuiSystemMetrics
webuiSystemUpdateCheck
webuiSystemVerify
isSuccessful "System information updated!"
}
@ -222,3 +223,79 @@ EOF
"$current_commit" "$latest_commit" \
"$behind" "$ahead" "$branch" "git" "$fetch_error"
}
# ---------------------------------------------------------------------------
# WebUI integrity verification
# ---------------------------------------------------------------------------
# Writes frontend/data/system/verify_status.json so the Admin → Overview
# "Updates" card can show whether the installed code still matches the signed
# release manifest. The actual check is lpVerifyInstall (scripts/source/verify.sh,
# sets LP_VERIFY_* globals); this just throttles it and serialises the result.
#
# Re-hashing the whole install tree is cheap (~1s for ~1100 files) but pointless
# to redo every minute, so it's throttled like the update check. Pass "force"
# (the manual "Verify now" action and the post-update hook) to bypass the
# throttle. Non-release installs have no manifest, so lpVerifyInstall reports a
# neutral "development" state and no hashing happens.
webuiSystemVerify() {
local force_flag="$1"
local system_dir="$containers_dir/libreportal/frontend/data/system"
local final_file="${system_dir}/verify_status.json"
local stamp_file="${system_dir}/.verify_check_stamp"
local interval="${CFG_VERIFY_CHECK_INTERVAL:-86400}"
createFolders "quiet" "$sudo_user_name" "$system_dir"
local install_mode="${CFG_INSTALL_MODE:-git}"
local do_run="false"
if [[ "$force_flag" == "force" || ! -f "$final_file" || ! -f "$stamp_file" ]]; then
do_run="true"
else
local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$stamp_file" 2>/dev/null || echo 0)
(( _now - _last >= interval )) && do_run="true"
fi
[[ "$do_run" == "true" ]] || return 0
declare -f lpVerifyInstall >/dev/null 2>&1 || return 0
lpVerifyInstall
runAsManager touch "$stamp_file" 2>/dev/null || touch "$stamp_file" 2>/dev/null
# Encode the sample-paths array (each path JSON-escaped).
local sample_json="[]"
if [[ -n "$LP_VERIFY_SAMPLE" ]]; then
local _items="" _p
while IFS= read -r _p; do
[[ -z "$_p" ]] && continue
_p=${_p//\\/\\\\}; _p=${_p//\"/\\\"}
_items+="${_items:+, }\"${_p}\""
done <<< "$LP_VERIFY_SAMPLE"
sample_json="[${_items}]"
fi
local version=""
[[ -f "$script_dir/VERSION" ]] && version=$(tr -d ' \t\n\r' < "$script_dir/VERSION")
local error_json="null"
[[ -n "$LP_VERIFY_ERROR" ]] && error_json="\"${LP_VERIFY_ERROR//\"/\\\"}\""
local temp_file; temp_file="$(mktemp)"
cat << EOF > "$temp_file"
{
"state": "${LP_VERIFY_STATE}",
"signed": ${LP_VERIFY_SIGNED:-false},
"signature_valid": ${LP_VERIFY_SIG_VALID:-null},
"files_total": ${LP_VERIFY_TOTAL:-0},
"files_ok": ${LP_VERIFY_OK:-0},
"files_modified": ${LP_VERIFY_MODIFIED:-0},
"files_missing": ${LP_VERIFY_MISSING:-0},
"modified_sample": ${sample_json},
"version": "${version}",
"install_mode": "${install_mode}",
"error": ${error_json},
"checked_at": "$(date -Iseconds)"
}
EOF
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
}