diff --git a/scripts/install-core.sh b/scripts/install-core.sh index 841f6d5..1d38714 100755 --- a/scripts/install-core.sh +++ b/scripts/install-core.sh @@ -1,10 +1,27 @@ #!/usr/bin/env bash set -e -# Install the Core CLI -# Either downloads a pre-built binary or builds from source +# Install the Core CLI (Unix/Linux/macOS) +# Run: ./scripts/install-core.sh +# +# REQUIREMENTS: +# - curl or wget +# - sha256sum (Linux) or shasum (macOS) +# - git and go (for building from source) +# +# SECURITY CONTROLS: +# - Version pinning prevents supply chain attacks via tag manipulation +# - SHA256 hash verification ensures binary integrity +# - Secure temp file creation prevents symlink attacks +# - Symlink detection prevents directory traversal attacks +# - GPG signature verification (when available) ensures code authenticity +# +# KNOWN LIMITATIONS: +# - Checksums are fetched from same origin as binaries (consider separate trust root) +# - No TLS certificate pinning (relies on system CA store) -REPO="Snider/Core" +REPO="host-uk/core" +VERSION="v0.1.0" # Pinned version - update when releasing new versions INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-auto}" @@ -38,49 +55,195 @@ detect_os() { esac } -# Try to download pre-built binary -download_binary() { - local os=$(detect_os) - local arch=$(detect_arch) - local url="https://github.com/$REPO/releases/latest/download/core-${os}-${arch}" - - if [[ "$os" == "windows" ]]; then - url="${url}.exe" - fi - - info "Attempting to download pre-built binary..." - info "URL: $url" - - if curl -fsSL -o /tmp/core "$url" 2>/dev/null; then - chmod +x /tmp/core - mkdir -p "$INSTALL_DIR" - mv /tmp/core "$INSTALL_DIR/core" - info "Downloaded to $INSTALL_DIR/core" - return 0 +# Compute SHA256 hash (cross-platform) +compute_sha256() { + local file=$1 + if has sha256sum; then + sha256sum "$file" | cut -d' ' -f1 + elif has shasum; then + shasum -a 256 "$file" | cut -d' ' -f1 else - warn "No pre-built binary available, will build from source" - return 1 + error "No SHA256 tool available (need sha256sum or shasum)" fi } -# Build from source +# Verify SHA256 hash of downloaded file +verify_hash() { + local file=$1 + local expected_hash=$2 + local actual_hash + + actual_hash=$(compute_sha256 "$file") + + if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then + rm -f "$file" + error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash. The downloaded file may be corrupted or tampered with." + fi + + info "Hash verification passed (SHA256: ${actual_hash:0:16}...)" +} + +# Check if directory is a symlink (security check) +check_not_symlink() { + local path=$1 + + if [[ -L "$path" ]]; then + error "Directory '$path' is a symbolic link. Possible symlink attack detected." + fi + + # Additional check using file type + if [[ -e "$path" ]] && [[ ! -d "$path" ]]; then + error "Path '$path' exists but is not a directory." + fi +} + +# Try to download pre-built binary with integrity verification +download_binary() { + local os=$(detect_os) + local arch=$(detect_arch) + local binary_name="core-${os}-${arch}" + local binary_url="https://github.com/$REPO/releases/download/$VERSION/${binary_name}" + local checksum_url="https://github.com/$REPO/releases/download/$VERSION/checksums.txt" + + if [[ "$os" == "windows" ]]; then + binary_name="${binary_name}.exe" + binary_url="${binary_url}.exe" + fi + + info "Attempting to download pre-built binary (version $VERSION)..." + info "URL: $binary_url" + + # Use secure temp file (prevents symlink attacks) + local temp_file + temp_file=$(mktemp "${TMPDIR:-/tmp}/core.XXXXXXXXXX") || error "Failed to create temp file" + + # Ensure cleanup on exit/interrupt + trap "rm -f '$temp_file'" EXIT INT TERM + + # Download binary + if ! curl -fsSL -o "$temp_file" "$binary_url" 2>/dev/null; then + rm -f "$temp_file" + trap - EXIT INT TERM + warn "No pre-built binary available, will build from source" + return 1 + fi + + # Download and verify checksum + info "Verifying download integrity..." + local checksums + checksums=$(curl -fsSL "$checksum_url" 2>/dev/null) || { + rm -f "$temp_file" + trap - EXIT INT TERM + warn "Could not download checksums, will build from source" + return 1 + } + + # Parse checksum file (format: "hash filename") + local expected_hash + expected_hash=$(echo "$checksums" | grep -E "^[a-fA-F0-9]{64}\s+.*${binary_name}$" | head -1 | cut -d' ' -f1) + + if [[ -z "$expected_hash" ]]; then + rm -f "$temp_file" + trap - EXIT INT TERM + error "Could not find checksum for $binary_name in checksums.txt" + fi + + # Verify hash BEFORE any move operation + verify_hash "$temp_file" "$expected_hash" + + # Verify install directory is safe + check_not_symlink "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir + + # Make executable and move to final location + chmod +x "$temp_file" + mv "$temp_file" "$INSTALL_DIR/core" + + trap - EXIT INT TERM # Clear trap since file was moved + + info "Downloaded and verified: $INSTALL_DIR/core" + return 0 +} + +# Verify GPG signature on git tag (if gpg is available) +verify_git_tag_signature() { + local repo_path=$1 + local tag=$2 + + if ! has gpg; then + warn "GPG not available - skipping tag signature verification" + warn "For enhanced security, install GPG and import the project signing key" + return 0 # Continue without verification + fi + + pushd "$repo_path" > /dev/null + local result + result=$(git tag -v "$tag" 2>&1) || { + # Check if tag is unsigned vs signature invalid + if echo "$result" | grep -q "error: no signature found"; then + warn "Tag $tag is not signed - continuing without signature verification" + popd > /dev/null + return 0 + else + popd > /dev/null + error "GPG signature verification FAILED for tag $tag. Possible tampering detected." + fi + } + info "GPG signature verified for tag $tag" + popd > /dev/null +} + +# Build from source with security checks build_from_source() { + if ! has git; then + error "Git is required to build from source. Run './scripts/install-deps.sh' first" + fi + if ! has go; then error "Go is required to build from source. Run './scripts/install-deps.sh' first" fi - local tmpdir=$(mktemp -d) - info "Cloning $REPO..." - git clone --depth 1 "https://github.com/$REPO.git" "$tmpdir/Core" + # Create secure temp directory + local tmpdir + tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/core-build.XXXXXXXXXX") || error "Failed to create temp directory" + + # Set restrictive permissions on temp directory + chmod 700 "$tmpdir" + + # Ensure cleanup on exit/interrupt + trap "rm -rf '$tmpdir'" EXIT INT TERM + + info "Cloning $REPO (version $VERSION)..." + local clone_dir="$tmpdir/Core" + + # Clone specific tag for reproducibility + if ! git clone --depth 1 --branch "$VERSION" "https://github.com/$REPO.git" "$clone_dir"; then + error "Failed to clone repository at version $VERSION" + fi + + # Verify GPG signature on tag (if available) + verify_git_tag_signature "$clone_dir" "$VERSION" info "Building core CLI..." - cd "$tmpdir/Core" - go build -o core ./cmd/core + pushd "$clone_dir" > /dev/null + if ! go build -o core ./cmd/core; then + popd > /dev/null + error "Go build failed" + fi + popd > /dev/null + # Verify install directory is safe + check_not_symlink "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" - mv core "$INSTALL_DIR/core" + check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir + # Move built binary to install location + mv "$clone_dir/core" "$INSTALL_DIR/core" + + trap - EXIT INT TERM # Clear trap rm -rf "$tmpdir" + info "Built and installed to $INSTALL_DIR/core" } @@ -118,9 +281,12 @@ verify() { } main() { - info "Installing Core CLI..." + info "Installing Core CLI (version $VERSION)..." + # Verify install directory is safe before starting + check_not_symlink "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" + check_not_symlink "$INSTALL_DIR" # Try download first, fallback to build if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then diff --git a/scripts/install-deps.ps1 b/scripts/install-deps.ps1 index f3ec533..e9c70b3 100644 --- a/scripts/install-deps.ps1 +++ b/scripts/install-deps.ps1 @@ -1,5 +1,10 @@ # Install system dependencies for Host UK development (Windows) # Run: .\scripts\install-deps.ps1 +# +# SECURITY NOTES: +# - Chocolatey installer is downloaded to temp file before execution +# - HTTPS is enforced for all downloads +# - For high-security environments, consider auditing install scripts $ErrorActionPreference = "Stop" @@ -12,6 +17,8 @@ function Test-Command($cmd) { } # Install Chocolatey if not present +# NOTE: Chocolatey's install script changes frequently, making checksum verification impractical. +# The script is fetched over HTTPS. For high-security environments, audit the script first. function Install-Chocolatey { if (Test-Command choco) { Write-Info "Chocolatey already installed" @@ -19,9 +26,26 @@ function Install-Chocolatey { } Write-Info "Installing Chocolatey..." + Write-Warn "This downloads and executes a script from chocolatey.org. Review at: https://community.chocolatey.org/install.ps1" + Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + # Download to temp file first (allows manual inspection if needed, avoids Invoke-Expression with direct download) + $tempScript = Join-Path ([System.IO.Path]::GetTempPath()) "choco-install.$([System.Guid]::NewGuid().ToString('N').Substring(0,8)).ps1" + + try { + Write-Info "Downloading Chocolatey installer..." + Invoke-WebRequest -Uri 'https://community.chocolatey.org/install.ps1' -OutFile $tempScript -UseBasicParsing + + Write-Info "Executing Chocolatey installer..." + & $tempScript + } finally { + # Clean up temp file + if (Test-Path $tempScript) { + Remove-Item -Path $tempScript -Force -ErrorAction SilentlyContinue + } + } # Refresh PATH $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User") diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh index 572e7ac..387aa36 100755 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -3,16 +3,55 @@ set -e # Install system dependencies for Host UK development # Supports: macOS (brew), Linux (apt/dnf), Windows (choco via WSL) +# +# SECURITY NOTES: +# - External install scripts (Homebrew, NodeSource) are downloaded over HTTPS +# - Go binary is verified via SHA256 checksum +# - Composer installer is verified via SHA256 checksum +# - Consider auditing external scripts before running in sensitive environments RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +# Pinned versions and checksums for security +GO_VERSION="1.22.0" +GO_AMD64_SHA256="f6c8a87aa03b92c4b0bf3d558e28ea03006eb29db78917daec5cfb6ec1046265" +COMPOSER_EXPECTED_SIG="dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6" + info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +# Compute SHA256 hash (cross-platform) +compute_sha256() { + local file=$1 + if command -v sha256sum &> /dev/null; then + sha256sum "$file" | cut -d' ' -f1 + elif command -v shasum &> /dev/null; then + shasum -a 256 "$file" | cut -d' ' -f1 + else + error "No SHA256 tool available (need sha256sum or shasum)" + fi +} + +# Verify SHA256 hash of downloaded file +verify_hash() { + local file=$1 + local expected_hash=$2 + local actual_hash + + actual_hash=$(compute_sha256 "$file") + + if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then + rm -f "$file" + error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash" + fi + + info "Hash verification passed" +} + # Detect OS detect_os() { case "$(uname -s)" in @@ -29,6 +68,9 @@ has() { } # Install Homebrew (macOS/Linux) +# NOTE: Homebrew's install script changes frequently, making checksum verification impractical. +# The script is fetched over HTTPS from GitHub. For high-security environments, +# consider auditing the script manually before running. install_brew() { if has brew; then info "Homebrew already installed" @@ -36,7 +78,18 @@ install_brew() { fi info "Installing Homebrew..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + warn "This downloads and executes a script from GitHub. Review at: https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + + # Download to temp file first (allows manual inspection if needed) + local temp_script + temp_script=$(mktemp "${TMPDIR:-/tmp}/brew-install.XXXXXXXXXX.sh") + curl -fsSL -o "$temp_script" https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh || { + rm -f "$temp_script" + error "Failed to download Homebrew installer" + } + + /bin/bash "$temp_script" + rm -f "$temp_script" # Add to PATH for this session if [[ -f /opt/homebrew/bin/brew ]]; then @@ -115,11 +168,24 @@ setup_linux_apt() { if ! has go; then info "Installing Go..." sudo apt-get install -y golang-go || { - # Fallback to manual install for newer version - curl -LO https://go.dev/dl/go1.22.0.linux-amd64.tar.gz + # Fallback to manual install for newer version with integrity verification + local go_tarball="go${GO_VERSION}.linux-amd64.tar.gz" + local go_url="https://go.dev/dl/${go_tarball}" + local temp_file + temp_file=$(mktemp "${TMPDIR:-/tmp}/go.XXXXXXXXXX.tar.gz") + + info "Downloading Go $GO_VERSION..." + curl -fsSL -o "$temp_file" "$go_url" || { + rm -f "$temp_file" + error "Failed to download Go" + } + + info "Verifying Go download integrity..." + verify_hash "$temp_file" "$GO_AMD64_SHA256" + sudo rm -rf /usr/local/go - sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz - rm go1.22.0.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf "$temp_file" + rm -f "$temp_file" export PATH=$PATH:/usr/local/go/bin } fi @@ -127,17 +193,47 @@ setup_linux_apt() { # PHP apt_install php - # Composer + # Composer (with installer signature verification) if ! has composer; then info "Installing Composer..." - curl -sS https://getcomposer.org/installer | php - sudo mv composer.phar /usr/local/bin/composer + local temp_dir + temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/composer.XXXXXXXXXX") + chmod 700 "$temp_dir" + + # Download installer + curl -fsSL -o "$temp_dir/composer-setup.php" https://getcomposer.org/installer + + # Verify installer signature (SHA384) + local actual_sig + actual_sig=$(php -r "echo hash_file('sha384', '$temp_dir/composer-setup.php');") + if [[ "$actual_sig" != "$COMPOSER_EXPECTED_SIG" ]]; then + rm -rf "$temp_dir" + error "Composer installer signature verification failed!" + fi + info "Composer installer signature verified" + + # Run installer + php "$temp_dir/composer-setup.php" --install-dir="$temp_dir" + sudo mv "$temp_dir/composer.phar" /usr/local/bin/composer + rm -rf "$temp_dir" fi - # Node + # Node (via NodeSource) + # NOTE: NodeSource setup script changes frequently, making checksum verification impractical. + # For high-security environments, consider using nvm or building from source. if ! has node; then info "Installing Node.js..." - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + warn "This downloads and executes a script from NodeSource. Review at: https://deb.nodesource.com/setup_20.x" + + local temp_script + temp_script=$(mktemp "${TMPDIR:-/tmp}/nodesource-setup.XXXXXXXXXX.sh") + curl -fsSL -o "$temp_script" https://deb.nodesource.com/setup_20.x || { + rm -f "$temp_script" + error "Failed to download NodeSource setup script" + } + + sudo -E bash "$temp_script" + rm -f "$temp_script" sudo apt-get install -y nodejs fi