A 4-lens adversarial security review of the Phase 2 applier raised 19 issues
and confirmed 17 after per-finding verification. All are trust-boundary (they
require the signing key), but several break the explicit "no code-exec, always
reversible, nothing-silent" contract, so all 17 are fixed:
Trust path — fail CLOSED, never misreport:
- lpFetchIndex now surfaces the real signature state (LP_INDEX_SIGSTATE);
artifactApply REFUSES to mutate unless the index is actually verified, and
_artifactFetchPayload refuses an unsigned payload. The read path still
tolerates dev/unsigned but now says "UNSIGNED" instead of "Signed + verified".
- valid_until and index_serial are now MANDATORY + numeric in lpFetchIndex
(missing = refuse) — closes the anti-withholding / anti-rollback fail-opens.
Injection / code-exec (defense in depth even for a signed payload):
- runFileWrite rootless branch no longer builds a `bash -c` shell string with the
destination interpolated — it uses the argv form (like runFileOp), so a path
with a quote can't inject a command as the install user. (shared-helper fix)
- op paths must match a safe-filename charset (no quotes/$/backtick/;/newline);
set-config-key values and set-compose-image refs are charset-guarded too.
- content_b64 is validated as real base64 at precheck.
Reversibility / honest failure:
- dockerComposeUp now returns the real compose exit status (it always returned 0,
so the updater's rollback gate AND the apply's start-failure detection were
fail-open). (shared-helper fix)
- set-config-key undo captures the WHOLE config file (lossless) instead of a
lossy re-parsed scalar; edit-only (rejects an absent key).
- _artifactReplayUndoFile returns non-zero if any inverse op fails; auto-rollback
and revert now record "rollback-incomplete"/"revert-incomplete" + isError
instead of falsely claiming success, and revert keeps the record for retry.
- applied-record write failure is checked — apply rolls back rather than leave an
un-revertable change. System-scope regen failure is no longer swallowed.
- Writes are path-aware (configs/ -> runInstallWrite, container tree ->
runFileWrite) so system-scope hotfixes write/restore correctly.
- Checked lazy-sourcing surfaces a clear error instead of a bare exit 127.
Unit-tested 35/35 (adds: command-sub value rejection, bad image-ref, invalid
base64, quote/metachar path-injection rejection, replay-failure reporting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
curl's raw "(6) Could not resolve host" / 404 noise leaked through on
the index.json download while the .minisig fetch was already silenced —
inconsistent and confusing. The caller's clean isError covers the
failure, so route the index download's stderr to /dev/null too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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>