From 3ec18b7dce5596912032ff0835ea2ff734acd962 Mon Sep 17 00:00:00 2001 From: Bill Golembieski Date: Mon, 13 Jul 2026 16:32:58 -0400 Subject: [PATCH] Add verifiable CI builds with signed provenance attestations Release binaries are currently built and uploaded from a local machine, so users have no way to prove a download corresponds to the published source. This makes cmdtab hard to adopt in security-conscious and corporate environments, where third-party binaries must be traceable to auditable source through a transparent build pipeline. This adds a GitHub Actions workflow that: - Builds cmdtab.exe from source with mingw-w64 GCC on GitHub-hosted runners on every push, PR, and version tag, mirroring the flags of the Makefile's release target so the published binary matches local development builds - Fails a tagged build if the tag disagrees with the FileVersion in res/cmdtab.rc - Packages the release zip and a SHA256SUMS file - Signs SLSA build provenance attestations for the exe and zip, verifiable with 'gh attestation verify' - Drafts a GitHub Release with the artifacts on v* tags, leaving publishing in the maintainer's hands The toolchain is the MSYS2 preinstalled on GitHub's Windows runners; no third-party toolchain actions are added, all actions are pinned to full commit SHAs, and jobs run with least-privilege permissions. README gains a 'Verifying releases' section documenting verification, and a Dependabot config keeps the SHA-pinned actions current by opening a PR whenever a pinned action publishes a new release. --- .github/dependabot.yml | 6 ++ .github/workflows/build.yml | 133 ++++++++++++++++++++++++++++++++++++ README.md | 22 ++++++ 3 files changed, 161 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f4e45c2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,133 @@ +# Verifiable builds: every artifact published by this repository is compiled +# from source on GitHub-hosted runners and accompanied by a signed SLSA build +# provenance attestation. See "Verifying releases" in README.md. +# +# Artifacts are built with mingw-w64 GCC, mirroring the flags of the +# Makefile's `release` target — the same toolchain used for local development — +# so the published binary matches what the maintainer builds and tests. +name: build + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build (mingw-w64 GCC, x86_64) + runs-on: windows-latest + permissions: + contents: read + # Required to sign and store build provenance attestations + id-token: write + attestations: write + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Read version from res/cmdtab.rc + id: version + shell: pwsh + run: | + $m = Select-String -Path res/cmdtab.rc -Pattern 'VALUE "FileVersion",\s*"([^"]+)"' + if (-not $m) { throw "FileVersion not found in res/cmdtab.rc" } + $version = $m.Matches[0].Groups[1].Value + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + Write-Host "Version: $version" + + - name: Check tag matches res/cmdtab.rc version + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + env: + TAG: ${{ github.ref_name }} + VERSION: ${{ steps.version.outputs.version }} + run: | + if ($env:TAG -ne "v$env:VERSION") { + throw "Tag $env:TAG does not match FileVersion $env:VERSION in res/cmdtab.rc" + } + + - name: Install mingw-w64 toolchain (preinstalled MSYS2) + shell: pwsh + run: | + C:\msys64\usr\bin\pacman.exe -S --noconfirm --needed ` + mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils + C:\msys64\ucrt64\bin\gcc.exe --version | Select-Object -First 1 + C:\msys64\ucrt64\bin\windres.exe --version | Select-Object -First 1 + + # Mirrors the Makefile's `release` target: + # CFLAGS = -std=c99 -Oz -DNDEBUG=1 -mwindows, LDFLAGS = -s + - name: Build + shell: pwsh + run: | + $env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH + windres -I res/ -o res/cmdtab.o res/cmdtab.rc + if ($LASTEXITCODE -ne 0) { throw "windres failed" } + gcc -std=c99 -Oz -DNDEBUG=1 -mwindows -s -o cmdtab.exe src/cmdtab.c res/cmdtab.o ` + -lole32 -lcomctl32 -lgdi32 -lshlwapi -ldwmapi -lpathcch -lversion -lwinmm -lshcore + if ($LASTEXITCODE -ne 0) { throw "gcc failed" } + Get-Item cmdtab.exe | Format-List Name, Length + + - name: Package + shell: pwsh + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + $zip = "cmdtab-v$env:VERSION-win-x86_64.zip" + Compress-Archive -Path cmdtab.exe -DestinationPath $zip + New-Item -ItemType Directory dist | Out-Null + Copy-Item cmdtab.exe dist/ + Copy-Item $zip dist/ + Get-ChildItem dist -File | Where-Object Name -ne 'SHA256SUMS' | ForEach-Object { + "{0} {1}" -f (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower(), $_.Name + } | Out-File -FilePath dist/SHA256SUMS -Encoding ascii + Get-Content dist/SHA256SUMS + + - name: Attest build provenance + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: | + dist/cmdtab.exe + dist/cmdtab-v*.zip + + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cmdtab-v${{ steps.version.outputs.version }}-win-x86_64 + path: dist/ + if-no-files-found: error + + release: + name: Draft release + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cmdtab-v${{ needs.build.outputs.version }}-win-x86_64 + path: dist + + - name: Create draft release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --draft \ + --verify-tag \ + --title "cmdtab $TAG" \ + --notes "Built from source by GitHub Actions run [\`$GITHUB_RUN_ID\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) at commit \`$GITHUB_SHA\`. See the *Verifying releases* section of the README to verify provenance." \ + dist/cmdtab-v*.zip dist/SHA256SUMS diff --git a/README.md b/README.md index fd3cefa..426f6f6 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,28 @@ You can further customize the scheduled task created by that command by running ### Uninstalling **cmdtab** leaves no trace on your system, except for a registry key if you choose "Yes" when **cmdtab** prompts you about autorun (and the scheduled task mentioned above if you manually created it). You can remove the autorun registry key by running **cmdtab** one last time before you delete `cmdtab.exe` and choose "No" to autorun. +## Verifying releases + +Releases are compiled from source by [GitHub Actions](.github/workflows/build.yml) on GitHub-hosted runners — not on a maintainer's machine — and every release artifact carries a signed [build provenance attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) (SLSA). You can cryptographically verify that a downloaded binary was built by this repository's workflow from a specific public commit. + +#### Verify provenance +Requires the [GitHub CLI](https://cli.github.com) (`gh`): +```console +gh attestation verify cmdtab.exe --repo stianhoiland/cmdtab +``` +This checks the artifact's signature against GitHub's attestation store and prints the source commit and workflow that produced it. It also works on the release zip itself. + +#### Verify checksums +Each release includes a `SHA256SUMS` file generated during the CI build: +```console +sha256sum -c SHA256SUMS +``` +Or on Windows: +```powershell +Get-FileHash cmdtab.exe -Algorithm SHA256 +``` +Compare the output against the corresponding line in `SHA256SUMS`. + ## Buildling from source #### MSVC/CMake