Repository navigation
fix(release): Sparkle appcast fails closed when the signing key is not the app's SUPublicEDKey - #27732
Conversation
Red on base: with the app's SUPublicEDKey different from the private key's public key, generate_appcast leaves the feed unsigned and the sign_update fallback signs it anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-401d85755f96aead5ecd
…trust generate_appcast leaves the feed unsigned when the private key's public key is not the app's SUPublicEDKey, and the sign_update fallback then signed it anyway: a feed that passed the edSignature grep but every installed app rejects. Before anything is generated, derive the private key's public key and compare it with SUPublicEDKey of the app inside the DMG; fail on a mismatch, a missing key or a key that cannot be derived. The fallback now verifies its signature with sign_update --verify and replaces the enclosure's length instead of adding a duplicate attribute, and the final check parses the feed and requires the DMG's enclosure to be signed with the DMG's exact size. Red/green: bash tests/test_sparkle_generate_appcast_no_deltas.sh (red at the previous commit, green here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-401d85755f96aead5ecd
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
|
|
Found 2 test failures on Blacksmith runners: Failures
|
|
Independent review (codex gpt-6-astra, reasoning high, fast, read-only) of 760ac10..660cb41: No P1/P2 findings. Sparkle 2.8.1's
Validation: Review (codex gpt-6-astra): SHIP at 660cb41 |
sparkle_generate_appcast.sh now mounts the new DMG to compare the signing key with the app's SUPublicEDKey and checks the enclosure length against the DMG size, so the fixture serves this build's app (with the fake swift's public key) through a fake hdiutil and writes each archive's real size. Earlier builds still never mount. bash tests/test_generate_nightly_deltas.sh: red at 660cb41 (hdiutil: command not found), green here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-401d85755f96aead5ecd
|
Review (codex gpt-6-astra high, next-review): SHIP at 8aedb93 No blocking findings. |
A newer verdict blocks 85c56d1 (no independent SHIP at the head).
|
Review (codex gpt-6.1-sol high fast, review-router; started by the backlog lander): SHIP at 85c56d1 SHIP
|
|
Independent review (gpt-6.1-sol high) at b947a5f: SHIP. |
|
Independent review (gpt-6.1-sol high) at 767df57: SHIP. |
|
Review (next-review patch-id; carried): SHIP at d88b047 The independent SHIP at 767df57 carries to this head: the PR's own change ( |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Fixes an internal report: the Sparkle appcast fallback signed the feed with a key the app does not trust.
What was wrong
scripts/sparkle_generate_appcast.shransign_update -p --ed-key-fileand injected the signature whenevergenerate_appcastleft the feed unsigned.generate_appcastleaves it unsigned exactly when the private key's public key is not the app'sSUPublicEDKey, so a wrong key produced a feed that looked signed and passed the script's only check (a grep forsparkle:edSignature), while every installed app rejects the update. The fallback also added a secondlengthattribute to an enclosure that already had one, which makes the feed unparsable, and usedstat -f%z(macOS only).Fix
SPARKLE_PRIVATE_KEY(scripts/ci/dev_app_linux.py sparkle-public-keythrough openssl, falling back toscripts/derive_sparkle_public_key.swiftwhere openssl is LibreSSL), mount the DMG copy read-only and read the app'sSUPublicEDKey. Fail closed on a mismatch, a missingSUPublicEDKey, or a key that cannot be derived.sign_update --verifyand replace the enclosure'slengthwith the DMG's exact size instead of adding a duplicate.edSignatureand alengthequal to the DMG's byte size. A failing feed is removed from the output path.Callers checked (all run on macOS, pass the DMG whose app the key must match, and need no change):
release.yml,nightly.yml,auto-resume-nightly-notarization.yml(4 calls),repair-v0-64-25-helper-rpaths.yml,scripts/build-sign-upload.sh,scripts/ci/nightly-steps/recover-generate-appcast.sh,scripts/ci/generate-nightly-deltas.sh. No environment, secret or workflow permission changes.Red/green
bash tests/test_sparkle_generate_appcast_no_deltas.sh(runs on Linux with stubgenerate_appcast,sign_updateandhdiutil; the keys are the RFC 8032 Ed25519 test vectors). New cases: mismatched key (signed and unsigned generator), app withoutSUPublicEDKey, a non-Ed25519 private key, a wrong enclosure length, a fallback signature that fails--verify, and the matching-key fallback (onelength, the DMG's size, signature verified).FAIL: mismatch: missing 'does not match the app's SUPublicEDKey'(the fallback signed the feed).PASSlines.Observed / Not run
Observed: the test above red at the test commit and green at the head on Linux bash 5.2;
prepush-gate.pyreports no new violations. Not run: a real macOS run ofhdiutil attachand the real Sparkle 2.8.1sign_update --verify(the fallback path only runs whengenerate_appcastdid not sign, which the up-front key check now makes rare).Risk
High tier (release/signing). A release, nightly or repair run whose private key does not match the built app now fails at appcast generation instead of publishing a feed nobody can install. Matching keys (the normal case, since every caller embeds the key derived from the same private key) behave as before, plus one read-only DMG mount and a key derivation (openssl, or one
swiftscript run on hosts with LibreSSL).Changelog
none
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Fixes the Sparkle appcast script signing the feed with a key the shipped app does not trust, which published updates every installed app would reject.
The script now derives the public key of
SPARKLE_PRIVATE_KEY, mounts the DMG read-only, and compares it to the app'sSUPublicEDKeybefore generating anything. It fails closed on a mismatch, a missingSUPublicEDKey, or an underivable key. Thesign_updatefallback now verifies the injected signature and replaces the enclosure'slengthinstead of adding a duplicate attribute, which made the feed unparsable. A final check parses the feed and requires the DMG's enclosure to be signed with the DMG's exact byte size, removing failing feeds from the output path.The nightly deltas test fixture now mounts this build's DMG through a fake
hdiutilto serve the key check and writes each archive's real enclosure length. Callers need no changes; matching keys (the normal case) behave as before, plus one read-only DMG mount and a key derivation.Written for commit d88b047. Summary will update on new commits.