mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
* fix: sign release checksums with a sigstore bundle The cosign update rejects the legacy `--output-certificate` / `--output-signature` flags alongside the new bundle format, which broke signing during the v1.53.0 release. - checksums are now signed into a single `checksums.txt.sigstore.json` bundle (signature, certificate, and tlog proof) - `install.sh -v` verifies with `--bundle` for releases >= v1.53.0 and keeps the `.sig` / `.pem` flow for older releases Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: smooth the cutover to sigstore bundle verification - `install.sh -v` now hints that bundle verification needs cosign v2.5.0 or newer when verification of a bundle-signed release fails - install tests keep exercising the legacy `.sig` / `.pem` path against v1.52.0 once the latest release is bundle-signed - touch the install test environment Dockerfiles so the cached images rebuild with a current cosign Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: only hint at the cosign minimum when cosign is actually too old - `install.sh -v` used to print the cosign v2.5.0 note on any failed bundle verification, including a missing bundle or a bad signature. It now only prints it when cosign reports it couldn't read the bundle (`bundle does not contain cert`), which is how v2.2.4 through v2.4.1 fail on a sigstore bundle. - `verify_sign` is now covered with a stub cosign, so the bundle path is exercised before any bundle-signed release is latest Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: stop the install when a release asset is missing or fails its checksum - `install.sh` now stops when the checksums file, the signature bundle, or the asset can't be downloaded, instead of handing an empty or error-page file to the next step - a sha256 mismatch between the asset and the checksums file now stops the install. Before this the mismatch was logged and the asset was installed anyway, so `-v` only ever vouched for `checksums.txt`, not the binary Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: pin cosign in the install test images The install test images pulled whatever cosign was latest, unverified. They now install cosign v3.1.3 checked against its published sha256, so the install tests always run with a cosign that can verify sigstore bundles (releases >= v1.53.0 need v2.5.0 or newer). Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Keith Zantow <kzantow@gmail.com>
901 lines
24 KiB
Bash
Executable File
901 lines
24 KiB
Bash
Executable File
#!/bin/sh
|
|
# note: we require errors to propagate (don't set -e)
|
|
set -u
|
|
|
|
PROJECT_NAME=syft
|
|
OWNER=anchore
|
|
REPO="${PROJECT_NAME}"
|
|
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
|
|
INSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME}
|
|
LEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
|
|
PROGRAM_ARGS=$@
|
|
|
|
# signature verification options
|
|
|
|
# the location to the cosign binary (allowed to be overridden by the user)
|
|
COSIGN_BINARY=${COSIGN_BINARY:-cosign}
|
|
VERIFY_SIGN=false
|
|
# this is the earliest tag in the repo where cosign sign-blob was introduced in the release process (see the goreleaser config)
|
|
VERIFY_SIGN_SUPPORTED_VERSION=v0.104.0
|
|
# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script
|
|
VERIFY_SIGN_FLAG_VERSION=v1.6.0
|
|
# releases at or after this version are signed with a sigstore bundle (.sigstore.json); earlier releases ship a separate .sig and .pem
|
|
VERIFY_SIGN_BUNDLE_VERSION=v1.53.0
|
|
|
|
# do not change the name of this parameter (this must always be backwards compatible)
|
|
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
|
|
|
|
# ------------------------------------------------------------------------
|
|
# https://github.com/client9/shlib - portable posix shell functions
|
|
# Public domain - http://unlicense.org
|
|
# https://github.com/client9/shlib/blob/master/LICENSE.md
|
|
# but credit (and pull requests) appreciated.
|
|
# ------------------------------------------------------------------------
|
|
|
|
is_command() (
|
|
command -v "$1" >/dev/null
|
|
)
|
|
|
|
echo_stderr() (
|
|
IFS=" "
|
|
printf "%b\n" "$*" 1>&2
|
|
)
|
|
|
|
_logp=2
|
|
log_set_priority() {
|
|
_logp="$1"
|
|
}
|
|
|
|
log_priority() (
|
|
if test -z "$1"; then
|
|
echo "$_logp"
|
|
return
|
|
fi
|
|
[ "$1" -le "$_logp" ]
|
|
)
|
|
|
|
init_colors() {
|
|
RED=''
|
|
BLUE=''
|
|
PURPLE=''
|
|
BOLD=''
|
|
RESET=''
|
|
# check if stdout is a terminal
|
|
if test -t 1 && is_command tput; then
|
|
# see if it supports colors
|
|
ncolors=$(tput colors)
|
|
if test -n "$ncolors" && test $ncolors -ge 8; then
|
|
RED='\033[0;31m'
|
|
BLUE='\033[0;34m'
|
|
PURPLE='\033[0;35m'
|
|
BOLD='\033[1m'
|
|
RESET='\033[0m'
|
|
fi
|
|
fi
|
|
}
|
|
|
|
init_colors
|
|
|
|
log_tag() (
|
|
case $1 in
|
|
0) printf "%b" "${RED}${BOLD}[error]${RESET}" ;;
|
|
1) printf "%b" "${RED}[warn]${RESET}" ;;
|
|
2) printf "%b" "[info]${RESET}" ;;
|
|
3) printf "%b" "${BLUE}[debug]${RESET}" ;;
|
|
4) printf "%b" "${PURPLE}[trace]${RESET}" ;;
|
|
*) printf "[%s]" "$1" ;;
|
|
esac
|
|
)
|
|
|
|
|
|
log_trace_priority=4
|
|
log_trace() (
|
|
priority=$log_trace_priority
|
|
log_priority "$priority" || return 0
|
|
echo_stderr "$(log_tag $priority)" "${@}" "${RESET}"
|
|
)
|
|
|
|
log_debug_priority=3
|
|
log_debug() (
|
|
priority=$log_debug_priority
|
|
log_priority "$priority" || return 0
|
|
echo_stderr "$(log_tag $priority)" "${@}" "${RESET}"
|
|
)
|
|
|
|
log_info_priority=2
|
|
log_info() (
|
|
priority=$log_info_priority
|
|
log_priority "$priority" || return 0
|
|
echo_stderr "$(log_tag $priority)" "${@}" "${RESET}"
|
|
)
|
|
|
|
log_warn_priority=1
|
|
log_warn() (
|
|
priority=$log_warn_priority
|
|
log_priority "$priority" || return 0
|
|
echo_stderr "$(log_tag $priority)" "${@}" "${RESET}"
|
|
)
|
|
|
|
log_err_priority=0
|
|
log_err() (
|
|
priority=$log_err_priority
|
|
log_priority "$priority" || return 0
|
|
echo_stderr "$(log_tag $priority)" "${@}" "${RESET}"
|
|
)
|
|
|
|
uname_os_check() (
|
|
os=$1
|
|
case "$os" in
|
|
darwin) return 0 ;;
|
|
dragonfly) return 0 ;;
|
|
freebsd) return 0 ;;
|
|
linux) return 0 ;;
|
|
android) return 0 ;;
|
|
nacl) return 0 ;;
|
|
netbsd) return 0 ;;
|
|
openbsd) return 0 ;;
|
|
plan9) return 0 ;;
|
|
solaris) return 0 ;;
|
|
windows) return 0 ;;
|
|
esac
|
|
log_err "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
|
|
return 1
|
|
)
|
|
|
|
uname_arch_check() (
|
|
arch=$1
|
|
case "$arch" in
|
|
386) return 0 ;;
|
|
amd64) return 0 ;;
|
|
arm64) return 0 ;;
|
|
armv5) return 0 ;;
|
|
armv6) return 0 ;;
|
|
armv7) return 0 ;;
|
|
ppc64) return 0 ;;
|
|
ppc64le) return 0 ;;
|
|
mips) return 0 ;;
|
|
mipsle) return 0 ;;
|
|
mips64) return 0 ;;
|
|
mips64le) return 0 ;;
|
|
riscv64) return 0 ;;
|
|
s390x) return 0 ;;
|
|
amd64p32) return 0 ;;
|
|
esac
|
|
log_err "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
|
return 1
|
|
)
|
|
|
|
unpack() (
|
|
archive=$1
|
|
|
|
log_trace "unpack(archive=${archive})"
|
|
|
|
case "${archive}" in
|
|
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${archive}" ;;
|
|
*.tar) tar --no-same-owner -xf "${archive}" ;;
|
|
*.zip) unzip -q "${archive}" ;;
|
|
*.dmg) extract_from_dmg "${archive}" ;;
|
|
*)
|
|
log_err "unpack unknown archive format for ${archive}"
|
|
return 1
|
|
;;
|
|
esac
|
|
)
|
|
|
|
extract_from_dmg() (
|
|
dmg_file=$1
|
|
|
|
mount_point="/Volumes/tmp-dmg"
|
|
hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}"
|
|
cp -fR "${mount_point}/." ./
|
|
hdiutil detach -quiet -force "${mount_point}"
|
|
)
|
|
|
|
http_download_curl() (
|
|
local_file=$1
|
|
source_url=$2
|
|
header=$3
|
|
|
|
log_trace "http_download_curl(local_file=$local_file, source_url=$source_url, header=$header)"
|
|
|
|
if [ -z "$header" ]; then
|
|
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
|
|
else
|
|
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
|
|
fi
|
|
|
|
if [ "$code" != "200" ]; then
|
|
log_err "received HTTP status=$code for url='$source_url'"
|
|
return 1
|
|
fi
|
|
return 0
|
|
)
|
|
|
|
http_download_wget() (
|
|
local_file=$1
|
|
source_url=$2
|
|
header=$3
|
|
|
|
log_trace "http_download_wget(local_file=$local_file, source_url=$source_url, header=$header)"
|
|
|
|
if [ -z "$header" ]; then
|
|
wget -q -O "$local_file" "$source_url"
|
|
else
|
|
wget -q --header "$header" -O "$local_file" "$source_url"
|
|
fi
|
|
)
|
|
|
|
http_download() (
|
|
log_debug "http_download(url=$2)"
|
|
if is_command curl; then
|
|
http_download_curl "$@"
|
|
return
|
|
elif is_command wget; then
|
|
http_download_wget "$@"
|
|
return
|
|
fi
|
|
log_err "http_download unable to find wget or curl"
|
|
return 1
|
|
)
|
|
|
|
http_copy() (
|
|
tmp=$(mktemp)
|
|
http_download "${tmp}" "$1" "$2" || return 1
|
|
body=$(cat "$tmp")
|
|
rm -f "${tmp}"
|
|
echo "$body"
|
|
)
|
|
|
|
hash_sha256() (
|
|
TARGET=${1:-/dev/stdin}
|
|
if is_command gsha256sum; then
|
|
hash=$(gsha256sum "$TARGET") || return 1
|
|
echo "$hash" | cut -d ' ' -f 1
|
|
elif is_command sha256sum; then
|
|
hash=$(sha256sum "$TARGET") || return 1
|
|
echo "$hash" | cut -d ' ' -f 1
|
|
elif is_command shasum; then
|
|
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
|
echo "$hash" | cut -d ' ' -f 1
|
|
elif is_command openssl; then
|
|
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
|
echo "$hash" | cut -d ' ' -f a
|
|
else
|
|
log_err "hash_sha256 unable to find command to compute sha-256 hash"
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
hash_sha256_verify() (
|
|
target=$1
|
|
checksums=$2
|
|
if [ -z "$checksums" ]; then
|
|
log_err "hash_sha256_verify checksum file not specified as argument"
|
|
return 1
|
|
fi
|
|
target_basename=${target##*/}
|
|
want=$(grep "${target_basename}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
|
if [ -z "$want" ]; then
|
|
log_err "hash_sha256_verify unable to find checksum for '${target}' in '${checksums}'"
|
|
return 1
|
|
fi
|
|
got=$(hash_sha256 "$target")
|
|
if [ "$want" != "$got" ]; then
|
|
log_err "hash_sha256_verify checksum for '$target' did not verify ${want} vs $got"
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# End of functions from https://github.com/client9/shlib
|
|
# ------------------------------------------------------------------------
|
|
|
|
# asset_file_exists [path]
|
|
#
|
|
# returns 1 if the given file does not exist
|
|
#
|
|
asset_file_exists() (
|
|
path="$1"
|
|
if [ ! -f "${path}" ]; then
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
|
|
# github_release_json [owner] [repo] [version]
|
|
#
|
|
# outputs release json string
|
|
#
|
|
github_release_json() (
|
|
owner=$1
|
|
repo=$2
|
|
version=$3
|
|
test -z "$version" && version="latest"
|
|
giturl="https://github.com/${owner}/${repo}/releases/${version}"
|
|
json=$(http_copy "$giturl" "Accept:application/json")
|
|
|
|
log_trace "github_release_json(owner=${owner}, repo=${repo}, version=${version}) returned '${json}'"
|
|
|
|
test -z "$json" && return 1
|
|
echo "${json}"
|
|
)
|
|
|
|
# extract_value [key-value-pair]
|
|
#
|
|
# outputs value from a colon delimited key-value pair
|
|
#
|
|
extract_value() (
|
|
key_value="$1"
|
|
IFS=':' read -r _ value << EOF
|
|
${key_value}
|
|
EOF
|
|
echo "$value"
|
|
)
|
|
|
|
# extract_json_value [json] [key]
|
|
#
|
|
# outputs value of the key from the given json string
|
|
#
|
|
extract_json_value() (
|
|
json="$1"
|
|
key="$2"
|
|
key_value=$(echo "${json}" | grep -o "\"$key\":[^,]*[,}]" | tr -d '",}')
|
|
|
|
extract_value "$key_value"
|
|
)
|
|
|
|
# github_release_tag [release-json]
|
|
#
|
|
# outputs release tag string
|
|
#
|
|
github_release_tag() (
|
|
json="$1"
|
|
tag=$(extract_json_value "${json}" "tag_name")
|
|
test -z "$tag" && return 1
|
|
echo "$tag"
|
|
)
|
|
|
|
# github_release_asset_url [release-url-prefix] [name] [version] [output-dir] [filename]
|
|
#
|
|
# outputs the url to the release asset
|
|
#
|
|
github_release_asset_url() (
|
|
download_url="$1"
|
|
name="$2"
|
|
version="$3"
|
|
filename="$4"
|
|
|
|
complete_filename="${name}_${version}_${filename}"
|
|
complete_url="${download_url}/${complete_filename}"
|
|
|
|
echo "${complete_url}"
|
|
)
|
|
|
|
# download_github_release_checksums_files [release-url-prefix] [name] [version] [output-dir] [filename]
|
|
#
|
|
# outputs path to the downloaded checksums related file
|
|
#
|
|
download_github_release_checksums_files() (
|
|
download_url="$1"
|
|
name="$2"
|
|
version="$3"
|
|
output_dir="$4"
|
|
filename="$5"
|
|
|
|
log_trace "download_github_release_checksums_files(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir}, filename=${filename})"
|
|
|
|
complete_filename="${name}_${version}_${filename}"
|
|
complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
|
|
output_path="${output_dir}/${complete_filename}"
|
|
|
|
http_download "${output_path}" "${complete_url}" "" || return 1
|
|
asset_file_exists "${output_path}" || return 1
|
|
|
|
log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
|
|
|
|
echo "${output_path}"
|
|
)
|
|
|
|
# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]
|
|
#
|
|
# outputs path to the downloaded checksums file
|
|
#
|
|
download_github_release_checksums() (
|
|
download_github_release_checksums_files "$@" "checksums.txt"
|
|
)
|
|
|
|
# github_release_checksums_sig_url [release-url-prefix] [name] [version]
|
|
#
|
|
# outputs the url to the release checksums signature file
|
|
#
|
|
github_release_checksums_sig_url() (
|
|
github_release_asset_url "$@" "checksums.txt.sig"
|
|
)
|
|
|
|
# github_release_checksums_cert_url [release-url-prefix] [name] [version]
|
|
#
|
|
# outputs the url to the release checksums certificate file
|
|
#
|
|
github_release_checksums_cert_url() (
|
|
github_release_asset_url "$@" "checksums.txt.pem"
|
|
)
|
|
|
|
# search_for_asset [checksums-file-path] [name] [os] [arch] [format]
|
|
#
|
|
# outputs name of the asset to download
|
|
#
|
|
search_for_asset() (
|
|
checksum_path="$1"
|
|
name="$2"
|
|
os="$3"
|
|
arch="$4"
|
|
format="$5"
|
|
|
|
log_trace "search_for_asset(checksum-path=${checksum_path}, name=${name}, os=${os}, arch=${arch}, format=${format})"
|
|
|
|
asset_glob="${name}_.*_${os}_${arch}.${format}"
|
|
output_path=$(grep -o "${asset_glob}" "${checksum_path}" || true)
|
|
|
|
log_trace "search_for_asset() returned '${output_path}'"
|
|
|
|
echo "${output_path}"
|
|
)
|
|
|
|
# uname_os
|
|
#
|
|
# outputs an adjusted os value
|
|
#
|
|
uname_os() (
|
|
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
case "$os" in
|
|
cygwin_nt*) os="windows" ;;
|
|
mingw*) os="windows" ;;
|
|
msys_nt*) os="windows" ;;
|
|
esac
|
|
|
|
uname_os_check "$os"
|
|
|
|
log_trace "uname_os() returned '${os}'"
|
|
|
|
echo "$os"
|
|
)
|
|
|
|
# uname_arch
|
|
#
|
|
# outputs an adjusted architecture value
|
|
#
|
|
uname_arch() (
|
|
arch=$(uname -m)
|
|
case $arch in
|
|
x86_64) arch="amd64" ;;
|
|
x86) arch="386" ;;
|
|
i686) arch="386" ;;
|
|
i386) arch="386" ;;
|
|
aarch64) arch="arm64" ;;
|
|
armv5*) arch="armv5" ;;
|
|
armv6*) arch="armv6" ;;
|
|
armv7*) arch="armv7" ;;
|
|
esac
|
|
|
|
uname_arch_check "${arch}"
|
|
|
|
log_trace "uname_arch() returned '${arch}'"
|
|
|
|
echo "${arch}"
|
|
)
|
|
|
|
# get_release_tag [owner] [repo] [tag]
|
|
#
|
|
# outputs tag string
|
|
#
|
|
get_release_tag() (
|
|
owner="$1"
|
|
repo="$2"
|
|
tag="$3"
|
|
|
|
log_trace "get_release_tag(owner=${owner}, repo=${repo}, tag=${tag})"
|
|
|
|
json=$(github_release_json "${owner}" "${repo}" "${tag}")
|
|
real_tag=$(github_release_tag "${json}")
|
|
if test -z "${real_tag}"; then
|
|
return 1
|
|
fi
|
|
|
|
log_trace "get_release_tag() returned '${real_tag}'"
|
|
|
|
echo "${real_tag}"
|
|
)
|
|
|
|
# tag_to_version [tag]
|
|
#
|
|
# outputs version string
|
|
#
|
|
tag_to_version() (
|
|
tag="$1"
|
|
value="${tag#v}"
|
|
|
|
log_trace "tag_to_version(tag=${tag}) returned '${value}'"
|
|
|
|
echo "$value"
|
|
)
|
|
|
|
# get_binary_name [os] [arch] [default-name]
|
|
#
|
|
# outputs a the binary string name
|
|
#
|
|
get_binary_name() (
|
|
os="$1"
|
|
arch="$2"
|
|
binary="$3"
|
|
original_binary="${binary}"
|
|
|
|
case "${os}" in
|
|
windows) binary="${binary}.exe" ;;
|
|
esac
|
|
|
|
log_trace "get_binary_name(os=${os}, arch=${arch}, binary=${original_binary}) returned '${binary}'"
|
|
|
|
echo "${binary}"
|
|
)
|
|
|
|
|
|
# get_format_name [os] [arch] [default-format]
|
|
#
|
|
# outputs an adjusted file format
|
|
#
|
|
get_format_name() (
|
|
os="$1"
|
|
arch="$2"
|
|
format="$3"
|
|
original_format="${format}"
|
|
|
|
case ${os} in
|
|
windows) format=zip ;;
|
|
esac
|
|
|
|
log_trace "get_format_name(os=${os}, arch=${arch}, format=${original_format}) returned '${format}'"
|
|
|
|
echo "${format}"
|
|
)
|
|
|
|
# download_and_install_asset [release-url-prefix] [download-path] [install-path] [name] [os] [arch] [version] [format] [binary]
|
|
#
|
|
# attempts to download the archive and install it to the given path.
|
|
#
|
|
download_and_install_asset() (
|
|
download_url="$1"
|
|
download_path="$2"
|
|
install_path=$3
|
|
name="$4"
|
|
os="$5"
|
|
arch="$6"
|
|
version="$7"
|
|
format="$8"
|
|
binary="$9"
|
|
|
|
if ! asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}"); then
|
|
log_err "could not download asset for os='${os}' arch='${arch}' format='${format}'"
|
|
return 1
|
|
fi
|
|
|
|
# don't continue if we couldn't download an asset
|
|
if [ -z "${asset_filepath}" ]; then
|
|
log_err "could not find release asset for os='${os}' arch='${arch}' format='${format}' "
|
|
return 1
|
|
fi
|
|
|
|
install_asset "${asset_filepath}" "${install_path}" "${binary}"
|
|
)
|
|
|
|
# verify_sign [checksums-file-path] [verification-material-flags...]
|
|
#
|
|
# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
|
|
# the verification material is either "--bundle <path>" or (for older releases) "--certificate <ref> --signature <ref>".
|
|
#
|
|
verify_sign() {
|
|
checksums_file=$1
|
|
shift
|
|
|
|
log_trace "verifying artifact ${checksums_file}"
|
|
|
|
log_file=$(mktemp)
|
|
|
|
${COSIGN_BINARY} \
|
|
verify-blob "$checksums_file" \
|
|
"$@" \
|
|
--certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
|
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
|
|
|
|
if [ $? -ne 0 ]; then
|
|
log_err "$(cat "${log_file}")"
|
|
# cosign older than v2.4.2 can't read the bundle's verification material and reports a missing cert
|
|
if [ "$1" = "--bundle" ] && grep -q "bundle does not contain cert" "${log_file}"; then
|
|
log_err "note: releases >= ${VERIFY_SIGN_BUNDLE_VERSION} are verified with a sigstore bundle, which requires cosign v2.5.0 or newer (check '${COSIGN_BINARY} version')"
|
|
fi
|
|
rm -f "${log_file}"
|
|
return 1
|
|
fi
|
|
|
|
rm -f "${log_file}"
|
|
}
|
|
|
|
|
|
# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary]
|
|
#
|
|
# outputs the path to the downloaded asset asset_filepath
|
|
#
|
|
download_asset() (
|
|
download_url="$1"
|
|
destination="$2"
|
|
name="$3"
|
|
os="$4"
|
|
arch="$5"
|
|
version="$6"
|
|
format="$7"
|
|
|
|
log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})"
|
|
|
|
if ! checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}"); then
|
|
log_err "unable to download checksums file"
|
|
return 1
|
|
fi
|
|
|
|
log_trace "checksums content:\n$(cat ${checksums_filepath})"
|
|
|
|
asset_filename=$(search_for_asset "${checksums_filepath}" "${name}" "${os}" "${arch}" "${format}")
|
|
|
|
# don't continue if we couldn't find a matching asset from the checksums file
|
|
if [ -z "${asset_filename}" ]; then
|
|
return 1
|
|
fi
|
|
|
|
if [ "$VERIFY_SIGN" = true ]; then
|
|
if compare_semver "${version}" "${VERIFY_SIGN_BUNDLE_VERSION}"; then
|
|
if ! checksums_bundle_filepath=$(download_github_release_checksums_files "${download_url}" "${name}" "${version}" "${destination}" "checksums.txt.sigstore.json"); then
|
|
log_err "unable to download checksums signature bundle"
|
|
return 1
|
|
fi
|
|
log_trace "checksums bundle: ${checksums_bundle_filepath}"
|
|
|
|
set -- --bundle "${checksums_bundle_filepath}"
|
|
else
|
|
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
|
|
log_trace "checksums signature url: ${checksum_sig_file_url}"
|
|
|
|
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
|
|
log_trace "checksums certificate url: ${checksums_cert_file_url}"
|
|
|
|
set -- --certificate "${checksums_cert_file_url}" --signature "${checksum_sig_file_url}"
|
|
fi
|
|
|
|
if ! verify_sign "${checksums_filepath}" "$@"; then
|
|
log_err "signature verification failed"
|
|
return 1
|
|
fi
|
|
log_info "signature verification succeeded"
|
|
fi
|
|
|
|
asset_url="${download_url}/${asset_filename}"
|
|
asset_filepath="${destination}/${asset_filename}"
|
|
if ! http_download "${asset_filepath}" "${asset_url}" ""; then
|
|
log_err "unable to download asset '${asset_url}'"
|
|
return 1
|
|
fi
|
|
|
|
# this is what ties the asset to the (optionally signature verified) checksums file, so a mismatch must stop the install
|
|
if ! hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"; then
|
|
return 1
|
|
fi
|
|
|
|
log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'"
|
|
|
|
echo "${asset_filepath}"
|
|
)
|
|
|
|
# install_asset [asset-path] [destination-path] [binary]
|
|
#
|
|
install_asset() (
|
|
asset_filepath="$1"
|
|
destination="$2"
|
|
binary="$3"
|
|
|
|
log_trace "install_asset(asset=${asset_filepath}, destination=${destination}, binary=${binary})"
|
|
|
|
# don't continue if we don't have anything to install
|
|
if [ -z "${asset_filepath}" ]; then
|
|
return
|
|
fi
|
|
|
|
archive_dir=$(dirname "${asset_filepath}")
|
|
|
|
# unarchive the downloaded archive to the temp dir
|
|
(cd "${archive_dir}" && unpack "${asset_filepath}")
|
|
|
|
# create the destination dir
|
|
test ! -d "${destination}" && install -d "${destination}"
|
|
|
|
# install the binary to the destination dir
|
|
install "${archive_dir}/${binary}" "${destination}/"
|
|
)
|
|
|
|
# compare two semver strings. Returns 0 if version1 >= version2, 1 otherwise.
|
|
# Note: pre-release (-) and metadata (+) are not supported.
|
|
compare_semver() {
|
|
# remove leading 'v' if present
|
|
version1=${1#v}
|
|
version2=${2#v}
|
|
|
|
IFS=. read -r major1 minor1 patch1 <<EOF
|
|
$version1
|
|
EOF
|
|
IFS=. read -r major2 minor2 patch2 <<EOF
|
|
$version2
|
|
EOF
|
|
|
|
if [ "$major1" -gt "$major2" ]; then
|
|
return 0
|
|
elif [ "$major1" -lt "$major2" ]; then
|
|
return 1
|
|
fi
|
|
|
|
if [ "$minor1" -gt "$minor2" ]; then
|
|
return 0
|
|
elif [ "$minor1" -lt "$minor2" ]; then
|
|
return 1
|
|
fi
|
|
|
|
if [ "$patch1" -gt "$patch2" ]; then
|
|
return 0
|
|
elif [ "$patch1" -lt "$patch2" ]; then
|
|
return 1
|
|
fi
|
|
|
|
# versions are equal
|
|
return 0
|
|
}
|
|
|
|
prep_signature_verification() {
|
|
version="$1"
|
|
|
|
if [ "$VERIFY_SIGN" != true ]; then
|
|
return 0
|
|
fi
|
|
|
|
# is there any cryptographic material produced at release that we can use for signature verification?
|
|
if ! compare_semver "$version" "$VERIFY_SIGN_SUPPORTED_VERSION"; then
|
|
log_err "${PROJECT_NAME} release '$version' does not support signature verification"
|
|
log_err "you can still install ${PROJECT_NAME} by removing the -v flag or using a release that supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')"
|
|
log_err "aborting installation"
|
|
return 1
|
|
else
|
|
log_trace "${PROJECT_NAME} release '$version' supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')"
|
|
fi
|
|
|
|
# will invoking an earlier version of this script work (considering the -v flag)?
|
|
if ! compare_semver "$version" "$VERIFY_SIGN_FLAG_VERSION"; then
|
|
# the -v argument did not always exist, so we cannot be guaranteed that invoking an earlier version of this script
|
|
# will work (error with "illegal option -v"). However, the user requested signature verification, so we will
|
|
# attempt to install the application with this version of the script (keeping signature verification).
|
|
DOWNLOAD_TAG_INSTALL_SCRIPT=false
|
|
log_debug "provided version install script does not support -v flag (>= '$VERIFY_SIGN_FLAG_VERSION'), using current script for installation"
|
|
else
|
|
log_trace "provided version install script supports -v flag (>= '$VERIFY_SIGN_FLAG_VERSION')"
|
|
fi
|
|
|
|
# check to see if the cosign binary is installed
|
|
if is_command "${COSIGN_BINARY}"; then
|
|
log_trace "${COSIGN_BINARY} binary is installed"
|
|
else
|
|
log_err "signature verification is requested but ${COSIGN_BINARY} binary is not installed (see https://docs.sigstore.dev/system_config/installation/ to install it)"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
main() (
|
|
# parse arguments
|
|
|
|
# note: never change default install directory (this must always be backwards compatible)
|
|
install_dir=${install_dir:-./bin}
|
|
|
|
# note: never change the program flags or arguments (this must always be backwards compatible)
|
|
while getopts "b:dvh?x" arg; do
|
|
case "$arg" in
|
|
b) install_dir="$OPTARG" ;;
|
|
d)
|
|
if [ "$_logp" = "$log_info_priority" ]; then
|
|
# -d == debug
|
|
log_set_priority $log_debug_priority
|
|
else
|
|
# -dd (or -ddd...) == trace
|
|
log_set_priority $log_trace_priority
|
|
fi
|
|
;;
|
|
v) VERIFY_SIGN=true;;
|
|
h | \?)
|
|
cat <<EOF
|
|
Download and install a released binary for ${OWNER}/${REPO} from the github releases page
|
|
|
|
Usage: $0 [-v] [-b DIR] [-d] [TAG]
|
|
-b DIR the installation directory (defaults to ./bin)
|
|
-d turns on debug logging
|
|
-dd turns on trace logging
|
|
-v verify checksum signature (requires cosign binary to be installed).
|
|
TAG the specific release to use (if missing, then the latest will be used)
|
|
EOF
|
|
exit 0
|
|
;;
|
|
x) set -x ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND - 1))
|
|
|
|
set +u
|
|
tag=$1
|
|
|
|
if [ -z "${tag}" ]; then
|
|
log_info "checking github for the current release tag"
|
|
tag=""
|
|
else
|
|
log_info "checking github for release tag='${tag}'"
|
|
fi
|
|
set -u
|
|
|
|
if ! tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}"); then
|
|
log_err "unable to find tag='${tag}'"
|
|
log_err "do not specify a version or select a valid version from https://github.com/${OWNER}/${REPO}/releases"
|
|
return 1
|
|
fi
|
|
|
|
# run the application
|
|
|
|
version=$(tag_to_version "${tag}")
|
|
os=$(uname_os)
|
|
arch=$(uname_arch)
|
|
format=$(get_format_name "${os}" "${arch}" "tar.gz")
|
|
binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}")
|
|
download_url="${GITHUB_DOWNLOAD_PREFIX}/${tag}"
|
|
|
|
if ! prep_signature_verification "$version"; then
|
|
return 1
|
|
fi
|
|
|
|
# we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not
|
|
# guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var
|
|
# to indicate if we should continue processing with the existing script or to download the script from the given tag.
|
|
if [ "${DOWNLOAD_TAG_INSTALL_SCRIPT}" = "true" ]; then
|
|
export DOWNLOAD_TAG_INSTALL_SCRIPT=false
|
|
log_info "fetching release script for tag='${tag}'"
|
|
if ! install_script=$(http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" ""); then
|
|
log_warn "failed to fetch from ${INSTALL_SH_BASE_URL}, trying fallback URL"
|
|
install_script=$(http_copy "${LEGACY_INSTALL_SH_BASE_URL}/${tag}/install.sh" "")
|
|
fi
|
|
echo "${install_script}" | sh -s -- ${PROGRAM_ARGS}
|
|
exit $?
|
|
fi
|
|
|
|
log_info "using release tag='${tag}' version='${version}' os='${os}' arch='${arch}'"
|
|
|
|
download_dir=$(mktemp -d)
|
|
trap 'rm -rf -- "$download_dir"' EXIT
|
|
|
|
log_debug "downloading files into ${download_dir}"
|
|
|
|
# don't continue if we couldn't install the asset
|
|
if ! download_and_install_asset "${download_url}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}"; then
|
|
log_err "failed to install ${PROJECT_NAME}"
|
|
return 1
|
|
fi
|
|
|
|
log_info "installed ${install_dir}/${binary}"
|
|
)
|
|
|
|
# entrypoint
|
|
|
|
set +u
|
|
if [ -z "${TEST_INSTALL_SH}" ]; then
|
|
set -u
|
|
main "$@"
|
|
exit $?
|
|
fi
|
|
set -u
|