mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
148 lines
6.9 KiB
Bash
Executable file
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.7.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})"
|