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 iosYou 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 iosThe 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
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.plist→HOT_UPDATER_PUBLIC_KEY - Android:
strings.xml→hot_updater_public_key
The command auto-detects native files. For custom paths, configure platform in your config:
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:
export default defineConfig({
signing: {
enabled: true,
privateKeyPath: "./keys/private-key.pem",
},
// ... other config
});When you run expo prebuild, the plugin:
- Reads the signing config
- Loads
./keys/public-key.pem(derived fromprivateKeyPath) - 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:
!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:
HOT_UPDATER_PRIVATE_KEYenvironment variable → Extract public key- Private key file at
privateKeyPath→ Extract public key - Public key file (derived from
privateKeyPath) → Use directly - 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 iosOther Platforms
Store keys in:
- GitHub Actions: Repository Secrets
- GitLab CI: CI/CD Variables (masked)
- AWS: Secrets Manager
- Azure: Key Vault
Configuration States
Config signing.enabled | Native Public Key | Result |
|---|---|---|
false | Not present | ✅ Normal updates |
false | Present | ❌ Updates rejected |
true | Present | ✅ Verified updates |
true | Not present | ❌ Error: 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 removePublicKeyNotConfigured Error
Cause: signing.enabled: true but no public key in native files.
Fix:
npx hot-updater keys export-public
# Rebuild and release appSignature Verification Failed
Causes: Tampered bundle, wrong public key, or key mismatch.
Fix:
# Verify key matches
npx hot-updater keys export-public --print-onlyKey Rotation
- Generate new key pair
- Update
privateKeyPathin config - Export new public key
- Release new app version
- Maintain compatibility until users update