Skip to content

Sign Windows executables and steer Smart App Control users - #592

Merged
jeremy merged 2 commits into
mainfrom
windows-signing
Jul 31, 2026
Merged

Sign Windows executables and steer Smart App Control users#592
jeremy merged 2 commits into
mainfrom
windows-signing

Conversation

@jeremy

@jeremy jeremy commented Jul 30, 2026

Copy link
Copy Markdown
Member

Smart App Control on Windows 11 blocks the unsigned basecamp.exe we ship — regardless of download source, with no per-app exceptions (Help Scout 3402953784). This PR signs everything Windows in the release pipeline and gives users on unsigned builds honest diagnosis and workarounds today.

Release pipeline (Part 1)

  • scripts/sign-windows.sh — env-gated Authenticode wrapper: jsign + DigiCert KeyLocker (cloud HSM), the same cert infrastructure bc3-desktop signs with, from ubuntu-latest. Non-Windows targets no-op; all-empty SM_* env skips with a notice (forks, make test-release); partial config or signing failure hard-fails so a tagged release can never silently ship unsigned. Contract pinned by e2e/sign_windows.bats.
  • goreleaser build post-hook signs each Windows exe in place before archiving — the zip, checksums.txt, cosign bundle, Scoop manifest hash, and provenance attestation all describe the signed binary. Verified: the hook fires per-target (windows_amd64_v1, windows_arm64_v8.0) and no-ops on the other 8.
  • Signed installer copy ships as basecamp_installer.ps1 (checksummed + attested), staged in gitignored .release-extra/ — goreleaser requires dist to be empty after before-hooks, so the plan's in-dist staging was adjusted. Raw-main install.ps1 stays unsigned on purpose.
  • release.yml: Windows secrets preflight (twin of the macOS one), temurin 21 + jsign 7.5 pinned by sha256, client cert decoded under umask 077 with always() cleanup, SIGN_ALIAS (DigiCert certificate ID, expires 2027-04-30, keep in sync with bc3-desktop) pinned in exactly one place. New windows-verify job: both arches assert Status=Valid, CN=37signals LLC, timestamp countersignature present; the installer asset verifies under both powershell and pwsh.
  • manage-release-env.sh: three SM_* manifest rows only. Fleet governance reconciliation (reviewer drift, audit assertions) is deliberately deferred — do not gate on audit.

Docs, installer messaging, doctor (Part 2)

Wording anchors on the fixed fact "v0.8.0-rc.1 and earlier ship an unsigned basecamp.exe" — nothing to revise at release time.

  • doctor "Binary Signature" check: presence-only Authenticode probe via pure debug/pe (cross-platform, unit-tested with a synthesized minimal PE32+). A signing-regression canary — warn + basecamp upgrade breadcrumb; never claims validity (Windows enforces that).
  • install.ps1: Get-FirstRunFailureMessage diagnoses the first-run failure (unsigned+SAC on → WSL2-preferred guidance with the stay-off caveat; unsigned+SAC off → Protection history; else generic), every branch leading with the original error. Copy-Item and missing-exe failures gain quarantine guidance. All three branches driven by AST-extraction tests under real pwsh 7.6.4.
  • install.sh verify_install surfaces captured stderr and adds the SAC/WSL2 hint on Windows platforms.
  • README + install.md: Smart App Control section — no per-app exceptions; WSL2 preferred; SAC off must stay off until upgraded to a signed build (March/April 2026 Win11 updates make re-enabling reset-free, older builds need a reset); separate SmartScreen note. basecamp doctor deliberately not mentioned yet (stable v0.7.2 lacks the check) — joins in a post-release follow-up.

Ops preflight before the first signed tag (human, out-of-repo)

  1. 1Password Development vault → "DigiCert CodeSigning Cert": confirm fields, add a base64 text field for the client p12 if absent.
  2. Push SM_API_KEY, SM_CLIENT_CERT_FILE_B64, SM_CLIENT_CERT_PASSWORD to the release environment; verify with gh api repos/basecamp/basecamp-cli/environments/release/secrets --jq '[.secrets[].name]'.
  3. Confirm KeyLocker quota headroom with the bc3-desktop cert owner (3 signatures/tag; rc + stable ≥ 6).

Then a signed rc prerelease, Windows verification on a SAC-enabled box with a version-pinned install, and the first signed stable.

Verification

  • bin/ci green (incl. 8 new sign-windows bats, 4 new installer bats, 11 new doctor tests).
  • make test-release green: skip notices on both Windows exes + installer copy; basecamp_installer.ps1 present in the snapshot checksums.txt (proves before-hook + extra_files ordering).
  • pwsh tests run for real against PowerShell 7.6.4 locally (not just skipped).
  • actionlint + zizmor clean on release.yml.

Note: this PR touches scripts/manage-release-env.sh, so the sensitive-change gate will fire — expected.


Summary by cubic

Signs all Windows executables and ships a signed PowerShell installer in CI, and adds clear guidance for Windows 11 Smart App Control blocks. This makes Windows releases verifiably signed and helps users on older unsigned builds.

  • New Features

    • CI signs Windows .exe and basecamp_installer.ps1 via jsign + DigiCert KeyLocker; skips when SM_* env is absent and fail-closes on partial config.
    • goreleaser signs Windows binaries before archiving so checksums, SBOM, and provenance describe the signed bits; publishes basecamp_installer.ps1.
    • Release workflow preflights Windows secrets, pins Temurin 21 and jsign 7.5 (sha256-verified), guards actions/setup-java and signing prep to the canonical repo (forks export no SM_* and take the unsigned skip path), and adds windows-verify to assert Valid status, CN=37signals LLC, and a timestamp on both arches.
    • basecamp doctor adds a "Binary Signature" check (presence-only) on Windows with an upgrade breadcrumb.
    • Installers surface first-run stderr and diagnose Smart App Control; README/install docs explain SAC workarounds (WSL2 preferred).
  • Dependencies

    • Adds jsign 7.5 and actions/setup-java (Temurin 21) to the release workflow.
    • Requires SM_API_KEY, SM_CLIENT_CERT_FILE_B64, and SM_CLIENT_CERT_PASSWORD in the release environment.

Written for commit 6c33d4e. Summary will update on new commits.

Review in cubic

Smart App Control blocks the unsigned basecamp.exe we ship, with no
per-app exceptions (Help Scout 3402953784). Sign everything Windows in
the release pipeline, and meet users on unsigned builds with real
diagnosis and workarounds.

Release pipeline:
- scripts/sign-windows.sh: env-gated Authenticode wrapper (jsign +
  DigiCert KeyLocker, the same cert infrastructure bc3-desktop uses).
  Skips cleanly when signing is unconfigured (forks, make test-release);
  hard-fails on partial config or signing errors so a tagged release can
  never silently ship unsigned.
- goreleaser build post-hook signs each Windows exe in place before
  archiving, so the zip, checksums.txt, cosign bundle, Scoop manifest
  hash, and provenance attestation all describe the signed binary.
- A signed copy of install.ps1 ships as basecamp_installer.ps1,
  checksummed and attested like every other artifact. Staged outside
  dist/ (goreleaser requires dist to be empty after before-hooks).
- release.yml: Windows secrets preflight, temurin + pinned jsign 7.5
  (sha256-verified), client cert decoded under umask 077 with always()
  cleanup, and a windows-verify job asserting Status=Valid,
  CN=37signals LLC, and a timestamp countersignature on both arches,
  plus the installer signature under both powershell and pwsh.
- e2e/sign_windows.bats pins the wrapper's fail-closed contract.

Docs and UX for unsigned builds (v0.8.0-rc.1 and earlier):
- doctor gains a "Binary Signature" check: presence-only Authenticode
  probe via debug/pe, a signing-regression canary with an upgrade
  breadcrumb. No validity claims: Windows enforces those.
- install.ps1 diagnoses first-run failures (SAC on, SAC off/quarantine,
  generic), always leading with the original error. install.sh stops
  swallowing verify_install stderr and adds the SAC hint on Windows.
- README and install.md document Smart App Control: WSL2 preferred, or
  SAC off staying off until upgraded to a signed build (the March/April
  2026 Windows 11 updates make re-enabling reset-free; older builds
  need a reset).
- RELEASING.md: SM_* secrets, cert identity and 2027-04-30 expiry,
  KeyLocker quota notes (3 signatures per tag).
Copilot AI review requested due to automatic review settings July 30, 2026 23:31
@github-actions

Copy link
Copy Markdown

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/release.yml
  • .goreleaser.yaml
  • scripts/manage-release-env.sh

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) ci CI/CD workflows docs labels Jul 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84cf74b9d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes a Windows distribution gap: Smart App Control on Windows 11 blocks the unsigned basecamp.exe, so the release pipeline now Authenticode-signs every Windows artifact (both .exe arches plus a basecamp_installer.ps1 copy) from Linux CI via jsign + DigiCert KeyLocker, and the installers/docs/doctor give users on unsigned builds honest diagnosis and workarounds. It fits into the existing goreleaser + release.yml release flow, mirroring the established macOS signing/notarization pattern.

Changes:

  • Adds scripts/sign-windows.sh (env-gated, fail-closed jsign wrapper) wired into goreleaser build post-hooks and a before-hook that signs a released installer copy; new windows-verify CI job asserts Status=Valid, CN=37signals LLC, and a timestamp.
  • Adds a presence-only "Binary Signature" doctor check (pure debug/pe) plus first-run Smart App Control diagnosis in install.ps1/install.sh.
  • Extensive docs (README/install.md/RELEASING.md) and tests (bats + Go) covering the new behavior.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/sign-windows.sh New fail-closed Authenticode wrapper (jsign + DigiCert KeyLocker).
scripts/manage-release-env.sh Adds three SM_* secret manifest rows.
scripts/install.sh verify_install surfaces captured stderr and a Windows SAC/WSL2 hint.
scripts/install.ps1 Adds Get-FirstRunFailureMessage and quarantine guidance for install failures.
RELEASING.md Documents Windows signing, secrets, quota, and failure behavior.
README.md / install.md New Smart App Control / SmartScreen troubleshooting sections.
Makefile Clears SM_* env for test-release so signing skips locally.
internal/commands/doctor.go New Windows "Binary Signature" check + upgrade breadcrumb.
internal/commands/doctor_test.go Synthesized-PE unit tests for the signature probe.
e2e/sign_windows.bats / e2e/installer.bats Contract tests for the wrapper and installer diagnosis.
.goreleaser.yaml Build post-hook signing, installer copy, checksum/release extra_files.
.gitignore Ignores gitignored .release-extra/ staging dir.
.github/workflows/release.yml Windows secrets preflight, jsign setup, cleanup, windows-verify job.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/release.yml
On a fork, the secrets preflight is skipped but "Prepare Windows
signing" still ran: decoding the empty SM_CLIENT_CERT_FILE_B64 secret
exported a nonempty SM_CLIENT_CERT_FILE path, which the signing wrapper
correctly treats as partial configuration and fails — breaking the
fork/unsigned-release fallback.

Guard the Java setup and preparation steps with the same repository
check the preflight uses. Forks now export no SM_* signing env, so the
wrapper takes its all-empty skip path and releases unsigned; canonical
releases keep the fail-closed contract.
Copilot AI review requested due to automatic review settings July 31, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

@jeremy
jeremy merged commit 73dd871 into main Jul 31, 2026
42 of 60 checks passed
@jeremy
jeremy deleted the windows-signing branch July 31, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD workflows commands CLI command implementations docs tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants