Skip to main content

Bundle signing

Every Swiftpatch release is signed with an Ed25519 keypair. The SDK refuses to apply a bundle whose signature doesn't verify under the configured public key. This is the core security boundary — without it, anyone who could redirect the download URL could ship code to your users.

Why Ed25519 and not RSA?

  • Small signatures. 64 bytes vs. 256+ for RSA.
  • Fast verification. ~100x faster than RSA verification on mobile ARM.
  • No padding pitfalls. RSA has a long history of signature-scheme footguns. Ed25519 has one correct way to use it.
  • Strong defaults. NIST-equivalent security without the parameter choices.

swiftpatch init generates an Ed25519 keypair in .swiftpatch/keys/. The private key stays on your machine (gitignored). The public key is uploaded to the dashboard and embedded into every OTA bundle request.

The signing flow

Developer machine (or CI):
swiftpatch deploy:
1. Bundle JS + assets → bundle.zip
2. Compute SHA-256 of bundle.zip
3. Sign the hash with the Ed25519 private key (~1 ms)
4. Upload bundle.zip + signature to the server
5. Server verifies the signature server-side before
even accepting the release

Device:
SDK downloads bundle.zip
1. Verify signature against the embedded public key
2. If invalid → reject, emit SWIFTPATCH_SIGNATURE_INVALID
3. If valid → verify SHA-256 of bytes
4. Promote to NEW slot

Both the server and the device verify the signature. Belt and suspenders.

Generating keys

swiftpatch init does this automatically. If you need a new pair:

swiftpatch generate-key-pair

Output:

Ed25519 keypair created.

Public: ./.swiftpatch/keys/public.pem
Private: ./.swiftpatch/keys/private.pem (mode 0600)
Key ID: ed25519:abc123...

Next: upload the PUBLIC key in your app settings.
Private key is required by `swiftpatch deploy` to sign releases.
Never commit the private key. .gitignore has been updated.

The private key is written with chmod 600 — owner read/write only.

Installing the public key in the app

Two places:

  1. Dashboard — upload the public PEM. The server uses it to verify releases on upload.
  2. Your app binary — the SDK embeds the same public key in Info.plist / strings.xml:
ios/YourApp/Info.plist
<key>SwiftPatchPublicKey</key>
<string>-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----</string>
android/app/src/main/res/values/strings.xml
<string name="SwiftPatchPublicKey">-----BEGIN PUBLIC KEY-----\nMCowBQY...\n-----END PUBLIC KEY-----</string>

Or pass it at runtime (rarely needed):

<SwiftPatchProvider config={{ publicKey: MY_PUBLIC_KEY }}>
<App />
</SwiftPatchProvider>

Rotating keys

When a private key is compromised, rotate it:

  1. In the dashboard: mark the old public key as revoked.
  2. Run swiftpatch generate-key-pair --force on your machine.
  3. Upload the new public key in the dashboard.
  4. Ship a new store build with the new public key embedded. This is the critical step — only store binaries holding the new public key can verify new releases.
  5. Once most users have adopted the new binary, you can stop shipping releases under the old key.

During the transition, the server signs releases with both old and new keys. Old binaries verify with the old key; new binaries verify with the new key. Devices on the old binary still get updates.

See the key rotation guide for a step-by-step runbook.

swiftpatch deploy --allow-unsigned will ship a release without a signature. The SDK will download it, but any app that has SwiftPatchPublicKey set will reject it at verification time. Use this only for short-lived dev channels.

What signatures don't protect

Signatures prove a bundle was produced with your private key. They don't prove:

  • The code is safe. A legitimately-signed bundle with a bug still ships the bug. For that, see staged rollouts and the risk score.
  • The user is authorized to receive it. Deployment keys enforce channel membership; signatures are separate.
  • The server is uncompromised. If your dashboard account is taken over, the attacker can sign releases with your key. Use SSO + 2FA on the dashboard.

Next steps