#!/bin/sh # Fystack cosigner-cli installer # # curl -sSL https://cosigner.fystack.io/install.sh | sh # curl -sSL https://cosigner.fystack.io/install.sh | sh -s -- --version v1.2.3 --dir /opt/cosigner-cli/bin # # Generated for cosigner-cli v0.1.1 (commit 175aa02bf75e846d8adb4039862971795c78765b), released 2026-10-11T06:43:10Z. # Binaries are downloaded from the fystack/cosigner-cli GitHub release, checked against # the SHA-256s pinned below, then verified with cosign against the release # workflow identity of fystack/cosigner-cli for this tag. # # Everything runs inside main(), so a partially downloaded script does nothing. set -eu VERSION="v0.1.1" COMMIT="175aa02bf75e846d8adb4039862971795c78765b" BASE_URL="https://cosigner.fystack.io" REPOSITORY="fystack/cosigner-cli" SHA256_LINUX_AMD64="a0ed93e5538a8c3995f80a9764a217ac3e22de88559b76dff3c32ff4af56793d" SHA256_LINUX_ARM64="6d62651522d2e8ee3a68fd9b58669250cce21c47832f7ac3718a076b41f2d6c6" SHA256_DARWIN_AMD64="14e503e3611ba918097878a2d0dbe1bfeff01d5316eefb1d1e7c06fe437332a5" SHA256_DARWIN_ARM64="15916849aa47057ebae04499870dd466a080c2775a2d4524f8439558a4941fc4" # Signature bundles (Sigstore) for each binary. BUNDLE_SHA256_LINUX_AMD64="5a3859b2e911aa68c9c89de3fb7cd70b106ba12fce8cf5278201f5bb70555355" BUNDLE_SHA256_LINUX_ARM64="4bdcca1a747d6105afba8c9309e2aa20f03e60f9b8417e15c0c3af4528354e31" BUNDLE_SHA256_DARWIN_AMD64="3f1590b515392a6424002545530087eb2ca7f196bccd84ad5aca309e392854e4" BUNDLE_SHA256_DARWIN_ARM64="a425412b056e0e41d2c58357913d261ad27791d458505f782328068201434b53" COSIGN_VERSION="v3.1.3" COSIGN_SHA256_LINUX_AMD64="4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71" COSIGN_SHA256_LINUX_ARM64="c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a" COSIGN_SHA256_DARWIN_AMD64="2347488e5d5b25336644024dfeca5601b190e91197a71a917bda44744aff106c" COSIGN_SHA256_DARWIN_ARM64="5cf948c2f4dfe59687bdd0b8523709067383e03982cc543475c8a7dc70e92a76" OIDC_ISSUER="https://token.actions.githubusercontent.com" # ---------------------------------------------------------------- output ---- if [ -t 2 ]; then BOLD="$(printf '\033[1m')"; DIM="$(printf '\033[2m')"; RED="$(printf '\033[31m')" GREEN="$(printf '\033[32m')"; YELLOW="$(printf '\033[33m')"; BLUE="$(printf '\033[34m')" RESET="$(printf '\033[0m')" else BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; BLUE=""; RESET="" fi say() { printf '%s\n' "$*" >&2; } step() { printf '%s==>%s %s%s%s\n' "$BLUE" "$RESET" "$BOLD" "$*" "$RESET" >&2; } ok() { printf ' %s✓%s %s\n' "$GREEN" "$RESET" "$*" >&2; } warn() { printf ' %s!%s %s\n' "$YELLOW" "$RESET" "$*" >&2; WARNINGS=$((WARNINGS + 1)); } die() { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; } usage() { cat >&2 </dev/null 2>&1; } download() { # url dest curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --silent --show-error --location --retry 3 \ --output "$2" "$1" || die "download failed: $1" } sha256_of() { if has sha256sum; then sha256sum "$1" | cut -d ' ' -f 1 elif has shasum; then shasum -a 256 "$1" | cut -d ' ' -f 1 elif has openssl; then openssl dgst -sha256 -r "$1" | cut -d ' ' -f 1 else die "need sha256sum, shasum or openssl to verify downloads" fi } check_sha256() { # file expected label actual="$(sha256_of "$1")" [ "$actual" = "$2" ] || die "checksum mismatch for $3 expected $2 got $actual Refusing to install. Report this to security@fystack.io." } # Runs a command as root when the target directory is not writable by us. as_owner() { # dir cmd... dir="$1"; shift if [ -w "$dir" ]; then "$@"; else $SUDO "$@"; fi } # ----------------------------------------------------------------- steps ---- detect_platform() { case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=darwin ;; MINGW* | MSYS* | CYGWIN*) die "native Windows is not supported. Run this inside WSL 2 (Ubuntu)." ;; *) die "unsupported OS $(uname -s). Linux or macOS required." ;; esac case "$(uname -m)" in x86_64 | amd64) ARCH=amd64 ;; arm64 | aarch64) ARCH=arm64 ;; *) die "unsupported CPU $(uname -m). amd64 or arm64 required." ;; esac PLATFORM="$(printf '%s_%s' "$OS" "$ARCH" | tr '[:lower:]' '[:upper:]')" eval "CLI_SHA256=\$SHA256_$PLATFORM" eval "BUNDLE_SHA256=\$BUNDLE_SHA256_$PLATFORM" eval "COSIGN_SHA256=\$COSIGN_SHA256_$PLATFORM" [ -n "$CLI_SHA256" ] || die "release $VERSION has no build for $OS/$ARCH" } choose_install_dir() { SUDO="" if [ "$(id -u)" -ne 0 ] && has sudo; then SUDO="sudo"; fi if [ -n "$INSTALL_DIR" ]; then case "$INSTALL_DIR" in /*) ;; *) die "--dir must be an absolute path" ;; esac elif [ -w /usr/local/bin ] || [ -n "$SUDO" ] || [ "$(id -u)" -eq 0 ]; then INSTALL_DIR=/usr/local/bin else INSTALL_DIR="$HOME/.local/bin" fi if [ ! -d "$INSTALL_DIR" ]; then parent="$(dirname "$INSTALL_DIR")" while [ ! -d "$parent" ]; do parent="$(dirname "$parent")"; done as_owner "$parent" mkdir -p "$INSTALL_DIR" fi if [ ! -w "$INSTALL_DIR" ] && [ -z "$SUDO" ]; then die "$INSTALL_DIR is not writable and sudo is not available. Re-run with --dir \$HOME/.local/bin" fi if [ ! -w "$INSTALL_DIR" ]; then say " ${DIM}$INSTALL_DIR needs root, you may be asked for your sudo password${RESET}" fi } # Prints the cosign major version, or nothing when it cannot be parsed. cosign_major() { "$1" version 2>/dev/null | sed -n 's/^GitVersion:[[:space:]]*v\{0,1\}\([0-9][0-9]*\)\..*/\1/p' | head -n 1 } ensure_cosign() { step "Checking cosign" COSIGN="" if has cosign; then major="$(cosign_major cosign)" if [ -n "$major" ] && [ "$major" -ge 3 ]; then COSIGN="$(command -v cosign)" ok "cosign v$major found at $COSIGN" return fi fi asset="cosign-$OS-$ARCH" download "https://github.com/sigstore/cosign/releases/download/$COSIGN_VERSION/$asset" "$TMP/cosign" check_sha256 "$TMP/cosign" "$COSIGN_SHA256" "cosign $COSIGN_VERSION" chmod 0755 "$TMP/cosign" if has cosign; then # An older cosign is on PATH. Use the verified copy for this install # only, and leave the user's own installation alone. COSIGN="$TMP/cosign" warn "cosign at $(command -v cosign) is older than v3. Using a verified $COSIGN_VERSION for this install. Upgrade it so cosigner-cli can verify runtime images reliably." else as_owner "$INSTALL_DIR" install -m 0755 "$TMP/cosign" "$INSTALL_DIR/cosign" COSIGN="$INSTALL_DIR/cosign" ok "installed cosign $COSIGN_VERSION to $COSIGN (sha256 verified)" fi } install_cli() { step "Installing cosigner-cli $VERSION" asset="cosigner-cli-$OS-$ARCH" base="https://github.com/$REPOSITORY/releases/download/$VERSION" download "$base/$asset" "$TMP/$asset" download "$base/$asset.sigstore.json" "$TMP/$asset.sigstore.json" check_sha256 "$TMP/$asset" "$CLI_SHA256" "$asset" check_sha256 "$TMP/$asset.sigstore.json" "$BUNDLE_SHA256" "$asset.sigstore.json" ok "sha256 matches the pinned release checksums" "$COSIGN" verify-blob "$TMP/$asset" \ --bundle "$TMP/$asset.sigstore.json" \ --certificate-identity "https://github.com/$REPOSITORY/.github/workflows/release.yml@refs/tags/$VERSION" \ --certificate-oidc-issuer "$OIDC_ISSUER" >/dev/null 2>"$TMP/cosign.log" \ || { cat "$TMP/cosign.log" >&2; die "signature verification failed for $asset. Refusing to install."; } ok "signature verified (built by $REPOSITORY release workflow for $VERSION)" previous="" if [ -x "$INSTALL_DIR/cosigner-cli" ]; then previous="$("$INSTALL_DIR/cosigner-cli" version 2>/dev/null | sed -n 's/^cosigner-cli \([^ ]*\).*/\1/p' | head -n 1 || true)" fi as_owner "$INSTALL_DIR" install -m 0755 "$TMP/$asset" "$INSTALL_DIR/cosigner-cli" if [ -n "$previous" ] && [ "$previous" != "$VERSION" ]; then ok "upgraded $previous → $VERSION at $INSTALL_DIR/cosigner-cli" else ok "installed to $INSTALL_DIR/cosigner-cli" fi } check_docker() { step "Checking Docker" if ! has docker; then if [ "$OS" = darwin ]; then warn "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/setup/install/mac-install/" elif grep -qi microsoft /proc/version 2>/dev/null; then warn "Docker is not installed. Install Docker Desktop for Windows and turn on WSL integration for this distro." else warn "Docker is not installed. Install Docker Engine: https://docs.docker.com/engine/install/" fi return fi ok "docker $(docker version --format '{{.Client.Version}}' 2>/dev/null || echo found)" if docker compose version >/dev/null 2>&1; then ok "docker compose $(docker compose version --short 2>/dev/null || echo found)" else warn "Docker Compose v2 is missing. Install the docker-compose-plugin package (or update Docker Desktop)." fi if docker info >/dev/null 2>&1; then ok "Docker daemon is reachable" elif docker info 2>&1 | grep -qi 'permission denied'; then warn "No permission to use Docker. Run: sudo usermod -aG docker \$USER, then log out and back in." elif [ "$OS" = darwin ]; then warn "Docker daemon is not running. Start Docker Desktop." else warn "Docker daemon is not running. Run: sudo systemctl enable --now docker" fi } # ------------------------------------------------------------------ main ---- main() { WARNINGS=0 REQUESTED_VERSION="${COSIGNER_VERSION:-}" INSTALL_DIR="${COSIGNER_INSTALL_DIR:-}" CHECK_DOCKER=1 FORWARD="" while [ $# -gt 0 ]; do case "$1" in --version) [ $# -ge 2 ] || die "--version needs a value"; REQUESTED_VERSION="$2"; shift 2 ;; --version=*) REQUESTED_VERSION="${1#*=}"; shift ;; --dir) [ $# -ge 2 ] || die "--dir needs a value"; INSTALL_DIR="$2"; shift 2 ;; --dir=*) INSTALL_DIR="${1#*=}"; shift ;; --skip-docker-check) CHECK_DOCKER=0; FORWARD="--skip-docker-check"; shift ;; -h | --help) usage; exit 0 ;; *) usage; die "unknown option: $1" ;; esac done has curl || die "curl is required" has uname || die "uname is required" TMP="$(mktemp -d 2>/dev/null || mktemp -d -t cosigner-cli)" trap 'rm -rf -- "$TMP"' EXIT INT TERM # Another release: hand over to that release's own installer, which carries # that release's checksums. if [ -n "$REQUESTED_VERSION" ] && [ "$REQUESTED_VERSION" != "$VERSION" ]; then case "$REQUESTED_VERSION" in v*) ;; *) REQUESTED_VERSION="v$REQUESTED_VERSION" ;; esac printf '%s' "$REQUESTED_VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \ || die "invalid version $REQUESTED_VERSION (expected vX.Y.Z)" step "Fetching the installer for $REQUESTED_VERSION" curl --proto '=https' --tlsv1.2 --fail --silent --location \ --output "$TMP/install.sh" "$BASE_URL/$REQUESTED_VERSION/install.sh" \ || die "release $REQUESTED_VERSION not found. See $BASE_URL for available versions." # Hosts can answer unknown paths with an HTML page. Only run the file if # it is the installer for exactly that release. grep -qx "VERSION=\"$REQUESTED_VERSION\"" "$TMP/install.sh" \ || die "release $REQUESTED_VERSION not found. See $BASE_URL for available versions." # shellcheck disable=SC2086 # FORWARD is empty or a single flag if [ -n "$INSTALL_DIR" ]; then sh "$TMP/install.sh" --dir "$INSTALL_DIR" $FORWARD else sh "$TMP/install.sh" $FORWARD fi exit $? fi say "" say " ${BOLD}Fystack cosigner-cli${RESET} ${DIM}$VERSION · $(printf '%.12s' "$COMMIT")${RESET}" say "" detect_platform choose_install_dir ensure_cosign install_cli if [ "$CHECK_DOCKER" -eq 1 ]; then check_docker; fi say "" case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) say " ${YELLOW}$INSTALL_DIR is not on your PATH.${RESET} Add it with:" say "" say " export PATH=\"$INSTALL_DIR:\$PATH\"" say "" ;; esac if [ "$WARNINGS" -gt 0 ]; then say " ${BOLD}cosigner-cli is installed.${RESET} Fix the warnings above before running it." else say " ${GREEN}${BOLD}cosigner-cli $VERSION is ready.${RESET}" fi say "" say " Next steps:" say " cosigner-cli doctor" say " cosigner-cli up --apex-endpoint https://YOUR-APEX-HOST --config /secure/config.yaml" say "" say " ${DIM}Guide: $BASE_URL/#get-started${RESET}" say "" } main "$@"