AeroPush

Security

Bundle signing

Every bundle already carries a SHA-256 checksum, which proves it arrived intact. Signing proves something different and stronger: that you published it. Without a signature, anyone who can write to the update server or its storage can serve arbitrary JavaScript to every install — and supply a matching checksum while they do it.

How it works

  • You hold an Ed25519 private key. It never leaves your machine or CI secret store — AeroPush never sees it.
  • The server holds only your public key, and rejects any release whose signature doesn’t verify.
  • The device verifies against a copy of the public key baked into the binary at build time — deliberately not one the server sends. This is the part that still protects you if the server itself is compromised.

What gets signed is the bundle’s SHA-256 digest, which the device already computes while downloading. Verification happens before the archive is opened, so a forged bundle is never even unzipped.

Set it up

Run this once, in your app root:

shell
npx aeropush signing keygen

This writes aeropush-signing-key.pem (mode 0600), adds it to .gitignore, and prints your public key.

Important
Never commit the private key. Anyone holding it can publish code to your entire install base. Back it up somewhere safe — if you lose it you must ship a new binary with a new key before you can publish again.

Bake the public key into both native projects:

shell
npx aeropush signing embed

This writes AeroPushSigningKey into your iOS Info.plist and AEROPUSH_SIGNING_KEY into android/app/build.gradle.

Ship the binary before you enforce

Important
Order matters, and getting it wrong strands your users. A device can only verify a signature if its binary carries the public key. Build and release a binary with the key embedded first. Only once that build is out should you register the key in the dashboard and start publishing signed bundles. Devices still on the older binary simply ignore signatures and keep updating as before.

When the new binary is live, open your project → Keys → Bundle signing and paste the public key. From that moment the server rejects any release that isn’t signed with the matching private key.

Publishing

Nothing changes. aeropush release finds the key and signs automatically:

shell
npx aeropush release --channel production

The key is located in this order:

SourceUse
--signing-key <path>Explicit path
AEROPUSH_SIGNING_KEYThe PEM itself — for CI, where secrets are strings rather than files
AEROPUSH_SIGNING_KEY_PATHPath via environment
./aeropush-signing-key.pemThe default, written by keygen

In CI

Store the PEM as a secret named AEROPUSH_SIGNING_KEY and the CLI picks it up with no extra flags:

.github/workflows/release.yml
- name: Publish OTA bundle
  env:
    AEROPUSH_APP_KEY: ${{ secrets.AEROPUSH_APP_KEY }}
    AEROPUSH_SIGNING_KEY: ${{ secrets.AEROPUSH_SIGNING_KEY }}
  run: npx aeropush release --channel production

What happens when verification fails

  • At publish. An unsigned or wrongly-signed release is rejected with a 400 and an explanation. Nothing is stored, so a broken signing setup fails loudly instead of shipping a bundle every device would refuse.
  • On device. The bundle is deleted without being unzipped and the app keeps running its current version. A failed signature is never fatal — the user sees nothing.
  • Misconfigured key. If the embedded key is present but malformed, the SDK treats signing as still required and refuses updates. It fails closed on purpose: a corrupted key must never quietly downgrade you to unsigned installs.

Rotating a key

Rotate only alongside a store release, in the same order as setup: embed the new key, ship the binary, then register the new key. Every binary carrying the old key keeps rejecting bundles signed with the new one until users update — which is exactly the property that makes signing worth having, and the reason rotation isn’t a routine operation.

Next: the CLI reference or SDK API.