15 KiB
Signed launcher updates
The application updater is separate from the signed Aeronautics modpack
manifest. Its only metadata endpoint is
https://shacraft.ru/launcher/updates/stable.json. Its artifact URLs are confined
to https://shacraft.ru/downloads/shacraft-launcher/<version>/<filename>.
The webview cannot choose a URL, signing key or executable path. The native
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. 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.
Release-line compatibility
The archived 2026-09-09 review bundle at
/home/emil/Desktop/shacraft-updater-review/README.md describes a different,
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 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
substitute the unreleased 0.2.0 prototype for an admission-compatible release.
Do not merge its updater key, endpoint or account flow blindly: that can break
both update continuity and server login. Future reconciliation requires an
explicit compatibility review retaining admission support and the key/feed
contract already distributed to players, or a separately designed migration.
Authentication contract
The public key embedded in the application is a dedicated Tauri updater
key, separate from the existing modpack manifest key. Tauri's minisign format
wraps the entire minisign public-key/signature text in standard base64. The
contents of a .sig file belong in metadata, not its filename or URL.
The stable feed contains the normal Tauri fields version, notes, pub_date
and platforms, and two additional fields:
signedPayload: standard base64 of the exact UTF-8 JSON bytes containing only the four normal fields. The publisher produces these bytes with sorted keys, compact separators, literal UTF-8 and no trailing newline.metadataSignature: the Tauri.sigcontents for those exact payload bytes, signed with the same updater key that signs the application packages.
The launcher authenticates the payload, requires it to equal the visible fields and then selects the signed platform artifact. This also authenticates the version and artifact URL: an old signed installer cannot be relabelled as a newer release by modifying unsigned metadata. Every artifact is separately 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 (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
/home/emil/.local/share/shacraft-updater/production.key, with owner-only
permissions. Its public companion is production.key.pub. Never transfer the
private key to the web server, GitHub, CI, logs, chat, a package or a public
artifact. Signing commands below pass the local path, not the key contents.
Keep a protected operator-controlled backup: replacing or losing the key will
break continuity for installations trusting the existing public key. There is
no automatic key rotation mechanism in this release.
Normal build.yml jobs explicitly merge scripts/tauri-unsigned.json to disable
updater signing. They upload ordinary packages and unsigned macOS .app.tar.gz
archives. CI does not receive the production key and does not publish the
stable feed. A release operator reviews/tests these build artifacts, then signs
the chosen packages locally. For a signed local Tauri bundle build, set
TAURI_SIGNING_PRIVATE_KEY to the protected key path; never disable verification
in the application to make a build pass.
Updater signatures authenticate ShaCraft's update channel. They are separate from Windows Authenticode, Apple signing/notarization, and Linux distribution package signatures; passing updater checks does not establish those assurances.
Local preparation and signing
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.
-
Stage immutable, tested packages below a local downloads root. The following 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;.sigis written beside each artifact: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.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 -
Prepare a deterministic payload after verifying every package signature. Repeat
--artifact PLATFORM=FILENAMEfor each tested platform included in this release. Keep the legacy AppImage alias. Do not list a dmg, nonexistent package or untested architecture:python3 scripts/publish_launcher_update.py prepare \ --version 0.1.4 \ --downloads-root /tmp/shacraft-release/downloads \ --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 -
Inspect the payload and sign its exact bytes locally:
npm run tauri -- signer sign \ --private-key-path /home/emil/.local/share/shacraft-updater/production.key \ /tmp/shacraft-release/release.payload.jsonEditing notes, timestamps, versions, signatures or URLs after this step invalidates the metadata signature. Prepare and sign again after any change.
Publication
Upload only the packages, their .sig files, release.payload.json, its
.sig, the public key and the publisher script. Stage and hash-check artifacts
before publishing metadata. Production paths are:
- Downloads root:
/root/shacraft/caddy/www/downloads/shacraft-launcher. - Stable feed:
/root/shacraft/data/launcher/updates/stable.json. - Public feed:
https://shacraft.ru/launcher/updates/stable.json.
Keep previous version directories immutable and save the current feed before replacing it. Run the publisher on the host with a public-key file and minisign available there. Neither operation needs a private key:
python3 publish_launcher_update.py publish \
--downloads-root /root/shacraft/caddy/www/downloads/shacraft-launcher \
--public-key /path/to/production.key.pub \
--payload /path/to/release.payload.json \
--signature /path/to/release.payload.json.sig \
--output /root/shacraft/data/launcher/updates/stable.json \
--dry-run
After that succeeds, repeat without --dry-run. The publisher verifies metadata
and all artifacts under the public key, authenticates the previous feed before
comparing versions, and refuses same-version replacement or downgrade. It holds
an exclusive publication lock and writes/fsyncs a temporary sibling before
atomically replacing stable.json. Dry-run validates everything but does not
replace the feed. Do not change staged artifacts concurrently with publication.
Do not overwrite a released version to add another platform: publish a higher
version containing the complete intended platform set.
Alternatively, run the same verification locally against byte-for-byte copies of the current feed and staged downloads, then deploy the resulting feed only after checking uploaded package and metadata hashes against those validated files. An initial publication has no previous feed; subsequent publications 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. 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.
Verification
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.
The artifact formats and signature encoding follow the official Tauri updater documentation.