Automatic Updates, and Why You Need One More Layer of Signing
In Part 1, we finished setting up the Developer ID certificate and notarization. With that, you’re ready to deliver the app to users for the first time. But an app isn’t done after a single release — you have to keep shipping new versions that fix bugs and add features.
For a Mac App Store app, the App Store handles updates for you. A directly distributed app doesn’t get that, so you have to build an automatic-update feature into the app yourself. On macOS, the de facto standard for this role is the open-source framework Sparkle. With Sparkle in place, the app periodically checks an “update feed (appcast),” and if a new version exists, it notifies the user, downloads it, and installs it.
This raises a question. You already sign the app with the Developer ID certificate created in Part 1, so why do you need yet another key?
The reason is that the two signatures verify different things.
- Developer ID certificate — used by macOS Gatekeeper to decide “is it OK to install this app?”
- Sparkle EdDSA key — used by Sparkle inside the app to decide “was the update file I just downloaded really made by this app’s developer?”
Automatic updates are a security-sensitive operation: the app downloads a file from the internet and overwrites itself. If someone intercepts the update server or the communication path and slips in a fake file, it becomes a serious problem. To prevent this, Sparkle only accepts updates signed with a private key that only the developer holds, and refuses to install anything whose signature doesn’t match. It’s effectively a separate verification layer from the certificate.
In this article, we’ll create the EdDSA (Ed25519) key pair that will be used for that verification.
As in Part 1, all names and paths (
FocusTimer,example.com, etc.) are example values. In practice, replace them with your own app’s information.
Prerequisite — Sparkle Must Already Be Added to the App
Before creating the key, the Sparkle framework must already be added as a dependency of your app project. If it isn’t yet, add it in Xcode via Swift Package Manager (SPM).
- Open your project in Xcode → File → Add Package Dependencies…
- Enter the repository address in the search box:
https://github.com/sparkle-project/Sparkle - Set the version rule to 2.x (the latest major) and add it
After doing this, build the project once so that SPM downloads the Sparkle package. The command-line tools that come bundled with it are the key to the next step.
Step 1 — Locate the Sparkle Command-Line Tools
The Sparkle package includes command-line tools used for key generation and signing. These tools live inside the folder where SPM downloaded the package, but that location varies depending on your Xcode version and DerivedData settings. So it’s safer to find it directly.
SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
-path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"
If a single path is printed, it worked. If nothing appears, you skipped the “build the project once” step above — run a build in Xcode and then try again.
Inside this folder are the following tools.
generate_keys— generates, backs up, restores, and verifies the signing key (used in this article)sign_update— signs update files (used during actual releases)generate_appcast— generates the update feed (appcast.xml) (appears in Part 3)
Step 2 — Generate the Signing Key
Now create the key pair.
"$SPARKLE_BIN/generate_keys"
Output similar to the following appears.
Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
This single command creates two keys.
- Public key — the
SUPublicEDKeyvalue shown in the output above. It is not a secret, and it is the key you will embed in the app. - Private key — does not appear on screen. It is automatically stored in the macOS Keychain as an item called “Private key for signing Sparkle updates.” It is a true secret that must never be left on disk as a plaintext file.
Embedding the Public Key in the App
Put the public key string from the output into the app’s Info.plist. For the example app, add the following keys to FocusTimer-Info.plist.
<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
SUPublicEDKey— the public key you just generated. The app uses this key to verify the signature of downloaded updates.SUFeedURL— the address of the update feed. This domain doesn’t exist yet; we create it in Part 3. For now, it’s just a placeholder.
Because the public key is embedded in the app and only the developer holds the private key, the app will only accept updates signed with the private key. This is the core structure of Sparkle update verification.
Step 3 — Back Up the Private Key (Absolutely!)
If you skip this step, you may deeply regret it later.
The private key is stored in the Keychain, so it’s fine on the computer you’re using now. But if you lose the computer, the disk fails, or you reinstall macOS, this key disappears along with it.
What happens if the private key is lost? Updates signed with a new key will be rejected by existing users’ apps (apps with the old public key embedded). In other words, you can never send automatic updates again to users who are already running your app. Your only option is to tell each user individually, “please download the new version yourself and reinstall it.”
So back up the key right after you create it.
"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key
The single-line base64 string printed by cat is the private key. Example:
Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=
Store this string as a secure note in a password manager such as 1Password. Give the note a name that’s easy to find later, such as FocusTimer Sparkle EdDSA Private Key.
Right after confirming the save, delete the plaintext file left on disk.
rm ~/focustimer-sparkle-private.key
The rule is to never leave the private key sitting on disk as a plaintext file. Keep the backup only inside an encrypted password manager.
A Key Pitfall — The % Symbol Is Not Part of the Key
When you print the key with cat, the terminal (especially zsh) may append a % symbol at the end of the line.
Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%
This % is just the shell’s indicator that “the output ended without a newline” — it is not part of the key. If you copy this % into your backup, the key will be broken when you restore it later. A base64 string usually ends with =, so exclude the % after the = when saving.
Step 4 — Restoring on Another Computer
When you need to build the app on a new computer, put the private key you backed up back into the Keychain.
echo "your_backed_up_base64_string" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key
The -f option means “import the key in the file into the Keychain.” Once the restore is done, delete the plaintext file here too, right away.
Step 5 — Verification
Check that the key was installed correctly.
"$SPARKLE_BIN/generate_keys" -p
A single line containing the public key is printed. This value must exactly match the value you put into SUPublicEDKey in the Info.plist in Step 2. If they differ, the public key embedded in the app and the actual signing key are out of sync, and update verification will fail.
Part 2 Wrap-Up
If you’ve followed along this far, you now have the following.
- ✅ A Sparkle EdDSA key pair generated (public key + private key)
- ✅ The public key embedded in the app’s
Info.plist(SUPublicEDKey) - ✅ The private key safely backed up in a password manager
- ✅ Familiarity with how to restore it on another computer
The app now has a way to verify “whether a downloaded update is genuine.” But one thing is still missing. In Part 2 you wrote https://updates.example.com/appcast.xml for SUFeedURL, but there is still nothing at that address.
In the next part, we’ll create the public repository where the update feed (appcast.xml) and .dmg files will live, connect it to a domain we control, and finish the build settings — wrapping up the one-time setup.