loading...

Secure Automatic Updates for Electron Apps

soatokdhole profile image Soatok Dreamseeker ・6 min read

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.

Soatok hacking

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.

I know this can seem frustrating, bear with me please

What Actually Works?

Digital Signatures work!

Eureka!

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:

  1. Generate a digital signature of your update files, offline.
  2. 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."

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:

  1. Digital signatures
  2. Reproducible builds
  3. Binary transparency (a.k.a. Userbase Consistency Verification)
    • This uses cryptographic ledgers, but be wary of anything with "blockchain" in its sales brochure
  4. 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.

  1. libvalence is the component you would add to an existing Electron.js project in order to facilitate secure updates
  2. valence-devtools is a npm package you'll want to install globally in order to package, sign, and release updates
  3. 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?

Soatok appreciates you

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.

Posted on by:

soatokdhole profile

Soatok Dreamseeker

@soatokdhole

I'm Soatok! I'm a Twitch affiliate and open source developer. I'm also a furry. (My fursona is a blue dhole.)

Discussion

markdown guide