All posts
3 min readBy BetterCodePush

OTA update security: code signing and the unsigned-bundle threat model

An over-the-air channel is a path to installed apps. Code signing lets a client verify who authorized an update before it runs. Here is the threat model and the controls around it.

SecurityCode SigningOTAReact Native

An over-the-air update system is, by design, a mechanism for running new code on every device that has your app installed. That is enormously useful and also a serious piece of attack surface: whoever can write to your update channel can, in effect, control your app on every one of those devices. Treating OTA as a security-sensitive system from day one is the right threat model.

The threat model

Think about every party who could get a bundle onto a device that you did not intend:

  • Compromised CDN or storage. If an attacker can write to where bundles are served, they may try to replace an update.
  • Man-in-the-middle on the update fetch. An attacker positioned on the network may try to substitute a bundle in transit.
  • Stolen publish credentials. A leaked release key lets an attacker publish as you.
  • Malicious or careless insider. Broad publish access means more people who can push code to production devices.

Transport security — TLS — protects the bytes in transit, but it does not prove who authored the bundle. If the origin itself is compromised, TLS happily delivers the attacker's code with a valid certificate. You need to authenticate the bundle, not just the connection.

What code signing does

Signing separates the ability to publish from the ability to be trusted. At publish time, your release tooling signs the bundle with a private key. The corresponding public key is embedded in the app when it is built. Before applying any update, the app verifies the signature on-device: a bundle that is unsigned, tampered with, or signed by the wrong key can be rejected before it runs. Expo documents this end-to-end signing model, including client-side verification and key rotation.

The important property is where the keys live. The private key stays in your release tooling and CI — it is never shipped inside the app. Only the public key or certificate is embedded. A compromised CDN still cannot forge a valid signature without the private key, assuming the client verifies it correctly and that key remains protected.

Why this matters for CodePush refugees

Classic CodePush supports optional bundle signing; its own setup documentationsays clients can verify a self-signed bundle before installation. If you are migrating, verify whether signing was actually configured rather than assuming it was. This is a good moment to close that gap rather than carry it forward.

A practical setup

  • Generate a signing key pair; keep the private key in a managed secret store, never in the repo.
  • Embed the public key in the native build.
  • Sign the bundle as part of your deploy step, not by hand.
  • Verify on-device before applying — reject anything that does not match.
  • Have a key rotation and incident-response plan written down before you need it.

Defense in depth

Signing is the core control, but it works best alongside the others: TLS for transport, least-privilege publish access, and keeping your Live API key strictly in the CLI and CI so it never ships to clients. In BetterCodePush, signing is optional while you evaluate and recommended before you distribute a production build; the API key is used only by the CLI to publish, and mobile clients talk to the public CDN without ever receiving your release credentials. Combined with a disciplined rollout, described in the rollout playbook, you get both integrity and a fast recovery path.

Ship your first update

Create an organization and deploy an over-the-air update in minutes. No billing setup during the beta.

Start with BetterCodePush