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>
This commit is contained in:
parent
9324c6a4f4
commit
b28268a61f
@ -53,6 +53,14 @@
|
|||||||
.admin-status-dot.warn { background: #fbbd23; }
|
.admin-status-dot.warn { background: #fbbd23; }
|
||||||
.admin-status-dot.none { background: rgba(var(--text-rgb), 0.25); }
|
.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 {
|
.admin-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -15,15 +15,16 @@ class AdminOverview {
|
|||||||
const r = this.root();
|
const r = this.root();
|
||||||
if (r) r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading…</div></div>';
|
if (r) r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading…</div></div>';
|
||||||
this.bindEvents();
|
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/update_status.json'),
|
||||||
|
this.fetchJson('/data/system/verify_status.json'),
|
||||||
this.fetchJson('/data/backup/generated/dashboard.json'),
|
this.fetchJson('/data/backup/generated/dashboard.json'),
|
||||||
this.fetchJson('/data/ssh/access.json'),
|
this.fetchJson('/data/ssh/access.json'),
|
||||||
this.fetchJson('/data/system/disk_usage.json'),
|
this.fetchJson('/data/system/disk_usage.json'),
|
||||||
this.fetchJson('/data/system/memory_usage.json'),
|
this.fetchJson('/data/system/memory_usage.json'),
|
||||||
this.fetchJson('/data/system/system_info.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();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +51,18 @@ class AdminOverview {
|
|||||||
const go = e.target.closest('[data-admin-go]');
|
const go = e.target.closest('[data-admin-go]');
|
||||||
if (go) { this.go(go.dataset.adminGo); return; }
|
if (go) { this.go(go.dataset.adminGo); return; }
|
||||||
if (e.target.closest('[data-admin-update]')) { this.runUpdate(); 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) {
|
go(where) {
|
||||||
@ -71,6 +83,44 @@ class AdminOverview {
|
|||||||
catch (e) { this.notify(`Failed to start update: ${e.message || e}`, 'error'); }
|
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: 'Can’t 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
|
/* A status card: kind sets the dot colour (ok/warn/none). actionsHtml is
|
||||||
the footer (Manage link / button). */
|
the footer (Manage link / button). */
|
||||||
card(title, kind, lines, actionsHtml) {
|
card(title, kind, lines, actionsHtml) {
|
||||||
@ -94,18 +144,27 @@ class AdminOverview {
|
|||||||
if (!root) return;
|
if (!root) return;
|
||||||
const d = this.d || {};
|
const d = this.d || {};
|
||||||
|
|
||||||
// Updates
|
// Updates (+ integrity)
|
||||||
const upd = d.upd || {};
|
const upd = d.upd || {};
|
||||||
const updAvail = upd.update_available === true;
|
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(
|
const updCard = this.card(
|
||||||
'Updates',
|
'Updates',
|
||||||
updAvail ? 'warn' : 'ok',
|
(updAvail || integrityBad) ? 'warn' : 'ok',
|
||||||
updAvail
|
updBody,
|
||||||
? this.line('Status', 'Update available') + this.line('Current → latest', `${upd.current_version || '?'} → ${upd.latest_version || '?'}`)
|
updActions
|
||||||
: 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>`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Backups
|
// Backups
|
||||||
|
|||||||
@ -29,9 +29,12 @@ class UpdateNotifier {
|
|||||||
if (this.fetching) return this.fetching;
|
if (this.fetching) return this.fetching;
|
||||||
this.fetching = (async () => {
|
this.fetching = (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/data/system/update_status.json', { cache: 'no-store' });
|
const [s, v] = await Promise.all([
|
||||||
if (!res.ok) return null;
|
fetch('/data/system/update_status.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : null).catch(() => null),
|
||||||
this.status = await res.json();
|
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;
|
return this.status;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@ -221,6 +224,8 @@ class UpdateNotifier {
|
|||||||
['Branch', s.branch || '—'],
|
['Branch', s.branch || '—'],
|
||||||
['Last checked', this._formatTime(s.checked_at)],
|
['Last checked', this._formatTime(s.checked_at)],
|
||||||
];
|
];
|
||||||
|
const integrity = this._integrityLabel();
|
||||||
|
if (integrity) rows.splice(2, 0, ['Integrity', integrity]);
|
||||||
|
|
||||||
const statusLine = local
|
const statusLine = local
|
||||||
? 'This is a local installation — updates are managed manually on the host.'
|
? 'This is a local installation — updates are managed manually on the host.'
|
||||||
@ -319,6 +324,18 @@ class UpdateNotifier {
|
|||||||
else console.log(`[update] ${message}`);
|
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 'Can’t verify';
|
||||||
|
case 'development': return 'Development build';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_versionLabel() {
|
_versionLabel() {
|
||||||
const s = this.status;
|
const s = this.status;
|
||||||
if (!s) return '';
|
if (!s) return '';
|
||||||
|
|||||||
65
scripts/cli/commands/verify/cli_verify_commands.sh
Normal file
65
scripts/cli/commands/verify/cli_verify_commands.sh
Normal 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
|
||||||
|
}
|
||||||
18
scripts/cli/commands/verify/cli_verify_header.sh
Normal file
18
scripts/cli/commands/verify/cli_verify_header.sh
Normal 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 ""
|
||||||
|
}
|
||||||
@ -50,7 +50,41 @@ OUT="$REPO_ROOT/dist/$CHANNEL"
|
|||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
echo "Building $TARBALL (channel=$CHANNEL, ref=$REF) ..."
|
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" )
|
( cd "$OUT" && sha256sum "$TARBALL" > "$TARBALL.sha256" )
|
||||||
SHA="$(cut -d' ' -f1 < "$OUT/$TARBALL.sha256")"
|
SHA="$(cut -d' ' -f1 < "$OUT/$TARBALL.sha256")"
|
||||||
@ -82,3 +116,5 @@ echo "✓ $OUT/$TARBALL"
|
|||||||
echo "✓ $OUT/$TARBALL.sha256 ($SHA)"
|
echo "✓ $OUT/$TARBALL.sha256 ($SHA)"
|
||||||
echo "✓ $OUT/latest.json"
|
echo "✓ $OUT/latest.json"
|
||||||
echo "$SIGNED"
|
echo "$SIGNED"
|
||||||
|
echo " ✓ SHA256SUMS ($MANIFEST_FILES files, inside tarball)"
|
||||||
|
echo "$MANIFEST_SIGNED"
|
||||||
|
|||||||
@ -44,6 +44,8 @@ cli_scripts=(
|
|||||||
"cli/commands/update/cli_update_header.sh"
|
"cli/commands/update/cli_update_header.sh"
|
||||||
"cli/commands/validation/cli_validation_commands.sh"
|
"cli/commands/validation/cli_validation_commands.sh"
|
||||||
"cli/commands/validation/cli_validation_header.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_commands.sh"
|
||||||
"cli/commands/webui/cli_webui_header.sh"
|
"cli/commands/webui/cli_webui_header.sh"
|
||||||
"cli/task/cli_task_run.sh"
|
"cli/task/cli_task_run.sh"
|
||||||
|
|||||||
@ -30,5 +30,6 @@ source_scripts=(
|
|||||||
"source/files/arrays/files_webui.sh"
|
"source/files/arrays/files_webui.sh"
|
||||||
"source/files/arrays/function_manifest.sh"
|
"source/files/arrays/function_manifest.sh"
|
||||||
"source/files/generate_function_manifest.sh"
|
"source/files/generate_function_manifest.sh"
|
||||||
|
"source/verify.sh"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -266,8 +266,10 @@ declare -gA LP_FN_MAP=(
|
|||||||
[cliHandleSystemCommands]="cli/commands/system/cli_system_commands.sh"
|
[cliHandleSystemCommands]="cli/commands/system/cli_system_commands.sh"
|
||||||
[cliHandleUpdateCommands]="cli/commands/update/cli_update_commands.sh"
|
[cliHandleUpdateCommands]="cli/commands/update/cli_update_commands.sh"
|
||||||
[cliHandleValidationCommands]="cli/commands/validation/cli_validation_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"
|
[cliHandleWebuiCommands]="cli/commands/webui/cli_webui_commands.sh"
|
||||||
[cliInitialize]="cli/cli_initialize.sh"
|
[cliInitialize]="cli/cli_initialize.sh"
|
||||||
|
[cliRunVerify]="cli/commands/verify/cli_verify_commands.sh"
|
||||||
[cliShowAppHelp]="cli/commands/app/cli_app_header.sh"
|
[cliShowAppHelp]="cli/commands/app/cli_app_header.sh"
|
||||||
[cliShowBackupHelp]="cli/commands/backup/cli_backup_header.sh"
|
[cliShowBackupHelp]="cli/commands/backup/cli_backup_header.sh"
|
||||||
[cliShowConfigHelp]="cli/commands/config/cli_config_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"
|
[cliShowSystemHelp]="cli/commands/system/cli_system_header.sh"
|
||||||
[cliShowUpdateHelp]="cli/commands/update/cli_update_header.sh"
|
[cliShowUpdateHelp]="cli/commands/update/cli_update_header.sh"
|
||||||
[cliShowValidationHelp]="cli/commands/validation/cli_validation_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"
|
[cliShowWebuiHelp]="cli/commands/webui/cli_webui_header.sh"
|
||||||
[cliTaskFollow]="cli/task/cli_task_run.sh"
|
[cliTaskFollow]="cli/task/cli_task_run.sh"
|
||||||
[cliTaskRun]="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"
|
[lpReleaseLatestFootprint]="source/fetch.sh"
|
||||||
[lpReleaseLatestVersion]="source/fetch.sh"
|
[lpReleaseLatestVersion]="source/fetch.sh"
|
||||||
[_lpSha256]="source/fetch.sh"
|
[_lpSha256]="source/fetch.sh"
|
||||||
|
[lpVerifyInstall]="source/verify.sh"
|
||||||
|
[lpVerifyPubKeyPath]="source/verify.sh"
|
||||||
[lpVersionGt]="source/fetch.sh"
|
[lpVersionGt]="source/fetch.sh"
|
||||||
[mainLoop]="crontab/task/crontab_task_processor.sh"
|
[mainLoop]="crontab/task/crontab_task_processor.sh"
|
||||||
[mainMenu]="menu/menu_main.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_compose]="prometheus/scripts/prometheus_install_hooks.sh"
|
||||||
[prometheus_install_post_start]="prometheus/scripts/prometheus_install_hooks.sh"
|
[prometheus_install_post_start]="prometheus/scripts/prometheus_install_hooks.sh"
|
||||||
[readTaskField]="crontab/task/crontab_task_processor.sh"
|
[readTaskField]="crontab/task/crontab_task_processor.sh"
|
||||||
|
[reclaimDockerSpace]="cli/commands/system/cli_system_commands.sh"
|
||||||
[reconcileConfigFile]="config/core/variables/config_scan_variables.sh"
|
[reconcileConfigFile]="config/core/variables/config_scan_variables.sh"
|
||||||
[reconcileContainersTopOwnership]="function/permission/libreportal_folders.sh"
|
[reconcileContainersTopOwnership]="function/permission/libreportal_folders.sh"
|
||||||
[reconcileDockerOwnership]="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"
|
[webuiSystemMetrics]="webui/data/generators/system/webui_system_metrics.sh"
|
||||||
[webuiSystemUpdate]="webui/data/generators/system/webui_system_update.sh"
|
[webuiSystemUpdate]="webui/data/generators/system/webui_system_update.sh"
|
||||||
[webuiSystemUpdateCheck]="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"
|
[webuiUpdateAppLog]="webui/data/utils/webui_app_log.sh"
|
||||||
[webuiUpdateAppStatus]="webui/data/generators/apps/webui_app_status.sh"
|
[webuiUpdateAppStatus]="webui/data/generators/apps/webui_app_status.sh"
|
||||||
[webuiUpdateSystemConfig]="webui/data/generators/config/webui_update_config.sh"
|
[webuiUpdateSystemConfig]="webui/data/generators/config/webui_update_config.sh"
|
||||||
@ -1155,8 +1162,10 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[cliHandleSystemCommands]="scripts"
|
[cliHandleSystemCommands]="scripts"
|
||||||
[cliHandleUpdateCommands]="scripts"
|
[cliHandleUpdateCommands]="scripts"
|
||||||
[cliHandleValidationCommands]="scripts"
|
[cliHandleValidationCommands]="scripts"
|
||||||
|
[cliHandleVerifyCommands]="scripts"
|
||||||
[cliHandleWebuiCommands]="scripts"
|
[cliHandleWebuiCommands]="scripts"
|
||||||
[cliInitialize]="scripts"
|
[cliInitialize]="scripts"
|
||||||
|
[cliRunVerify]="scripts"
|
||||||
[cliShowAppHelp]="scripts"
|
[cliShowAppHelp]="scripts"
|
||||||
[cliShowBackupHelp]="scripts"
|
[cliShowBackupHelp]="scripts"
|
||||||
[cliShowConfigHelp]="scripts"
|
[cliShowConfigHelp]="scripts"
|
||||||
@ -1174,6 +1183,7 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[cliShowSystemHelp]="scripts"
|
[cliShowSystemHelp]="scripts"
|
||||||
[cliShowUpdateHelp]="scripts"
|
[cliShowUpdateHelp]="scripts"
|
||||||
[cliShowValidationHelp]="scripts"
|
[cliShowValidationHelp]="scripts"
|
||||||
|
[cliShowVerifyHelp]="scripts"
|
||||||
[cliShowWebuiHelp]="scripts"
|
[cliShowWebuiHelp]="scripts"
|
||||||
[cliTaskFollow]="scripts"
|
[cliTaskFollow]="scripts"
|
||||||
[cliTaskRun]="scripts"
|
[cliTaskRun]="scripts"
|
||||||
@ -1453,6 +1463,8 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[lpReleaseLatestFootprint]="scripts"
|
[lpReleaseLatestFootprint]="scripts"
|
||||||
[lpReleaseLatestVersion]="scripts"
|
[lpReleaseLatestVersion]="scripts"
|
||||||
[_lpSha256]="scripts"
|
[_lpSha256]="scripts"
|
||||||
|
[lpVerifyInstall]="scripts"
|
||||||
|
[lpVerifyPubKeyPath]="scripts"
|
||||||
[lpVersionGt]="scripts"
|
[lpVersionGt]="scripts"
|
||||||
[mainLoop]="scripts"
|
[mainLoop]="scripts"
|
||||||
[mainMenu]="scripts"
|
[mainMenu]="scripts"
|
||||||
@ -1553,6 +1565,7 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[prometheus_install_post_compose]="containers"
|
[prometheus_install_post_compose]="containers"
|
||||||
[prometheus_install_post_start]="containers"
|
[prometheus_install_post_start]="containers"
|
||||||
[readTaskField]="scripts"
|
[readTaskField]="scripts"
|
||||||
|
[reclaimDockerSpace]="scripts"
|
||||||
[reconcileConfigFile]="scripts"
|
[reconcileConfigFile]="scripts"
|
||||||
[reconcileContainersTopOwnership]="scripts"
|
[reconcileContainersTopOwnership]="scripts"
|
||||||
[reconcileDockerOwnership]="scripts"
|
[reconcileDockerOwnership]="scripts"
|
||||||
@ -1773,6 +1786,7 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[webuiSystemMetrics]="scripts"
|
[webuiSystemMetrics]="scripts"
|
||||||
[webuiSystemUpdate]="scripts"
|
[webuiSystemUpdate]="scripts"
|
||||||
[webuiSystemUpdateCheck]="scripts"
|
[webuiSystemUpdateCheck]="scripts"
|
||||||
|
[webuiSystemVerify]="scripts"
|
||||||
[webuiUpdateAppLog]="scripts"
|
[webuiUpdateAppLog]="scripts"
|
||||||
[webuiUpdateAppStatus]="scripts"
|
[webuiUpdateAppStatus]="scripts"
|
||||||
[webuiUpdateSystemConfig]="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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
cliHandleWebuiCommands() { source "${install_scripts_dir}cli/commands/webui/cli_webui_commands.sh"; cliHandleWebuiCommands "$@"; }
|
||||||
cliInitialize() { source "${install_scripts_dir}cli/cli_initialize.sh"; cliInitialize "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
cliTaskFollow() { source "${install_scripts_dir}cli/task/cli_task_run.sh"; cliTaskFollow "$@"; }
|
||||||
cliTaskRun() { source "${install_scripts_dir}cli/task/cli_task_run.sh"; cliTaskRun "$@"; }
|
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 "$@"; }
|
lpReleaseLatestFootprint() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestFootprint "$@"; }
|
||||||
lpReleaseLatestVersion() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestVersion "$@"; }
|
lpReleaseLatestVersion() { source "${install_scripts_dir}source/fetch.sh"; lpReleaseLatestVersion "$@"; }
|
||||||
_lpSha256() { source "${install_scripts_dir}source/fetch.sh"; _lpSha256 "$@"; }
|
_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 "$@"; }
|
lpVersionGt() { source "${install_scripts_dir}source/fetch.sh"; lpVersionGt "$@"; }
|
||||||
mainLoop() { source "${install_scripts_dir}crontab/task/crontab_task_processor.sh"; mainLoop "$@"; }
|
mainLoop() { source "${install_scripts_dir}crontab/task/crontab_task_processor.sh"; mainLoop "$@"; }
|
||||||
mainMenu() { source "${install_scripts_dir}menu/menu_main.sh"; mainMenu "$@"; }
|
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_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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
reconcileConfigFile() { source "${install_scripts_dir}config/core/variables/config_scan_variables.sh"; reconcileConfigFile "$@"; }
|
||||||
reconcileContainersTopOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileContainersTopOwnership "$@"; }
|
reconcileContainersTopOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileContainersTopOwnership "$@"; }
|
||||||
reconcileDockerOwnership() { source "${install_scripts_dir}function/permission/libreportal_folders.sh"; reconcileDockerOwnership "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
webuiUpdateSystemConfig() { source "${install_scripts_dir}webui/data/generators/config/webui_update_config.sh"; webuiUpdateSystemConfig "$@"; }
|
||||||
|
|||||||
112
scripts/source/verify.sh
Normal file
112
scripts/source/verify.sh
Normal 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
|
||||||
|
}
|
||||||
@ -65,6 +65,7 @@ webuiRunUpdate()
|
|||||||
dockerInstallApp "libreportal"
|
dockerInstallApp "libreportal"
|
||||||
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
||||||
webuiSystemUpdateCheck "force"
|
webuiSystemUpdateCheck "force"
|
||||||
|
webuiSystemVerify "force"
|
||||||
isSuccessful "LibrePortal has been updated to v${lat}."
|
isSuccessful "LibrePortal has been updated to v${lat}."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@ -117,6 +118,7 @@ webuiRunUpdate()
|
|||||||
# out-of-date flag.
|
# out-of-date flag.
|
||||||
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
||||||
webuiSystemUpdateCheck "force"
|
webuiSystemUpdateCheck "force"
|
||||||
|
webuiSystemVerify "force"
|
||||||
|
|
||||||
isSuccessful "LibrePortal has been updated."
|
isSuccessful "LibrePortal has been updated."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ webuiSystemUpdate() {
|
|||||||
webuiSystemMemory
|
webuiSystemMemory
|
||||||
webuiSystemMetrics
|
webuiSystemMetrics
|
||||||
webuiSystemUpdateCheck
|
webuiSystemUpdateCheck
|
||||||
|
webuiSystemVerify
|
||||||
isSuccessful "System information updated!"
|
isSuccessful "System information updated!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,3 +223,79 @@ EOF
|
|||||||
"$current_commit" "$latest_commit" \
|
"$current_commit" "$latest_commit" \
|
||||||
"$behind" "$ahead" "$branch" "git" "$fetch_error"
|
"$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"
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user