Files
pulse/scripts/validate-published-release.sh
2026-08-31 04:55:15 +01:00

217 lines
7.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Remote release validator.
# Downloads the published (or draft) assets straight from GitHub Releases,
# authenticates checksums.txt against the configured release trust root,
# recalculates every listed SHA256 sum, and authenticates each artifact's
# *.sshsig sidecar. This prevents broken or forged updates when artifacts are
# re-uploaded without regenerating their integrity metadata (see issue #698).
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <tag> [owner/repo]" >&2
echo "Example: $0 v4.28.0 rcourtman/Pulse" >&2
exit 1
fi
TAG="$1"
REPO="${2:-rcourtman/Pulse}"
BASE_URL="https://github.com/${REPO}/releases/download/${TAG}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
curl_args=(curl -fsSL --connect-timeout 10 --max-time 600 --retry 3 --retry-delay 2 --retry-all-errors)
for command in curl go sha256sum ssh-keygen; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "${command} is required to authenticate published release assets." >&2
exit 1
fi
done
UPDATE_PUBLIC_KEY="$(printf '%s' "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" | tr -d '\r\n[:space:]')"
if [[ -z "$UPDATE_PUBLIC_KEY" ]]; then
echo "PULSE_UPDATE_SIGNING_PUBLIC_KEY is required to authenticate published release assets." >&2
exit 1
fi
if ! UPDATE_SSH_PUBLIC_KEY="$(
go -C "$REPO_ROOT" run ./scripts/release_update_key.go public-key-ssh \
--public-key "$UPDATE_PUBLIC_KEY" \
--comment pulse-installer
)"; then
echo "PULSE_UPDATE_SIGNING_PUBLIC_KEY is not a valid release signing public key." >&2
exit 1
fi
ALLOWED_SIGNERS_PATH="${TMP_DIR}/allowed_signers"
printf 'pulse-installer namespaces="pulse-install" %s\n' "$UPDATE_SSH_PUBLIC_KEY" >"$ALLOWED_SIGNERS_PATH"
verify_signature() {
local payload_path="$1"
local signature_path="$2"
local label="$3"
if ! ssh-keygen -Y verify \
-f "$ALLOWED_SIGNERS_PATH" \
-I pulse-installer \
-n pulse-install \
-s "$signature_path" <"$payload_path" >/dev/null 2>&1; then
echo "SSH signature verification failed for ${label}." >&2
return 1
fi
}
CHECKSUMS_PATH="${TMP_DIR}/checksums.txt"
echo "Downloading ${BASE_URL}/checksums.txt"
if ! "${curl_args[@]}" "${BASE_URL}/checksums.txt" >"$CHECKSUMS_PATH"; then
echo "Failed to download checksums.txt for ${TAG}" >&2
exit 1
fi
CHECKSUMS_SIG_PATH="${TMP_DIR}/checksums.txt.sshsig"
echo "Downloading ${BASE_URL}/checksums.txt.sshsig"
if ! "${curl_args[@]}" "${BASE_URL}/checksums.txt.sshsig" >"$CHECKSUMS_SIG_PATH"; then
echo "Failed to download checksums.txt.sshsig for ${TAG}" >&2
exit 1
fi
if [[ ! -s "$CHECKSUMS_SIG_PATH" ]]; then
echo "checksums.txt.sshsig is empty for ${TAG}" >&2
exit 1
fi
if ! verify_signature "$CHECKSUMS_PATH" "$CHECKSUMS_SIG_PATH" "checksums.txt for ${TAG}"; then
exit 1
fi
REQUIRED_SIGNED_INSTALLERS=(
install.sh
install-docker.sh
install-mcp.sh
install-mcp.ps1
install.ps1
pulse-auto-update.sh
)
for installer in "${REQUIRED_SIGNED_INSTALLERS[@]}"; do
if ! awk -v name="$installer" '$2 == name { found = 1 } END { exit !found }' "$CHECKSUMS_PATH"; then
echo "Authenticated checksums.txt must contain exactly one valid entry for published installer ${installer}." >&2
exit 1
fi
done
RELEASE_SBOM="pulse-${TAG}-release.sbom.spdx.json"
if ! awk '{print $2}' "$CHECKSUMS_PATH" | grep -Fx "$RELEASE_SBOM" >/dev/null 2>&1; then
echo "checksums.txt does not list ${RELEASE_SBOM} for ${TAG}" >&2
exit 1
fi
RELEASE_SBOM_PATH="${TMP_DIR}/${RELEASE_SBOM}"
echo "Downloading ${BASE_URL}/${RELEASE_SBOM}"
if ! "${curl_args[@]}" "${BASE_URL}/${RELEASE_SBOM}" >"$RELEASE_SBOM_PATH"; then
echo "Failed to download ${RELEASE_SBOM} for ${TAG}" >&2
exit 1
fi
if [[ ! -s "$RELEASE_SBOM_PATH" ]]; then
echo "${RELEASE_SBOM} is empty for ${TAG}" >&2
exit 1
fi
status=0
SEEN_FILENAMES_PATH="${TMP_DIR}/checksums-filenames.txt"
: >"$SEEN_FILENAMES_PATH"
while read -r checksum filename extra || [[ -n "${checksum:-}${filename:-}${extra:-}" ]]; do
[[ -z "${checksum:-}" ]] && continue
[[ "$checksum" =~ ^# ]] && continue
if [[ -z "${filename:-}" ]]; then
echo "Malformed checksums line (missing filename): $checksum" >&2
status=$((status + 1))
continue
fi
if [[ -n "${extra:-}" ]]; then
echo "Malformed checksums line (unexpected fields for ${filename})." >&2
status=$((status + 1))
continue
fi
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
echo "Malformed checksums line (invalid SHA-256 for ${filename}): ${checksum}" >&2
status=$((status + 1))
continue
fi
if [[ ! "$filename" =~ ^[A-Za-z0-9._+-]+$ ]]; then
echo "Unsafe release asset filename in checksums.txt: ${filename}" >&2
status=$((status + 1))
continue
fi
if grep -Fxq -- "$filename" "$SEEN_FILENAMES_PATH"; then
echo "Duplicate release asset filename in checksums.txt: ${filename}" >&2
status=$((status + 1))
continue
fi
printf '%s\n' "$filename" >>"$SEEN_FILENAMES_PATH"
artifact_url="${BASE_URL}/${filename}"
echo "Verifying ${filename}..."
artifact_path="${TMP_DIR}/${filename}"
if ! "${curl_args[@]}" "$artifact_url" >"$artifact_path"; then
echo "Failed to download ${filename}" >&2
status=$((status + 1))
rm -f "$artifact_path"
continue
fi
actual_checksum="$(sha256sum "$artifact_path" | awk '{print $1}')"
if [[ "$actual_checksum" != "$checksum" ]]; then
echo "Checksum mismatch for ${filename}: expected ${checksum}, got ${actual_checksum}" >&2
status=$((status + 1))
fi
sha_url="${artifact_url}.sha256"
if ! sha_content=$("${curl_args[@]}" "$sha_url" | tr -d '\r' | sed 's/[[:space:]]*$//'); then
echo "Failed to download ${filename}.sha256" >&2
status=$((status + 1))
rm -f "$artifact_path"
continue
fi
expected_line="${checksum} ${filename}"
if [[ "$sha_content" != "$expected_line" ]]; then
echo "${filename}.sha256 content mismatch (expected '${expected_line}', got '${sha_content}')" >&2
status=$((status + 1))
fi
sshsig_path="${TMP_DIR}/${filename}.sshsig"
if ! "${curl_args[@]}" "${artifact_url}.sshsig" >"$sshsig_path"; then
echo "Failed to download ${filename}.sshsig" >&2
status=$((status + 1))
rm -f "$artifact_path" "$sshsig_path"
continue
fi
if [[ ! -s "$sshsig_path" ]]; then
echo "${filename}.sshsig is empty" >&2
status=$((status + 1))
rm -f "$artifact_path"
continue
fi
if ! verify_signature "$artifact_path" "$sshsig_path" "$filename"; then
status=$((status + 1))
fi
rm -f "$artifact_path"
done < "$CHECKSUMS_PATH"
if [[ "$status" -ne 0 ]]; then
echo "Published release validation failed for ${TAG} (${status} error(s))." >&2
exit 1
fi
for installer in "${REQUIRED_SIGNED_INSTALLERS[@]}"; do
if ! grep -Fxq -- "$installer" "$SEEN_FILENAMES_PATH"; then
echo "Authenticated checksums.txt must contain exactly one valid entry for published installer ${installer}." >&2
exit 1
fi
done
echo "Published release assets for ${TAG} match authenticated checksums.txt, *.sha256 files, and verified *.sshsig sidecars."