fix(bootstrap): verify the go toolchain tarball against a pinned digest
install_go_toolchain downloaded the tarball to a fixed /tmp name and unpacked it into /usr/local as root, with no digest check. Another local user could plant that file first, and nothing would notice a tampered download. The tarball is now staged in a mktemp -d directory that the exit cleanup removes, and its sha256 must match before the old toolchain is touched, so a refusal leaves the host as it was. The default 1.26.4 carries pinned amd64 and arm64 digests next to its version; they are the ones https://go.dev/dl/?mode=json&include=all publishes. Any other FELIS_GO_VERSION has to bring its own FELIS_GO_SHA256, documented in the header, because no pin can cover a version chosen at run time. Where and which version gets installed is unchanged.
This commit is contained in:
2 files changed
+76
-7
No files matched your search
+24
-7
@@ -41,6 +41,8 @@
|
|||||||
# FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar
|
# FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar
|
||||||
# above is set; the install refuses on a mismatch.
|
# above is set; the install refuses on a mismatch.
|
||||||
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
|
# FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4)
|
||||||
|
# FELIS_GO_SHA256 sha256 of that version's linux tarball for this host's architecture.
|
||||||
|
# REQUIRED for a non-default FELIS_GO_VERSION; the default's is pinned.
|
||||||
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
# FELIS_REPO_URL git URL to build from (raw script mode only)
|
||||||
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
# FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
|
||||||
# release DOWNLOADS the prebuilt felis binary published for the newest
|
# release DOWNLOADS the prebuilt felis binary published for the newest
|
||||||
@@ -112,7 +114,14 @@ FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-}"
|
|||||||
# needs this. Overridable because adding a second 1.8 backend otherwise means editing this
|
# needs this. Overridable because adding a second 1.8 backend otherwise means editing this
|
||||||
# script; it is still a restart-time list, not one that follows the CRs.
|
# script; it is still a restart-time list, not one that follows the CRs.
|
||||||
FELIS_LEGACY_FORWARDING_SERVERS="${FELIS_LEGACY_FORWARDING_SERVERS:-legacy18}"
|
FELIS_LEGACY_FORWARDING_SERVERS="${FELIS_LEGACY_FORWARDING_SERVERS:-legacy18}"
|
||||||
FELIS_GO_VERSION="${FELIS_GO_VERSION:-1.26.4}"
|
# The Go tarball is unpacked and run as root, so the default version is pinned by the sha256
|
||||||
|
# go.dev/dl publishes for each architecture install_go_toolchain handles. Move all three
|
||||||
|
# together; any other FELIS_GO_VERSION has to bring its own FELIS_GO_SHA256.
|
||||||
|
GO_PINNED_VERSION="1.26.4"
|
||||||
|
GO_PINNED_SHA256_AMD64="1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f"
|
||||||
|
GO_PINNED_SHA256_ARM64="ef758ae7c6cf9267c9c0ef080b8965f453d89ab2d25d9eb22de4405925238768"
|
||||||
|
FELIS_GO_VERSION="${FELIS_GO_VERSION:-$GO_PINNED_VERSION}"
|
||||||
|
FELIS_GO_SHA256="${FELIS_GO_SHA256:-}"
|
||||||
PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}"
|
PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}"
|
||||||
APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}"
|
APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}"
|
||||||
|
|
||||||
@@ -2253,25 +2262,33 @@ prompt_install_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_go_toolchain() {
|
install_go_toolchain() {
|
||||||
local arch tarball url
|
local arch tarball url tmp want have
|
||||||
if [ -x "${GOROOT_DIR}/bin/go" ] && "${GOROOT_DIR}/bin/go" version | grep -q "go${FELIS_GO_VERSION} "; then
|
if [ -x "${GOROOT_DIR}/bin/go" ] && "${GOROOT_DIR}/bin/go" version | grep -q "go${FELIS_GO_VERSION} "; then
|
||||||
ok "go ${FELIS_GO_VERSION} already installed at ${GOROOT_DIR}"
|
ok "go ${FELIS_GO_VERSION} already installed at ${GOROOT_DIR}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in
|
||||||
x86_64|amd64) arch="amd64" ;;
|
x86_64|amd64) arch="amd64"; want="$GO_PINNED_SHA256_AMD64" ;;
|
||||||
aarch64|arm64) arch="arm64" ;;
|
aarch64|arm64) arch="arm64"; want="$GO_PINNED_SHA256_ARM64" ;;
|
||||||
*) die "no Go toolchain build for architecture $(uname -m); set FELIS_GO_VERSION or pre-stage ${GOROOT_DIR}" ;;
|
*) die "no Go toolchain build for architecture $(uname -m); set FELIS_GO_VERSION or pre-stage ${GOROOT_DIR}" ;;
|
||||||
esac
|
esac
|
||||||
|
[ "$FELIS_GO_VERSION" = "$GO_PINNED_VERSION" ] || want="$FELIS_GO_SHA256"
|
||||||
|
[ -n "$want" ] || die "no pinned sha256 for Go ${FELIS_GO_VERSION}; set FELIS_GO_SHA256 to the linux-${arch} digest https://go.dev/dl/ lists for it, or pre-stage ${GOROOT_DIR}"
|
||||||
|
|
||||||
tarball="go${FELIS_GO_VERSION}.linux-${arch}.tar.gz"
|
tarball="go${FELIS_GO_VERSION}.linux-${arch}.tar.gz"
|
||||||
url="https://go.dev/dl/${tarball}"
|
url="https://go.dev/dl/${tarball}"
|
||||||
log "installing Go ${FELIS_GO_VERSION} (${arch}) to ${GOROOT_DIR}"
|
log "installing Go ${FELIS_GO_VERSION} (${arch}) to ${GOROOT_DIR}"
|
||||||
curl -fsSL "$url" -o "/tmp/${tarball}" || die "failed to download the Go toolchain: ${url}"
|
# A private directory, not a fixed /tmp name another local user could have planted first.
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
remember_temp "$tmp"
|
||||||
|
curl -fsSL "$url" -o "${tmp}/${tarball}" || die "failed to download the Go toolchain: ${url}"
|
||||||
|
# Checked before the old toolchain is removed, so a refusal leaves the host as it was.
|
||||||
|
# Hash stdin, never the path — same reason as install_via_plugins.
|
||||||
|
have="$(sha256sum <"${tmp}/${tarball}" | cut -d' ' -f1)"
|
||||||
|
[ "$have" = "$want" ] || die "Go ${FELIS_GO_VERSION} (${arch}) checksum mismatch: got ${have}, expected ${want}"
|
||||||
rm -rf "$GOROOT_DIR"
|
rm -rf "$GOROOT_DIR"
|
||||||
tar -C "$(dirname "$GOROOT_DIR")" -xzf "/tmp/${tarball}" || die "failed to unpack ${tarball}"
|
tar -C "$(dirname "$GOROOT_DIR")" -xzf "${tmp}/${tarball}" || die "failed to unpack ${tarball}"
|
||||||
rm -f "/tmp/${tarball}"
|
|
||||||
ok "go toolchain at ${GOROOT_DIR}/bin/go"
|
ok "go toolchain at ${GOROOT_DIR}/bin/go"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,58 @@ else
|
|||||||
"$(run_mode "$sdir/felis-nano.service" "$sdir/absent.done" "" 1)"
|
"$(run_mode "$sdir/felis-nano.service" "$sdir/absent.done" "" 1)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- install_go_toolchain checks the tarball before it replaces anything ----------------
|
||||||
|
# The tarball is unpacked into /usr/local and run as root, so a download that does not hash
|
||||||
|
# to the pin is refused -- and refused before the working toolchain is removed.
|
||||||
|
|
||||||
|
gblock="$(awk '/^install_go_toolchain\(\) \{/,/^}/' "$BS")"
|
||||||
|
[ -n "$gblock" ] || { echo "FAIL: no install_go_toolchain found in $BS"; exit 1; }
|
||||||
|
[ "$(printf '%s\n' "$gblock" | wc -l)" -lt 50 ] \
|
||||||
|
|| { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; }
|
||||||
|
|
||||||
|
gsum="$(printf 'stand-in go toolchain\n' | sha256sum | cut -d' ' -f1)"
|
||||||
|
groot="$sdir/go"
|
||||||
|
|
||||||
|
run_go() { # FELIS_GO_VERSION pinned-amd64-digest [FELIS_GO_SHA256]
|
||||||
|
FELIS_GO_VERSION="$1" GO_PINNED_VERSION=1.26.4 GO_PINNED_SHA256_AMD64="$2" \
|
||||||
|
GO_PINNED_SHA256_ARM64=unused FELIS_GO_SHA256="${3:-}" GOROOT_DIR="$groot" TMPDIR="$sdir" bash -c '
|
||||||
|
die() { printf "DIE: %s\n" "$*"; exit 1; }
|
||||||
|
log() { printf "LOG: %s\n" "$*"; }
|
||||||
|
ok() { printf "OK: %s\n" "$*"; }
|
||||||
|
remember_temp() { printf "TEMP: %s\n" "$1"; }
|
||||||
|
uname() { echo x86_64; }
|
||||||
|
curl() { while [ "$#" -gt 1 ] && [ "$1" != "-o" ]; do shift; done
|
||||||
|
printf "stand-in go toolchain\n" > "$2"; printf "CURL: %s\n" "$2"; }
|
||||||
|
tar() { printf "TAR: %s\n" "$*"; }
|
||||||
|
'"$gblock"'
|
||||||
|
install_go_toolchain'
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$groot" && : > "$groot/KEEP"
|
||||||
|
out="$(run_go 1.26.4 deadbeef)"
|
||||||
|
expect "a Go download that does not match the pin is refused" \
|
||||||
|
"DIE: Go 1.26.4 (amd64) checksum mismatch: got ${gsum}, expected deadbeef" "$out"
|
||||||
|
case "$out" in
|
||||||
|
*TAR:*) echo "FAIL a refused Go download must not be unpacked"; fails=$((fails + 1)) ;;
|
||||||
|
*) echo "PASS a refused Go download is not unpacked" ;;
|
||||||
|
esac
|
||||||
|
if [ -e "$groot/KEEP" ]; then
|
||||||
|
echo "PASS a refused Go download leaves the old toolchain in place"
|
||||||
|
else
|
||||||
|
echo "FAIL a refused Go download must not remove the old toolchain"; fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
expect "an unpinned FELIS_GO_VERSION without a digest is refused" "DIE: no pinned sha256 for Go 1.99.0" \
|
||||||
|
"$(run_go 1.99.0 "$gsum")"
|
||||||
|
expect "an unpinned FELIS_GO_VERSION installs with its own FELIS_GO_SHA256" "TAR: " \
|
||||||
|
"$(run_go 1.99.0 deadbeef "$gsum")"
|
||||||
|
|
||||||
|
out="$(run_go 1.26.4 "$gsum")"
|
||||||
|
expect "a Go download matching the pin is unpacked" "TAR: " "$out"
|
||||||
|
gtmp="$(printf '%s\n' "$out" | sed -n 's/^TEMP: //p')"
|
||||||
|
expect "the Go download is staged in a directory the cleanup removes" \
|
||||||
|
"CURL: ${gtmp:-<none>}/go1.26.4.linux-amd64.tar.gz" "$out"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------
|
||||||
if [ "$fails" -eq 0 ]; then
|
if [ "$fails" -eq 0 ]; then
|
||||||
echo "ALL PASS"
|
echo "ALL PASS"
|
||||||
|
|||||||
Reference in new issue
Block a user