#!/bin/sh # # jwtd installer for macOS and Linux. # # Downloads the release archive matching the detected OS and architecture, # verifies it against the release's checksums.txt (and, when cosign is # installed, verifies the keyless signature over that checksum file), then # installs the binary into ~/.local/bin. No root privileges are required. # # curl -fsSL https://jwtd.sh/install.sh | sh # curl -fsSL https://jwtd.sh/install.sh | sh -s -- --version v5.3.0 # curl -fsSL https://jwtd.sh/install.sh | sh -s -- --dir /usr/local/bin # # Windows is served by install.ps1 instead; see the README. set -eu REPO="webcodr/jwtd" CERTIFICATE_IDENTITY_REGEXP="^https://github.com/webcodr/jwtd/\.github/workflows/release\.yml@" CERTIFICATE_OIDC_ISSUER="https://token.actions.githubusercontent.com" info() { printf '%s\n' "$*" >&2 } warn() { printf 'warning: %s\n' "$*" >&2 } die() { printf 'error: %s\n' "$*" >&2 exit 1 } have() { command -v "$1" >/dev/null 2>&1 } usage() { cat <<'EOF' Install jwtd, a CLI that decodes and pretty-prints JWT, JWS, and JWE tokens. Usage: install.sh [--version ] [--dir ] Options: -v, --version Release to install (default: the latest release). Accepts "5.3.0" or "v5.3.0". -d, --dir Installation directory (default: ~/.local/bin). -h, --help Show this help. Environment: JWTD_VERSION Same as --version. JWTD_INSTALL_DIR Same as --dir. The archive is always verified against the release's checksums.txt. When cosign is installed, the keyless Cosign bundle over checksums.txt is verified as well. EOF } # detect_os and detect_arch map uname output onto the GOOS/GOARCH pair used in # the release archive names (jwtd--.tar.gz). detect_os() { kernel=$(uname -s) case "$kernel" in Linux) printf 'linux\n' ;; Darwin) printf 'darwin\n' ;; *) die "unsupported operating system: $kernel (this script installs on Linux and macOS; on Windows run 'irm https://jwtd.sh/install.ps1 | iex' in PowerShell)" ;; esac } detect_arch() { machine=$(uname -m) case "$machine" in x86_64 | amd64) printf 'amd64\n' ;; aarch64 | arm64) printf 'arm64\n' ;; *) die "unsupported architecture: $machine (release binaries are built for amd64 and arm64)" ;; esac } # Under Rosetta 2 a translated shell reports x86_64, which would install the # Intel binary on Apple silicon. sysctl.proc_translated is set only in that # case, so it distinguishes translation from a genuine Intel Mac. correct_rosetta_arch() { if [ "$1" = "darwin" ] && [ "$2" = "amd64" ] && have sysctl && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || printf '0\n')" = "1" ]; then printf 'arm64\n' else printf '%s\n' "$2" fi } download() { url=$1 destination=$2 if have curl; then curl -fsSL --proto '=https' --tlsv1.2 -o "$destination" "$url" || die "could not download $url (check the release tag and your network connection)" elif have wget; then wget -q --https-only -O "$destination" "$url" || die "could not download $url (check the release tag and your network connection)" else die "neither curl nor wget is available; install one of them and re-run" fi } # verify_checksum matches the archive against its checksums.txt entry. The # entry is selected by exact file name and written out verbatim so the # checksum tool sees the original " " formatting. verify_checksum() { archive=$1 if ! awk -v want="$archive" '$2 == want { print $0; found = 1 } END { exit !found }' \ checksums.txt >"$archive.sha256"; then die "checksums.txt has no entry for $archive" fi if have sha256sum; then sha256sum -c "$archive.sha256" >/dev/null || die "checksum mismatch for $archive; refusing to install" elif have shasum; then shasum -a 256 -c "$archive.sha256" >/dev/null || die "checksum mismatch for $archive; refusing to install" else die "neither sha256sum nor shasum is available; cannot verify the download" fi info "Checksum verified: $archive" } # verify_signature is best-effort by design: cosign is not a dependency most # machines have, and the checksum above already pins the archive bytes. When # cosign is present the bundle is verified and a failure is fatal. verify_signature() { base_url=$1 if ! have cosign; then info "cosign not found - skipping signature verification (install cosign to verify the release signature)" return fi download "$base_url/checksums.txt.sigstore.json" checksums.txt.sigstore.json cosign verify-blob \ --bundle checksums.txt.sigstore.json \ --certificate-identity-regexp "$CERTIFICATE_IDENTITY_REGEXP" \ --certificate-oidc-issuer "$CERTIFICATE_OIDC_ISSUER" \ checksums.txt >/dev/null 2>&1 || die "cosign could not verify checksums.txt against the jwtd release workflow; refusing to install" info "Signature verified: checksums.txt (cosign, keyless)" } # report_path_hint keeps the installer honest about the one thing it cannot do # for the user: ~/.local/bin is not on every PATH by default. report_path_hint() { directory=$1 case ":$PATH:" in *":$directory:"*) return ;; esac warn "$directory is not on your PATH. Add it with one of:" # $PATH stays literal here: the hint is a command for the user to run. # shellcheck disable=SC2016 printf ' bash/zsh: echo '\''export PATH="%s:$PATH"'\'' >> ~/.profile\n' "$directory" >&2 printf ' fish: fish_add_path %s\n' "$directory" >&2 } version=${JWTD_VERSION-} install_dir=${JWTD_INSTALL_DIR-} while [ $# -gt 0 ]; do case "$1" in -v | --version) [ $# -ge 2 ] || die "--version requires a release tag" version=$2 shift 2 ;; -d | --dir) [ $# -ge 2 ] || die "--dir requires a path" install_dir=$2 shift 2 ;; -h | --help) usage exit 0 ;; *) die "unknown option: $1 (run with --help for usage)" ;; esac done [ -n "$install_dir" ] || install_dir="$HOME/.local/bin" # The download happens from a temporary working directory, so a relative --dir # has to be anchored to the caller's directory before that cd. case "$install_dir" in /*) ;; *) install_dir="$PWD/$install_dir" ;; esac os=$(detect_os) arch=$(detect_arch) arch=$(correct_rosetta_arch "$os" "$arch") archive="jwtd-$os-$arch.tar.gz" if [ -n "$version" ]; then case "$version" in v*) ;; *) version="v$version" ;; esac base_url="https://github.com/$REPO/releases/download/$version" info "Installing jwtd $version ($os/$arch)" else base_url="https://github.com/$REPO/releases/latest/download" info "Installing the latest jwtd release ($os/$arch)" fi work_dir=$(mktemp -d 2>/dev/null || mktemp -d -t jwtd-install) staged="" trap 'rm -rf "$work_dir"; [ -z "$staged" ] || rm -f "$staged"' EXIT INT HUP TERM cd "$work_dir" download "$base_url/$archive" "$archive" download "$base_url/checksums.txt" checksums.txt verify_checksum "$archive" verify_signature "$base_url" tar -xzf "$archive" jwtd [ -f jwtd ] || die "the release archive did not contain a jwtd binary" mkdir -p "$install_dir" || die "could not create $install_dir" # Copy into the target directory first, then rename within it. A cross-device # mv would rewrite the destination in place, which fails with ETXTBSY when the # running shell's jwtd is being upgraded; rename(2) inside one filesystem # replaces the old binary atomically instead. installed="$install_dir/jwtd" staged="$install_dir/.jwtd.install.$$" cp jwtd "$staged" || die "could not write to $install_dir (choose another directory with --dir)" chmod 0755 "$staged" mv -f "$staged" "$installed" || die "could not install into $install_dir (choose another directory with --dir)" staged="" info "Installed $("$installed" --version 2>/dev/null || printf 'jwtd\n') to $installed" report_path_hint "$install_dir"