desktop: keep the mac signing cert off the windows build - #1797
Merged
Conversation
CSC_LINK is electron-builder cross-platform, not mac-only: WinPackager falls back to it when WIN_CSC_LINK is unset. Setting it unconditionally handed the Apple Developer ID cert to the windows leg, which wrote its subject CN into app-update.yml as publisherName. Every Windows client then rejected the update it downloaded because the installer carries no signature Windows trusts. Scope CSC_LINK/CSC_KEY_PASSWORD to the mac legs, and set win.verifyUpdateCodeSignature so the updater manifest stops claiming a publisher the build cannot back up.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | c11bb87 | Commit Preview URL Branch Preview URL |
Aug 28 2026, 03:34 AM |
Contributor
Cloudflare previewTorn down — the PR is closed. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | c11bb87 | Aug 28 2026, 03:35 AM |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
RhysSullivan
marked this pull request as ready for review
August 28, 2026 05:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release-pipeline change — needs explicit review before merge; next Windows release after merge should be manually verified to auto-update.
Windows desktop auto-updates fail on every release with:
An Apple certificate name has no business appearing in a Windows Authenticode check. It gets there because the publish workflow leaks the macOS signing certificate into the Windows build.
Cause
CSC_LINKreads like a macOS variable, but in electron-builder it is the cross-platform certificate variable.WinPackagerfalls back to it wheneverWIN_CSC_LINKis unset:publish-desktop.ymlsetCSC_LINK/CSC_KEY_PASSWORDin theBuild desktop distributablesstep unconditionally, and the build matrix includes awindows-latestleg. So the Windows leg received the Apple Developer ID.p12.From there the failure is fully determined, and it does not depend on whether signing itself succeeded:
win.verifyUpdateCodeSignaturedefaults totrue, soWinPackager.isForceCodeSigningVerificationis true.PublishManager.getAppUpdatePublishConfigurationtherefore asks for a publisher name and writes it intoapp-update.yml.WindowsSignToolManager.computedPublisherNamehas no explicitpublisherNameconfigured, so it falls through to the certificate's subject Common Name — parsed out of whatever.p12was supplied, with no check that it is a Windows code-signing certificate. That yieldsDeveloper ID Application: ....NsisUpdater.doDownloadUpdatecallsverifySignature, which shells out toGet-AuthenticodeSignature. The installer either carries no signature or one chaining to Apple's CA, which Windows does not trust for code signing.data.Status !== 0, the name comparison is never reached, and the updater throwsERR_UPDATER_INVALID_SIGNATUREwith the expected publisher name in the message.Worth noting for anyone reading the error: the name printed after
publisherNames:is what the updater expected, not what it found. It reads like a mismatch when the real problem is that there is no trusted signature at all. The observed certificate is in theraw info:JSON that follows.forceCodeSigningdefaults tofalse, so a failed Windows signing attempt never failed the build — the release shipped an installer that could not update itself.The fix
Two changes, both making the build honest about the fact that we have no Windows certificate.
1.
.github/workflows/publish-desktop.yml— scope the certificate variables to the mac legs:Empty string is the documented "no certificate" value, not a hack —
WindowsSignToolManagershort-circuits on it explicitly:This is also already the fork/local behaviour today, where the secrets are unset and evaluate to
''. The mac legs are unaffected: onmatrix.platform == 'mac'the expression yields exactly the previous value.APPLE_API_KEY/APPLE_API_KEY_ID/APPLE_API_ISSUERare deliberately left unscoped — they are only read by notarytool on darwin and are inert elsewhere. Scoping them would have been diff noise in a release workflow.2.
apps/desktop/electron-builder.config.ts— state the invariant in config:Change 1 alone fixes the bug: with no certificate, no
publisherNameis written, andNsisUpdater.verifySignaturereturnsnullearly and skips verification entirely. Change 2 is the durable guard — it stops the manifest from ever claiming a publisher we cannot back up, so a future edit that reintroduces a certificate variable globally cannot silently break updates again.Two details that make this safe rather than a blunt "turn off security":
isForceCodeSigningVerificationis read in exactly one place inapp-builder-lib—PublishManager.js:204, computing the updater manifest. Nothing in the signing path consults it.publisherName: []would fail every update ([] == nullis false, the match loop never runs); omitting the key is what skips verification. This change omits the key.Verification
This cannot be end-to-end verified locally, and I want to be plain about that. Proving the fix requires building a signed-manifest NSIS installer on a Windows runner, publishing it to a real GitHub release, and having a previously-installed Windows client download and accept it. No part of that is reproducible on this machine, and no test in this repo covers the release pipeline.
What I did validate:
.github/workflows/publish-desktop.ymlparses as YAML, and theBuild desktop distributablesenv block resolves as intended across all four matrix legs.actionlinton the changed workflow reports no new findings. The one finding it does report — theblacksmith-4vcpu-ubuntu-2404custom runner label — is present identically onmainand is unrelated.bun run typecheckpasses inapps/desktop, confirmingverifyUpdateCodeSignatureis a real field onWindowsConfigurationin the pinned version rather than a silently-ignored key. This matters:win.publisherNamefrom older electron-builder majors is not a valid key in the installed version and would have been silently dropped.electron-updater6.8.3 andapp-builder-lib26.8.1 as pinned in the lockfile. Every quoted snippet is fromnode_modules, cross-checked against upstreammaster.The real verification is the next Windows release. After merging, publish a release, install it on Windows, then publish a subsequent one and confirm the client updates itself. Until that happens this fix is reasoned, not demonstrated.
Long-term: get a real Windows certificate
Shipping unsigned Windows installers is the honest description of where we are, not a good end state. Unsigned builds still trigger SmartScreen warnings on download and install, and unverified updates mean a compromised release asset would be accepted by clients. Signature verification is genuinely worth having; we just cannot claim it without a certificate.
The recommended path as of now is Azure Trusted Signing (recently rebranded Azure Artifact Signing). Microsoft runs the identity validation and holds the key material, so there is no EV hardware token or HSM to buy and manage, and no three-year certificate to rotate by hand. It is markedly cheaper and less operationally painful than a traditional EV certificate.
What adopting it would take:
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET, scoped to the Windows leg exactly as the Apple secrets are now scoped to mac.win.azureSignOptions:endpoint(region-matched),codeSigningAccountName,certificateProfileName, andpublisherName.verifyUpdateCodeSignature: falsein the same change, or verification stays off for a signed build.Two sharp edges to plan around when we do it:
publisherNameis required and has no certificate-derived fallback —WindowsSignAzureManager.computedPublisherNamereturnsnullwhen it is unset, which silently disables update verification rather than erroring. It must be set explicitly and must match the certificate subject.app-builder-lib26.8.1 implements Azure signing by installing a PowerShell module (TrustedSigning) and callingInvoke-TrustedSigning, so it needs the Windows runner and build-time network access to Azure. Upstream has since moved to asigntool /dlibapproach and calls the PowerShell route deprecated, so an electron-builder bump is worth pairing with this work.A traditional OV/EV certificate from a CA is the alternative. It is more expensive, requires managing a hardware token in CI, and for a fresh OV certificate SmartScreen reputation still has to be earned from zero — Trusted Signing is the better trade.
Residual risks
latest.yml, not on a code signature.verifyUpdateCodeSignature: falseis a footgun if forgotten. If a Windows certificate is added later and this line is left behind, builds will be signed but updates will not be verified — failing open, silently. The inline comment flags it; the failure mode is no worse than today's post-fix state, which is why it is preferred over leaving the manifest able to claim a publisher we cannot honour.matrix.platform == 'mac', but a release-pipeline reviewer should confirm the mac signing and notarization steps still behave as expected on the next release.