5 Commits

Author SHA1 Message Date
librelad
2df4e28a85 feat(distribution): Phase 2 — artifact apply/revert pipeline + ops interpreter
The mutating side of the unified distribution primitive (spec §8.3). Hotfixes
can now be applied and reverted, first-party, through the task system.

New scripts/cli/commands/artifact/cli_artifact_apply.sh:
- artifactApply <id>: resolve+gate (applies_when / min_lp / max_lp /
  max_footprint / publishers-map role) → fetch+verify payload (sha256 pinned by
  the signed index + minisig) → dry-precheck ALL ops (all-or-nothing) → best-
  effort snapshot → apply each op recording a precise inverse → bring app up →
  auto-rollback (replay undo LIFO, snapshot fallback) → applied-record + History.
- artifactRevert <id>: replay the applied-record's undo log (LIFO).
- Bounded, CLOSED op vocabulary (no run-script/exec, ever): set-config-key,
  set-compose-image, patch-file-if-checksum-matches, set-data-file. An
  unsupported op rejects the whole artifact at precheck (fail-closed).
- Write-target firewall: scope:app → containers/<app>/ only; scope:system →
  configs/ only; the install tree (our code) is off-limits to hotfixes (fork 1).
  Drift guards (expect_current / checksum) skip cleanly rather than clobber.
- Two-tier trust: index minisig-verified vs the footprint key (lpFetchIndex)
  covers the envelope; payload sha256-pinned + minisig-verified; publishers-map
  role gate (a non-official publisher can't claim official). Community per-
  artifact-key sigs are gated off until that tier is enabled.

cli_artifact_commands.sh: apply/revert via the task system (artifact_apply /
artifact_revert types — no allowlist needed), + read-only `applied` list.

cli_updater_commands.sh:
- FIX verified safety bug: updaterApplyApp/RollbackApp called `libreportal backup
  app "$app"` and `... restore latest`, which parse the app name as the ACTION,
  hit the dispatcher's `*)` default (exits 0) — so updates ran with NO snapshot
  and rollback was a silent no-op. Call backupAppStart / restoreAppStart directly.
- FIX updaterRecordHistory jq-silent-skip: was `command -v jq || return 0`
  (silently dropped the audit entry). Now fail-closed with a brace-agnostic
  bash-native prepend fallback; extended with artifact_id/serial/undo_id.

fetch.sh: add _lpJsonEsc (shared JSON-escape for the jq-free fallbacks).
Regenerated source arrays + lazy-load manifest for the new file/functions.

Unit-tested 31/31: every op apply+precheck+undo round-trip, the path-allowlist
firewall (incl. .. traversal + install-tree + cross-app rejection), all-or-
nothing abort, unsupported-op rejection, and the History bash-native fallback
(records + preserves prior entries without jq). A full signed-apply e2e needs
minisign + the signing key (Phase 5 make_hotfix.sh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 20:01:11 +01:00
librelad
caee74bd76 feat(distribution): signed artifact-index fetch+verify primitive (Phase 1)
Build the read side of the unified distribution primitive from
docs/roadmap/updates-and-distribution.md: one team-signed catalog
(index.json) on the same channel as latest.json, listing type-tagged
artifact envelopes. A hotfix is the first artifact type; apps/themes/
components are future envelope rows through the SAME pipe — the
marketplace seam is just the `type` + `payload.kind` fields.

Phase 1 is fetch + verify + parse only (NO mutation; the snapshot →
ops → rollback → History apply verb is Phase 2):

- Factor `lpVerifyMinisig` out of `lpFetchRelease` (scripts/source/
  fetch.sh) — one trust anchor (the root-owned footprint key) now
  shared by releases and the index; refactor `lpFetchRelease` to use
  it (behaviour-preserving, still fail-closed).
- scripts/source/artifacts.sh: `lpFetchIndex` — download →
  verify-before-parse → `valid_until` freshness (anti-withholding) →
  `index_serial` monotonic high-water (anti-rollback, TUF-lite) → emit
  verified JSON. Trust core is jq-free; parsing accessors prefer jq
  with a grep fallback.
- `libreportal artifact index` (scripts/cli/commands/artifact/) —
  read-only front door that fetches, verifies and lists. Runs directly
  like `updater check` (no task; no mutation).
- Regenerate the source arrays + lazy-load function manifest for the
  new files.

Doc: promote the format from vision to spec (§8) — 3 layers
(INDEX/ENVELOPE/PIPELINE), the bounded declarative op vocabulary (no
run-script, ever), the apply pipeline mapped onto existing functions,
the marketplace seam, and resolutions for all five open forks.

Self-tested 12/12: trust core fails closed (real key + no minisign →
refuse), happy path, stale-refused, rollback-refused, signature-refused,
jq + grep parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 16:48:06 +01:00
librelad
5700f78c6b feat(release): minisign signature signing + verification
The sha256 only proves a download is intact; a compromised host could swap the
tarball + its checksum. Add minisign signatures, which prove authenticity (the host
can't forge them without the offline secret key). Ships INACTIVE behind a REPLACE_ME
placeholder, so installs work until a real key is generated; then it's REQUIRED.

- make_release.sh: signs the tarball when LP_MINISIGN_SECKEY is set -> <tarball>.minisig.
- libreportal.pub: the public key (placeholder), ships in the tarball and is installed
  to the ROOT-OWNED footprint (/usr/local/lib/libreportal/libreportal.pub) by init.sh
  -> the manager can't swap it to accept forged updates. footprint_version -> 2.
- install.sh: LP_MINISIGN_PUBKEY constant; once non-placeholder, downloads + verifies
  the .minisig (minisign -P) and REFUSES on invalid/missing (auto-installs minisign if
  needed). --no-verify-signature is a dev-only escape hatch.
- fetch.sh (update path): verifies against the footprint .pub (minisign -p), refuses on
  invalid/missing.
- docs/DEVELOPMENT.md: keygen (minisign -G), paste pubkey into libreportal.pub +
  install.sh, keep the secret key offline, sign builds via LP_MINISIGN_SECKEY, bump
  footprint_version on key rotation.

Verified end-to-end with a real throwaway key: good signature accepted; tampered,
wrong-key, and missing-signature all refused; placeholder skips (sha256 still enforced).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 19:40:30 +01:00
librelad
3014965b66 feat(update): FOOTPRINT_VERSION drift detection — flag when a root re-install is needed
A manager-run 'update apply' refreshes code/apps/WebUI but CANNOT rewrite the
root-owned footprint (helpers/wrapper/uninstall/unit/sudoers) — that immutability
is the de-sudo boundary. Previously a release that changed those would silently
leave them stale. Make it explicit:

- init.sh: footprint_version=1 constant, baked at install into
  /usr/local/lib/libreportal/.footprint_version (root:root 0644) by initRootHelpers.
  Bump it whenever a root component changes.
- make_release.sh: publishes footprint_version in latest.json.
- fetch.sh: lpInstalledFootprintVersion (marker) + lpReleaseLatestFootprint (manifest).
- check_update.sh: 'update apply' REFUSES when the release's footprint_version
  exceeds the installed one, directing to a root re-install (which fetches +
  re-bakes everything atomically). No half-applied updates.
- webui_system_update.sh: badge sets footprint_update_needed + clears can_update so
  the WebUI won't offer a one-click apply for a footprint-bumping release.
- docs/DEVELOPMENT.md: the bump rule + the footprint exception explained.

Verified: manifest carries footprint_version; drift decision correct both ways
(no marker/older -> needs re-install; equal -> no drift).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 19:07:16 +01:00
librelad
90663a077a feat(install): release fetch mode + lpFetchSource abstraction (phase C)
scripts/source/fetch.sh (sourced at runtime via files_source.sh):
- lpFetchRelease [ver]: resolve channel manifest -> download tarball -> VERIFY
  sha256 (refuse on mismatch/absence) -> replace the install tree ( is
  code-only now; configs/logs live in the separate system tree, so no backup
  dance). Host/channel from LP_RELEASE_BASE_URL/CFG_RELEASE_BASE_URL + channel.
- lpFetchSource: dispatch release|git|local.
- lpVersionGt: numeric dotted semver compare (used by the updater + badge).

init.sh initGIT is now release-aware: the bootstrap (install.sh) stages+verifies
the code and sets LP_ALREADY_FETCHED=1 (skip re-fetch); a direct release run sources
fetch.sh; a bare /root reinstall is directed to install.sh. install.sh exports
LP_ALREADY_FETCHED + LP_RELEASE_BASE_URL on hand-off. validateUnattended already
accepts release (git-url is gated on git mode).

Config: CFG_INSTALL_MODE default -> release, + CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL
(add-only reconcile preserves existing installs' git/local mode).

Verified: lpVersionGt across cases; lpFetchRelease downloads+verifies+extracts a
clean tree against a local server. The updater + reset/reinstall release paths are
phase D.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:08:39 +01:00