nyx/action-scripts/download.sh

148 lines
6.9 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
REPO="elicpeter/nyx"
VERSION="${NYX_VERSION:-latest}"
INSTALL_DIR="${RUNNER_TOOL_CACHE:-/tmp}/nyx"
# Optional: pin a GPG key fingerprint here (40-char, no spaces) or set
# NYX_GPG_FINGERPRINT in the calling env to require GPG-signed SHA256SUMS.
# Empty ⇒ GPG verification is skipped (SHA256 + SLSA attestation still run).
PINNED_GPG_FINGERPRINT="${NYX_GPG_FINGERPRINT:-}"
# ── Detect runner OS and architecture ─────────────────────────────────────────
OS="$(uname -s)"
ARCH="$(uname -m)"
case "${OS}-${ARCH}" in
Linux-x86_64) TARGET="x86_64-unknown-linux-gnu" ;;
Linux-aarch64) TARGET="aarch64-unknown-linux-gnu" ;;
Darwin-x86_64) TARGET="x86_64-apple-darwin" ;;
Darwin-arm64) TARGET="aarch64-apple-darwin" ;;
*)
echo "::error::Unsupported platform: ${OS} ${ARCH}"
exit 1
;;
esac
# ── Resolve "latest" to an actual release tag ────────────────────────────────
if [[ "$VERSION" == "latest" ]]; then
echo "::warning::version: latest follows a mutable tag. Pin to a specific release (e.g. v0.6.0) for supply-chain safety."
API_URL="https://api.github.com/repos/${REPO}/releases/latest"
CURL_ARGS=(-fsSL)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: token ${GITHUB_TOKEN}")
fi
RELEASE_JSON="$(curl "${CURL_ARGS[@]}" "$API_URL")"
VERSION="$(echo "$RELEASE_JSON" | grep -o '"tag_name":\s*"[^"]*"' | head -1 | cut -d'"' -f4)"
if [[ -z "$VERSION" ]]; then
echo "::error::Failed to resolve latest release tag from ${API_URL}"
exit 1
fi
echo "Resolved latest version: ${VERSION}"
fi
# ── Download the release asset into an isolated staging dir ──────────────────
ASSET_NAME="nyx-${TARGET}.zip"
RELEASE_BASE="https://github.com/${REPO}/releases/download/${VERSION}"
DOWNLOAD_URL="${RELEASE_BASE}/${ASSET_NAME}"
STAGING="$(mktemp -d)"
trap 'rm -rf "$STAGING"' EXIT
CURL_COMMON=(-fsSL)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_COMMON+=(-H "Authorization: token ${GITHUB_TOKEN}")
fi
echo "Downloading nyx ${VERSION} for ${TARGET}..."
curl "${CURL_COMMON[@]}" -o "${STAGING}/${ASSET_NAME}" "$DOWNLOAD_URL"
# SHA256SUMS is required — the whole release signing chain hinges on it.
echo "Downloading SHA256SUMS..."
curl "${CURL_COMMON[@]}" -o "${STAGING}/SHA256SUMS" "${RELEASE_BASE}/SHA256SUMS"
# SHA256SUMS.asc is optional (GPG signing was wired up mid-0.x); fetch it if
# present so we can attempt signature verification.
SIG_PATH=""
if curl "${CURL_COMMON[@]}" -o "${STAGING}/SHA256SUMS.asc" "${RELEASE_BASE}/SHA256SUMS.asc" 2>/dev/null; then
SIG_PATH="${STAGING}/SHA256SUMS.asc"
fi
# ── Mandatory: verify the binary's SHA256 matches SHA256SUMS ─────────────────
(
cd "$STAGING"
# --ignore-missing: SHA256SUMS lists every platform archive; we only have one.
if ! sha256sum --ignore-missing -c SHA256SUMS >/dev/null 2>&1; then
echo "::error::SHA256 verification failed for ${ASSET_NAME}. Release may be tampered."
echo "Expected (from SHA256SUMS):"
grep -F "${ASSET_NAME}" SHA256SUMS || true
echo "Actual:"
sha256sum "${ASSET_NAME}" || true
exit 1
fi
)
echo "::notice::SHA256 checksum verified for ${ASSET_NAME}."
# ── Best-effort: GPG verify SHA256SUMS.asc against a pinned fingerprint ──────
# Trust model: only accept a signature from a fingerprint we have pinned. A
# signature from any other key is treated as a failure, not a success. If no
# fingerprint is pinned, GPG verification is skipped (SHA256+SLSA still run).
if [[ -n "$SIG_PATH" ]]; then
if [[ -z "$PINNED_GPG_FINGERPRINT" ]]; then
echo "::warning::SHA256SUMS.asc found but no GPG fingerprint pinned. Set NYX_GPG_FINGERPRINT (40-char, no spaces) to enforce GPG verification."
elif ! command -v gpg >/dev/null 2>&1; then
echo "::warning::gpg not installed on runner; skipping SHA256SUMS.asc verification."
else
# Fetch the pinned key from keys.openpgp.org into an ephemeral keyring.
GNUPGHOME="$(mktemp -d)"
export GNUPGHOME
chmod 700 "$GNUPGHOME"
trap 'rm -rf "$STAGING" "$GNUPGHOME"' EXIT
if ! gpg --batch --keyserver hkps://keys.openpgp.org \
--recv-keys "$PINNED_GPG_FINGERPRINT" >/dev/null 2>&1; then
echo "::error::Failed to fetch GPG key ${PINNED_GPG_FINGERPRINT} from keys.openpgp.org."
exit 1
fi
# --status-fd 1 gives machine-readable output; VALIDSIG + the pinned fpr
# is the only accept condition.
GPG_STATUS="$(gpg --batch --status-fd 1 --verify \
"$SIG_PATH" "${STAGING}/SHA256SUMS" 2>/dev/null || true)"
if ! grep -q "^\[GNUPG:\] VALIDSIG ${PINNED_GPG_FINGERPRINT} " <<<"$GPG_STATUS"; then
echo "::error::GPG signature on SHA256SUMS does not match pinned fingerprint ${PINNED_GPG_FINGERPRINT}."
echo "$GPG_STATUS"
exit 1
fi
echo "::notice::GPG signature verified against ${PINNED_GPG_FINGERPRINT}."
fi
else
echo "::warning::SHA256SUMS.asc not published for ${VERSION}; relying on SHA256 + SLSA only."
fi
# ── Best-effort: SLSA build-provenance attestation (Sigstore) ────────────────
# gh attestation verify ships with the gh CLI (preinstalled on GH-hosted
# runners) and validates attestations produced by actions/attest-build-
# provenance against the Sigstore public-good transparency log. Unlike GPG
# this requires no pre-shared key and is the preferred trust root.
if command -v gh >/dev/null 2>&1; then
if gh attestation verify "${STAGING}/${ASSET_NAME}" --repo "${REPO}" >/dev/null 2>&1; then
echo "::notice::SLSA build provenance verified for ${ASSET_NAME}."
else
echo "::warning::gh attestation verify failed or no attestation present for ${VERSION}. (Expected for releases predating attest-build-provenance.)"
fi
else
echo "::warning::gh CLI not available; skipping SLSA attestation verification."
fi
# ── Extract and install ──────────────────────────────────────────────────────
mkdir -p "$INSTALL_DIR"
# The zip stores target/{TARGET}/release/nyx — use -j to flatten paths
unzip -o -j "${STAGING}/${ASSET_NAME}" "*/nyx" -d "$INSTALL_DIR"
chmod +x "${INSTALL_DIR}/nyx"
# ── Add to PATH for subsequent steps ─────────────────────────────────────────
echo "${INSTALL_DIR}" >> "$GITHUB_PATH"
# ── Verify and set output ────────────────────────────────────────────────────
INSTALLED_VERSION="$("${INSTALL_DIR}/nyx" --version 2>&1 | head -1 || echo "unknown")"
echo "nyx-version=${INSTALLED_VERSION}" >> "$GITHUB_OUTPUT"
echo "Installed nyx: ${INSTALLED_VERSION} (${TARGET})"