Skip to content

Sign Windows release binaries via SignPath#70

Draft
jamescrosswell wants to merge 2 commits into
mainfrom
chore/windows-signing
Draft

Sign Windows release binaries via SignPath#70
jamescrosswell wants to merge 2 commits into
mainfrom
chore/windows-signing

Conversation

@jamescrosswell
Copy link
Copy Markdown
Contributor

@jamescrosswell jamescrosswell commented Jun 4, 2026

Adds Authenticode signing for the Windows release binaries via SignPath (Foundation plan — free for OSS), mirroring the macOS signing gating pattern. Split out of #43 (Windows distribution ships unsigned in the interim).

History: the first commit implemented local .pfx + signtool. That was dropped because, since the June-2023 CA/Browser Forum code-signing baseline requirements, publicly-trusted OV/EV private keys must live on FIPS-140 hardware — so a freely-exportable .pfx for CI is no longer realistic. SignPath signs in the cloud (no key on the runner) and is the closest "free + properly trusted" route for an open-source project.

What this does

  • HAVE_WINDOWS_SIGNING env gate — derived from secrets.SIGNPATH_API_TOKEN presence, same shape as HAVE_SIGNING_CERT/HAVE_NOTARY. Auto-enables once the token exists, no-ops otherwise.
  • On the win-* matrix legs, after Publish / before Archive:
    1. Upload unsigned Windows binary — uploads publish/TuiCode.exe as an unsigned-<rid> GitHub artifact (1-day retention).
    2. Sign Windows binary via SignPathsignpath/github-action-submit-signing-request@v1 submits a signing request that pulls that artifact, signs it in SignPath's cloud, and drops the signed binary back into publish/ so Archive zips the signed one. No key material touches the runner.
  • Release job now filters download-artifact with pattern: tuicode-*, so the unsigned-* signing inputs never get swept into the published release.
  • AGENTS.md § Release workflow documents the secret/variables and the flow.

Configuration surface

Kind Name Purpose
Secret SIGNPATH_API_TOKEN CI user API token; also the gate.
Variable SIGNPATH_ORGANIZATION_ID SignPath org GUID.
Variable SIGNPATH_PROJECT_SLUG SignPath project.
Variable SIGNPATH_SIGNING_POLICY_SLUG e.g. release-signing.
Variable SIGNPATH_ARTIFACT_CONFIGURATION_SLUG how SignPath processes the artifact.

Only the token is secret; the identifiers are non-sensitive repo variables.


Next steps to go live with SignPath

1. Apply to the SignPath Foundation (free OSS) program

  • Submit TuiCode at https://signpath.org/ → Foundation / open-source program and wait for approval. They provide the OV code-signing certificate; you don't buy or hold one.

2. Set up the SignPath organization (in the SignPath web portal)

  • Note the Organization ID (GUID) → repo variable SIGNPATH_ORGANIZATION_ID.
  • Install the SignPath GitHub App on mentaldesk/TuiCode. SignPath origin-verifies every signing request against the repo/workflow via this App — without it the action can't fetch the artifact or prove provenance.
  • Create a ProjectSIGNPATH_PROJECT_SLUG.
  • Create an Artifact Configuration that takes the uploaded artifact (a zip containing TuiCode.exe) and Authenticode-signs that PE → SIGNPATH_ARTIFACT_CONFIGURATION_SLUG.
  • Create a Signing Policy (e.g. release-signing) bound to the Foundation certificate → SIGNPATH_SIGNING_POLICY_SLUG. Decide approval mode (see caveats).
  • Create a CI user + API token → secret SIGNPATH_API_TOKEN.

3. Wire the GitHub repo

  • Settings → Secrets and variables → Actions → Secrets: add SIGNPATH_API_TOKEN.
  • → Variables: add SIGNPATH_ORGANIZATION_ID, SIGNPATH_PROJECT_SLUG, SIGNPATH_SIGNING_POLICY_SLUG, SIGNPATH_ARTIFACT_CONFIGURATION_SLUG.
  • Confirm the SignPath GitHub App is installed on the repo (step 2).

4. Trigger and verify

  • Push a v* tag (or Actions → Release → Run workflow on an existing tag).
  • If the signing policy requires approval, approve the request in the SignPath UI — the workflow waits at wait-for-completion (timeout 1800s).
  • Confirm in the run logs: Sign Windows binary via SignPath succeeded and the archived win-* zip contains a signed TuiCode.exe.

Caveats to decide on

  • Approval mode. Foundation release policies often require a maintainer to approve each signing request. With approval on, every release pauses until someone clicks approve (within the 1800s timeout — bump it if that's too tight). An automatic policy removes the pause; choose per your risk appetite.
  • SmartScreen warm-up. SignPath Foundation issues an OV certificate, so reputation still accrues over download volume — early downloads may still warn. This is not EV (no instant reputation). It's the same trade-off as any OV path, just free and CI-native.
  • GitHub App is mandatory. The action fails origin verification if the SignPath GitHub App isn't installed on the repo.

Acceptance criteria

  • release.yml signs both win-x64 and win-arm64 TuiCode.exe before archiving, gated on secret presence.
  • Cloud signing (SignPath) — no key material on the runner.
  • unsigned-* signing inputs excluded from the published release.
  • SignPath Foundation project approved + org/project/policy/artifact-config created (manual — see above).
  • SIGNPATH_API_TOKEN secret + 4 SIGNPATH_* variables set; GitHub App installed.
  • A tagged release produces a signed win-* binary (verified in logs).
  • Clean-machine SmartScreen check (OV warm-up caveat applies).

🤖 Generated with Claude Code

Mirrors the macOS signing pattern: a secret-presence env gate
(HAVE_WINDOWS_SIGNING) so the step auto-enables once a cert is
configured and no-ops otherwise. Signs TuiCode.exe between Publish
and Archive on the win-* matrix legs so the zipped binary is the
signed one.

Uses a local .pfx (base64 GitHub secret) + signtool. signtool ships
with the Windows SDK but isn't on PATH, so the step locates it under
Windows Kits; its own arch is irrelevant, so the same lookup covers
the windows-11-arm leg without a cross-sign. Always RFC3161-timestamps
so signatures survive cert expiry, then verifies with `verify /pa`.

Cert provisioning + the WINDOWS_PFX_BASE64 / WINDOWS_PFX_PASSWORD
secrets are a manual follow-up; until they exist the step no-ops,
exactly like macOS signing does today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the local-.pfx + signtool approach with SignPath (Foundation
plan, free for OSS). Since the June-2023 CA/Browser Forum baseline
requirements, publicly-trusted OV/EV code-signing keys must live on
FIPS-140 hardware, so a freely-exportable .pfx for CI is no longer a
realistic route. SignPath signs in the cloud — no key material on the
runner — and is the closest OSS equivalent to "free, properly trusted".

Flow on the win-* legs: upload the unsigned TuiCode.exe as an
`unsigned-<rid>` artifact, submit a SignPath signing request that pulls
it, and drop the returned signed binary back into publish/ before
Archive. The release job's download now filters `pattern: tuicode-*`
so the unsigned signing inputs never ship in the release.

Gating moves to SIGNPATH_API_TOKEN (secret); the non-secret identifiers
(org id + slugs) are repo variables. Step no-ops until the token exists,
same as macOS signing. Documented in AGENTS.md § Release workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamescrosswell jamescrosswell changed the title Sign Windows release binaries with signtool Sign Windows release binaries via SignPath Jun 4, 2026
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