#!/bin/sh # Wirken installer # Usage: curl -fsSL https://raw.githubusercontent.com/gebruder/wirken/main/install.sh | sh # # Exit codes: # 1 generic failure (unsupported OS/arch, download failure, etc.) # 2 could not fetch checksums.sha256 # 3 target binary not listed in checksums.sha256 # 4 neither sha256sum nor shasum is installed # 5 signature missing, invalid, or ssh-keygen unavailable # # Escape hatch: # WIRKEN_ALLOW_UNVERIFIED=1 proceed despite checksum or signature failure. # Warns once. Use only for disaster recovery. # # Never binds services to 0.0.0.0. Wirken binds local endpoints to 127.0.0.1. set -eu REPO="gebruder/wirken" INSTALL_DIR="${WIRKEN_INSTALL_DIR:-$HOME/.local/bin}" # -------------------------------------------------------------------------- # Allowed signers for ssh-keygen -Y verify. The public key is embedded in # this script so verification does not depend on a network fetch of KEYS. # REPLACE: paste the public key line from KEYS after offline key generation. # -------------------------------------------------------------------------- ALLOWED_SIGNERS='releases@gebruder.ottenheimer.app ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPcGuqLSfpN+ibltPVbif7i9LgclLtRIaL+1TuYp+RjV releases@gebruder.ottenheimer.app' SIGNER_IDENTITY="releases@gebruder.ottenheimer.app" UNVERIFIED_WARNED=0 main() { detect_platform get_latest_version download_binary install_binary verify } detect_platform() { OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) OS_NAME="unknown-linux-musl" ;; Darwin) OS_NAME="apple-darwin" ;; *) echo "Error: unsupported OS: $OS" >&2 exit 1 ;; esac case "$ARCH" in x86_64|amd64) ARCH_NAME="x86_64" ;; aarch64|arm64) ARCH_NAME="aarch64" ;; *) echo "Error: unsupported architecture: $ARCH" >&2 exit 1 ;; esac TARGET="${ARCH_NAME}-${OS_NAME}" echo "Platform: ${OS} ${ARCH} -> ${TARGET}" } get_latest_version() { VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' \ | head -1 \ | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') if [ -z "$VERSION" ]; then echo "Error: could not determine latest version" >&2 exit 1 fi echo "Version: ${VERSION}" } # Handle a verification failure. Honors the WIRKEN_ALLOW_UNVERIFIED escape # hatch. Exits with $1 unless the escape hatch is set, in which case it # warns once and returns 0. verification_failure() { code="$1" message="$2" if [ "${WIRKEN_ALLOW_UNVERIFIED:-0}" = "1" ]; then if [ "$UNVERIFIED_WARNED" -eq 0 ]; then echo "WARNING: WIRKEN_ALLOW_UNVERIFIED=1 set. Proceeding without verification." >&2 echo "WARNING: You are running an unverified binary. Do not do this in production." >&2 UNVERIFIED_WARNED=1 fi echo "WARNING: ${message}" >&2 return 0 fi echo "Error: ${message}" >&2 echo "Hint: set WIRKEN_ALLOW_UNVERIFIED=1 to override (not recommended)." >&2 rm -rf "${TMPDIR:-/nonexistent}" exit "$code" } download_binary() { BINARY_NAME="wirken-${TARGET}" URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}" CHECKSUM_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.sha256" SIG_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.sha256.sig" TMPDIR=$(mktemp -d) TMPFILE="${TMPDIR}/wirken" CHECKSUM_FILE="${TMPDIR}/checksums.sha256" SIG_FILE="${TMPDIR}/checksums.sha256.sig" SIGNERS_FILE="${TMPDIR}/allowed_signers" echo "Downloading ${URL}" curl -fsSL -o "$TMPFILE" "$URL" verify_signature verify_checksum chmod +x "$TMPFILE" } # Fetch checksums.sha256.sig and verify with ssh-keygen -Y against the # embedded allowed_signers entry. Exit 5 on failure unless the escape # hatch is set. verify_signature() { # Fetch signature first; an unavailable sig is a hard failure. if ! curl -fsSL -o "$SIG_FILE" "$SIG_URL"; then verification_failure 5 "could not fetch signature file ${SIG_URL}" return 0 fi # We also need checksums.sha256 to verify the signature over. if ! curl -fsSL -o "$CHECKSUM_FILE" "$CHECKSUM_URL"; then verification_failure 2 "could not fetch ${CHECKSUM_URL}" return 0 fi if ! command -v ssh-keygen >/dev/null 2>&1; then verification_failure 5 "ssh-keygen not found; install OpenSSH 8.1+ to verify release signatures" return 0 fi # Reject the placeholder public key rather than silently accepting. case "$ALLOWED_SIGNERS" in *REPLACE_ME_WITH_PUBLIC_KEY*) verification_failure 5 "installer has placeholder signing key; cannot verify" return 0 ;; esac printf '%s\n' "$ALLOWED_SIGNERS" > "$SIGNERS_FILE" if ! ssh-keygen -Y verify \ -f "$SIGNERS_FILE" \ -I "$SIGNER_IDENTITY" \ -n file \ -s "$SIG_FILE" \ < "$CHECKSUM_FILE" >/dev/null 2>&1; then verification_failure 5 "signature on checksums.sha256 did not verify against release signing key" return 0 fi echo "Signature verified: ${SIGNER_IDENTITY}" } # Verify the binary's SHA-256 against the signed checksums file. All # failure paths are hard errors unless the escape hatch is set. verify_checksum() { # Re-fetch only if verify_signature did not already. if [ ! -s "$CHECKSUM_FILE" ]; then if ! curl -fsSL -o "$CHECKSUM_FILE" "$CHECKSUM_URL"; then verification_failure 2 "could not fetch ${CHECKSUM_URL}" return 0 fi fi EXPECTED=$(grep " ${BINARY_NAME}\$" "$CHECKSUM_FILE" | awk '{print $1}') if [ -z "$EXPECTED" ]; then verification_failure 3 "${BINARY_NAME} not listed in checksums.sha256" return 0 fi if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$TMPFILE" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$TMPFILE" | awk '{print $1}') else verification_failure 4 "neither sha256sum nor shasum found; install coreutils or perl-shasum" return 0 fi if [ "$EXPECTED" != "$ACTUAL" ]; then verification_failure 1 "checksum mismatch: expected ${EXPECTED}, got ${ACTUAL}" return 0 fi echo "Checksum verified: ${EXPECTED}" } install_binary() { mkdir -p "$INSTALL_DIR" 2>/dev/null || true EXISTING="${INSTALL_DIR}/wirken" if [ -x "$EXISTING" ]; then CURRENT=$("$EXISTING" --version 2>/dev/null | awk '{print $NF}') TAG_VERSION=$(echo "$VERSION" | sed 's/^v//') if [ "$CURRENT" = "$TAG_VERSION" ]; then echo "wirken ${CURRENT} is already installed." rm -rf "$TMPDIR" return fi echo "Upgrading wirken ${CURRENT} -> ${TAG_VERSION}" fi if [ ! -w "$INSTALL_DIR" ]; then echo "Error: ${INSTALL_DIR} is not writable." >&2 echo "Set WIRKEN_INSTALL_DIR to a writable directory, or make ${INSTALL_DIR} writable." >&2 rm -rf "$TMPDIR" exit 1 fi mv "$TMPFILE" "${INSTALL_DIR}/wirken" echo "Installed to ${INSTALL_DIR}/wirken" rm -rf "$TMPDIR" case ":$PATH:" in *":${INSTALL_DIR}:"*) ;; *) echo "" echo "Add to your PATH:" echo " export PATH=\"${INSTALL_DIR}:\$PATH\"" echo "" ;; esac } verify() { # Report the binary we just installed, not whatever `wirken` resolves # to on PATH. A user installing to a non-PATH directory may have an # older wirken elsewhere on PATH; querying PATH first misreports the # installed version and makes a successful install look like a no-op. if [ -x "${INSTALL_DIR}/wirken" ]; then echo "" "${INSTALL_DIR}/wirken" --version echo "" echo "Run 'wirken setup' to get started." elif command -v wirken >/dev/null 2>&1; then echo "" wirken --version echo "" echo "Run 'wirken setup' to get started." fi } # Test seam: sourcing this script with WIRKEN_INSTALLER_SOURCE_ONLY=1 # defines all functions but does not run main. Used by scripts/test-install.sh. if [ "${WIRKEN_INSTALLER_SOURCE_ONLY:-0}" != "1" ]; then main fi