Support signed deb self-updates with system authorization in 0.1.4

This commit is contained in:
Emil
2026-09-10 03:28:51 +03:00
parent bb4b1f8051
commit 260e4577d0
19 changed files with 1202 additions and 65 deletions
+36 -1
View File
@@ -23,7 +23,8 @@ read-only `https://shacraft.ru/api/online/aoc` endpoint. It is display-only:
the result never controls files, versions, URLs, or the launch command.
Version 0.1.3 adds signed application updates, separate from modpack sync.
Linux AppImage replacement is supported; the first upgrade from 0.1.2 is manual.
Version 0.1.4 adds installed deb updates with system administrator confirmation.
The first AppImage upgrade from 0.1.2 and deb upgrade from 0.1.3 are manual.
Windows/macOS packages still require publication and actual installation tests.
Not yet implemented: a user-selectable profile directory and a "reset managed
files only" recovery action. OS code signing/notarization is separate from the
@@ -195,6 +196,24 @@ same-directory temporary file, signature verification, preserved permissions,
atomic rename and file/directory fsync. Windows/macOS retain Tauri's platform
installers. Unsupported Linux formats show manual installation instructions.
For installed deb packages, 0.1.4 selects only `linux-x86_64-deb`. The feed also
retains the identical legacy `linux-x86_64` and explicit `linux-x86_64-appimage`
AppImage entries so installed 0.1.3 readers remain compatible. Remote metadata
cannot switch a deb installation into an AppImage installation.
`deb_updater.rs` checks root ownership of the installed executable and its
parents and dpkg's ownership/version record. One `pkexec` invocation starts an
early non-GUI mode of `/usr/bin/shacraft-launcher`; no password is collected by
the launcher and no fallback prompt runs after cancellation. Bounded stdin
framing carries the signed envelope and package bytes, never user paths.
The helper authenticates both again as root, checks exact package identity
`sha-craft-launcher`, architecture and version, stages the package under a
root-only temporary directory and invokes the fixed dpkg installer. An explicit
`--refuse-downgrade` protects against another installation winning the version
race. Failed/partial package transactions require honest system-package recovery;
they are not reported as completed or automatically retried. Restart launches
the fixed installed executable even after dpkg replaces the running inode.
The native updater holds installation, account and game permits while installing
and until restart. The game permit remains held until the launched Java child
exits. These guards cover this launcher process, not other launcher instances.
@@ -248,3 +267,19 @@ The original source AppImage was retained. The installed 0.1.3 AppImage was
then started from `~/Applications` and its captured runtime paths verified.
This is not a full GUI update/restart cycle or a Windows/macOS installation test.
The Linux build host remains Ubuntu 26.04.
The 0.1.4 checkpoint passed 84 native tests (6 ignored), 32 UI tests and 18
publisher tests; 12 browser scenarios use mocked IPC. In a disposable Ubuntu
26.04 Docker container without network or production mounts, the actual signed
deb helper passed 10 scenarios: unprivileged invocation, truncated/trailing
input, damaged metadata/package, dpkg lock, unsafe temporary directory,
successful installation, replay and downgrade refusal. The fixture installed
the genuine old 0.1.3 package and bootstrapped the new verifier binary over its
package record; it then installed the genuine signed 0.1.4. It did not relabel
signed versions. Dpkg reported 0.1.4 and a fixture profile marker survived.
This tests the elevated helper and dpkg, not a real desktop PolicyKit dialog.
Cancellation/error rendering is covered by unit/browser scenarios. Published
metadata and deb bytes match the locally verified files; both website download
buttons target 0.1.4. No user host package installation was performed for QA.
The live native AppImage smoke also passed against the published 0.1.4 feed,
including corruption rejection and replacement of only a temporary source copy.
+85 -14
View File
@@ -9,9 +9,13 @@ updater verifies signatures before installing; an unavailable or invalid feed
does not prevent playing with the installed launcher.
The first version containing the updater must be installed manually. Version
0.1.2 has no code capable of installing this feature itself. On Linux, automatic
replacement is for AppImage installations; deb installations and development
binaries use the manual download path. Windows/macOS publication and actual
0.1.2 has no code capable of installing this feature itself. AppImage supports
self-updates from 0.1.3; deb adds them in 0.1.4. An existing 0.1.3 deb therefore
needs one manual upgrade to 0.1.4 before its own update button can work. A deb
installation keeps its package format and asks for system administrator
authorization when installing an update. The launcher itself runs as the normal
user. Development binaries use the manual download path. Windows/macOS
publication and actual
installation tests remain separate release work; supporting a platform in the
feed schema does not certify a working release on it.
@@ -22,7 +26,7 @@ The archived 2026-09-09 review bundle at
unreleased updater prototype: GitHub-hosted `latest.json`, a different pinned
key and the former LoginSystem/Game Bridge proof flow. Its successful CI and
prototype version numbers do not establish compatibility with the deployed
admission protocol. The current 0.1.3 release follows the deployed 0.1.2
admission protocol. The 0.1.3 and 0.1.4 releases follow the deployed 0.1.2
admission line based on `bf43254`, using the ShaCraft-hosted feed described here.
Never publish the archived `CI-NOT-FOR-RELEASE`/`CI_NOT_FOR_RELEASE` packages or
@@ -56,13 +60,65 @@ verified through Tauri's built-in updater signature check. The current version
must increase; there is no unsigned or automatic downgrade fallback.
The stable publisher accepts only plain `MAJOR.MINOR.PATCH` versions and these
platforms: `linux-x86_64` (`.AppImage`), `windows-x86_64` (`.exe` or `.msi`),
platforms: `linux-x86_64` (legacy `.AppImage`), `linux-x86_64-appimage`
(`.AppImage`), `linux-x86_64-deb` (`.deb`), `windows-x86_64` (`.exe` or `.msi`),
`darwin-x86_64` and `darwin-aarch64` (`.app.tar.gz`). Artifact filenames contain
only ASCII letters, digits, dots, underscores and hyphens. Files must already
exist in the matching version directory, must not be symlinks and must be
between 1 byte and 256 MiB. A platform without a tested signed artifact is
omitted, never represented by an empty signature or another platform's file.
Format-aware Linux feeds must contain both AppImage keys with exactly the same
URL and signature. This keeps 0.1.3 clients on their original AppImage path.
New AppImage clients prefer `linux-x86_64-appimage` and can read the legacy key;
deb clients require `linux-x86_64-deb` and never fall back to an AppImage.
Preserve all three entries when publishing a release that supports both formats.
After verifying each signature, the publisher also checks Linux package format.
AppImage must have the ELF64 little-endian x86_64 and type-2 AppImage header.
For deb, `/usr/bin/dpkg-deb` must report package `sha-craft-launcher`, architecture
`amd64` and the exact signed release version. Inspection uses fixed arguments,
no shell, a cleared environment, a 10-second timeout and a 4 KiB output limit.
It does not install a package or execute its maintainer scripts. This protects
against accidental publication of the wrong signed package; an installer
signature remains mandatory and is checked before package inspection.
## Debian installation boundary
The installed launcher must be `/usr/bin/shacraft-launcher`, owned by root in
root-owned directories that other users cannot write. The package database
must assign that file to an installed `sha-craft-launcher` of the expected
architecture. The updater needs the system `pkexec` authorization agent; it
does not collect a password or fall back to running a shell with privileges.
After the normal-user downloader verifies the update, `pkexec` launches the
fixed installed binary with `--shacraft-install-deb`. This mode runs before
Tauri/GTK initialization. It accepts only length-bounded signed metadata and
package bytes over stdin, never a user-provided package path. The root helper
independently verifies the metadata, selects only the exact deb target, checks
the package signature and requires a higher version than the current dpkg
database. It writes the verified bytes to a root-created mode-0700 temporary
directory under the validated `/var/tmp`; the file has mode 0600.
The helper checks the package's exact name, version and architecture with
`dpkg-deb`, then invokes fixed `dpkg --refuse-downgrade --install` arguments in
an environment without inherited variables. Dpkg's own downgrade refusal
protects against a competing newer installation between the version check and
the package-manager lock. A successful result also requires the package
database to report the intended version as installed. The temporary package
is removed on completion. A signed deb may include maintainer scripts, which
dpkg runs with administrator privileges as part of normal installation: review
release package contents before signing.
Cancellation of system authorization, missing authorization support, signature
rejection, a busy package manager and installation failure have distinct
messages. There is no automatic retry with weaker checks. Dpkg installation
is not an atomic file replacement: dependency/configuration failures or power
loss can require normal package-manager recovery. The launcher reports failure
instead of claiming the old installation is intact. Successful deb updates
restart the fixed installed binary as the ordinary user. These guarantees
are separate from AppImage's same-directory atomic replacement.
## Keys and builds
The production private key stays **only on the operator's local machine** at
@@ -88,33 +144,40 @@ package signatures; passing updater checks does not establish those assurances.
## Local preparation and signing
The publisher requires Python 3.10+ and `minisign`. It performs verification
The publisher requires Python 3.10+ and `minisign`; releases containing deb also
require `/usr/bin/dpkg-deb` (Debian/Ubuntu's `dpkg` package). It performs verification
through the standard minisign CLI, without implementing cryptography in Python.
`--minisign /absolute/path/to/minisign` supports a locally extracted tool without
installing a global package. Run these examples from the launcher repository,
substituting the actual release version and tested filenames.
1. Stage immutable, tested packages below a local downloads root. The following
example assumes the Linux artifact already exists at
`/tmp/shacraft-release/downloads/0.1.3/ShaCraft.Launcher_0.1.3_amd64.AppImage`
example assumes both tested Linux artifacts already exist below
`/tmp/shacraft-release/downloads/0.1.4/`
and release notes exist at `/tmp/shacraft-release/notes.txt`. Create signatures
with the Tauri CLI; `.sig` is written beside each artifact:
```bash
npm run tauri -- signer sign \
--private-key-path /home/emil/.local/share/shacraft-updater/production.key \
/tmp/shacraft-release/downloads/0.1.3/ShaCraft.Launcher_0.1.3_amd64.AppImage
/tmp/shacraft-release/downloads/0.1.4/ShaCraft.Launcher_0.1.4_amd64.AppImage
npm run tauri -- signer sign \
--private-key-path /home/emil/.local/share/shacraft-updater/production.key \
/tmp/shacraft-release/downloads/0.1.4/ShaCraft.Launcher_0.1.4_amd64.deb
```
2. Prepare a deterministic payload after verifying every package signature.
Repeat `--artifact PLATFORM=FILENAME` for each tested platform included in this
release. Do not list a deb, dmg, nonexistent package or untested architecture:
release. Keep the legacy AppImage alias. Do not list a dmg, nonexistent
package or untested architecture:
```bash
python3 scripts/publish_launcher_update.py prepare \
--version 0.1.3 \
--version 0.1.4 \
--downloads-root /tmp/shacraft-release/downloads \
--artifact linux-x86_64=ShaCraft.Launcher_0.1.3_amd64.AppImage \
--artifact linux-x86_64=ShaCraft.Launcher_0.1.4_amd64.AppImage \
--artifact linux-x86_64-appimage=ShaCraft.Launcher_0.1.4_amd64.AppImage \
--artifact linux-x86_64-deb=ShaCraft.Launcher_0.1.4_amd64.deb \
--notes-file /tmp/shacraft-release/notes.txt \
--public-key /home/emil/.local/share/shacraft-updater/production.key.pub \
--payload /tmp/shacraft-release/release.payload.json
@@ -173,8 +236,12 @@ must validate against the actual deployed feed, not an empty staging directory.
Caddy should serve this feed as JSON with `Cache-Control: no-store`. Check the
public response, decoded metadata, signatures and downloadable artifact hashes
after deployment. Exercise a real installed AppImage updating to a higher
version, including relaunch and retained settings/account state. Unit tests,
packaging or a browser mock alone do not establish successful installation.
version, including relaunch and retained settings/account state. For deb, also
exercise administrator cancellation, package-manager lock conflicts, failed
installation and a successful package upgrade/relaunch. Use an isolated system
for destructive package-manager failure cases; never modify player data as a
test fixture. Unit tests, packaging or a browser mock alone do not establish
successful installation or distribution compatibility.
If a release is faulty, stop offering it and publish a corrected higher version;
do not weaken signature checks or silently downgrade users.
@@ -187,6 +254,10 @@ python3 -m unittest discover -s scripts -p 'test_*.py'
Publisher tests exercise the real minisign CLI with temporary test keys,
including valid publication, modified packages and metadata, authenticated
previous-version checks, downgrade refusal, URL/path restrictions and dry-run.
Linux tests also build real temporary deb packages with `dpkg-deb`, validate
package/version/architecture, ensure inspection never executes maintainer
scripts, retain the legacy AppImage feed alias, and reject malformed signed
Linux packages. Package-inspection output and time bounds are exercised.
No test private key is checked into the repository. CI installs minisign so
the signature tests run; locally they explicitly skip if the tool is absent.
Set `SHACRAFT_TEST_MINISIGN` to use an extracted executable.