Every desktop project I have worked on has made the same estimate: two days at the end for signing and distribution. Every one of them has been wrong, usually by about a week, and always at the worst possible moment — after the code is done, when someone has already been told a date.

Signing is not hard. It is fiddly, undocumented in the places you look first, and full of failure modes whose error messages actively mislead. Here is what is actually involved, so you can price it before you promise it.

What you are actually buying

Both major platforms now treat unsigned software as hostile by default.

On macOS, Gatekeeper refuses to open an unsigned app downloaded from the internet. Not a warning — a dialog saying the app is damaged and should be moved to the Trash, which is a spectacular thing for a paying customer to see.

On Windows, SmartScreen shows a full-screen blue warning naming your app as unrecognised, with the “Run anyway” option hidden behind a “More info” link. Measured conversion loss at that screen is brutal.

Signing is how you get out of both. It does not assert your software is good — only that it comes from an identity someone verified, and has not been altered since.

macOS: sign, notarize, staple

Three steps, and skipping any one leaves you with an app that fails on someone else’s machine while working perfectly on yours.

Signing attaches your Developer ID certificate (Apple Developer Program, $99/year) to the bundle. The rule that catches everyone: you must sign inside out. Every nested framework, helper binary, and embedded executable gets signed before the bundle that contains it. Electron apps ship several helper executables, so a naive single codesign on the .app produces something that looks signed and fails validation.

You also need the hardened runtime, and the hardened runtime needs entitlements — an XML file declaring what your app is allowed to do. Electron typically needs entitlements permitting JIT and unsigned executable memory, because that is how a JavaScript engine works. Omit them and your app launches, then crashes the moment V8 tries to compile anything.

Notarization uploads the signed build to Apple, which scans it and returns a ticket. This is automated, takes anywhere from two minutes to an hour, and is a hard requirement for anything distributed outside the App Store.

Stapling attaches that ticket to the bundle so Gatekeeper can verify offline. Skip it and your app works fine on your machine — which has the ticket cached from notarization — and shows a warning on a user’s machine with no network. This is the single most common “works on my machine” bug in desktop distribution.

Windows: the certificate rules changed

Windows signing used to be simple: buy a certificate, get a .pfx, drop it in CI as a secret, sign in a build step.

That has not been possible since the CA/Browser Forum tightened the rules. Code signing private keys must now live in certified hardware — a physical USB token or a cloud HSM. You cannot export the key, which means you cannot put a .pfx in a CI secret, which means every pipeline built the old way broke.

The practical options are cloud signing services: Azure Trusted Signing, DigiCert KeyLocker, SSL.com eSigner. Your CI authenticates to the service, sends a hash, and gets a signature back. The key never leaves the HSM. This works well once configured, and configuring it is a day you did not plan for.

Then there is SmartScreen reputation, which is the part that surprises people who did everything right.

A standard (OV) certificate starts with zero reputation. Your correctly signed application still shows the warning until enough users install it without incident — days to weeks, entirely outside your control. An EV certificate costs substantially more and grants immediate reputation.

The detail that matters: reputation attaches to the certificate, not to you or your product. Rotate to a new certificate and you start over. Teams discover this at renewal, when a release that worked last month starts frightening customers.

Linux: no gatekeeper, but sign anyway

No OS-level blocking. But package repositories expect GPG-signed packages, Flatpak and Snap have their own processes, and AppImage signing is conventional. Less urgent, still worth doing — an unsigned binary from the internet is a fair thing for a cautious user to refuse.

Timestamping: the detail that saves you

Sign without a timestamp and your signature becomes invalid the day your certificate expires. Every copy already installed. Every copy on every mirror.

Sign with a timestamp — a trusted authority attesting the signature existed at a moment when the certificate was valid — and the signature stays valid forever, regardless of expiry.

Timestamp servers are also flaky and rate-limited, which produces intermittent CI failures that look like random build breakage. Retry with backoff, and keep a fallback server configured. This is a two-line fix that prevents a genuinely baffling class of failure.

Doing it in CI without leaking anything

A few practices that hold up:

  • macOS certificates go into a temporary keychain created in the job and destroyed after, never the default login keychain on a shared runner.
  • Notarization credentials use an app-specific password or API key, scoped to notarization and nothing else.
  • Windows signing goes through the cloud service, authenticated by short-lived credentials rather than a stored key.
  • Never log the signing command with arguments expanded. Certificate paths and identity strings end up in build logs that are more widely readable than you think.
  • Verify in CI, not by eye. codesign --verify --deep --strict, spctl --assess, and a notarization status check. A build that says “signed” and fails Gatekeeper is the outcome you are trying to prevent.

Treating this as pipeline work rather than a manual release-day ritual is most of the battle, and it is the same discipline as any other CI/CD and infrastructure concern: if a human has to remember the steps, the steps will eventually be done wrong at the worst time.

What to budget

For a first release, honestly: a week, spread across certificate acquisition (Windows OV/EV verification can take days of back-and-forth with the CA), getting the pipeline working, and discovering the platform-specific surprises above.

After that it is close to zero, until:

  • A certificate expires — plan the renewal a month out, and remember the SmartScreen reputation reset.
  • Apple changes a requirement, which happens roughly annually.
  • You add a new binary to the bundle and forget to sign it.

The reason this catches teams is that it sits outside the mental model of “building the app.” It is a distribution problem, and distribution is where desktop work differs most from the web — a bad web deploy is a rollback, while a bad desktop release is on thousands of machines you do not control. That asymmetry is worth respecting in the schedule, and it is the same reason the shell you pick matters less than the release story around it.

If you have a desktop app that is finished and stuck at the distribution step — or a signing pipeline that broke when the hardware-key rules landed and has been manual ever since — that is a well-defined problem we can take off your hands.