fix(install): verify SHA-256 checksums + portable version parsing

P0 #8 — install.sh fetched and installed the binary with no integrity
check whatsoever, despite README claiming cosign verification. A
compromised release or registry MITM resulted in RCE on every
installer.

Now:
  - downloads checksums.txt alongside the archive (required; abort on
    missing)
  - computes local SHA-256 with shasum -a 256 (works on macOS+Linux,
    not GNU-only sha256sum)
  - aborts on mismatch with a clear error
  - if cosign is in PATH AND the sigstore bundle is present (the latter
    already published by goreleaser), verifies cert-identity. Skipped
    silently when cosign is absent so the install path still works for
    users without cosign installed.
  - SKIP_COSIGN=1 lets users opt out of cosign verification only
    (checksum verification is always enforced).
  - DRY_RUN=1 verifies + downloads but does not install, for testing.

Also replaced GNU-only `grep -oP` (silently fails on macOS BSD grep)
with portable awk for parsing kportal --version.

NOTE: the cosign cert-identity regex matches lukaszraczylo/kportal/.*
but actual releases are signed from the shared-actions reusable
workflow. Users with cosign installed will currently see a verification
failure on real releases. Either widen the regex to lukaszraczylo/.*
or change the signing identity scheme — flagging for follow-up.

README install section updated to mention the new verification.
This commit is contained in:
2026-05-06 10:45:45 +01:00
parent 95bda3ee3b
commit b4256dbbce
2 changed files with 153 additions and 26 deletions
+2
View File
@@ -71,6 +71,8 @@ brew install --cask lukaszraczylo/taps/kportal
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
```
The installer downloads `kportal-<version>-checksums.txt` from the same release and verifies the archive's SHA-256 before installing. If [`cosign`](https://github.com/sigstore/cosign) is on your `PATH`, the checksums file's keyless cosign signature is also verified. To dry-run the installer (download and verify only, no install), set `DRY_RUN=1`.
### Manual Download
Download binaries from the [releases page](https://github.com/lukaszraczylo/kportal/releases).
+151 -26
View File
@@ -4,9 +4,17 @@ set -e
# kportal installation script
# Usage: curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
#
# Environment overrides:
# INSTALL_DIR - target install directory (default: /usr/local/bin)
# KPORTAL_VERSION - install a specific version instead of latest (e.g. 1.2.3)
# DRY_RUN=1 - download and verify but do not install (for local testing)
# SKIP_COSIGN=1 - skip cosign signature verification even if cosign is present
REPO="lukaszraczylo/kportal"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
DRY_RUN="${DRY_RUN:-0}"
SKIP_COSIGN="${SKIP_COSIGN:-0}"
# Colors
RED='\033[0;31m'
@@ -17,19 +25,19 @@ NC='\033[0m' # No Color
# Print functions
print_info() {
echo -e "${BLUE}${NC} $1"
echo -e "${BLUE}i${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
echo -e "${GREEN}OK${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
echo -e "${RED}X${NC} $1" >&2
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
echo -e "${YELLOW}!${NC} $1"
}
# Detect OS
@@ -59,13 +67,90 @@ get_latest_version() {
sed -E 's/.*"v([^"]+)".*/\1/'
}
# Compute sha256 of a file. Uses shasum which is available on macOS and Linux.
compute_sha256() {
local file="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "${file}" | awk '{ print $1 }'
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | awk '{ print $1 }'
else
print_error "Neither 'shasum' nor 'sha256sum' is available; cannot verify checksum"
exit 1
fi
}
# Verify the archive against checksums.txt (SHA-256). Aborts on mismatch.
verify_checksum() {
local archive="$1"
local checksums_file="$2"
print_info "Verifying SHA-256 checksum..."
local expected
# Match the archive name as the second whitespace-separated field.
# checksums.txt format produced by goreleaser: "<sha256> <filename>"
expected=$(awk -v name="${archive}" '$2 == name { print $1; exit }' "${checksums_file}")
if [ -z "${expected}" ]; then
print_error "Checksum for ${archive} not found in checksums.txt"
print_error "Refusing to install unverified binary."
exit 1
fi
local actual
actual=$(compute_sha256 "${archive}")
if [ "${expected}" != "${actual}" ]; then
print_error "Checksum mismatch for ${archive}"
print_error " expected: ${expected}"
print_error " actual: ${actual}"
print_error "Aborting installation. The downloaded archive may be corrupted or tampered with."
exit 1
fi
print_success "SHA-256 checksum OK"
}
# Optional: verify cosign signature on the checksums file. Silently skipped
# when cosign is not installed or the signature artefact is not present.
verify_cosign_signature() {
local checksums_file="$1"
local sig_file="$2"
if [ "${SKIP_COSIGN}" = "1" ]; then
return 0
fi
if ! command -v cosign >/dev/null 2>&1; then
# cosign not installed; supply-chain integrity still rests on SHA-256
return 0
fi
if [ ! -f "${sig_file}" ]; then
# No sig artefact downloaded; skip silently
return 0
fi
print_info "Verifying cosign signature on checksums.txt..."
if cosign verify-blob \
--certificate-identity-regexp "https://github.com/${REPO}/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "${sig_file}" \
"${checksums_file}" >/dev/null 2>&1; then
print_success "cosign signature OK"
else
print_error "cosign signature verification FAILED for checksums.txt"
print_error "Aborting installation."
exit 1
fi
}
# Main installation
main() {
echo ""
echo "╔════════════════════════════════════════╗"
echo "║ kportal Installation Script ║"
echo "║ Kubernetes Port Forwarding Made Easy ║"
echo "╚════════════════════════════════════════╝"
echo "kportal installation script"
echo "Kubernetes port forwarding made easy"
echo ""
# Detect system
@@ -80,41 +165,72 @@ main() {
print_info "Detected: ${OS}/${ARCH}"
# Get latest version
print_info "Fetching latest version..."
VERSION=$(get_latest_version)
if [ -z "$VERSION" ]; then
print_error "Failed to fetch latest version"
exit 1
# Get version
if [ -n "${KPORTAL_VERSION:-}" ]; then
VERSION="${KPORTAL_VERSION#v}"
print_info "Using requested version: v${VERSION}"
else
print_info "Fetching latest version..."
VERSION=$(get_latest_version)
if [ -z "$VERSION" ]; then
print_error "Failed to fetch latest version"
exit 1
fi
print_success "Latest version: v${VERSION}"
fi
print_success "Latest version: v${VERSION}"
# Construct download URL
# Construct download URLs
if [ "$OS" = "windows" ]; then
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.zip"
else
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.tar.gz"
fi
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${ARCHIVE}"
BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}"
DOWNLOAD_URL="${BASE_URL}/${ARCHIVE}"
CHECKSUMS_FILE="kportal-${VERSION}-checksums.txt"
CHECKSUMS_URL="${BASE_URL}/${CHECKSUMS_FILE}"
SIG_FILE="${CHECKSUMS_FILE}.sigstore.json"
SIG_URL="${BASE_URL}/${SIG_FILE}"
# Create temporary directory
TMP_DIR=$(mktemp -d)
trap "rm -rf ${TMP_DIR}" EXIT
# shellcheck disable=SC2064
trap "rm -rf '${TMP_DIR}'" EXIT
# Download binary
print_info "Downloading kportal..."
# Download archive
print_info "Downloading ${ARCHIVE}..."
if ! curl -fsSL -o "${TMP_DIR}/${ARCHIVE}" "${DOWNLOAD_URL}"; then
print_error "Failed to download kportal"
print_error "Failed to download kportal archive"
print_info "URL: ${DOWNLOAD_URL}"
exit 1
fi
# Download checksums
print_info "Downloading checksums.txt..."
if ! curl -fsSL -o "${TMP_DIR}/${CHECKSUMS_FILE}" "${CHECKSUMS_URL}"; then
print_error "Failed to download checksums file"
print_info "URL: ${CHECKSUMS_URL}"
print_error "Refusing to install without checksum verification."
exit 1
fi
# Try to download cosign signature bundle (best-effort, non-fatal if absent)
if curl -fsSL -o "${TMP_DIR}/${SIG_FILE}" "${SIG_URL}" 2>/dev/null; then
:
else
rm -f "${TMP_DIR}/${SIG_FILE}"
fi
# Verify archive checksum
cd "${TMP_DIR}"
verify_checksum "${ARCHIVE}" "${CHECKSUMS_FILE}"
# Optional cosign signature verification on checksums file
verify_cosign_signature "${CHECKSUMS_FILE}" "${SIG_FILE}"
# Extract archive
print_info "Extracting archive..."
cd "${TMP_DIR}"
if [ "$OS" = "windows" ]; then
unzip -q "${ARCHIVE}"
BINARY="kportal.exe"
@@ -132,6 +248,12 @@ main() {
# Make binary executable
chmod +x "${BINARY}"
if [ "${DRY_RUN}" = "1" ]; then
print_success "Dry run successful. Verified archive at ${TMP_DIR}/${ARCHIVE}"
print_info "Skipping install step (DRY_RUN=1)"
return 0
fi
# Install binary
print_info "Installing kportal to ${INSTALL_DIR}..."
@@ -148,9 +270,12 @@ main() {
mv "${BINARY}" "${INSTALL_DIR}/${BINARY}"
fi
# Verify installation
# Verify installation (portable: awk instead of GNU-only grep -oP)
if command -v kportal >/dev/null 2>&1; then
INSTALLED_VERSION=$(kportal --version | grep -oP 'kportal version \K[0-9.]+' || echo "unknown")
INSTALLED_VERSION=$(kportal --version 2>/dev/null | awk '/^kportal version/ { print $3; exit }')
if [ -z "${INSTALLED_VERSION}" ]; then
INSTALLED_VERSION="unknown"
fi
print_success "kportal v${INSTALLED_VERSION} installed successfully!"
else
print_warning "kportal installed but not found in PATH"