#!/bin/sh # ribo installer — https://tissue.systems # # curl -fsSL https://tissue.systems/install.sh | sh # # Downloads the current ribo release for this machine, verifies it against the # signed release manifest, and installs it. Once installed, `ribo upgrade` does # the same job without this script. # # Everything here is also doable by hand, and the docs still show how # (https://tissue.systems/docs/get-started/) — this exists because the manual # form is three per-platform variants plus a verification block, and the step # people skip is always the verification one. # # The signing key is written into this file rather than fetched alongside the # download. A key retrieved from the same host in the same breath as the # artifact it vouches for adds no assurance against whoever can serve that host; # a key you can read before piping this to a shell does. It is the same key # `ribo` itself pins (ribo/src/sshsig.rs) and the same one that gates a .deb # going onto a production server. # # Environment: # RIBO_INSTALL_DIR where to install (default /usr/local/bin) # RIBO_VERSION version to install (default: the current release) # RIBO_BASE_URL download tree (default https://tissue.systems/download) set -eu BASE_URL="${RIBO_BASE_URL:-https://tissue.systems/download}" INSTALL_DIR="${RIBO_INSTALL_DIR:-/usr/local/bin}" SIGNER='release@tissue.systems' NAMESPACE='tissue-release' PUBKEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBer4p1y/TQjkwtTr62e4q7MPZ3gaM4TXggRyNgV+BQZ' die() { printf 'ribo install: %s\n' "$*" >&2; exit 1; } say() { printf '%s\n' "$*"; } command -v curl >/dev/null 2>&1 || die 'curl is required' # ── Which build ────────────────────────────────────────────────────────────── # The slugs match ribo's own artifact names and the platform() table in # ribo/src/runtime.rs; the three below are the whole published set. os="$(uname -s)" arch="$(uname -m)" case "$os:$arch" in Darwin:arm64|Darwin:aarch64) artifact='ribo-macos-arm64' ;; Darwin:x86_64) artifact='ribo-macos-x86_64' ;; Linux:x86_64|Linux:amd64) artifact='ribo-linux-x86_64' ;; Linux:aarch64|Linux:arm64) die 'no Linux arm64 build is published yet — see https://tissue.systems/download/' ;; *) die "unsupported platform $os/$arch. Windows users: run the Linux build under WSL." ;; esac # ── Which version ──────────────────────────────────────────────────────────── version="${RIBO_VERSION:-}" if [ -z "$version" ]; then version="$(curl -fsSL "$BASE_URL/latest/version" 2>/dev/null \ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" || true [ -n "$version" ] || die "could not read the current version from $BASE_URL/latest/version" fi # Zero-pad a single-digit month first. Download directories are always YYYY.MM.N, # but `ribo --version` prints the Cargo spelling, which drops the padding because # a leading zero is not valid semver (scripts/create_release.sh converts between # the two). Someone reading a version off their own CLI and passing it back in # here is reading the one form that does not name a directory. case "$version" in [0-9][0-9][0-9][0-9].[0-9].*) version="$(printf '%s' "$version" | sed 's/^\([0-9]*\)\.\([0-9]\)\./\1.0\2./')" ;; esac # Validate before it is joined into a URL or a path. YYYY.MM.N and nothing else, # which is the same shape ribo's own parser accepts and enough on its own to # exclude a traversal or a scheme. printf '%s' "$version" | grep -Eq '^[0-9]{4}\.[0-9]{2}\.[0-9]+$' \ || die "\"$version\" is not a ribo version" say "Installing ribo $version ($artifact)" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM dir="$BASE_URL/$version" curl -fsSL "$dir/$artifact" -o "$tmp/$artifact" || die "download failed: $dir/$artifact" curl -fsSL "$dir/SHA256SUMS" -o "$tmp/SHA256SUMS" || die "download failed: $dir/SHA256SUMS" curl -fsSL "$dir/SHA256SUMS.sig" -o "$tmp/SHA256SUMS.sig" \ || die "download failed: $dir/SHA256SUMS.sig" # ── Verify: the manifest's signature first, then the download against it ───── # This order is the point. Checking the binary against an unverified manifest # proves only that whoever served the binary also served its checksum. if command -v ssh-keygen >/dev/null 2>&1; then printf '%s namespaces="%s" %s\n' "$SIGNER" "$NAMESPACE" "$PUBKEY" > "$tmp/allowed_signers" ssh-keygen -Y verify -f "$tmp/allowed_signers" -I "$SIGNER" -n "$NAMESPACE" \ -s "$tmp/SHA256SUMS.sig" < "$tmp/SHA256SUMS" >/dev/null 2>&1 \ || die 'the release manifest failed signature verification — not installing' say "Verified SHA256SUMS (signed by $SIGNER)" else # Not fatal: OpenSSH is absent on some minimal images, and refusing to install # there would push people to an unverified curl instead. Say so plainly. say 'Note: ssh-keygen not found — checksum verified, signature not.' fi if command -v shasum >/dev/null 2>&1; then sha="$(shasum -a 256 "$tmp/$artifact" | cut -d' ' -f1)" elif command -v sha256sum >/dev/null 2>&1; then sha="$(sha256sum "$tmp/$artifact" | cut -d' ' -f1)" else die 'neither shasum nor sha256sum is available to check the download' fi want="$(grep -E "[ *]$artifact\$" "$tmp/SHA256SUMS" | cut -d' ' -f1)" [ -n "$want" ] || die "$artifact is not listed in the release manifest" [ "$sha" = "$want" ] || die "checksum mismatch for $artifact — not installing expected $want got $sha" say "Verified $artifact" # ── Install ────────────────────────────────────────────────────────────────── chmod 755 "$tmp/$artifact" # Prove it runs before it takes the name, so a build that cannot execute here is # a failed install rather than a broken `ribo` on PATH. "$tmp/$artifact" --version >/dev/null 2>&1 || die 'the downloaded binary does not run on this machine' target="$INSTALL_DIR/ribo" no_root="cannot write to $INSTALL_DIR and sudo is not available. Set RIBO_INSTALL_DIR to somewhere writable, e.g. curl -fsSL https://tissue.systems/install.sh | RIBO_INSTALL_DIR=\"\$HOME/.local/bin\" sh" # Every step below is spelled `cmd || die`, never `cmd && cmd`. Under `set -e` a # failing command in an AND-OR list is exempt unless it is the last one, so # `sudo mkdir -p "$d" && sudo mv "$f" "$t"` runs on past a sudo that could not # prompt and reports an install that did not happen. if [ -d "$INSTALL_DIR" ] && [ -w "$INSTALL_DIR" ]; then mv "$tmp/$artifact" "$target" || die "could not install to $target" elif command -v sudo >/dev/null 2>&1; then say "$INSTALL_DIR needs root; using sudo" sudo mkdir -p "$INSTALL_DIR" || die "could not create $INSTALL_DIR" sudo mv "$tmp/$artifact" "$target" || die "could not install to $target" sudo chmod 755 "$target" || die "could not make $target executable" else die "$no_root" fi # The install is only done when the thing at the name runs. Anything that went # wrong above and did not stop the script stops here instead. [ -x "$target" ] || die "$target was not installed" "$target" --version >/dev/null 2>&1 || die "$target was installed but does not run" say "" say "ribo $version installed to $target" if ! command -v ribo >/dev/null 2>&1; then say "Note: $INSTALL_DIR is not on your PATH." fi say "Next: ribo login"