From c968e6f7ab1514c3c5303f31a6a7a5bca9bf8fa6 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Thu, 11 Dec 2025 15:25:58 -0500 Subject: [PATCH 1/3] fix(installer): add SHA256 checksum verification - Generate SHA256 checksums for all release archives during CI build - Publish .sha256 files alongside release artifacts - Update install.sh to download and verify checksum before extraction - Update install.ps1 to download and verify checksum before extraction - Abort installation if checksum is missing or doesn't match Closes #87 --- .github/workflows/release.yml | 37 +++++++++++++++++++++++-- install.ps1 | 49 +++++++++++++++++++++++++++++++++ install.sh | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3adc40..5ba1764 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -209,11 +209,38 @@ jobs: cd .. shell: bash + - name: Generate SHA256 checksum (Unix) + if: matrix.goos != 'windows' + run: | + cd dist + ARCHIVE_NAME="dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}" + if command -v sha256sum &> /dev/null; then + sha256sum "$ARCHIVE_NAME" > "$ARCHIVE_NAME.sha256" + else + shasum -a 256 "$ARCHIVE_NAME" > "$ARCHIVE_NAME.sha256" + fi + echo "Generated checksum:" + cat "$ARCHIVE_NAME.sha256" + shell: bash + + - name: Generate SHA256 checksum (Windows) + if: matrix.goos == 'windows' + run: | + $archiveName = "dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}" + $archivePath = "dist/$archiveName" + $hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower() + "$hash $archiveName" | Out-File -FilePath "dist/$archiveName.sha256" -Encoding ASCII -NoNewline + Write-Host "Generated checksum:" + Get-Content "dist/$archiveName.sha256" + shell: pwsh + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-${{ matrix.asset_name_suffix }} - path: dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }} + path: | + dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }} + dist/dtvem-${{ github.event.inputs.version }}-${{ matrix.asset_name_suffix }}.${{ matrix.archive_ext }}.sha256 retention-days: 1 - name: Upload install scripts (linux-amd64 only) @@ -274,10 +301,15 @@ jobs: tag_name: v${{ github.event.inputs.version }} files: | artifacts/build-linux-amd64/dtvem-${{ github.event.inputs.version }}-linux-amd64.tar.gz + artifacts/build-linux-amd64/dtvem-${{ github.event.inputs.version }}-linux-amd64.tar.gz.sha256 artifacts/build-macos-amd64/dtvem-${{ github.event.inputs.version }}-macos-amd64.tar.gz + artifacts/build-macos-amd64/dtvem-${{ github.event.inputs.version }}-macos-amd64.tar.gz.sha256 artifacts/build-macos-arm64/dtvem-${{ github.event.inputs.version }}-macos-arm64.tar.gz + artifacts/build-macos-arm64/dtvem-${{ github.event.inputs.version }}-macos-arm64.tar.gz.sha256 artifacts/build-windows-amd64/dtvem-${{ github.event.inputs.version }}-windows-amd64.zip + artifacts/build-windows-amd64/dtvem-${{ github.event.inputs.version }}-windows-amd64.zip.sha256 artifacts/build-windows-arm64/dtvem-${{ github.event.inputs.version }}-windows-arm64.zip + artifacts/build-windows-arm64/dtvem-${{ github.event.inputs.version }}-windows-arm64.zip.sha256 install.sh install.ps1 body: | @@ -314,7 +346,8 @@ jobs: ## Checksums - See the assets below for SHA256 checksums. + SHA256 checksums are provided for each archive (`.sha256` files). + The installers automatically verify checksums before extraction. draft: false prerelease: false generate_release_notes: true # GitHub will auto-generate additional notes diff --git a/install.ps1 b/install.ps1 index 13ec90e..e74f318 100644 --- a/install.ps1 +++ b/install.ps1 @@ -46,6 +46,34 @@ function Get-LatestVersion { } } +function Test-Checksum { + param( + [string]$FilePath, + [string]$ChecksumPath + ) + + if (-not (Test-Path $ChecksumPath)) { + Write-Error-Custom "Checksum file not found: $ChecksumPath" + return $false + } + + # Read expected hash from checksum file (format: "hash filename") + $checksumContent = Get-Content $ChecksumPath -Raw + $expectedHash = ($checksumContent -split '\s+')[0].ToLower() + + # Calculate actual hash + $actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash.ToLower() + + if ($expectedHash -ne $actualHash) { + Write-Error-Custom "Checksum verification failed!" + Write-Error-Custom "Expected: $expectedHash" + Write-Error-Custom "Actual: $actualHash" + return $false + } + + return $true +} + function Main { Write-Host "" Write-Host "========================================" -ForegroundColor Blue @@ -105,6 +133,27 @@ function Main { exit 1 } + # Download and verify checksum + $CHECKSUM_URL = "$DOWNLOAD_URL.sha256" + $CHECKSUM_PATH = Join-Path $TMP_DIR "$ARCHIVE_NAME.sha256" + + Write-Info "Downloading checksum..." + try { + Invoke-WebRequest -Uri $CHECKSUM_URL -OutFile $CHECKSUM_PATH -UseBasicParsing + } + catch { + Write-Error-Custom "Failed to download checksum file: $_" + Write-Error-Custom "URL: $CHECKSUM_URL" + exit 1 + } + + Write-Info "Verifying checksum..." + if (-not (Test-Checksum -FilePath $ARCHIVE_PATH -ChecksumPath $CHECKSUM_PATH)) { + Write-Error-Custom "Archive integrity check failed - aborting installation" + exit 1 + } + Write-Success "Checksum verified" + # Extract archive Write-Info "Extracting archive..." Expand-Archive -Path $ARCHIVE_PATH -DestinationPath $TMP_DIR -Force diff --git a/install.sh b/install.sh index e885bea..6543125 100644 --- a/install.sh +++ b/install.sh @@ -92,6 +92,40 @@ download() { fi } +# Verify SHA256 checksum +verify_checksum() { + local file=$1 + local checksum_file=$2 + + if [ ! -f "$checksum_file" ]; then + error "Checksum file not found: $checksum_file" + return 1 + fi + + # Extract expected hash from checksum file (format: "hash filename") + local expected_hash=$(awk '{print $1}' "$checksum_file") + + # Calculate actual hash + local actual_hash + if command -v sha256sum &> /dev/null; then + actual_hash=$(sha256sum "$file" | awk '{print $1}') + elif command -v shasum &> /dev/null; then + actual_hash=$(shasum -a 256 "$file" | awk '{print $1}') + else + warning "Neither sha256sum nor shasum found - skipping checksum verification" + return 0 + fi + + if [ "$expected_hash" != "$actual_hash" ]; then + error "Checksum verification failed!" + error "Expected: $expected_hash" + error "Actual: $actual_hash" + return 1 + fi + + return 0 +} + main() { echo "" echo -e "${BLUE}========================================${NC}" @@ -152,6 +186,24 @@ main() { success "Downloaded successfully" + # Download and verify checksum + CHECKSUM_URL="${DOWNLOAD_URL}.sha256" + CHECKSUM_PATH="$TMP_DIR/${ARCHIVE_NAME}.sha256" + + info "Downloading checksum..." + if ! download "$CHECKSUM_URL" "$CHECKSUM_PATH"; then + error "Failed to download checksum file" + error "URL: $CHECKSUM_URL" + exit 1 + fi + + info "Verifying checksum..." + if ! verify_checksum "$ARCHIVE_PATH" "$CHECKSUM_PATH"; then + error "Archive integrity check failed - aborting installation" + exit 1 + fi + success "Checksum verified" + # Extract archive info "Extracting archive..." tar -xzf "$ARCHIVE_PATH" -C "$TMP_DIR" From ba57e9d5be948b71fda8862fc27a006d8c7e451d Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Thu, 11 Dec 2025 15:28:34 -0500 Subject: [PATCH 2/3] fix(installer): resolve shellcheck warnings in install.sh --- install.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 6543125..513be09 100644 --- a/install.sh +++ b/install.sh @@ -103,7 +103,8 @@ verify_checksum() { fi # Extract expected hash from checksum file (format: "hash filename") - local expected_hash=$(awk '{print $1}' "$checksum_file") + local expected_hash + expected_hash=$(awk '{print $1}' "$checksum_file") # Calculate actual hash local actual_hash @@ -172,7 +173,7 @@ main() { # Create temporary directory TMP_DIR=$(mktemp -d) - trap "rm -rf $TMP_DIR" EXIT + trap 'rm -rf $TMP_DIR' EXIT # Download archive info "Downloading dtvem..." @@ -265,9 +266,11 @@ main() { if [ -n "$SHELL_CONFIG" ]; then # Check if already in config if ! grep -q "$INSTALL_DIR" "$SHELL_CONFIG" 2>/dev/null; then - echo "" >> "$SHELL_CONFIG" - echo "# Added by dtvem installer" >> "$SHELL_CONFIG" - echo "$EXPORT_CMD" >> "$SHELL_CONFIG" + { + echo "" + echo "# Added by dtvem installer" + echo "$EXPORT_CMD" + } >> "$SHELL_CONFIG" success "Added to $SHELL_CONFIG" else info "Already in $SHELL_CONFIG" From 9b379a074fcf08f79b953e289bbff1a40955e93a Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Thu, 11 Dec 2025 15:29:41 -0500 Subject: [PATCH 3/3] ci(lint): exclude Write-Host rule for installer scripts --- .github/workflows/script-lint.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/script-lint.yml b/.github/workflows/script-lint.yml index cfceb80..61f92ee 100644 --- a/.github/workflows/script-lint.yml +++ b/.github/workflows/script-lint.yml @@ -40,7 +40,8 @@ jobs: shell: pwsh run: | Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser - $results = Invoke-ScriptAnalyzer -Path ./install.ps1 -Severity Error,Warning + # Exclude PSAvoidUsingWriteHost - Write-Host is intentional for colored installer output + $results = Invoke-ScriptAnalyzer -Path ./install.ps1 -Severity Error,Warning -ExcludeRule PSAvoidUsingWriteHost,PSUseBOMForUnicodeEncodedFile if ($results) { $results | Format-Table -AutoSize exit 1