From 2a2dd91fccf20f8c35eee5c335578534ab164a81 Mon Sep 17 00:00:00 2001 From: Arthur Pastel Date: Mon, 16 Feb 2026 20:22:39 +0100 Subject: [PATCH] feat: add SHA256 hash verification for the installer script Verify the installer script's SHA256 hash against pinned values before execution, closing a supply chain gap. The expected hashes are stored in .codspeed-runner-installer-hashes.json (sourced from GitHub API digests). - For pinned release versions: download to temp file, verify hash, fail on mismatch or missing hash - For latest/branch/rev: warn that hash verification is not available - Add `skip-hash-check` input to bypass verification if needed - Update bump-runner-version workflow to fetch and store the hash Ref: COD-2243 --- .codspeed-runner-installer-hashes.json | 37 ++++++++++++++++ .github/workflows/bump-runner-version.yml | 25 ++++++++--- .github/workflows/ci.yml | 35 +++++++++++++++ action.yml | 46 +++++++++++++++++--- scripts/check-hashes.sh | 53 +++++++++++++++++++++++ 5 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 .codspeed-runner-installer-hashes.json create mode 100755 scripts/check-hashes.sh diff --git a/.codspeed-runner-installer-hashes.json b/.codspeed-runner-installer-hashes.json new file mode 100644 index 0000000..4205180 --- /dev/null +++ b/.codspeed-runner-installer-hashes.json @@ -0,0 +1,37 @@ +{ + "3.6.0": "e24d2883509f40e26105fabef599a8455417263a58c823cdeb29d18b417ddcbf", + "3.6.1": "9af2aaf441ae11753d610db852f7bf34cf0472c88763b342e02a39dc49a54f62", + "3.7.0": "ec0d7d35d9339dae8033a25e111647316148de62b9a6e1481e332da3bd372c8b", + "3.8.0": "a4a9f80c116011853931fcd27bbab67f03dab2bdb39cd2a85a806e1680a51ce8", + "3.8.1": "5921cce317a66d633b243503254498c60e3d19ae39d25a3cbbef168321a184d0", + "4.0.0": "c224e1269c3d9b77038aad6d198b1122439a8b99c14cd650503d4be29bb28cea", + "4.0.1": "e59b7e54e4085380939d326772743f5ed3eb391f5437be11657485587520a4d8", + "4.1.0": "2a9aba44aabf093cca8e77ef67d3d72182420d102470ee937e6d60a32ded2327", + "4.1.1": "cdab34d3468a5168900fbda54e2a24bbc7ed995f6a7c6c1a79407fc6f2e9a65a", + "4.2.0": "37267e289dcc13dc903d193390707882ac5363a4ba649836a2deb35d491ddc97", + "4.2.1": "4fbb63773a7d3faf6ff0e8a20d6a64de9df95e47f0ea741d92f068d798964d9e", + "4.3.0-beta.0": "c9d3e98234d7b18545e1f035e6afac295938ba5ccf96f3da780290b2a7bdfccb", + "4.3.0": "ff285a76118fb74e0b159b09c286b1e4dff061fbc9fc6ae5031165d2b922993c", + "4.3.1": "69cc9a4fd8369b4a2f44b8d8abb4ddd04880a1f19897433e1e8fc45acebe4ca9", + "4.3.2": "98390ad959286a6c42ef01402f88dfa8059cda81ca24d7d48383f55f704297e7", + "4.3.3": "a3419c98f5af093c25ad6a9fef915b3717d67845afd492155052bf65af639cae", + "4.3.4": "d71e683b8dbd74d66e996115f6cab6142162d9ff446333b67b7a19c2eb4cdae2", + "4.4.0": "6aeb8381c25b04ecfec8074234feb8957b811369ae0262f668867d0e572acb8f", + "4.4.1": "44f8a5e46171c62bd1d9de013eda76e4a45320ed3da1dddf7a7090b7eabe4a91", + "4.5.0": "4f6c7fa08d4487eed3bfe6ef4e96e8fe5d7408155807e1ce83a83ade42a367bd", + "4.5.1": "da5465f29b621b9f52ad763739683318d4b9be05f13ff1a888f5b40e70912d7d", + "4.5.2": "68d1909e13d48e6ad7c23dbde1f811bdb39f8fc86a1355bc5b8309961d9b9eee", + "4.6.0": "3fea5e6a032f7158464c0ff1160563b4d39a5f0f04e3b1659777d1c2c6f9100f", + "4.7.0": "984620cc7db083c86b0291264e1d4e2b8d93f2de779f07b7c711d9b1555f819b", + "4.8.0": "7d4ba02d343452ba561eb103e1978add0206118ced02e800cd43a12c7e45abea", + "4.8.1": "ffcb433c3f62204becec1a78ddf8e6b500fd80fbd4ca2b6d1a478b857b87e9b7", + "4.8.2": "a1539fc5641e782e53b9f8eea2114f99bb95e23ddb6c0debc9c9702093cd767d", + "4.9.0": "b046947fc18874867e877a6286db0cb462bac0b02c8cbf580c48dde9948a0256", + "4.10.0": "74bf52fa589dff1a798fbf1a82312237a61d8cd4e0849a12e94ab17cf13a5cac", + "4.10.1": "aa4af09bfd7f560fdfac43cb8b7b0d9e6885a9cf219e3becdc19fe264c2a4ff5", + "4.10.2": "0c953e8e40c4768ccee7d80733504d7860e7667c6a251754059221dae44a4e5b", + "4.10.3": "adcb40bef1b1ed6703f00f242917b2a4356f30a438a89b12ea9417cb76f740bb", + "4.10.4": "e9a766e3ed3b8ac9e0266eba1584e1b6d810335bd5f11a29b5cc6b36445451d5", + "4.10.5": "304ec26633ae75797bf0dfca379e495a22f43e0461543b04521c0ee1efe9bed6", + "4.10.6": "a0ae6903e852af82af78ef1d908240d4bbf9a0088dc3f24ee94cbde3ee48bdb3" +} diff --git a/.github/workflows/bump-runner-version.yml b/.github/workflows/bump-runner-version.yml index 4c8d9be..c6fe5e3 100644 --- a/.github/workflows/bump-runner-version.yml +++ b/.github/workflows/bump-runner-version.yml @@ -33,13 +33,26 @@ jobs: exit 1 fi + VERSION="${{ github.event.inputs.version }}" + + # Fetch the installer hash from the GitHub API + INSTALLER_HASH=$(gh api "repos/CodSpeedHQ/codspeed/releases/tags/v${VERSION}" --jq '.assets[] | select(.name == "codspeed-runner-installer.sh") | .digest | ltrimstr("sha256:")') + if [ -z "$INSTALLER_HASH" ]; then + echo "Error: Could not fetch installer hash for version $VERSION" + exit 1 + fi + echo "Installer hash for $VERSION: $INSTALLER_HASH" + git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" - echo "Bumping runner version to ${{ github.event.inputs.version }}" - BRANCH_NAME=bump-runner-version/${{ github.event.inputs.version }} + echo "Bumping runner version to $VERSION" + BRANCH_NAME=bump-runner-version/$VERSION git checkout -b $BRANCH_NAME - echo ${{ github.event.inputs.version }} > .codspeed-runner-version - git add .codspeed-runner-version - git commit -m "chore: bump runner version to ${{ github.event.inputs.version }}" + + echo $VERSION > .codspeed-runner-version + jq --arg v "$VERSION" --arg h "$INSTALLER_HASH" '. + {($v): $h}' .codspeed-runner-installer-hashes.json > .codspeed-runner-installer-hashes.json.tmp && mv .codspeed-runner-installer-hashes.json.tmp .codspeed-runner-installer-hashes.json + + git add .codspeed-runner-version .codspeed-runner-installer-hashes.json + git commit -m "chore: bump runner version to $VERSION" git push origin $BRANCH_NAME - gh pr create --title "chore: bump runner version to ${{ github.event.inputs.version }}" --body "Bump runner version to ${{ github.event.inputs.version }}" --base main --head $BRANCH_NAME + gh pr create --title "chore: bump runner version to $VERSION" --body "Bump runner version to $VERSION" --base main --head $BRANCH_NAME diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6917d..7871a1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,41 @@ jobs: mode: walltime run: echo "Testing version format ${{ matrix.version }}!" + get-recent-pinned-runner-versions: + runs-on: ubuntu-slim + outputs: + versions: ${{ steps.get-versions.outputs.versions }} + steps: + - uses: actions/checkout@v4 + - id: get-versions + run: echo "versions=$(jq -c 'keys_unsorted | .[-5:]' .codspeed-runner-installer-hashes.json)" >> "$GITHUB_OUTPUT" + + test-recent-pinned-runner-versions: + needs: get-recent-pinned-runner-versions + strategy: + fail-fast: false + matrix: + version: ${{ fromJson(needs.get-recent-pinned-runner-versions.outputs.versions) }} + runs-on: ubuntu-latest + env: + CODSPEED_SKIP_UPLOAD: true + steps: + - uses: actions/checkout@v4 + - name: Check action with pinned runner version ${{ matrix.version }} + uses: ./ + with: + allow-empty: true + runner-version: ${{ matrix.version }} + mode: walltime # simulation not yet supported for all versions (was instrumentation before) + run: echo "Testing pinned version ${{ matrix.version }}!" + + check-installer-hashes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify pinned installer hashes + run: ./scripts/check-hashes.sh + test-config-file: runs-on: ubuntu-latest env: diff --git a/action.yml b/action.yml index c30eeed..c89e13a 100644 --- a/action.yml +++ b/action.yml @@ -81,6 +81,11 @@ inputs: description: "Path to a CodSpeed configuration file (codspeed.yml). If not specified, the runner will look for a codspeed.yml file in the repository root." required: false + skip-hash-check-warning: + description: "Suppress the warning emitted when hash verification is not available (e.g. when using 'latest', branch, or revision versions)." + required: false + default: "false" + runs: using: "composite" steps: @@ -145,24 +150,55 @@ runs: fi # Install the runner + SKIP_HASH_CHECK_WARNING_WARNING="${{ inputs.skip-hash-check-warning }}" + if [ "$VERSION_TYPE" = "latest" ]; then + if [ "$SKIP_HASH_CHECK_WARNING" != "true" ]; then + echo "::warning::Hash verification is not available when using 'latest' version. Consider pinning a specific version for supply chain security." + fi curl -fsSL https://codspeed.io/install.sh | bash -s -- --quiet elif [ "$VERSION_TYPE" = "branch" ]; then # Install from specific branch using cargo + if [ "$SKIP_HASH_CHECK_WARNING" != "true" ]; then + echo "::warning::Hash verification is not available when installing from a branch." + fi source $HOME/.cargo/env cargo install --locked --git https://github.com/CodSpeedHQ/codspeed --branch "$RUNNER_VERSION" codspeed-runner elif [ "$VERSION_TYPE" = "rev" ]; then # Install from specific commit/rev using cargo + if [ "$SKIP_HASH_CHECK_WARNING" != "true" ]; then + echo "::warning::Hash verification is not available when installing from a revision." + fi source $HOME/.cargo/env cargo install --locked --git https://github.com/CodSpeedHQ/codspeed --rev "$RUNNER_VERSION" codspeed-runner else - # Release version - if ! install_script=$(curl -sSL --fail-with-body https://codspeed.io/v$RUNNER_VERSION/install.sh 2>/dev/null); then - error_msg=$(echo "$install_script" | jq -r '.error // empty' 2>/dev/null) - echo "::error title=Failed to install CodSpeed CLI::Installation of CodSpeed CLI with version $RUNNER_VERSION failed.%0AReason: ${error_msg:-$install_script}" + # Release version - download to temp file and verify hash + INSTALLER_URL="https://codspeed.io/v$RUNNER_VERSION/install.sh" + INSTALLER_TMP=$(mktemp) + trap "rm -f $INSTALLER_TMP" EXIT + + if ! curl -sSL --fail-with-body "$INSTALLER_URL" -o "$INSTALLER_TMP" 2>/dev/null; then + error_msg=$(jq -r '.error // empty' "$INSTALLER_TMP" 2>/dev/null) + install_output=$(cat "$INSTALLER_TMP") + echo "::error title=Failed to install CodSpeed CLI::Installation of CodSpeed CLI with version $RUNNER_VERSION failed.%0AReason: ${error_msg:-$install_output}" exit 1 fi - echo "$install_script" | bash -s -- --quiet + + HASHES_FILE="$GITHUB_ACTION_PATH/.codspeed-runner-installer-hashes.json" + EXPECTED_HASH=$(jq -r --arg v "$RUNNER_VERSION" '.[$v] // empty' "$HASHES_FILE") + if [ -z "$EXPECTED_HASH" ]; then + echo "::error::No pinned hash found for installer version $RUNNER_VERSION. Update .codspeed-runner-installer-hashes.json." + exit 1 + fi + + ACTUAL_HASH=$(sha256sum "$INSTALLER_TMP" | awk '{print $1}') + if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then + echo "::error::Installer hash mismatch for version $RUNNER_VERSION. Expected: $EXPECTED_HASH, Got: $ACTUAL_HASH" + exit 1 + fi + echo "Installer hash verified for version $RUNNER_VERSION" + + bash "$INSTALLER_TMP" --quiet fi # Build the runner arguments array diff --git a/scripts/check-hashes.sh b/scripts/check-hashes.sh new file mode 100755 index 0000000..acd45d5 --- /dev/null +++ b/scripts/check-hashes.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Usage: ./scripts/check-hashes.sh +# Verifies that all hashes in .codspeed-runner-installer-hashes.json match the +# actual SHA256 of the corresponding installer downloaded from GitHub releases. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HASHES_FILE="$SCRIPT_DIR/../.codspeed-runner-installer-hashes.json" + +if [ ! -f "$HASHES_FILE" ]; then + echo "Error: $HASHES_FILE not found" + exit 1 +fi + +VERSIONS=$(jq -r 'keys_unsorted[]' "$HASHES_FILE") +TOTAL=$(echo "$VERSIONS" | wc -l | tr -d ' ') +FAILED=0 +PASSED=0 + +echo "Checking $TOTAL installer hashes..." +echo + +for VERSION in $VERSIONS; do + EXPECTED_HASH=$(jq -r --arg v "$VERSION" '.[$v]' "$HASHES_FILE") + URL="https://github.com/CodSpeedHQ/codspeed/releases/download/v${VERSION}/codspeed-runner-installer.sh" + + INSTALLER_TMP=$(mktemp) + trap 'rm -f $INSTALLER_TMP' EXIT + + if ! curl -fsSL "$URL" -o "$INSTALLER_TMP" 2>/dev/null; then + echo "FAIL $VERSION - download failed ($URL)" + FAILED=$((FAILED + 1)) + continue + fi + + ACTUAL_HASH=$(sha256sum "$INSTALLER_TMP" | awk '{print $1}') + rm -f "$INSTALLER_TMP" + + if [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ]; then + echo "OK $VERSION" + PASSED=$((PASSED + 1)) + else + echo "FAIL $VERSION - expected $EXPECTED_HASH, got $ACTUAL_HASH" + FAILED=$((FAILED + 1)) + fi +done + +echo +echo "$PASSED/$TOTAL passed, $FAILED failed" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi