#!/bin/sh
# The install script behind `curl -fsSL https://securechain.tuxcare.com/get/securechain | sh`
# (doc 08, "The install script" - the contract lives there, this file carries
# the reasons).
#
# It runs on a machine we know nothing about, so it assumes nothing: POSIX sh
# (a pipe into `sh` must not require bash), curl (the one-liner already used
# it, so it is there by construction), uname, and a sha256 tool - sha256sum on
# Linux, shasum on macOS, both checked. No sudo, ever: a script piped into a
# shell has no business escalating; when /usr/local/bin is not writable the
# binary goes to ~/.local/bin instead.
#
# Verification is layered the same way the release is: the checksum gates
# unconditionally - a mismatch aborts before anything is installed - and the
# cosign signature is verified when BOTH a cosign binary and a public key are
# at hand, because a customer must not need a tool to install a tool. The
# checksum protects against a corrupted or truncated download; the signature,
# when it runs, protects against the download source itself.
#
# Everything comes from one host, the public mirror the release pipeline
# fills (doc 08, "The public mirror"): <base>/latest/latest names the newest
# final release, <base>/<tag>/ holds its files. The GitLab project is
# internal and is never asked - a customer machine cannot reach it.
#
# Knobs, all optional:
#   SECURECHAIN_VERSION        vX.Y.Z to install; unset means the newest final
#                              release, read from <base>/latest/latest
#   SECURECHAIN_INSTALL_DIR    where the binary goes (default: /usr/local/bin,
#                              falling back to ~/.local/bin)
#   SECURECHAIN_DOWNLOAD_BASE  the mirror root (default:
#                              https://securechain.tuxcare.com/get) - a staging
#                              prefix or a private mirror goes here
#   SECURECHAIN_PUBKEY         path to the cosign public key; with cosign
#                              installed, turns signature verification on
#   SECURECHAIN_AUTH_HEADER    a curl header for a non-public mirror, e.g.
#                              "PRIVATE-TOKEN: ..." - internal testing only
#
# Exit codes (doc 08, "The install script"). The script never exits 1: exit 1
# of `securechain sca` and `check` means "a finding failed the gate", and the
# CI guide tells a GitLab job `allow_failure: exit_codes: [1]`. An install
# that failed with 1 read as a scan with findings, and the job went amber
# (Danil Khabibullin, Imunify, 3 November 2026: the jobs wrapped the script
# with `|| exit 100`). The codes take the meaning they have in the CLI
# (doc 03, "Exit codes"):
#   0  installed, and the binary runs
#   5  the customer's side must change: the OS or the CPU type, a directory
#      that cannot be written, a server that does not answer, a download that
#      failed, no sha256 tool, a SECURECHAIN_VERSION of the wrong form, a
#      binary that does not run here
#   7  our release is wrong: an empty or malformed latest/latest, no checksum
#      line for this file, a checksum or signature that does not match
# A command that fails outside these checks would end a `set -e` script with
# its own code, and that code can be 1. The exit trap turns 1 and 2 into 7:
# such a stop is a defect of this script.
set -eu

base="${SECURECHAIN_DOWNLOAD_BASE:-https://securechain.tuxcare.com/get}"
base="${base%/}"

say() { printf 'securechain install: %s\n' "$*"; }
# fix_setup: the customer must fix something (exit 5). bad_release: our
# release is wrong (exit 7). There is no die() that exits 1.
fix_setup() { printf 'securechain install: ERROR: %s\n' "$*" >&2; exit 5; }
bad_release() {
  printf 'securechain install: ERROR: %s\n' "$*" >&2
  printf 'securechain install: this is a defect of the TuxCare release, not of your setup; please report it to TuxCare support\n' >&2
  exit 7
}

# is_version: v1.2.3, or a pre-release such as v1.2.3-rc.1 - the tags the
# release pipeline makes. A malformed latest/latest is our release's defect
# (exit 7), and must not reach a download that then fails with the customer's
# advice (review 3 of task SC-1029: `v1garbage` passed a `v[0-9]*` test).
is_version() {
  printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'
}

# Every download goes through here: -f so an HTTP error is an exit code and
# never a saved error page, and an explicit -o so the file lands under the
# name this script expects whatever the server calls it.
fetch() {
  if [ -n "${SECURECHAIN_AUTH_HEADER:-}" ]; then
    curl -fsSL --header "$SECURECHAIN_AUTH_HEADER" -o "$2" "$1"
  else
    curl -fsSL -o "$2" "$1"
  fi
}

os="$(uname -s)" || fix_setup "uname -s failed: cannot tell the operating system"
case "$os" in
  Linux)  os=linux ;;
  Darwin) os=darwin ;;
  *) fix_setup "unsupported OS: $os. This script installs the linux and macOS binaries; on Windows download securechain-windows-amd64.exe from https://securechain.tuxcare.com/get/, or use npm (npx @tuxcare/securechain) or PyPI (pipx install securechain)" ;;
esac
arch="$(uname -m)" || fix_setup "uname -m failed: cannot tell the CPU type"
case "$arch" in
  x86_64|amd64)  arch=amd64 ;;
  aarch64|arm64) arch=arm64 ;;
  *) fix_setup "unsupported architecture: $arch. amd64 and arm64 are the tier 1 targets (doc 08); tier 3 is built on request" ;;
esac
artifact="securechain-$os-$arch"

workdir="$(mktemp -d)" || fix_setup "mktemp -d failed: cannot make a temporary directory (check TMPDIR)"
tmp=""
# The cleanup, and the guard that no path exits 1 or 2 (see "Exit codes"
# above). `exit` inside an EXIT trap sets the status of the script.
cleanup() {
  status=$?
  # Best effort: a cleanup that fails must not end the trap under `set -e`
  # with the code of rm, which can be 1.
  set +e
  rm -rf "$workdir" 2>/dev/null || :
  [ -z "$tmp" ] || rm -f "$tmp" 2>/dev/null || :
  case "$status" in
    1|2)
      printf 'securechain install: ERROR: a step of the install script stopped with exit %s; please report it to TuxCare support\n' "$status" >&2
      exit 7
      ;;
  esac
  exit "$status"
}
trap cleanup EXIT
cd "$workdir" || fix_setup "cannot enter the temporary directory $workdir"

version="${SECURECHAIN_VERSION:-}"
if [ -z "$version" ]; then
  # The newest final release, as one line of text the release pipeline
  # writes. Only a final version ever moves it (doc 08): a customer who did
  # not pin never receives a release candidate.
  fetch "$base/latest/latest" latest \
    || fix_setup "cannot read $base/latest/latest: check the network, a proxy, or SECURECHAIN_DOWNLOAD_BASE; or pin a version with SECURECHAIN_VERSION=vX.Y.Z"
  version="$(head -1 latest | tr -d '[:space:]')"
  [ -n "$version" ] || bad_release "$base/latest/latest is empty: it names no version"
  is_version "$version" || bad_release "$base/latest/latest names no version of the form v1.2.3: $version"
else
  is_version "$version" || fix_setup "SECURECHAIN_VERSION must look like v1.2.3, got: $version"
fi

src="$base/$version"

say "downloading $artifact $version from $src"
fetch "$src/$artifact" "$artifact" \
  || fix_setup "cannot download $src/$artifact: check the network, a proxy, and that $version exists"
fetch "$src/checksums.txt" checksums.txt \
  || fix_setup "cannot download $src/checksums.txt: check the network or a proxy"

# The checksum gate. Only this artifact's line is checked - the others name
# files that were deliberately not downloaded - and a missing line is the same
# refusal as a wrong hash: a checksums.txt that does not know this artifact
# is not the release's checksums.txt.
grep -E "[[:space:]]${artifact}\$" checksums.txt > checksum.line \
  || bad_release "checksums.txt carries no line for $artifact - refusing to install"
if command -v sha256sum >/dev/null 2>&1; then
  # -c, not --check --strict: the BusyBox sha256sum of an alpine image
  # knows neither long option, and the install stopped there (portal
  # pipeline 38134, golang:1.26-alpine). With the one line of this file,
  # -c alone fails on a wrong hash and on a malformed line, in GNU
  # coreutils and in BusyBox.
  sha256sum -c checksum.line \
    || bad_release "the checksum of $artifact does not match checksums.txt - refusing to install"
elif command -v shasum >/dev/null 2>&1; then
  shasum -a 256 --check --strict checksum.line \
    || bad_release "the checksum of $artifact does not match checksums.txt - refusing to install"
else
  fix_setup "neither sha256sum nor shasum found - cannot verify the download, refusing to install"
fi

if [ -n "${SECURECHAIN_PUBKEY:-}" ] && command -v cosign >/dev/null 2>&1; then
  fetch "$src/$artifact.sig" "$artifact.sig" \
    || fix_setup "cannot download $src/$artifact.sig: check the network or a proxy"
  # --insecure-ignore-tlog skips the transparency-log LOOKUP, not the
  # signature check: releases sign with --tlog-upload=false (.goreleaser.yaml
  # has the policy note), so there is no Rekor entry to find.
  cosign verify-blob --key "$SECURECHAIN_PUBKEY" --signature "$artifact.sig" \
    --insecure-ignore-tlog "$artifact" \
    || bad_release "cosign signature verification FAILED for $artifact with the key $SECURECHAIN_PUBKEY"
  say "cosign signature verified"
fi

dir="${SECURECHAIN_INSTALL_DIR:-}"
if [ -z "$dir" ]; then
  if [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
    dir=/usr/local/bin
  else
    dir="$HOME/.local/bin"
  fi
fi
mkdir -p "$dir" || fix_setup "cannot make the install directory $dir; set SECURECHAIN_INSTALL_DIR to a directory you can write"
# A rename, never a write into the old file: Linux refuses to open a running
# executable for writing ("Text file busy"), and `securechain upgrade` runs
# this script while its own binary runs (doc 08, measured 29 October 2026).
# The temporary file is in the same directory, so the mv is a rename and not
# a copy across file systems.
tmp="$dir/.securechain.new.$$"
cp "$artifact" "$tmp" || { rm -f "$tmp"; fix_setup "cannot write to $dir; set SECURECHAIN_INSTALL_DIR to a directory you can write"; }
chmod 0755 "$tmp" && mv -f "$tmp" "$dir/securechain" \
  || { rm -f "$tmp"; fix_setup "cannot replace $dir/securechain"; }
say "installed $dir/securechain"
case ":$PATH:" in
  *":$dir:"*) : ;;
  *) say "NOTE: $dir is not in your PATH" ;;
esac

# An install that cannot execute what it installed has failed, and must say so
# here rather than at the customer's first real use.
#
# The checksum matched, so the bytes are the release's: a binary that does not
# run is this system's answer (a noexec mount, a CPU that uname misnamed), and
# exit 5. Its own exit code is not passed on, because that code can be 1.
"$dir/securechain" version \
  || fix_setup "the installed $dir/securechain does not run on this system (a noexec mount, or another CPU type)"
