PSG Launcher
An all-in-one launcher for PSG tools. Fetches a signed manifest from your Gitea instance, displays available apps, and handles install/update/launch — with OTA updates for both the launcher itself and every app it manages.
Security model
| Threat | Mitigation |
|---|---|
| MITM on the wire | All fetches use https_only(true) — plain HTTP is rejected at the Rust layer |
| Tampered manifest | apps.json has a detached ed25519 signature (apps.json.sig); the launcher verifies it before parsing |
| Tampered package | Each app package has an inline SHA-256 hash and a separate ed25519 signature; both are verified before writing a single byte to disk |
| Rollback / replay | Tauri's self-updater rejects packages whose version ≤ the running version |
| Compromised CDN / server | The private key never touches the server; only signed content is accepted |
| Supply chain (npm) | Frontend is a thin React shell; all crypto lives in Rust with audited crates |
Two separate keys are used:
manifest-private.pem— signsapps.jsonand each app package (your custom key)- Tauri signer key — signs the launcher installer for self-updates (generated by Tauri CLI)
Prerequisites
| Tool | Version |
|---|---|
| Rust | 1.77+ |
| Node.js | 20+ |
| OpenSSL | 3.x (winget install ShiningLight.OpenSSL.Light) |
| WebView2 | Bundled with Windows 11 |
First-time setup
1 — Generate the manifest signing keypair
.\scripts\keygen.ps1
This writes keys\manifest-private.pem and keys\manifest-public.pem.
Copy the printed base64 value into src-tauri\src\config.rs as MANIFEST_PUBLIC_KEY_B64.
⚠
keys\is in.gitignore. Never commit the private key.
2 — Generate the Tauri self-updater keypair
npm run tauri signer generate -- -w keys\tauri.key
Copy the printed public key into src-tauri\tauri.conf.json under plugins.updater.pubkey.
3 — Update config.rs
Open src-tauri\src\config.rs and replace the three YOURDOMAIN / OWNER placeholders with your actual Gitea instance URL and repository owner name.
4 — Update tauri.conf.json
Replace the YOURDOMAIN / OWNER placeholders in src-tauri\tauri.conf.json.
5 — Install npm dependencies
npm install
Development
npm run tauri dev
The Tauri dev server starts Vite at http://localhost:1420 and hot-reloads the UI.
Building a release locally
npm run tauri build
Installer goes to src-tauri\target\release\bundle\nsis\.
Adding an app to the manifest
- Build your app and note its
.exe/.zippath. - Sign the package and get its hash + signature:
.\scripts\sign-package.ps1 -PackagePath .\path\to\your-app.exe - Copy the printed values into
manifests\apps.json(add a new entry or updatecurrent_version). - Re-sign the manifest:
.\scripts\sign-manifest.ps1 - Commit, push, and tag:
git add manifests\ git commit -m "feat: add your-app v1.0.0" git tag v0.2.0 git push && git push --tags
The release workflow triggers automatically, builds the launcher, and pushes the updated manifests to the releases branch where the launcher can reach them.
CI secrets (Gitea repository settings → Secrets)
| Secret | Value |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Content of keys\tauri.key |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Passphrase (or empty string) |
MANIFEST_SIGNING_KEY |
Content of keys\manifest-private.pem |
GITEA_TOKEN |
Personal access token with repo write scope |
Manifest structure
manifests/apps.json
{
"schema_version": 1,
"generated_at": "ISO 8601 timestamp",
"minimum_launcher_version": "0.1.0",
"apps": [
{
"id": "my-app", // stable, URL-safe ID
"name": "My App",
"description": "...",
"category": "tools", // tools | utilities | games | media | development | network | other
"current_version": "1.0.0",
"icon_url": null, // optional HTTPS URL
"changelog": "...",
"tags": [],
"platforms": {
"windows-x86_64": {
"download_url": "https://...",
"hash_sha256": "lowercase hex",
"size_bytes": 1234567,
"install_type": "portable", // portable | zip | installer
"install_path": null, // null = default (%APPDATA%\PSG\<id>)
"signature": "base64 ed25519 sig of download bytes"
}
}
}
]
}
manifests/apps.json.sig
Plain text file containing the base64-encoded ed25519 signature of the raw apps.json bytes.
manifests/latest.json
Tauri's self-update format — updated automatically by the release workflow.
Project layout
PSG-Conduit/
├── src/ React + TypeScript frontend
│ ├── components/ UI components (AppCard, Sidebar, TitleBar, …)
│ ├── hooks/ useAppManifest, useLauncherUpdate
│ ├── lib/commands.ts Type-safe Tauri invoke wrappers
│ └── types/manifest.ts Shared TypeScript types
├── src-tauri/ Rust backend
│ ├── src/
│ │ ├── commands/ Tauri commands (manifest, apps, updater)
│ │ ├── models/ Serde types (manifest, installed)
│ │ ├── config.rs Public key + manifest URLs
│ │ └── error.rs Serialisable error enum
│ ├── tauri.conf.json
│ └── capabilities/
├── manifests/ Cloud manifests (committed to `releases` branch via CI)
│ ├── apps.json
│ ├── apps.json.sig ← generated by sign-manifest.ps1
│ └── latest.json ← generated by release workflow
├── scripts/
│ ├── keygen.ps1 Generate ed25519 keypair
│ ├── sign-manifest.ps1 Sign apps.json → apps.json.sig
│ └── sign-package.ps1 Get hash + sig for a single app package
└── .gitea/workflows/
└── release.yml Build + sign + publish on tag push