Skip to content

feat(updates): harden in-place update safety and recovery - #506

Open
spandan11106 wants to merge 7 commits into
lgse:mainfrom
spandan11106:feat/148-harden-update-safety
Open

spandan11106 wants to merge 7 commits into
lgse:mainfrom
spandan11106:feat/148-harden-update-safety

Conversation

@spandan11106

@spandan11106 spandan11106 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Description

Hardens in-place updates for #148 and replaces the GitHub CLI verifier with native signed-manifest verification.

  • Verify Ed25519 signatures against public keys embedded in Strata. The signed manifest binds the repository, tag, source commit, target architecture, asset name, size, and SHA-256. Users need no gh, account, token, keyring, or external verification tool.
  • Sign manifests in a separate, approval-protected release job using tools from the workflow revision—not the selected prerelease source. Publish only after signing succeeds. Support overlapping signatures for planned key rotation.
  • Authenticate metadata before downloading the archive; bound transfers and extraction; reject unsafe archive entries; verify the packaged source and runtime before replacing anything.
  • Preserve atomic rollback, restart recovery, and cancellation from Settings and the update dialog.

Rollout: existing unsigned releases are refused with a manual-install fallback. The first signed release must be published after this workflow lands; there is no checksum-only or gh fallback. GitHub attestations and checksum assets remain available for independent verification and packaging. Trust format, key operations, rotation, and limitations are documented in docs/signed-updates.md.

Visual evidence

N/A for the signed-manifest revision: authentication/release infrastructure changed, not interface layout or controls. The existing cancellation controls were exercised during guided testing; no media was captured. The earlier successful live update used the previous verifier, not this new protocol.

How to test

  1. Once a signed release is available, run this branch as an older disposable user-local installation, without gh or credentials. In Settings → Updates, install that release and cancel; confirm the existing executable is unchanged and installation can be retried.
  2. Repeat through the sidebar dialog, then let the install finish and restart. Confirm the application starts and an update check reports up to date.
  3. Attempt an update to an older unsigned release. Confirm it explains the missing signed manifest and leaves the executable untouched.
  4. When cutting the first signed release, approve the release-signing environment only after reviewing the selected source/version, then approve publication. Confirm both manifest files accompany the architecture archives. The signing key must match data/update-keys.json.

Expected result: public updates authenticate natively before extraction or execution, invalid/unsigned updates fail closed, cancellation preserves the working install, and successful replacement restarts normally. A target predating this PR cannot perform the new rollback-cleanup callback.

Related issue

Closes #148

Name the release an install is for rather than pointing at a URL: the
installer derives the download location from the tag and asset name and
refuses a release feed that advertises anything else. Cap both the archive
and the published checksum against advertised and streamed byte counts.

Replace the tar subprocess with in-process extraction that rejects
symlinks, hard links, devices, absolute paths and parent-directory escapes
before anything reaches the filesystem, and bounds entry count and
extracted size.

Authenticate the archive with its GitHub build-provenance attestation and
match its SOURCE_COMMIT against the commit the release tag resolves to,
failing closed on either. A checksum published beside the archive proves
only that the two agree.

Preserve the previous executable across a replacement, restore it if the
new one does not run, and let the restart waiter put it back when the
replacement fails to start. Installs can now be cancelled from both the
settings row and the update dialog.
@l0gicgate

Copy link
Copy Markdown
Contributor

Merged current main and pushed review fixes in fa800d6:

  • Fixed two install-stopping problems: running a writable-open staging file (ETXTBSY), and checking stable SOURCE_COMMIT against the version-bump commit rather than its parent. Published binaries also lack --version; validation now uses their existing headless runtime probe.
  • Restricted provenance to the release workflow and bound the authenticated package directory to the selected version/architecture asset. Verification subprocesses now have output/time limits, and gh verification is cancellable.
  • Made rollback creation/restoration atomic, synchronized replacement files/directories, and prevented an older cleanup timer from deleting a later install's backup.
  • Enforced checksum overflow rejection and rejected duplicate archive files and extension headers that could bypass extraction accounting. Added regression tests and removed redundant comments throughout the PR.

Manual verification: authenticated the published v0.11.2 archive with the restricted verifier, confirmed its source SHA matches v0.11.2~1, and successfully ran its headless probe without a display. Exercised the actual restart shell script with disposable executables: early failure restored/relaunched the backup; failure outside the grace window left it alone (including paths containing spaces).

Visual evidence: N/A for these review fixes; no interface layout, styling, or controls were changed. I did not exercise a complete live GUI self-update or capture cancellation media.

@l0gicgate

Copy link
Copy Markdown
Contributor

Guided testing exposed a verification hang: gh probes Secret Service even with an empty configuration, and an isolated/locked desktop keyring can stall that lookup. Fixed in a93e746: the verifier now uses disposable CLI configuration, removes token environment variables, disables prompting, and prevents Secret Service access for this public-release operation. No account or token is required, and signature/repository/workflow checks remain enforced.

Confirmed in a fresh private D-Bus session without credentials: the published v0.11.2 archive verified in 3.9 seconds; the wrong signer workflow failed. Added regression coverage for the subprocess environment. The rebuilt guided fixture is staged for another user testing pass. No visual controls/layout changed.

@l0gicgate

Copy link
Copy Markdown
Contributor

Guided user verification completed successfully after a93e746: Settings cancellation left the executable unchanged; the sidebar install completed through anonymous provenance verification; Restart now reopened the disposable installation with TEST-FIXTURE.txt still visible; a subsequent update check reported up to date. The target was the real published v0.11.2 release. Sidebar cancellation was not separately completed, and the target predates rollback-cleanup support, so those remain unverified manually.

@l0gicgate

l0gicgate commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Updated from main (c74e33a), resolving the settings conflict by retaining the new preference bindings and the structured install request. Replaced the CLI verifier in a430d39 with native Ed25519 signed-manifest authentication and in-process SHA-256 verification; no gh, account, token, or keyring is involved in update authentication.

The separate signing job runs trusted workflow-revision tools, uses the approval-protected/main-only release-signing environment, and must succeed before publication. The production public key is embedded, the private key is provisioned as an environment secret, and dual-signature rotation plus compromise/rollout procedures are documented in docs/signed-updates.md. No release was dispatched or existing release modified.

Verified OpenSSL/Rust interoperability with non-production keys and exercised valid/tampered signed archives through verification, extraction, staging, and executable replacement. Also ran all 11 native manifest/pipeline tests with PATH=/nonexistent, an empty home, no credentials, and no desktop bus; all passed. The first real signed-release workflow run remains a post-merge maintainer step; existing unsigned releases deliberately fail closed. This replaces the previous gh-based guided-testing path, not merely its credential configuration.

@wmfeht

wmfeht commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Merge Blocker

Release packager and extractor still disagree on GNU tar LongLink. release.yml packs with default GNU tar -czf; extract_release_archive uses entries.raw(true) and treats ././@LongLink as a second package root before the entry-type gate.

Reproduced: stable 0.12.0 paths (≤100 bytes) extract; 0.12.0-rc.1 / nightly aarch64 layouts with longer portal service paths fail with more than one package directory. Every Preview/Nightly/RC in-place update is unusable once this client is the updater.

Required before land: pack with tar --format=ustar (and/or reconstruct GNU long names in the client while still rejecting unsafe entry types), plus a fixture packed with real GNU tar using an rc/nightly-length layout. Current tests only pack short names through the Rust tar crate, which is why CI stays green.

@l0gicgate

Copy link
Copy Markdown
Contributor

Merged current main (1ea889e) into this PR with merge commit b9b2b61, without conflicts or rebasing. The signed-manifest updater remains intact; no additional code fixes were needed. Full local checks passed, including isolated GTK tests and the canonical rootless-Podman GUI suite. No visual changes beyond those already on main.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(updates): harden in-place update safety and recovery

3 participants