security: harden shell scripts with integrity verification
- Add SHA256 checksum verification to install-core.sh binary download - Add version pinning (v0.1.0) to install-core.sh matching PowerShell version - Use mktemp for secure temp files instead of predictable /tmp/core - Add symlink detection to prevent directory traversal attacks - Add GPG signature verification for git tags when available - Add checksum verification for Go binary in install-deps.sh - Add SHA384 signature verification for Composer installer - Replace Invoke-Expression with download-verify-execute in install-deps.ps1 - Download Homebrew/NodeSource scripts to temp files before execution - Add security documentation and warnings for external scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
05bd711219
commit
d9a8fe279e
3 changed files with 329 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue