HotupdaterHot Updater
Guides

Bundle Signing

Since 0.23.0+

Sign OTA bundles to verify integrity and prevent tampering

Why Bundle Signing?

Bundle signing prevents man-in-the-middle attacks, bundle tampering, and unauthorized deployments by using RSA-SHA256 signatures.

Setup

# 1. Generate keys
npx hot-updater keys generate

# 2. Add to hot-updater.config.ts
# signing: { enabled: true, privateKeyPath: "./keys/private-key.pem" }

# 3. Export public key to native apps
npx hot-updater keys export-public

# 4. Rebuild and release native app
# 5. Deploy signed bundles
npx hot-updater deploy -p ios

You need to manually export the public key to native configuration files.

# 1. Generate keys
npx hot-updater keys generate

# 2. Add to hot-updater.config.ts
# signing: { enabled: true, privateKeyPath: "./keys/private-key.pem" }

# 3. Run prebuild (public key automatically embedded)
expo prebuild

# 4. Rebuild and release native app
# 5. Deploy signed bundles
npx hot-updater deploy -p ios

The Expo config plugin automatically extracts the public key from your private key during expo prebuild.

Important: You must release a new app version with the public key before deploying signed bundles. Add keys/ to .gitignore.

Configuration

hot-updater.config.ts
import { defineConfig } from "hot-updater";

export default defineConfig({
  // ... other config
  signing: {
    enabled: true,
    privateKeyPath: "./keys/private-key.pem",
  },
});

Commands

Generate Keys

npx hot-updater keys generate [--output ./keys] [--key-size 4096]

Creates private-key.pem (keep secure) and public-key.pem in ./keys directory.

Export Public Key

npx hot-updater keys export-public [--print-only] [--yes]

Writes public key to:

  • iOS: Info.plistHOT_UPDATER_PUBLIC_KEY
  • Android: strings.xmlhot_updater_public_key

The command auto-detects native files. For custom paths, configure platform in your config:

hot-updater.config.ts
export default defineConfig({
  platform: {
    android: {
      stringResourcePaths: [
        "android/app/src/main/res/values/strings.xml",
        "android/app/src/prod/res/values/strings.xml", // production flavor
      ],
    },
    ios: {
      infoPlistPaths: [
        "ios/MyApp/Info.plist",
        "ios/MyAppPro/Info.plist", // pro target
      ],
    },
  },
});

Remove Public Keys

npx hot-updater keys remove [--yes]

Manual Configuration

iOS (Info.plist):

<key>HOT_UPDATER_PUBLIC_KEY</key>
<string>-----BEGIN PUBLIC KEY-----\nMIIB...your-key...\n-----END PUBLIC KEY-----</string>

Android (res/values/strings.xml):

<string name="hot_updater_public_key">-----BEGIN PUBLIC KEY-----
MIIBIjANBgkq...your-key...
-----END PUBLIC KEY-----</string>

Expo Integration

Expo projects automatically embed the public key during expo prebuild when signing is enabled in hot-updater.config.ts.

How It Works

The Expo config plugin (@hot-updater/react-native) reads your hot-updater.config.ts:

hot-updater.config.ts
export default defineConfig({
  signing: {
    enabled: true,
    privateKeyPath: "./keys/private-key.pem",
  },
  // ... other config
});

When you run expo prebuild, the plugin:

  1. Reads the signing config
  2. Loads ./keys/public-key.pem (derived from privateKeyPath)
  3. Embeds it in native files automatically

No need to run npx hot-updater keys export-public manually for Expo projects.

EAS Build

For EAS builds, the Expo plugin needs access to the private key to extract and embed the public key during prebuild. Store the private key as an environment variable:

Using EAS Secrets

# Store private key in EAS Secrets
eas env:create --name HOT_UPDATER_PRIVATE_KEY --value "$(cat keys/private-key.pem)"

The plugin automatically extracts the public key from HOT_UPDATER_PRIVATE_KEY during prebuild.

Using .easignore (Alternative)

For simpler setup, include the keys directory in EAS builds:

.easignore
!keys/

This tells EAS to include the keys/ directory even though it's in .gitignore. The plugin will use the private key file directly during prebuild.

Note: This approach is especially useful for local EAS builds (eas build --local) where you don't need to configure environment variables.

How It Works

During EAS build, the plugin checks for the public key in this order:

  1. HOT_UPDATER_PRIVATE_KEY environment variable → Extract public key
  2. Private key file at privateKeyPath → Extract public key
  3. Public key file (derived from privateKeyPath) → Use directly
  4. All sources failed → Build fails with error

CI/CD Integration

GitHub Actions

- name: Setup signing key
  run: |
    mkdir -p keys
    echo "${{ secrets.HOT_UPDATER_PRIVATE_KEY }}" > keys/private-key.pem
    chmod 600 keys/private-key.pem

- name: Deploy
  run: npx hot-updater deploy -p ios

Other Platforms

Store keys in:

  • GitHub Actions: Repository Secrets
  • GitLab CI: CI/CD Variables (masked)
  • AWS: Secrets Manager
  • Azure: Key Vault

Configuration States

Config signing.enabledNative Public KeyResult
falseNot present✅ Normal updates
falsePresentUpdates rejected
truePresent✅ Verified updates
trueNot presentError: PublicKeyNotConfigured

Warning: Config and native files must match. Mismatches cause update failures.

Troubleshooting

Updates Rejected

Cause: Public key in native files but signing.enabled: false in config.

Fix:

# Enable signing OR remove keys
npx hot-updater keys remove

PublicKeyNotConfigured Error

Cause: signing.enabled: true but no public key in native files.

Fix:

npx hot-updater keys export-public
# Rebuild and release app

Signature Verification Failed

Causes: Tampered bundle, wrong public key, or key mismatch.

Fix:

# Verify key matches
npx hot-updater keys export-public --print-only

Key Rotation

  1. Generate new key pair
  2. Update privateKeyPath in config
  3. Export new public key
  4. Release new app version
  5. Maintain compatibility until users update