There are a lot of popular desktop applications today written in Javascript and HTML, thanks to frameworks like Electron. The most noteworthy example that comes to mind is Streamlabs OBS, which is popular among Twitch streamers.
A lot of these apps even include a self-update mechanism for ensuring users are always on a recent version of the software. However, self-updaters are a land mine (or a gold mine, depending on your perspective) of security risks.
However, they're definitely worth the risk. It's just important to do them right.
Understanding the Risks Inherent to Automatic Updates
In general, the best way to understand security risks is to think like a bad guy, then try to outsmart yourself.
If you wanted to install malware on thousands (or millions) of computers, and all of the targets you were interested in were running some software that has a self-update mechanism, wouldn't it make perfect sense to attack the update server and replace the update file with your malware?
This isn't just a theoretical risk. Both download links and self-updaters have historically been used to spread malware in the past.
Let's assume someone hacks into your update server and publishes a fake update for your app that contains their malware of choice. How can we stop them from infecting our users?
Can we use cryptographic hash functions?
No! Hash functions don't help us here.
There's a lot of "old school" ideas about download authenticity. The idea of "just verify hashes/checksums" doesn't work because there are no secrets the attacker cannot access.
Isn't HTTPS (HTTP over TLS) enough?
TLS is good, and I would argue, necessary for solving this problem. But it is, in and of itself, inadequate.
As the name Transport-Layer Security implies, TLS protects data in-transit. It provides no at-rest authenticity for the update file sitting on the server. If someone can hack the other endpoint, TLS doesn't help you.
What Actually Works?
Digital Signatures work!
Digital Signatures are a class of asymmetric cryptography algorithms that compute a signature of a message, generated by a secret signing key (or "private key" in Academic Speak), which can be verified by a publicly known verification key (a.k.a. "public key").
Due to the nature of asymmetric cryptography, only your signing key needs to remain secret.
So what you have to do is:
- Generate a digital signature of your update files, offline.
- Upload the signature alongside your update files to the update server.
And viola! Now even if someone hacks into the update server, they cannot push malware onto your users without further attacks in order to steal your signing key. If you keep this key in a computer that is never connected to the Internet, stealing it becomes prohibitively expensive for most attackers.
But is a digital signature by itself adequate for developing a secure automatic update system?
The experts say, "No."
- The Triangle of Secure Code Delivery
- Guide to Automatic Security Updates (For PHP Developers)
- Proposal to secure Go's module ecosystem
That being said, digital signatures are a fundamental component to any effort to secure software updates. You cannot remove them from the equation without making the system less secure.
The full solution consists of each of the following:
- Digital signatures
- Reproducible builds
- Binary transparency (a.k.a. Userbase Consistency Verification)
- This uses cryptographic ledgers, but be wary of anything with "blockchain" in its sales brochure
- Transport-Layer Security (to prevent Man-in-the-Middle replay attacks to keep targeted systems vulnerable forever)
That might sound daunting, but I didn't just write this post to talk about the theory of secure automatic updates with respect to Electron apps. Experts have already talked about the problems and solutions at length before.
Today, I'd like to introduce you to my solution to the problem (which was based off the work done to secure WordPress's auto-updater).
Project Valence
Project Valence (named after valence electrons) is my framework for self-updating Electron apps. It consists of three main projects.
- libvalence is the component you would add to an existing Electron.js project in order to facilitate secure updates
-
valence-devtools is a
npm
package you'll want to install globally in order to package, sign, and release updates - valence-updateserver is a web application that exposes an API that the other two projects can communicate with in order to upload/download updates and signatures
The cryptography used by Valence is Dhole cryptography, an easy-to-use libsodium wrapper.
For signatures, Dhole uses Ed25519 (with an additional 256-bit random nonce to make fault attacks more difficult if reimplemented in embedded systems).
Valence Update Server
The install/setup instructions are available on Github.
This exposes a REST + JSON API that the other components communicate with. In order to publish anything on the update server, you will need a publisher account and at least one project. You will need a publisher token to use the dev tools.
Valence Dev Tools
The dev tools documentation fits well within the README on Github.
The devtools were designed so that you can quickly run the ship
command to build, sign, and upload a new release all in one fell swoop, or break each step into an atomic command (i.e. to facilitate offline signatures with an airgapped machine).
Libvalence
This is the meat and potatoes of this post: Making your code self-update.
My goal with this project was to ensure you don't need a cryptography engineering background to set this up properly. Once you have access to an update server and the dev tools installed, the rest of the work should just be using a simple API to solve this problem.
The API looks like this:
const { Bond, Utility } = require('libvalence');
let bond = Bond.fromConfig(
'Project Name',
__dirname + "/app", // Path
['https://valence.example.com'],
[] // Serialized public keys (generated by dhole-crypto)
);
/**
* @param {string} channel
* @param {string|null} accessToken
*/
async function autoUpdate(channel = 'public', accessToken = null) {
if (accessToken) {
bond.setAccessToken(accessToken);
}
let obj = await bond.getUpdateList(channel);
if (obj.updates.length < 1) {
// No updates available
return;
}
let mirror = obj.mirror;
let update = obj.updates.shift();
let updateInfo = await fetch.fetchUpdate(update.url, mirror, bond.verifier);
if (updateInfo.verified) {
await bond.applier.unzipRelease(updateInfo);
}
}
You can also ensure all updates are published on a cryptographic ledger, specify your own automatic update policy (the default policy is semver: patch updates are auto-installed, minor/major updates are not).
An important (but easily overlooked) feature is the concept of release channels.
You can, from the update server, generate access tokens that have access to a specific subset of channels (e.g. public
and beta
releases but not alpha
or nightly
releases).
This concept is implemented so that developers can offer exclusive access to early releases to their paid supporters (e.g. via Patreon), and bake that access directly into their automatic updates.
Want to Contribute?
All of these projects are open source on Github, but my development efforts are funded through Patreon supporters.
I also stream most of my open source development on my Twitch channel.
Top comments (1)
Very interesting article Soatok Dreamseeker it really got me thinking about security, which unfortunately many times we take for granted when using or implementing software.