How does EAS Update code signing prove an update came from you, and why does adding or rotating its certificate need a new runtime version?
answer
- sign, do not encrypt
- npx expo-updates codesigning:generate
- private key stays on your machine
- certificate embedded via codeSigningCertificate
- certificate is part of the runtime
basics
~20 sYou generate a key pair and certificate with npx expo-updates codesigning:generate, embed the certificate in builds via updates.codeSigningCertificate, and sign each update locally with the private key; expo-updates verifies the signature before applying and rejects a missing or invalid one.
solid answer
~50 sEAS Update code signing is end-to-end: the proof is checked on the device, so neither the network, the CDN nor the update service itself can swap in different code. `npx expo-updates codesigning:generate` creates a private key, a public key and a certificate; `codesigning:configure` writes `updates.codeSigningCertificate` and `updates.codeSigningMetadata` (`keyid`, `alg: rsa-v1_5-sha256`) into the app config. You keep the private key out of source control and publish with `eas update --private-key-path`, which signs locally so the key never leaves your machine. A build with a certificate verifies each downloaded update before applying it and rejects one without a valid signature. Because the certificate is compiled into the binary, adding, rotating or removing it changes what a build accepts, so it is treated as a new runtime: you set a new runtime version so old builds and new builds each receive updates signed for them.
go deeper
Know that updates can be signed, that the private key stays secret and that the certificate goes into the app.
Walk through generate, configure, build and publish with --private-key-path, and say what the device checks before applying.
Explain why the certificate forces a new runtime version, how rotation and expiry play out on installed builds, and why rollbacks need signing.
Decide whether signing is worth its operational cost, who holds the key, and what validity period balances leak exposure against forced rebuilds.
## What code signing protects against An **EAS Update** is JavaScript that a release build downloads and runs. Without signing, the build trusts whatever its update URL returns over TLS. **Code signing** adds a check that only you can pass: each update carries a **signature** made with your **private key**, and the build verifies it against a **certificate** embedded at build time. The Expo docs list who this rules out: ISPs, CDNs, cloud providers and EAS itself cannot tamper with updates the app runs. It signs, it does not encrypt: the update's content is still readable in transit and on disk. The guarantee is **origin and integrity**, not secrecy. ## Setting it up 1. **Generate** a key pair and certificate: `npx expo-updates codesigning:generate --key-output-directory ../keys --certificate-output-directory certs --certificate-validity-duration-years 10 --certificate-common-name "Your Organization"`. This writes `private-key.pem` and `public-key.pem` to the key directory and `certificate.pem` to the certificate directory. 2. **Configure** the project: `npx expo-updates codesigning:configure --certificate-input-directory certs --key-input-directory ../keys` adds the config below. With prebuild (Continuous Native Generation) that is all; bare projects add matching entries to `AndroidManifest.xml` and `Expo.plist`. 3. **Build** with a **new runtime version**, so the certificate ships in the binary. 4. **Publish** signed updates: `eas update --private-key-path ../keys/private-key.pem`. The CLI signs locally and uploads only the signature. ```json { "expo": { "updates": { "codeSigningCertificate": "./certs/certificate.pem", "codeSigningMetadata": { "keyid": "main", "alg": "rsa-v1_5-sha256" } } } } ``` ## What each file is for | File | Where it lives | Secret? | |---|---|---| | `private-key.pem` | Outside the repo, in a secret store | Yes: whoever holds it can sign updates | | `public-key.pem` | Next to the private key | No | | `certificate.pem` | Checked into the project, embedded in builds | No: it carries the public key | Every command that publishes on your behalf, including `eas update:rollback`, `eas update:revert-update-rollout` and `eas channel:rollout`, accepts `--private-key-path`, because rollbacks and reverts are new publishes and must be signed too. ## Why the certificate changes the runtime The certificate is part of what a build will accept, just like its native modules. A build **without** a certificate accepts unsigned updates; a build **with** one rejects an update that has no signature or a signature from another key. If old and new builds shared a runtime version, one publish could not satisfy both. So each of these gets a **new runtime version**: - **Adding** signing to an app that had none. - **Rotating** keys: back up the old pair, generate a new one (optionally with a new `keyid`), build with a new runtime version, and sign new updates with the new key. - **Removing** signing, described in the docs as rotating to a null key. ## What verification looks like on the device Verification happens after the update downloads and **before** it is applied. expo-updates reads the signature the server returns with the update, finds the key named by `keyid` in its embedded configuration and checks the signature. If the signature is missing, names an unknown key or does not match, the update is rejected and the app keeps running what it had. The client log, readable in a release build with `Updates.readLogEntriesAsync()`, records such failures with codes like `UpdateHasInvalidSignature` and `UpdateCodeSigningError`. The most common real cause is mundane: someone published without `--private-key-path`, or with the wrong key after a rotation, so every signed build silently ignored the update. ## Expiry and compromise - A certificate has a **validity period**. Once it expires, binaries carrying it will not apply newly downloaded updates, although updates downloaded before expiry keep working. Rotate well before that date so users are on builds with the new certificate. - A **leaked private key** lets anyone sign updates your builds will accept; the response is a rotation, which again means new builds with a new runtime version. - Shorter validity limits the damage of a leak but forces rotations, and so new binaries, more often.
- What happens to users whose build's signing certificate has expired?Their binaries stop applying newly downloaded updates, because the signature can no longer be validated against a valid certificate. Updates downloaded before the expiry keep working. The fix is to have shipped builds with a rotated certificate, and a new runtime version, well before the expiry date.
- Why do eas update:rollback and eas update:revert-update-rollout also take --private-key-path?Both work by publishing: a republished earlier update or a roll-back-to-embedded directive. A build with a certificate rejects anything unsigned, so the rollback itself must be signed, or the devices you are trying to rescue will refuse it.
saying these in an interview costs you the question
- Thinks EAS Update code signing encrypts the bundle contents
- Commits the private key next to the certificate in the repo
- Believes the server signs updates with a key stored in EAS
- Rotates keys without changing the runtime version of new builds
- Forgets that rollbacks must be signed too