fix(artifacts): propagate LP_INDEX_SIGSTATE to callers via lpFetchIndexInto

Every caller captured the index with var=$(lpFetchIndex), which runs the
fetch in a command-substitution subshell — the LP_INDEX_SIGSTATE global it
sets never reached the caller. On a box with real signing active the
artifactApply/apply-auto gates would therefore refuse a correctly signed
index (fail-closed, but the apply path would be dead on arrival the day
signing activates), and artifact index / the WebUI scan would report a
verified feed as UNSIGNED.

New lpFetchIndexInto <var> [cache] runs the fetch in the calling shell and
assigns via printf -v; all four call sites converted. Verified with a
source-and-mock harness against a locally served index: 10/10 (sigstate
reaches caller, serial high-water, anti-rollback refuse, staleness refuse,
id enumeration, envelope round-trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-07-03 20:43:59 +01:00
parent b0a194dd48
commit 36a5c87397
5 changed files with 24 additions and 4 deletions

View File

@ -75,7 +75,7 @@ _artifactRmFile() {
# ----------------------------------------------------------------------------
_artifactResolve() {
local id="$1"
_ART_INDEX="$(lpFetchIndex)" || { isError "artifact: could not fetch/verify the index."; return 1; }
lpFetchIndexInto _ART_INDEX || { isError "artifact: could not fetch/verify the index."; return 1; }
local art; art="$(printf '%s' "$_ART_INDEX" | jq -ce --arg id "$id" '.artifacts[]? | select(.id==$id)' 2>/dev/null)"
[[ -n "$art" ]] || { isError "artifact: id '$id' not found in the signed index."; return 1; }
@ -534,7 +534,7 @@ artifactApplyAuto() {
local policy="${CFG_HOTFIX_AUTO:-security-breakage}"
[[ "$policy" == "off" ]] && { isNotice "Hotfix auto-apply is off (CFG_HOTFIX_AUTO=off)."; return 0; }
local index; index="$(lpFetchIndex)" || { isNotice "artifact apply-auto: no index available."; return 0; }
local index; lpFetchIndexInto index || { isNotice "artifact apply-auto: no index available."; return 0; }
if [[ "$LP_INDEX_SIGSTATE" != "verified" ]]; then
isNotice "artifact apply-auto: index is unsigned (signing not activated) — not auto-applying."; return 0
fi

View File

@ -97,7 +97,7 @@ artifactListIndex()
isHeader "Artifact index ($(lpReleaseChannel))"
local json
if ! json="$(lpFetchIndex)"; then
if ! lpFetchIndexInto json; then
isError "Could not fetch or verify the artifact index from $(lpArtifactIndexUrl)."
isNotice "Nothing is published yet, or the channel is unreachable. (This is expected before the first index ships.)"
return 1

View File

@ -108,6 +108,23 @@ lpFetchIndex() {
return 0
}
# lpFetchIndexInto <varname> [cache_path] — lpFetchIndex run IN THIS SHELL so
# the LP_INDEX_SIGSTATE global actually reaches the caller. A plain
# var="$(lpFetchIndex)" capture strands that assignment in the substitution's
# subshell: the caller then reads the file-scope "" and the apply-path gate
# refuses even a correctly signed index. Any caller that inspects
# LP_INDEX_SIGSTATE after fetching MUST use this wrapper, not $(…).
lpFetchIndexInto() {
local __lpfi_var="$1" __lpfi_tmp __lpfi_rc=0
__lpfi_tmp="$(mktemp)"
lpFetchIndex "${2:-}" > "$__lpfi_tmp" || __lpfi_rc=$?
if (( __lpfi_rc == 0 )); then
printf -v "$__lpfi_var" '%s' "$(cat "$__lpfi_tmp")"
fi
rm -f "$__lpfi_tmp"
return "$__lpfi_rc"
}
# --- Parsing accessors -------------------------------------------------------
# The trust-critical fields (index_serial / valid_until / signature) are read
# jq-free above so the security core has no jq dependency. Enumerating the

View File

@ -596,6 +596,7 @@ declare -gA LP_FN_MAP=(
[lpArtifactSerialFile]="source/artifacts.sh"
[_lpDownload]="source/fetch.sh"
[lpFetchIndex]="source/artifacts.sh"
[lpFetchIndexInto]="source/artifacts.sh"
[lpFetchRelease]="source/fetch.sh"
[lpFetchSource]="source/fetch.sh"
[_lpFetchTool]="source/fetch.sh"
@ -1554,6 +1555,7 @@ declare -gA LP_FN_ROOT=(
[lpArtifactSerialFile]="scripts"
[_lpDownload]="scripts"
[lpFetchIndex]="scripts"
[lpFetchIndexInto]="scripts"
[lpFetchRelease]="scripts"
[lpFetchSource]="scripts"
[_lpFetchTool]="scripts"
@ -2533,6 +2535,7 @@ lpArtifactRecordSerial() { source "${install_scripts_dir}source/artifacts.sh"; l
lpArtifactSerialFile() { source "${install_scripts_dir}source/artifacts.sh"; lpArtifactSerialFile "$@"; }
_lpDownload() { source "${install_scripts_dir}source/fetch.sh"; _lpDownload "$@"; }
lpFetchIndex() { source "${install_scripts_dir}source/artifacts.sh"; lpFetchIndex "$@"; }
lpFetchIndexInto() { source "${install_scripts_dir}source/artifacts.sh"; lpFetchIndexInto "$@"; }
lpFetchRelease() { source "${install_scripts_dir}source/fetch.sh"; lpFetchRelease "$@"; }
lpFetchSource() { source "${install_scripts_dir}source/fetch.sh"; lpFetchSource "$@"; }
_lpFetchTool() { source "${install_scripts_dir}source/fetch.sh"; _lpFetchTool "$@"; }

View File

@ -29,7 +29,7 @@ webuiArtifactScan() {
fi
local index
if ! index="$(lpFetchIndex)"; then
if ! lpFetchIndexInto index; then
isNotice "webuiArtifactScan: no verified index available — keeping the prior file."
return 0
fi