Skip to content

Sign the executables Velopack generates - #621

Merged
erikdarlingdata merged 1 commit into
devfrom
ci/sign-velopack-exes
Sep 29, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
ci/sign-velopack-exes

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

vpk pack creates three executables: PerformanceStudio-win-Setup.exe, and the launcher (PerformanceStudio.exe) and Update.exe at the root of PerformanceStudio-win-Portable.zip. The App signing request runs before vpk pack, so it never sees them. Every release through v1.28.0 shipped these three files unsigned. New Windows users can get an "unknown publisher" warning when they run the installer.

This PR signs them after packing, the same way PerformanceMonitor does (its #3288). PerformanceMonitor shipped this in v3.7.1 and v3.8.0.

Before the next release, add the SignTemplate artifact configuration to the PerformanceStudio project on signpath.io. It is the same XML that PerformanceMonitor uses: **/*.exe and **/*.dll with min-matches="0", Authenticode. If the configuration is missing, the new signing step fails, and the release stops before anything is published.

A release run now waits for three SignPath approvals: the App, then these three files, then the SSMS files.

Changes

  • .github/workflows/release.yml
    • vpk pack now runs before Create release, right after the App signing. The GitHub release is still created only after all signing is done.
    • New steps after vpk pack do four things:
      • Collect the three executables.
      • Send them to SignPath with the SignTemplate configuration.
      • Write the signed files back.
      • Fail the job if any .exe in the Velopack output is unsigned.
    • These steps are required, like the App signing. If one fails or the approval times out, nothing is published.
    • SHA256SUMS.txt is now written with LF line endings and no BOM (see below).
  • .github/scripts/postpack_signing.py (new):
    • collect stages Setup.exe and every .exe at the root of Portable.zip.
    • apply replaces Setup.exe, and rebuilds Portable.zip entry by entry. Every entry it does not replace keeps its original bytes.
    • It checks the rebuilt zip against the original before it replaces the original.
  • .github/scripts/verify_release_signatures.py (new):
    • --verify-dir is the release guard. It also has a tag mode that checks a published release, and reads only the byte ranges it needs.
    • The guard allows exactly two unsigned files: the launcher stub and Squirrel.exe inside a .nupkg. releases.win.json records each .nupkg's SHA256, and the delta is built against those bytes. So the .nupkg must not change after packing.
  • .github/workflows/release-signatures.yml (new):
    • It runs both scripts' self-tests on any PR that changes them.
    • It can also check a published release when you start it by hand.
  • .github/workflows/nightly.yml: the same SHA256SUMS.txt fix.

Both scripts are copied from PerformanceMonitor. Only their docstrings, usage text, issue references and the --repo default changed. The workflow comes from PerformanceMonitor too. It pins actions/checkout to the same SHA as the other Studio workflows.

The checksum file

Out-File writes CRLF on Windows. sha256sum -c and shasum -c then read the \r as part of each file name, and report every file as missing. I reproduced this locally with GNU sha256sum 8.32 and shasum: a CRLF line fails with "No such file or directory", and the same line with LF passes. The v1.28.0 file is 572 bytes: the 6 lines add up to 560 characters plus 12 line-end bytes, so it uses CRLF. The new line in both workflows writes LF, and both tools accept the result.

Test Plan

  • Both self-tests pass locally (35 assertions each).
  • Rehearsal on the real v1.28.0 files: Setup.exe, Portable.zip, both 1.28.0 packages, and the 1.27.0 full package that vpk download leaves in the folder.
    • The guard fails with exactly 3 unsigned files: Setup.exe, PerformanceStudio.exe and Update.exe. It allows the 2 members in each .nupkg.
    • collect stages those 3 files.
    • I added a certificate table to each staged file (the self-test's helper). This is the part of a signature that the guard reads. apply then wrote them back: "Setup.exe signed", and "Portable.zip rebuilt with 2 signed member(s), 441 entries preserved".
    • The guard then passes: 10 executables, 0 unsigned, 4 allowed.
    • When the signing response is missing, apply fails and leaves Portable.zip byte-for-byte unchanged.
  • I extracted the original and rebuilt Portable.zip with .NET's ZipFile. Both have 441 files. Only PerformanceStudio.exe and Update.exe differ.
  • The tag mode reports the same 3 unsigned files on v1.28.0, v1.27.0 and v1.20.0. On PerformanceMonitor's v3.8.0 it reports its Setup.exe and launchers as signed.
  • release-signatures passes on this PR.
  • Add SignTemplate on signpath.io before the next release.
  • At the next release, approve three requests. Then check that Setup.exe, PerformanceStudio.exe and Update.exe are signed: run the "Release signatures" workflow by hand with the new tag.

A pull request cannot test the SignPath request itself, or how the real signatures get written back and uploaded. Only a release run does those things. PerformanceMonitor's v3.7.1 and v3.8.0 went through the same steps, with the same SignPath configuration.

Generated with Claude Code

https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX

vpk pack generates Setup.exe, and the launcher (PerformanceStudio.exe)
and Update.exe at the root of Portable.zip. They did not exist when the
App signing request ran, so every release through v1.28.0 shipped them
unsigned while the app payload was signed.

release.yml now packs before the release is created, sends the three
generated executables to SignPath in one more request (the new
SignTemplate artifact configuration), writes the signed files back, and
fails the job if any .exe in the Velopack output is still unsigned. The
two .nupkg members stay unsigned, because releases.win.json pins the
.nupkg's SHA256 and the delta is built against it.

The scripts come from PerformanceMonitor (its #3288), with only the
docstrings, usage text and --repo default changed. release-signatures.yml
runs their self-tests on any PR that touches them, as in PerformanceMonitor.

Also write SHA256SUMS.txt with LF line endings and no BOM, in release.yml
and nightly.yml. Out-File wrote CRLF, and sha256sum -c and shasum -c then
report every file as missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX
@claude

claude Bot commented Sep 29, 2026

Copy link
Copy Markdown

Reviewed the workflow changes and skimmed the two Python scripts. I found nothing blocking.

  • Moving the Velopack pack ahead of the SSMS signing steps looks safe. Nothing in the SSMS steps touches releases/velopack, and vpk upload still runs later against the signed output.
  • Signing failures are meant to stop the job before release creation. The new SignPath step and the --verify-dir guard both run without continue-on-error.
  • I didn't run the scripts. release-signatures.yml runs both --self-test suites on PRs, so CI covers that.
  • The SHA256SUMS.txt change to LF and no BOM is applied in both nightly.yml and release.yml.
  • No app code changed, so the version-sync, TRY_CAST and Blazor linking checks don't apply.

Minor: nightly.yml doesn't sign the Velopack-generated executables. That's fine if nightly is unsigned by design, but it means the nightly Setup.exe and launcher are not comparable to release ones.

@erikdarlingdata
erikdarlingdata merged commit e7f119a into dev Sep 29, 2026
4 checks passed
@erikdarlingdata
erikdarlingdata deleted the ci/sign-velopack-exes branch September 29, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant