Sign Windows executables and steer Smart App Control users - #592
Conversation
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).
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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; newwindows-verifyCI job assertsStatus=Valid,CN=37signals LLC, and a timestamp. - Adds a presence-only "Binary Signature"
doctorcheck (puredebug/pe) plus first-run Smart App Control diagnosis ininstall.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 rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Smart App Control on Windows 11 blocks the unsigned
basecamp.exewe 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, fromubuntu-latest. Non-Windows targets no-op; all-emptySM_*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 bye2e/sign_windows.bats.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.basecamp_installer.ps1(checksummed + attested), staged in gitignored.release-extra/— goreleaser requiresdistto be empty after before-hooks, so the plan's in-dist staging was adjusted. Raw-maininstall.ps1stays unsigned on purpose.umask 077withalways()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 assertStatus=Valid,CN=37signals LLC, timestamp countersignature present; the installer asset verifies under bothpowershellandpwsh.SM_*manifest rows only. Fleet governance reconciliation (reviewer drift, audit assertions) is deliberately deferred — do not gate onaudit.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.
debug/pe(cross-platform, unit-tested with a synthesized minimal PE32+). A signing-regression canary — warn +basecamp upgradebreadcrumb; never claims validity (Windows enforces that).Get-FirstRunFailureMessagediagnoses 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.verify_installsurfaces captured stderr and adds the SAC/WSL2 hint on Windows platforms.basecamp doctordeliberately 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)
SM_API_KEY,SM_CLIENT_CERT_FILE_B64,SM_CLIENT_CERT_PASSWORDto thereleaseenvironment; verify withgh api repos/basecamp/basecamp-cli/environments/release/secrets --jq '[.secrets[].name]'.Then a signed rc prerelease, Windows verification on a SAC-enabled box with a version-pinned install, and the first signed stable.
Verification
bin/cigreen (incl. 8 new sign-windows bats, 4 new installer bats, 11 new doctor tests).make test-releasegreen: skip notices on both Windows exes + installer copy;basecamp_installer.ps1present in the snapshotchecksums.txt(proves before-hook + extra_files ordering).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
.exeandbasecamp_installer.ps1viajsign+ DigiCert KeyLocker; skips whenSM_*env is absent and fail-closes on partial config.goreleasersigns Windows binaries before archiving so checksums, SBOM, and provenance describe the signed bits; publishesbasecamp_installer.ps1.jsign7.5 (sha256-verified), guardsactions/setup-javaand signing prep to the canonical repo (forks export noSM_*and take the unsigned skip path), and addswindows-verifyto assert Valid status,CN=37signals LLC, and a timestamp on both arches.basecamp doctoradds a "Binary Signature" check (presence-only) on Windows with an upgrade breadcrumb.Dependencies
jsign7.5 andactions/setup-java(Temurin 21) to the release workflow.SM_API_KEY,SM_CLIENT_CERT_FILE_B64, andSM_CLIENT_CERT_PASSWORDin thereleaseenvironment.Written for commit 6c33d4e. Summary will update on new commits.