Security
How to sign desktop app updates so installs can trust them
By the Omnihorn team · · 8 min read
To sign desktop app updates, list every file of a release with its SHA-256 hash in a manifest, sign that manifest with a private key kept on the server, and ship the matching public key inside the installed app. Before applying anything, the app checks the signature, then checks each downloaded file against its hash.
Why do desktop app updates need a signature?
An auto-updater downloads code and runs it with the user’s permissions, often without asking. Whoever can change what it downloads can run code on every machine that has your app. A signature makes the updater refuse anything you did not publish, even when the file came from a server or network it normally trusts.
HTTPS alone is not enough. It protects the connection, but it trusts whatever the server sends. If the server, the storage bucket or a CDN in between is compromised or misconfigured, HTTPS will faithfully deliver the attacker’s file.
What attacks does update signing protect against?
Update signing protects against anyone who can change bytes between your build and the user’s disk but does not hold your private key. That covers three common situations: a compromised download server, an attacker on the network path, and a file corrupted or swapped in storage. It does not protect against a stolen key or a malicious build you signed yourself.
| Threat | Without signing | With a signed manifest |
|---|---|---|
| The download server or bucket is compromised | The updater installs whatever is there | Changed files fail their hash; a forged manifest fails its signature |
| Someone intercepts traffic (a hostile proxy, a bad certificate) | The updater accepts substituted files | Substituted content fails verification |
| A file is truncated or corrupted in transit or storage | A broken app may be installed | The hash mismatch stops the update before anything is applied |
| The signing key is stolen | Not applicable | Not protected: the attacker can sign anything |
| You sign a build that contains malware | Not applicable | Not protected: a signature proves origin, not safety |
What does a signature actually prove?
A valid signature proves two things: the signed bytes were produced by someone holding the private key, and they have not changed since. It says nothing about whether the code is good, safe or the newest version. Those questions need malware scanning, testing and a server that decides which build is current.
That last point matters. An old, validly signed release is still validly signed. If an attacker can replay an older manifest, signing alone will not stop a downgrade. Systems that care about this record the highest build a machine has installed and refuse to go below it, or include an expiry in the signed data. Others treat going back as a feature, because it is how a vendor rolls back a bad release, and rely on the update server being the authority on which build is current.
What is Ed25519, in plain words?
Ed25519 is a public-key signature scheme. You keep a private key that can create signatures and publish a public key that can only check them. Keys are 32 bytes, signatures are 64 bytes, verification is fast, and there are few parameters to get wrong. Those properties make it a common default for signing updates.
- Signing takes the private key and the exact bytes of a message and produces a signature.
- Verifying takes the public key, the same bytes and the signature and answers yes or no.
- Any change to the bytes, even one character of whitespace, turns the answer into no.
The third point catches teams out. If a server parses the signed JSON and serialises it again, key order or spacing can change and the signature stops matching. Sign a byte string and serve exactly that byte string.
Should you sign the manifest or each file?
Sign one manifest that lists every file with its path, size and SHA-256 hash. The updater verifies the manifest once, then hashes each downloaded file and compares. You get the same protection as signing every file separately, with one signature per release instead of thousands, and the manifest also pins which files belong together.
{
"version": "1.4.0",
"buildNumber": 42,
"files": [
{ "path": "MyApp.exe", "size": 4813312, "hash": "9f2c…e1a0" },
{ "path": "data/app.dat", "size": 120443, "hash": "51b7…0c3d" }
]
}Keep download URLs out of the signed part. URLs change (they expire, move to a CDN, get re-signed), while the list of files and hashes for a release does not. Anything you display but do not sign, such as release notes, should be treated as untrusted text by the updater.
Hash every file at build time
Compute SHA-256 for each file of the release and record path, size and hash.
Write the manifest once
Serialise it to bytes and never re-serialise it. Those bytes are what you sign and what you serve.
Sign on the server, not in the client
Use the private key in one controlled place, ideally a separate signing service or a key management system.
Verify before touching the disk
The updater checks the signature with its built-in public key. Any failure stops the update.
Hash each download
Download to a temporary folder, hash each file and compare with the manifest. Reject the whole update on any mismatch.
Apply atomically
Replace files only after everything verified, keep backups, and roll back if any step fails.
Where should the private key live?
Keep the private key somewhere that only the signing step can reach: a key management service, a hardware security module, or at minimum encrypted at rest with a key that lives elsewhere. Never put it in the repository, the installer, a CI log or a developer laptop that also browses the web.
Use one key per app. If one key leaks, only one app is exposed, and you can reason about which installs trust which key.
What should you know about key rotation?
Installed copies trust the public key they were built with, so rotating the signing key is a release in its own right. If you switch keys on the server first, every existing install rejects every future update. Ship the new trusted key inside a build signed with the old key, and switch only once that build is installed widely.
- Plan for rotation before you need it. An updater that can trust a short list of keys makes rotation much easier than one with a single hard-coded key.
- Accept that some machines lag. PCs that stay offline for months will still hold the old key. Keep the old one valid until you are willing to strand them.
- After a leak, speed beats elegance. Stop signing with the leaked key at once and expect to ask some users to reinstall.
How Omnihorn signs updates
Omnihorn creates an Ed25519 key pair for each app when the app is created. The private key stays on the server, stored encrypted; when KMS signing is enabled, a cloud key management service encrypts it instead of the server’s own master key. You can copy the app’s public key from its settings page.
- Signed after checks. When you upload a build, the server verifies every file against its declared SHA-256 hash, and only then signs the manifest that lists path, size and hash for each file. A malware scan runs too where the platform has it switched on.
- Served verbatim. The update service hands out the signed bytes exactly as signed. Download links are requested separately, are short-lived and are not part of the signed manifest. Release notes are display-only.
- Verified on the user’s PC. The installer carries the app’s public key. It checks the signature before it downloads anything, refuses unsafe file paths, checks each downloaded file’s hash, and applies the update atomically with a rollback if any step fails.
- Rollback is allowed. The server decides which build a channel offers, and pointing a channel at an older signed build is how you roll back.
There is no self-serve key rotation in the dashboard today. The details are in Security and Updates on user machines.
How is update signing different from Windows code signing?
They answer different questions for different checkers. Update signing lets your own updater confirm that a release came from you. Windows code signing (Authenticode) lets Windows and the user see who published an executable, and feeds SmartScreen’s reputation check. Most shipping apps need both.
| Update signing | Windows code signing (Authenticode) | |
|---|---|---|
| Who checks | Your own updater | Windows, SmartScreen, antivirus, the user |
| What is signed | The release manifest | Each executable or installer file |
| Key | Your own key pair, no certificate authority | A certificate issued by a certificate authority |
| Protects | Every update after the first install | The first download and every run of the signed file |
In Omnihorn, update signing is always on. Signing the Windows installer with a code-signing certificate is a separate step that is off by default; the SmartScreen guide explains what that means for your users.
›Is HTTPS enough to secure desktop app updates?
No. HTTPS protects the connection but trusts the server. If the server or storage is compromised, HTTPS delivers the tampered file. A signature checked by the app with a built-in public key catches that.
›Why Ed25519 instead of RSA for update signing?
Both work. Ed25519 has small keys and signatures, fast verification and few settings to get wrong, which makes it a simple, safe default for a new updater.
›Do I sign every file or just the manifest?
Sign the manifest and put each file’s SHA-256 hash in it. Verifying the manifest and then each hash gives the same guarantee as signing every file, with one signature per release.
›Does a signed update mean the update is safe?
No. It means the update came from the key holder unchanged. Scan builds for malware and test them before you sign and publish.