diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 152ddf2..77f2e1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,6 +104,20 @@ jobs: working-directory: artifacts run: sha256sum * > SHA256SUMS.txt + - name: sign the checksums (Ed25519) + env: + KEY: ${{ secrets.SCIMESH_SIGNING_KEY }} + working-directory: artifacts + run: | + if [ -n "$KEY" ]; then + printf '%s\n' "$KEY" > /tmp/scimesh-sign-key.pem + openssl pkeyutl -sign -inkey /tmp/scimesh-sign-key.pem \ + -in SHA256SUMS.txt -out SHA256SUMS.txt.sig + echo "signed SHA256SUMS.txt" + else + echo "SCIMESH_SIGNING_KEY is not set; releasing without a signature" + fi + - uses: softprops/action-gh-release@v2 with: files: | diff --git a/install.ps1 b/install.ps1 index 58a9204..7ede40b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,6 +8,11 @@ # coordinator serve --open $ErrorActionPreference = "Stop" +# Public half of the Ed25519 key that signs SHA256SUMS.txt in releases (see +# install.sh). Verification needs the openssl binary; without it the installer +# falls back to checksum verification with a warning. +$ScimeshSigningPubKey = "MCowBQYDK2VwAyEAZfOXciD5AIvC6/1YXjOp4KjA0DDNWKZ0nQ0dx76XUUw=" + $Repo = "emil28092005/SciMesh" $Component = if ($env:SCIMESH_COMPONENT) { $env:SCIMESH_COMPONENT } else { "coordinator" } $Version = if ($env:SCIMESH_VERSION) { $env:SCIMESH_VERSION } else { "latest" } @@ -63,19 +68,51 @@ Invoke-WebRequest -Uri $Url -OutFile "$Target.tmp" if ($env:SCIMESH_SKIP_VERIFY -ne "1") { try { $SumUrl = "https://github.com/$Repo/releases/download/$Version/SHA256SUMS.txt" - $Sums = (Invoke-WebRequest -Uri $SumUrl).Content + + # Ed25519 signature over the checksum file, when openssl is present. + # Both files are fetched with -OutFile so their bytes match the + # release exactly (string pipelines would rewrite line endings). + $openssl = Get-Command openssl -ErrorAction SilentlyContinue + if ($env:SCIMESH_SKIP_SIGNATURE -ne "1" -and $openssl) { + $PubFile = Join-Path $env:TEMP "scimesh-signing-pub.pem" + $SumFile = Join-Path $env:TEMP ("scimesh-sums-" + [guid]::NewGuid().ToString("N") + ".txt") + $SigFile = "$SumFile.sig" + Set-Content -Path $PubFile -Value @("-----BEGIN PUBLIC KEY-----", $ScimeshSigningPubKey, "-----END PUBLIC KEY-----") + try { + Invoke-WebRequest -Uri $SumUrl -OutFile $SumFile + Invoke-WebRequest -Uri "$SumUrl.sig" -OutFile $SigFile + & $openssl.Source pkeyutl -verify -pubin -inkey $PubFile -sigfile $SigFile -in $SumFile 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host "Signature verified (Ed25519)" + } else { + Remove-Item -Force "$Target.tmp" + throw "the release signature does not verify; the download channel may be tampered with" + } + } catch { + Remove-Item -Force "$Target.tmp" + throw "signature verification failed: $($_.Exception.Message)" + } finally { + Remove-Item -Force $PubFile, $SumFile, $SigFile -ErrorAction SilentlyContinue + } + } elseif ($env:SCIMESH_SKIP_SIGNATURE -ne "1") { + Write-Host "WARNING: openssl not found; falling back to checksum verification only" + } + $BinaryName = Split-Path $Url -Leaf - $Line = ($Sums -split "`n") | Where-Object { $_.Trim().EndsWith(" " + $BinaryName) } | Select-Object -First 1 + $SumFileCheck = Join-Path $env:TEMP ("scimesh-sums-check-" + [guid]::NewGuid().ToString("N") + ".txt") + Invoke-WebRequest -Uri $SumUrl -OutFile $SumFileCheck + $Line = (Get-Content $SumFileCheck -Raw -ErrorAction SilentlyContinue -split "`n") | Where-Object { $_.Trim().EndsWith(" " + $BinaryName) } | Select-Object -First 1 if ($Line) { $Expected = ($Line -split "\s+")[0] $Actual = (Get-FileHash -Algorithm SHA256 -Path "$Target.tmp").Hash.ToLower() if ($Actual -ne $Expected.ToLower()) { - Remove-Item -Force "$Target.tmp" + Remove-Item -Force "$Target.tmp", $SumFileCheck throw "checksum mismatch for $Binary (got $Actual, want $Expected)" } Write-Host "Checksum verified ($($Expected.Substring(0,12))...)" } else { Write-Host "WARNING: no checksum entry for $Binary; skipping verification" + Remove-Item -Force $SumFileCheck -ErrorAction SilentlyContinue } } catch { Write-Host "WARNING: could not verify checksum ($($_.Exception.Message)); continuing" diff --git a/install.sh b/install.sh index 2395c48..798fd82 100644 --- a/install.sh +++ b/install.sh @@ -14,6 +14,13 @@ set -eu REPO="emil28092005/SciMesh" + +# Public half of the Ed25519 key that signs SHA256SUMS.txt in releases. The +# private half lives in the repository secret SCIMESH_SIGNING_KEY. Verification +# uses openssl when available; without openssl the installer falls back to the +# checksum-only check with a warning. +SCIMESH_SIGNING_PUBKEY='MCowBQYDK2VwAyEAZfOXciD5AIvC6/1YXjOp4KjA0DDNWKZ0nQ0dx76XUUw=' + COMPONENT="${1:-coordinator}" VERSION="${SCIMESH_VERSION:-latest}" INSTALL_DIR="${SCIMESH_INSTALL_DIR:-$HOME/.local/bin}" @@ -68,7 +75,24 @@ curl -fsSL -o "$TARGET.tmp" "$URL" if [ "${SCIMESH_SKIP_VERIFY:-0}" != "1" ]; then if SUMFILE=$(mktemp) && curl -fsSL -o "$SUMFILE" "https://github.com/${REPO}/releases/download/${VERSION}/SHA256SUMS.txt"; then EXPECTED=$(awk '$2 == "'"$(basename "$URL")"'" {print $1}' "$SUMFILE" 2>/dev/null | head -1) - rm -f "$SUMFILE" + SIGFILE="$SUMFILE.sig" + if [ "${SCIMESH_SKIP_SIGNATURE:-0}" != "1" ] && command -v openssl >/dev/null 2>&1 \ + && curl -fsSL -o "$SIGFILE" "https://github.com/${REPO}/releases/download/${VERSION}/SHA256SUMS.txt.sig" 2>/dev/null; then + PUBKEY_FILE=$(mktemp) + printf '%s\n' '-----BEGIN PUBLIC KEY-----' "$SCIMESH_SIGNING_PUBKEY" '-----END PUBLIC KEY-----' > "$PUBKEY_FILE" + if openssl pkeyutl -verify -pubin -inkey "$PUBKEY_FILE" -sigfile "$SIGFILE" -in "$SUMFILE" >/dev/null 2>&1; then + echo "Signature verified (Ed25519)" + else + rm -f "$PUBKEY_FILE" "$SIGFILE" "$SUMFILE" "$TARGET.tmp" + echo "ERROR: the release signature does not verify; the download channel may be tampered with." >&2 + echo "Retry later, or bypass with SCIMESH_SKIP_SIGNATURE=1." >&2 + exit 1 + fi + rm -f "$PUBKEY_FILE" + elif [ "${SCIMESH_SKIP_SIGNATURE:-0}" != "1" ] && ! command -v openssl >/dev/null 2>&1; then + echo "WARNING: openssl not found; falling back to checksum verification only" + fi + rm -f "$SUMFILE" "$SIGFILE" if [ -n "$EXPECTED" ]; then ACTUAL=$(sha256sum "$TARGET.tmp" | awk '{print $1}') if [ "$ACTUAL" != "$EXPECTED" ]; then