Skip to content

Auto-updates via Sparkle#44

Open
hcaiano wants to merge 2 commits into
mainfrom
feat/sparkle-updates
Open

Auto-updates via Sparkle#44
hcaiano wants to merge 2 commits into
mainfrom
feat/sparkle-updates

Conversation

@hcaiano

@hcaiano hcaiano commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Replaces the homegrown "check GitHub releases, open the browser" updater with Sparkle, so Lineup updates in place and verifies each update with an EdDSA signature (a tampered or man-in-the-middled download is rejected).

App

  • Package.swift — add Sparkle (2.x, pinned 2.9.3 in Package.resolved); add an @executable_path/../Frameworks rpath so the bundled app loads the embedded framework.
  • Updater.swift — one SPUStandardUpdaterController, started at launch for scheduled background checks. The menu "Check for Updates…" and the Settings → About button target it directly. UpdateChecker.swift removed (SemVer stays in LineupCore for its tests).
  • Info.plistSUFeedURL (the site), SUPublicEDKey, SUEnableAutomaticChecks, daily check interval.

Packaging

  • build-app.sh — embeds Sparkle.framework (via ditto, preserving the Versions symlinks) and re-signs it inside-out (XPC services, Autoupdate, Updater.app, then the framework) with the app's identity + hardened runtime, before signing the app. No --deep. Verified: codesign --verify --deep --strict valid, app launches with the framework loaded and satisfies its designated requirement.

Release tooling

  • web/appcast.xml — the feed (empty until the first signed release; auto-deploys).
  • Scripts/sparkle-keygen.sh / Scripts/sparkle-appcast.sh — keygen + per-release EdDSA signing of the DMG. Full flow in BUILDING.md.

Signing key — reused from Synclock

generate_keys stores one EdDSA private key per Keychain (not per app), so the key already created for Synclock is reused: its public key is in Info.plist, and the same private key (in the Keychain) signs Lineup's updates. No new key, nothing else to set up. Verified sign_update signs with it. The private key and notary credential stay only in the Keychain — never in this repo.

swift build clean, swift run lineup-tests → 316 checks passed. App build + inside-out signing + deep-verify + launch all verified locally.

Replace the homegrown "check GitHub releases, open the browser" updater with
Sparkle, so Lineup updates in place and verifies each update with an EdDSA
signature (a tampered or MITM'd download is rejected).

- Package.swift: add Sparkle (2.x); add an @executable_path/../Frameworks rpath
  so the bundled app loads the embedded framework.
- Updater.swift: one SPUStandardUpdaterController for the app, started at launch
  for scheduled background checks. The menu "Check for Updates…" and the
  Settings → About button target it directly, so Sparkle owns the update UI and
  the item's enabled state. Removes UpdateChecker.swift (SemVer stays in
  LineupCore for tests).
- Info.plist: SUFeedURL (https://lineup.caiano.com/appcast.xml),
  SUPublicEDKey (PLACEHOLDER — replace via sparkle-keygen before the next
  release), SUEnableAutomaticChecks, daily SUScheduledCheckInterval.
- build-app.sh: embed Sparkle.framework (ditto preserves the Versions symlinks)
  and re-sign it INSIDE-OUT (XPC services, Autoupdate, Updater.app, then the
  framework) with the app's identity + hardened runtime, before signing the app.
  No --deep (mis-signs Sparkle). Verified: deep signature valid, app launches
  with the framework loaded, satisfies its designated requirement.
- web/appcast.xml: the feed (empty until the first signed release).
- Scripts/sparkle-keygen.sh: one-time EdDSA keygen (private key -> Keychain).
- Scripts/sparkle-appcast.sh: per-release, EdDSA-signs the DMG and regenerates
  the appcast. Documented end-to-end in BUILDING.md.

Security: the EdDSA private key and notary credential live only in the
developer's Keychain, never in this (public) repo. swift build clean, 316 tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8121a5d193

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Resources/Info.plist Outdated
<key>SUFeedURL</key>
<string>https://lineup.caiano.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>REPLACE_WITH_SUPublicEDKey_FROM_generate_keys</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don't ship a placeholder Sparkle public key

This value is copied verbatim into Contents/Info.plist by build-app.sh, and AppUpdater starts Sparkle at launch. With this placeholder instead of a base64 EdDSA key, any released build will fail update validation/checks and cannot accept future signed appcast updates, so affected users would need to reinstall manually; make the build fail while the placeholder remains or omit SUPublicEDKey until the real key is available.

Useful? React with 👍 / 👎.

Comment thread Scripts/build-app.sh
if [ -n "${DEVID}" ]; then
SIGN_ID="${DEVID}"; sig_kind="developer-id"; TIMESTAMP_FLAG="--timestamp"
echo "==> signing with Developer ID (notarizable): ${DEVID}"
elif security find-identity -p codesigning -v 2>/dev/null | grep -q "${SIGN_IDENTITY}"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the signing probe for self-signed builds

This detection now relies on security find-identity -p codesigning -v, but the repo's own Scripts/setup-signing.sh:27-29 notes that this reports zero valid identities for the untrusted Lineup Self-Signed cert even though codesign can use it. In the documented local flow (setup-signing.sh then build-app.sh without a Developer ID cert), this branch is skipped and the app falls back to ad-hoc signing, losing the stable Accessibility requirement and making REQUIRE_STABLE_SIGNATURE=1/DMG packaging fail; keep the previous probe that actually attempts to sign with the self-signed identity.

Useful? React with 👍 / 👎.

generate_keys stores one EdDSA private key per Keychain, not per app, so the key
created for Synclock already exists locally. Drop its public key into Info.plist
(replacing the placeholder); the same private key signs Lineup's updates too.
No new key generated. Verified: sign_update signs with the Keychain key, the
public key is a 32-byte ed25519 value, Info.plist lints clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f1e86782a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Scripts/build-app.sh
fi

# Sign the app LAST (identifier pinned; hardened runtime). This seals the embedded framework.
codesign --force --options runtime ${TIMESTAMP_FLAG} --sign "${SIGN_ID}" --identifier "${BUNDLE_ID}" "${APP}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid hardening non-Team-ID Sparkle builds

When this script falls back to the documented local/self-signed path or to SIGN_ID="-", the main app is still signed with the hardened runtime while the embedded Sparkle framework has no Apple Team ID. Hardened Runtime enables library validation, which only allows Apple-signed code or code with the same Team ID, so these non-Developer-ID builds can pass codesign --verify but fail at launch when dyld tries to load Sparkle.framework; either skip --options runtime or sign the app with a disable-library-validation entitlement for non-Apple local builds.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant