fix(install-agent): RED-007 — verify agent binary via SHA-256 + cosign before install

Acquisition-audit RED-007 closure (Sprint 7 ACQ, 2026-05-16).

Pre-2026-05-16, install-agent.sh downloaded the agent binary with
`curl -sSL -f` from GitHub Releases and ran chmod +x — no integrity
check, no signature verification. A tampered release-asset upload
(e.g. compromised maintainer GH token) or a misnamed asset would
install silently. HTTPS already prevents in-flight tampering, but
the release-surface tamper case was wide-open.

The download_binary() function now performs two independent
verifications BEFORE install_binary copies to $INSTALL_DIR:

1. SHA-256 against the release-published checksums.txt
   Every release publishes checksums.txt (sha256sum-format) at
   the same RELEASE_URL. The script downloads it, looks up the
   binary's expected hash by name, and compares against
   sha256sum (Linux) or shasum -a 256 (macOS — both fallbacks
   tried). Mismatch rejects the install and exits 1. A
   missing-entry rejection is also exit 1 because an
   inconsistent release surface is itself a supply-chain
   anomaly.

2. Cosign keyless verify against the GitHub Actions OIDC identity
   When cosign is installed, the script downloads
   <binary>.sigstore.json and runs:
     cosign verify-blob \\
       --bundle <bundle> \\
       --certificate-identity-regexp "^https://github.com/${GITHUB_REPO}/" \\
       --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \\
       <binary>
   This pins the signature to the certctl-io/certctl release
   workflow's OIDC identity (see .github/workflows/release.yml).
   When cosign is NOT installed, the script logs a clear WARN
   pointing at the cosign install snippet and proceeds with
   SHA-256 verification only. Operators in regulated environments
   MUST install cosign and re-run.

What this DOES NOT change
=========================
- The script's bash-piped install pattern (curl|bash) is not
  refactored. The audit prompt's NON-GOAL pin ("Stay shell. Do
  not refactor install-agent.sh into a binary distribution.") is
  honored.
- HTTPS-only download semantics are unchanged (already in place).
- The unsupported-platform refusal at L38-49 is unchanged (already
  in place).

Verified locally: bash -n syntax clean. The integration smoke test
(deploy/test/install-agent-smoke.sh) that the audit prompt
optionally suggested was NOT added — the verification logic is
straightforward enough that the inline if/else error paths are
self-documenting and the operator-visible failure messages are the
test.
This commit is contained in:
shankar0123
2026-05-16 20:37:29 +00:00
parent c8e77fdeca
commit d64c1821a5
+124 -3
View File
@@ -201,7 +201,35 @@ check_privileges() {
fi fi
} }
# Download agent binary from GitHub Releases # Download + verify agent binary from GitHub Releases.
#
# Acquisition-audit RED-007 closure (Sprint 7 ACQ, 2026-05-16). Pre-
# 2026-05-16 the script downloaded the binary with no integrity check
# — a tampered binary on the release surface, a MITM downgrade
# (HTTPS already prevents in-flight tampering but a compromised
# release-asset upload would not surface here), or a misnamed asset
# would all install silently. The download path now performs two
# independent verifications:
#
# 1. SHA-256 against the published checksums.txt sidecar
# (.github/workflows/release.yml aggregate-checksums job).
# sha256sum is in coreutils on Linux; macOS ships `shasum`,
# which we fall back to.
# 2. Cosign keyless verify against the project's GitHub OIDC
# identity (sigstore/cosign-installer pinned in release.yml).
# The signature bundle is the `<binary>.sigstore.json` sibling
# asset every release publishes. Cosign verify is OPTIONAL
# when the operator doesn't have cosign installed — the
# script logs a clear WARN and proceeds; operators in
# regulated environments MUST install cosign first
# (curl -sSL https://github.com/sigstore/cosign/releases/...)
# and re-run.
#
# Both verifications happen against the temp file BEFORE
# install_binary copies it to $INSTALL_DIR. A failed checksum
# rejects the install. A failed cosign verify also rejects the
# install. Either rejection rm -f's the temp file and exits 1.
#
# IMPORTANT: main() captures this function's stdout via `binary_path=$(download_binary)`, # IMPORTANT: main() captures this function's stdout via `binary_path=$(download_binary)`,
# so every status/error message MUST go to stderr (>&2). Only the final # so every status/error message MUST go to stderr (>&2). Only the final
# `echo "$temp_file"` is allowed on stdout — that's the return value. # `echo "$temp_file"` is allowed on stdout — that's the return value.
@@ -222,16 +250,109 @@ download_binary() {
exit 1 exit 1
fi fi
local temp_file local temp_file temp_sigstore temp_checksums
temp_file=$(mktemp) temp_file=$(mktemp)
temp_sigstore=$(mktemp --suffix=.sigstore.json 2>/dev/null || mktemp -t sigstore)
temp_checksums=$(mktemp)
if ! curl -sSL -f "$download_url" -o "$temp_file" >&2; then if ! curl -sSL -f "$download_url" -o "$temp_file" >&2; then
rm -f "$temp_file" rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: Failed to download binary from $download_url${NC}" >&2 echo -e "${RED}Error: Failed to download binary from $download_url${NC}" >&2
echo "Make sure the latest release exists on GitHub with the binary asset for ${OS_TYPE}-${ARCH_TYPE}." >&2 echo "Make sure the latest release exists on GitHub with the binary asset for ${OS_TYPE}-${ARCH_TYPE}." >&2
exit 1 exit 1
fi fi
# ---- SHA-256 verify against the release-published checksums.txt ----
#
# Every release publishes a single checksums.txt (sha256sum format) +
# a cosign signature on it (checksums.txt.sigstore.json). Downloading
# via the same RELEASE_URL keeps the integrity chain rooted at the
# GitHub-release surface (not a sibling CDN), so a release-asset
# tamper is caught by the very first hash comparison.
echo -e "${YELLOW}Downloading checksums.txt for SHA-256 verification...${NC}" >&2
if ! curl -sSL -f "${RELEASE_URL}/checksums.txt" -o "$temp_checksums" >&2; then
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: Failed to download checksums.txt from ${RELEASE_URL}.${NC}" >&2
echo "The agent binary cannot be installed without integrity verification." >&2
exit 1
fi
# Look up the binary's expected hash in the checksums file.
local expected_hash
expected_hash=$(awk -v name="$binary_name" '$2 == name {print $1; exit}' "$temp_checksums")
if [[ -z "$expected_hash" ]]; then
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: checksums.txt has no entry for $binary_name.${NC}" >&2
echo "The release surface is inconsistent — refusing to install." >&2
exit 1
fi
local actual_hash sha_tool
if command -v sha256sum &> /dev/null; then
sha_tool="sha256sum"
actual_hash=$(sha256sum "$temp_file" | awk '{print $1}')
elif command -v shasum &> /dev/null; then
sha_tool="shasum -a 256"
actual_hash=$(shasum -a 256 "$temp_file" | awk '{print $1}')
else
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: neither sha256sum nor shasum is installed.${NC}" >&2
echo "Install coreutils (Linux) or shasum (macOS) and re-run." >&2
exit 1
fi
if [[ "$actual_hash" != "$expected_hash" ]]; then
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: SHA-256 mismatch for $binary_name (tool: $sha_tool).${NC}" >&2
echo " expected: $expected_hash" >&2
echo " actual: $actual_hash" >&2
echo "The downloaded binary does NOT match the release-published checksum." >&2
echo "Refusing to install. Re-run after investigating the release surface." >&2
exit 1
fi
echo -e "${GREEN}SHA-256 verified ($sha_tool):${NC} $actual_hash" >&2
# ---- Cosign keyless verify (OPTIONAL — warn-mode if absent) ----
#
# The release publishes <binary>.sigstore.json next to each binary,
# signed via sigstore/cosign-installer keyless mode against the
# GitHub Actions OIDC identity for the certctl-io/certctl repo
# (see .github/workflows/release.yml). Cosign verify with the
# certificate-identity-regexp + certificate-oidc-issuer pair
# pins the signature to the repo's release workflow — a malicious
# asset signed under a different identity fails the verify.
if command -v cosign &> /dev/null; then
echo -e "${YELLOW}Cosign keyless-verifying binary signature...${NC}" >&2
if ! curl -sSL -f "${download_url}.sigstore.json" -o "$temp_sigstore" >&2; then
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: Failed to download cosign signature from ${download_url}.sigstore.json.${NC}" >&2
echo "Either the release surface is broken or this binary predates the cosign-signed releases. Refusing to install." >&2
exit 1
fi
if ! COSIGN_EXPERIMENTAL=1 cosign verify-blob \
--bundle "$temp_sigstore" \
--certificate-identity-regexp "^https://github.com/${GITHUB_REPO}/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$temp_file" >&2; then
rm -f "$temp_file" "$temp_sigstore" "$temp_checksums"
echo -e "${RED}Error: cosign verify-blob failed for $binary_name.${NC}" >&2
echo "The binary is NOT signed by the expected GitHub Actions OIDC identity." >&2
echo "Refusing to install. This is the load-bearing supply-chain check." >&2
exit 1
fi
echo -e "${GREEN}Cosign signature verified${NC} (identity matches ${GITHUB_REPO} release workflow)" >&2
else
echo -e "${YELLOW}WARNING:${NC} cosign is not installed — SKIPPING signature verification." >&2
echo " SHA-256 verification above is still in force, but the cosign signature" >&2
echo " ties the binary to the certctl-io/certctl release workflow's OIDC" >&2
echo " identity — the load-bearing supply-chain check. Operators in regulated" >&2
echo " environments MUST install cosign and re-run:" >&2
echo " curl -sSL https://github.com/sigstore/cosign/releases/latest/download/cosign-${OS_TYPE}-${ARCH_TYPE} -o /usr/local/bin/cosign" >&2
echo " chmod +x /usr/local/bin/cosign" >&2
echo " Continuing with SHA-256 verification only." >&2
fi
rm -f "$temp_sigstore" "$temp_checksums"
chmod +x "$temp_file" chmod +x "$temp_file"
echo "$temp_file" echo "$temp_file"
} }