WIP: ci(windows): optionally sign builder executables with Authenticode #98

Draft
mooki wants to merge 1 commit from mooki/Longshot:ci/windows-authenticode into main
First-time contributor

Why

Signed release checksums do not give the Windows builder a verified publisher. This prepares optional Authenticode signing before packaging, without buying a certificate, choosing a legal publisher, or claiming that SmartScreen warnings are solved.

What changed

  • scripts/authenticode.py: certificate_for permits only this repository's main/version-tag pushes; absent configuration logs not signed: no certificate configured and succeeds unsigned. sign_builder names only the setup and the published WPF apphost, uses Windows' built-in PowerShell to sign with SHA256 and a timestamp, then requires a trusted embedded signature by the selected certificate and a timestamp certificate. Failed signing or verification stops packaging. Provider output is never echoed.
  • .forgejo/workflows/release.yml and build-windows.yml: explicit optional certificate-selector secret, boolean configuration output, release-only runner selection and signing after compilation/publish but before both setup copies and the zip. The triggering commit must match. PRs and manual build dispatches do not sign. No action pins, tools.lock, scripts/tools.py, or checksum-signature rules changed; no new tool is downloaded in CI.
  • tests/test_authenticode.py: 12 portable decision/command/failure/packaging tests, with a stand-in subprocess result rather than real Windows signing.
  • tests/authenticode_standin.py: an explicit Linux mechanics test with a temporary self-signed certificate and authored PE. It verifies embedded signatures in the three package paths and rejects altered code. It does not exercise Windows PowerShell, a provider, a timestamp server or public publisher trust.
  • docs/releases.md, RELEASING.md, docs/forgejo-runners.md: separate the two signature layers, inventory the files, explain setup/rotation and source the certificate/cost choices. A signing runner's label is not an access control: its admission and provider authorization must exclude PR workloads before any identity is provisioned.

This covers two unique Longshot builder executables under three zip paths: LongshotBuilder.exe (setup), LongshotBuilder/LongshotBuilder-Setup.exe (Repair's identical copy), and LongshotBuilder/LongshotBuilder.exe (WPF apphost). Third-party executables are untouched. The launcher is built in its own repository and needs its own integration. The player's locally compiled game is not signed by this step.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
AUTHENTICODE_CERTIFICATE_SHA1 new repository secret: Selects the exact 40-hex certificate thumbprint in the signing account's CurrentUser/My store; not a private key absent, unsigned Off by default: until publisher/provider/isolation are agreed configured Windows releases PRs, Linux and local game binaries real Windows signing provider (should help on: approved certificate-store providers)
build-windows.yml input authenticode new: Selects the isolated signing runner and guarded step, only on a matching release push false Off by default: release.yml computes it from the secret configured release builds PR/manual builds Forgejo workflow execution (should help on: release integration)
authenticode.py plan / sign --setup ... --publish ... new script commands: Plans the boolean configuration or signs/verifies the two named outputs before packaging only called by the workflow Off by default: explicit release commands Windows packaging game runtime actual Windows PowerShell (should help on: approved certificate-store providers)
authenticode_standin.py --osslsigncode ... new test argument: Names an already available signing tool for the explicit self-signed mechanics test not run by the suite Off by default: optional test Linux mechanics proof public trust or Windows execution other hosts

The timestamp URL is http://timestamp.digicert.com. This is the Windows cmdlet's documented HTTP Authenticode protocol, not a new RFC 3161 client; a missing timestamp fails verification. Private keys stay with the chosen provider. Azure/SignPath service APIs are not implemented by this certificate-store route.

Measurements

Base: 77f605a25f
Head: 57698f8b0f
Suite totals: main 509 passed, 0 failed, 19 skipped; this PR 510 passed, 0 failed, 19 skipped.

main: 77f605a25 (2026-11-13 UTC)
this PR: move/ci/windows-authenticode @ 57698f8b
Hardware: Linux x86-64, kernel 7.2.15-1-omarchy, Python 3.14.8; clang/LLD 23.1.3 for the authored PE
Scene: game-free packaging; no GPU or gameplay measurement
Method: one complete source-suite result per commit, named unit tests, and one cryptographic stand-in. These are correctness checks, not an interleaved performance comparison or a Windows trust measurement.
How to reproduce: python -I -B tests/run_all.py --jobs 6 --strict; python -I -B tests/test_authenticode.py; python -I -B tests/test_tools_lock.py; python -I -B tests/test_recomplauncher_json.py; python -I -B tests/test_release.py; optional python -I -B tests/authenticode_standin.py --osslsigncode <tool> with osslsigncode 2.15, openssl, clang and lld-link available.

main @ 77f605a25 this PR @ 57698f8b
Whole game-free suite 509 passed, 0 failed, 19 skipped 510 passed, 0 failed, 19 skipped
Signing decision/command tests not present 12 passed
Authored PE mechanics stand-in not run embedded signature verifies, all 3 package paths verify, altered code rejected
main: 509 passed, 0 failed, 19 skipped in 203 s (linux, 6 at once)
this PR: 510 passed, 0 failed, 19 skipped in 272 s (linux, 6 at once)
stand-in: authored PE embedded signature verifies with explicit throwaway-certificate trust
stand-in: unsigned 1536 bytes; signed 3472 bytes
stand-in: all 3 packaged EXE paths preserve the signature; release checksum covers the finished zip
stand-in: modifying .text is rejected by embedded-signature verification
PASS: mechanics stand-in only; no Windows execution, public trust, provider or timestamp-service proof

Testing

Tested: on Linux, the final-head source suite above; 12 Authenticode unit tests, 10 tools-lock tests, 4 RecompLauncher/package tests and 18 release tests pass. The release tests emitted ResourceWarnings about their local HTTP-test sockets/cleanup, not failures. The first Authenticode test run had 5 failures and 4 errors because the authored fake thumbprint had 42 rather than 40 characters; correcting only that fixture made all 12 pass. The production guard was not relaxed.

The actual cryptographic stand-in used a throwaway self-signed RSA certificate and a tiny cross-compiled PE, not a game, builder or launcher binary. osslsigncode trusted that certificate explicitly for the test, verified all copied signatures and rejected a .text mutation. The temporary key/files were removed; no system trust store changed. This proves mechanics, not publisher trust.

Not tested: Windows PowerShell or Windows trust-store execution, a hardware/cloud provider, the timestamp service, real Forgejo jobs or release publication, the Windows builder/launcher on Windows, or SmartScreen/Defender behaviour. The authored PE was not executed. No game build/run or performance measurement; no runtime code changes. No real certificate was obtained or configured.

Needs other testers:

  • Windows 10/11 with the chosen certificate/provider: verify all three project EXE paths have the agreed publisher and timestamp, and wrong/missing/invalid signatures fail before publication (request to open)
  • Forgejo release and fork-PR jobs: check missing-certificate success/logging, the matching-commit signing runner, PR exclusion and independent key-access denial, and checksums made only after successful signing (request to open)

Screenshots

Nothing on screen. Windows prompts have not been observed.

Risk and rollback

Configured signing can stop releases if the provider, trust chain or timestamp service fails. This is intentional: no package follows failed verification. A runner label alone cannot protect a signing identity from an untrusted workflow; runner admission and provider authorization must do so before the secret is enabled.

Reverting this commit removes the optional step. Removing the certificate-selector secret returns to explicitly unsigned output; do not do that silently once a channel promises signed releases. Existing project-key release signatures are unchanged.

Reviewer notes

Read certificate_for, sign_builder, verify_result and the two workflow guards first. The private key stays outside the repository and command arguments. Only a certificate selector is stored as the repository secret. Windows' OS-supplied PowerShell needs no new downloadable tool pin or player tool archive.

Maintainers still choose whether to buy/apply, the legal publisher, OV/EV or a signing service, custodians and release approvers, and which channels will eventually require a certificate even if configuration is missing. Sources and estimated costs are in the release docs; EV is not an immediate SmartScreen bypass. Setup resource versioning and the launcher's separate integration remain parts of issue 46. This stays WIP for the real Windows/provider/workflow proof and those decisions.

Issues

See #46

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). Requests are still to open.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. One source-suite result per commit; no timing claim.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README). Release-only settings documented in release docs; no runtime setting.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.

New on this forge; not opened on GitHub.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]

## Why Signed release checksums do not give the Windows builder a verified publisher. This prepares optional Authenticode signing before packaging, without buying a certificate, choosing a legal publisher, or claiming that SmartScreen warnings are solved. ## What changed - `scripts/authenticode.py`: `certificate_for` permits only this repository's main/version-tag pushes; absent configuration logs `not signed: no certificate configured` and succeeds unsigned. `sign_builder` names only the setup and the published WPF apphost, uses Windows' built-in PowerShell to sign with SHA256 and a timestamp, then requires a trusted embedded signature by the selected certificate and a timestamp certificate. Failed signing or verification stops packaging. Provider output is never echoed. - `.forgejo/workflows/release.yml` and `build-windows.yml`: explicit optional certificate-selector secret, boolean configuration output, release-only runner selection and signing **after compilation/publish but before both setup copies and the zip**. The triggering commit must match. PRs and manual build dispatches do not sign. No action pins, `tools.lock`, `scripts/tools.py`, or checksum-signature rules changed; no new tool is downloaded in CI. - `tests/test_authenticode.py`: 12 portable decision/command/failure/packaging tests, with a stand-in subprocess result rather than real Windows signing. - `tests/authenticode_standin.py`: an explicit Linux mechanics test with a temporary self-signed certificate and authored PE. It verifies embedded signatures in the three package paths and rejects altered code. It does **not** exercise Windows PowerShell, a provider, a timestamp server or public publisher trust. - `docs/releases.md`, `RELEASING.md`, `docs/forgejo-runners.md`: separate the two signature layers, inventory the files, explain setup/rotation and source the certificate/cost choices. A signing runner's label is not an access control: its admission and provider authorization must exclude PR workloads before any identity is provisioned. This covers two unique Longshot builder executables under three zip paths: `LongshotBuilder.exe` (setup), `LongshotBuilder/LongshotBuilder-Setup.exe` (Repair's identical copy), and `LongshotBuilder/LongshotBuilder.exe` (WPF apphost). Third-party executables are untouched. The launcher is built in its own repository and needs its own integration. The player's locally compiled game is not signed by this step. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `AUTHENTICODE_CERTIFICATE_SHA1` | new repository secret: Selects the exact 40-hex certificate thumbprint in the signing account's CurrentUser/My store; not a private key | absent, unsigned | Off by default: until publisher/provider/isolation are agreed | configured Windows releases | PRs, Linux and local game binaries | real Windows signing provider (should help on: approved certificate-store providers) | | `build-windows.yml` input `authenticode` | new: Selects the isolated signing runner and guarded step, only on a matching release push | false | Off by default: release.yml computes it from the secret | configured release builds | PR/manual builds | Forgejo workflow execution (should help on: release integration) | | `authenticode.py plan` / `sign --setup ... --publish ...` | new script commands: Plans the boolean configuration or signs/verifies the two named outputs before packaging | only called by the workflow | Off by default: explicit release commands | Windows packaging | game runtime | actual Windows PowerShell (should help on: approved certificate-store providers) | | `authenticode_standin.py --osslsigncode ...` | new test argument: Names an already available signing tool for the explicit self-signed mechanics test | not run by the suite | Off by default: optional test | Linux mechanics proof | public trust or Windows execution | other hosts | The timestamp URL is `http://timestamp.digicert.com`. This is the Windows cmdlet's documented HTTP Authenticode protocol, not a new RFC 3161 client; a missing timestamp fails verification. Private keys stay with the chosen provider. Azure/SignPath service APIs are not implemented by this certificate-store route. ## Measurements <!-- branch-state --> Base: 77f605a25f175fbda9ad131ac6be96bbde85d9c2 Head: 57698f8b0f0c7e849fc403fd5d74e7e47fbcc6a3 Suite totals: main 509 passed, 0 failed, 19 skipped; this PR 510 passed, 0 failed, 19 skipped. <!-- /branch-state --> main: 77f605a25 (2026-11-13 UTC) this PR: move/ci/windows-authenticode @ 57698f8b Hardware: Linux x86-64, kernel 7.2.15-1-omarchy, Python 3.14.8; clang/LLD 23.1.3 for the authored PE Scene: game-free packaging; no GPU or gameplay measurement Method: one complete source-suite result per commit, named unit tests, and one cryptographic stand-in. These are correctness checks, not an interleaved performance comparison or a Windows trust measurement. How to reproduce: `python -I -B tests/run_all.py --jobs 6 --strict`; `python -I -B tests/test_authenticode.py`; `python -I -B tests/test_tools_lock.py`; `python -I -B tests/test_recomplauncher_json.py`; `python -I -B tests/test_release.py`; optional `python -I -B tests/authenticode_standin.py --osslsigncode <tool>` with osslsigncode 2.15, openssl, clang and lld-link available. | | main @ 77f605a25 | this PR @ 57698f8b | |---|---|---| | Whole game-free suite | 509 passed, 0 failed, 19 skipped | 510 passed, 0 failed, 19 skipped | | Signing decision/command tests | not present | 12 passed | | Authored PE mechanics stand-in | not run | embedded signature verifies, all 3 package paths verify, altered code rejected | ``` main: 509 passed, 0 failed, 19 skipped in 203 s (linux, 6 at once) this PR: 510 passed, 0 failed, 19 skipped in 272 s (linux, 6 at once) stand-in: authored PE embedded signature verifies with explicit throwaway-certificate trust stand-in: unsigned 1536 bytes; signed 3472 bytes stand-in: all 3 packaged EXE paths preserve the signature; release checksum covers the finished zip stand-in: modifying .text is rejected by embedded-signature verification PASS: mechanics stand-in only; no Windows execution, public trust, provider or timestamp-service proof ``` ## Testing **Tested:** on Linux, the final-head source suite above; 12 Authenticode unit tests, 10 tools-lock tests, 4 RecompLauncher/package tests and 18 release tests pass. The release tests emitted ResourceWarnings about their local HTTP-test sockets/cleanup, not failures. The first Authenticode test run had 5 failures and 4 errors because the authored fake thumbprint had 42 rather than 40 characters; correcting only that fixture made all 12 pass. The production guard was not relaxed. The actual cryptographic **stand-in** used a throwaway self-signed RSA certificate and a tiny cross-compiled PE, not a game, builder or launcher binary. osslsigncode trusted that certificate explicitly for the test, verified all copied signatures and rejected a `.text` mutation. The temporary key/files were removed; no system trust store changed. This proves mechanics, **not publisher trust**. **Not tested:** Windows PowerShell or Windows trust-store execution, a hardware/cloud provider, the timestamp service, real Forgejo jobs or release publication, the Windows builder/launcher on Windows, or SmartScreen/Defender behaviour. The authored PE was not executed. No game build/run or performance measurement; no runtime code changes. No real certificate was obtained or configured. **Needs other testers:** - Windows 10/11 with the chosen certificate/provider: verify all three project EXE paths have the agreed publisher and timestamp, and wrong/missing/invalid signatures fail before publication (request to open) - Forgejo release and fork-PR jobs: check missing-certificate success/logging, the matching-commit signing runner, PR exclusion and independent key-access denial, and checksums made only after successful signing (request to open) ## Screenshots Nothing on screen. Windows prompts have not been observed. ## Risk and rollback Configured signing can stop releases if the provider, trust chain or timestamp service fails. This is intentional: no package follows failed verification. A runner label alone cannot protect a signing identity from an untrusted workflow; runner admission and provider authorization must do so before the secret is enabled. Reverting this commit removes the optional step. Removing the certificate-selector secret returns to explicitly unsigned output; do not do that silently once a channel promises signed releases. Existing project-key release signatures are unchanged. ## Reviewer notes Read `certificate_for`, `sign_builder`, `verify_result` and the two workflow guards first. The private key stays outside the repository and command arguments. Only a certificate selector is stored as the repository secret. Windows' OS-supplied PowerShell needs no new downloadable tool pin or player tool archive. Maintainers still choose whether to buy/apply, the legal publisher, OV/EV or a signing service, custodians and release approvers, and which channels will eventually require a certificate even if configuration is missing. Sources and estimated costs are in the release docs; EV is not an immediate SmartScreen bypass. Setup resource versioning and the launcher's separate integration remain parts of issue 46. This stays WIP for the real Windows/provider/workflow proof and those decisions. ## Issues See #46 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). Requests are still to open. - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. One source-suite result per commit; no timing claim. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). Release-only settings documented in release docs; no runtime setting. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. --- New on this forge; not opened on GitHub. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <[email protected]>
ci(windows): optionally sign builder executables with Authenticode
Some checks failed
PR description / Description follows the template (pull_request_target) Waiting to run
Linux AppImage / appimage (pull_request) Blocked by required conditions
build-windows.yml / ci(windows): optionally sign builder executables with Authenticode (pull_request) Failing after 0s
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
57698f8b0f
Sign the setup and WPF apphost before packaging only for configured release pushes. Check Windows trust, the selected signer and a timestamp before allowing the package to proceed. Keep absent-certificate builds explicitly unsigned and signing identities off PR workers.

Document the separate release-checksum trust layer, certificate choices, ownership and rotation. Add portable decision tests and an explicit self-signed PE mechanics stand-in; neither claims public publisher trust or Windows execution.

See #46

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Some checks failed
PR description / Description follows the template (pull_request_target) Waiting to run
Linux AppImage / appimage (pull_request) Blocked by required conditions
build-windows.yml / ci(windows): optionally sign builder executables with Authenticode (pull_request) Failing after 0s
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'ci/windows-authenticode:mooki-ci/windows-authenticode'
git switch 'mooki-ci/windows-authenticode'
Sign in to join this conversation.
No description provided.