Ctrl K
ComunidadGuías…

docs/DESKTOP_SIGNING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Desktop installer and code signing

What the Windows installer looks like now, how code signing switches itself
on, and what you (the owner) have to buy, apply for and configure. The
reasoning behind the choices is in
[DESKTOP_SIGNING_INSTALLER_RESEARCH_2026-10.md](DESKTOP_SIGNING_INSTALLER_RESEARCH_2026-10.md)
and ADR-005 in [ARCHITECTURE_DECISIONS.md](ARCHITECTURE_DECISIONS.md).

**Today: nothing is signed, and every build is green.** Signing turns on by
itself for a platform as soon as its secrets exist in GitHub. Until then
Windows shows "Windows protected your PC" and macOS blocks the first open, as
before.

## The Windows installer

### What users see

Double-clicking `LobbyForge_<version>_x64-setup.exe` installs without
questions, the way Discord does:

1. No admin prompt. The app installs for the current user, in
   `%LOCALAPPDATA%\LobbyForge`.
2. A small dark window with the LobbyForge header and a progress bar. It
   closes itself after a second or two.
3. LobbyForge opens.

The installer also creates a Start Menu entry and a desktop shortcut,
registers `lobbyforge://` links, and appears in Settings → Apps with an
uninstaller. The uninstaller asks for confirmation and offers to delete the
app data; the data stays unless you tick that box.

The installer speaks English and Turkish, and picks the Windows display
language without asking. Any other language gets English.

**Updating:** download the new installer and run it. If LobbyForge is open,
the installer closes it, replaces the files in place, keeps the shortcuts and
settings, and opens the new version. Running the same version again
reinstalls it; Tauri does that by uninstalling first, so a LobbyForge icon
pinned to the taskbar has to be pinned again.

### Command-line switches

| Switch | Effect |
|---|---|
| (none) | One-click install, then the app opens |
| `/WIZARD` | The classic wizard: welcome, install folder, finish page with "Run LobbyForge" and "Create desktop shortcut" |
| `/S` | Silent: no window, the app does not open (winget, scripts, the CI smoke test) |
| `/P` | Passive: progress window only, the app opens only with `/R` |
| `/R` | With `/S` or `/P`: open the app when done |
| `/NS` | No shortcuts |
| `/D=C:\path` | Install folder (NSIS standard; must be the last switch) |

Silent uninstall: `"%LOCALAPPDATA%\LobbyForge\uninstall.exe" /S`.

### The MSI

The MSI stays as a second download for organisations that deploy software
centrally. It always installs for all users, so it asks for admin rights; that
is fixed in Tauri's WiX template. It carries the same branding, in English
only (WiX builds one MSI per language). CI builds it only for versions without
a pre-release suffix (WiX rejects `rc.1`), and not in Certum-signed builds
(see below).

### Artwork

`node apps/desktop/scripts/make-installer-assets.mjs` (or
`pnpm --filter @lobbyforge/desktop installer:assets`) draws every installer
image from `apps/desktop/src-tauri/appicon.png` in the web theme's dark
colours and writes them to `apps/desktop/src-tauri/installer/`:

| File | Size | Used by |
|---|---|---|
| `installer.ico` | 16–256 px | installer and uninstaller icon |
| `nsis/header.bmp` | 150×57 | header band of every installer and uninstaller page |
| `nsis/sidebar.bmp` | 164×314 | welcome and finish pages (`/WIZARD`) |
| `wix/banner.bmp` | 493×58 | top of the MSI dialogs |
| `wix/dialog.bmp` | 493×312 | MSI welcome and finish dialogs |

Re-run it after changing the icon and commit the result. The text in the
images is rendered with the fonts of your machine (Segoe UI on Windows).

### How it is built

`apps/desktop/src-tauri/installer/nsis/installer.nsi` is a copy of Tauri's
own NSIS template (tauri-bundler 2.9.4, the one inside `@tauri-apps/cli`
2.11.4) with four small, fenced changes:

1. Brand colours for the header band and the welcome/finish pages.
2. One-click by default: Tauri's passive mode, unless `/WIZARD`, `/P`, `/S` or
   `/UPDATE` is given.
3. In passive mode the progress page shows only the heading ("Installing,
   please wait") and the bar: the per-file status line, the log, Back/Next and
   the branding line are hidden, the page turns dark, and the window shrinks
   to the header, the bar and Cancel. Cancel stays so that a failed install
   can be closed; the heading then says the install was aborted, and
   `/WIZARD` shows the full log. The wizard keeps Tauri's status line.
4. A one-click install opens the app at the end.

Uninstall, upgrade, WebView2 setup, shortcuts, deep links and the signing
hooks are Tauri's code, unchanged. No closed-source NSIS skin is used:
SignPath Foundation accepts no closed-source components.

## How signing works

[`desktop-release.yml`](../.github/workflows/desktop-release.yml) is the only
place desktop installers are built. `release.yml` calls it for every `v*` tag
and publishes the result with the server release; you can also run it by hand
(Actions → desktop-release → Run workflow) for a test build, which adds a
Windows install/launch/reinstall/uninstall smoke test.

[`apps/desktop/scripts/release-signing.mjs`](../apps/desktop/scripts/release-signing.mjs)
looks at which secrets are set and decides:

| Secrets set | Windows | macOS |
|---|---|---|
| none | unsigned, green | unsigned, green |
| `CERTUM_EMAIL` + `CERTUM_OTP` | everything signed by Certum: app, uninstaller, installer | — |
| `SIGNPATH_API_TOKEN` + `SIGNPATH_ORGANIZATION_ID` | app and installers signed by SignPath (release tags) | — |
| `APPLE_CERTIFICATE` + `APPLE_CERTIFICATE_PASSWORD` | — | Developer ID signed |
| … plus notarization secrets | — | signed and notarized |
| half of a set, or Certum and SignPath together | **build fails** with a message naming the missing secret | **build fails** |

Failing on half a configuration is deliberate: a release that silently comes
out unsigned is what you added the secrets to avoid.

Safety rules the workflow follows:
- Secrets live in a GitHub **environment** called `desktop-signing`, not in
  repository secrets. Add required reviewers there and every signing run
  waits for your click.
- The app is compiled in a step that sees no secret, so no build script or
  dependency can read them. Only the bundling step gets the secrets of the
  one provider in use.
- Secret values are never printed; the logs say which variable names are set.
- After bundling, the workflow checks the signatures (Windows:
  `Get-AuthenticodeSignature` on the installers and on the app inside the
  NSIS installer; macOS: `codesign`, `stapler`, `spctl`). A platform that
  fails uploads nothing, so it is missing from the release instead of
  published unsigned. The server release still goes out.
- `ssign` is downloaded from its GitHub release and checked against a pinned
  SHA-256; the SignPath action is pinned to a commit.

**Signing does not end the SmartScreen warning on day one.** A newly signed
installer still shows a warning until the certificate builds reputation, which
can take weeks and hundreds of clean installs. The difference is that the
warning names you as the publisher, and Smart App Control lets the app run.
Keep one signing identity: reputation belongs to the certificate.

## What you have to do

Pick **one** Windows route. The research recommends: apply to SignPath
Foundation now (free, but probably not accepted yet), and buy Certum when
public distribution gets close or SignPath says no.

### Route A: Certum Open Source code signing (SimplySign cloud)

Costs from €49. Your name appears on the certificate, as "Open Source
Developer, <your name>". Revoked if the software is ever sold.

1. Buy "Open Source Code Signing in the Cloud" at
   <https://shop.certum.eu/open-source-code-signing-on-simplysign.html>.
2. Complete identity verification: an ID photo, a bill in your name (proof of
   address) and the repository URL, <https://github.com/Juanka-e/LobbyForge>.
3. Install the SimplySign mobile app and activate it. **Before you scan the
   activation QR code**, save the secret it contains: the QR encodes an
   `otpauth://totp/...?secret=XXXX` link, and `XXXX` (base32) is the TOTP
   seed. Anyone with the seed and your e-mail can sign as you, so treat it
   like a private key; if it leaks, re-issue the QR code in Certum.
4. In GitHub: Settings → Environments → **New environment** → `desktop-signing`.
   - Deployment protection rules → **Required reviewers** → add yourself
     (recommended: every signing build waits for your approval).
   - Do not add branch or tag restrictions: manual test builds run from
     `main` and would be rejected.
   - Environment secrets:
     - `CERTUM_EMAIL`: your SimplySign account e-mail.
     - `CERTUM_OTP`: the base32 seed from step 3 (the full `otpauth://` link
       also works).
5. Run Actions → desktop-release → Run workflow, approve the deployment, and
   check the log of "Plan code signing" (`Windows signing: Certum SimplySign`)
   and "Verify Windows signatures" (`Valid`, signer = your certificate).
6. Download the `desktop-windows` artifact and verify it yourself (below).

From then on every `v*` release is signed. Certum builds ship the NSIS
installer only: `ssign` cannot sign MSI files yet, and an unsigned MSI next to
a signed EXE invites the wrong download.

Note: whether Certum's terms allow logging in automatically with the TOTP
seed could not be confirmed. If you prefer not to store the seed at all, use
the release-day fallback below instead of steps 4–5.

#### Release-day fallback: sign on your own Windows machine

The seed never leaves your phone; you type a one-time code instead.

1. Install the prerequisites from [DESKTOP.md](DESKTOP.md#build-from-source)
   (Rust, MSVC, Node, pnpm) and `ssign`: download
   `ssign-windows-x86_64.zip` from
   <https://github.com/Le-Syl21/ssign/releases> (version 0.1.7 has SHA-256
   `4ee5389a74ddbeb67e18fe8c8b963734ca13de7b24beffbe430c3f427ecc5e40`) and
   unzip `ssign.exe` somewhere on your `PATH`.
2. After the `v*` release workflow has finished, check out the tag and run:
   ```powershell
   git checkout v0.3.0
   pnpm install --frozen-lockfile
   pnpm --filter @lobbyforge/desktop sign:local -- --version 0.3.0
   ```
   It compiles first, then asks for your Certum e-mail and the current
   6-digit code from the SimplySign app (a code lasts 30 seconds; ssign keeps
   the session for 20 minutes, so one code signs everything), bundles the
   signed installer, checks the signature, and prints its SHA-256.
3. Run the two `gh release upload … --clobber` commands it prints: they
   replace the unsigned `desktop-windows-…-setup.exe` and its `.sha256` in the
   release. Update the matching line in the release's `SHA256SUMS.txt` the
   same way.

With SimplySign Desktop logged in (the certificate then sits in the Windows
certificate store) you can use the store instead of ssign:
`… sign:local -- --version 0.3.0 --thumbprint <certificate SHA-1 thumbprint>`.

### Route B: SignPath Foundation (free, if accepted)

The certificate says "SignPath Foundation", so your name does not appear.
Every release needs your manual approval in SignPath.

1. Keep two-factor authentication on for every GitHub account with write
   access.
2. Add a "Code signing policy" section to the project homepage or README:
   "Free code signing provided by SignPath.io, certificate by SignPath
   Foundation", the team roles (author, reviewer, approver: you) and a
   privacy sentence (the app only connects to the server the user enters).
3. Apply at <https://signpath.org/apply>.
4. Once accepted, in SignPath:
   - Create (or use) the project with slug `LobbyForge`.
   - Add two artifact configurations by pasting the files in
     [`apps/desktop/signing/signpath/`](../apps/desktop/signing/signpath/):
     `app-exe.xml` with slug `app-exe`, and `installers.xml` with slug
     `installers`.
   - Signing policies: `release-signing` (manual approval) and, optionally,
     `test-signing`.
   - Trusted build system: link the GitHub repository (GitHub.com connector).
   - Create an API token for a CI user that may submit to these policies.
5. In GitHub, environment `desktop-signing` (create it as in Route A):
   - secret `SIGNPATH_API_TOKEN`;
   - variable (or secret) `SIGNPATH_ORGANIZATION_ID`;
   - optional variables: `SIGNPATH_PROJECT_SLUG` (default `LobbyForge`),
     `SIGNPATH_POLICY_SLUG` (default `release-signing`),
     `SIGNPATH_TEST_POLICY_SLUG` (set it to `test-signing` to also sign manual
     test builds), `SIGNPATH_APP_ARTIFACT_CONFIG` (default `app-exe`),
     `SIGNPATH_INSTALLERS_ARTIFACT_CONFIG` (default `installers`).
   - Remove any `CERTUM_*` secrets: two providers fail the build.
6. On each `v*` release you get two signing requests per Windows build: first
   the app exe, then the installers. Approve both within an hour each.

How it works and what it cannot do: SignPath only signs files uploaded as
workflow artifacts, so it cannot hook into the Tauri build. The workflow
compiles the app, has SignPath sign the exe, builds the installers around the
signed exe, then has SignPath sign the installers. SignPath cannot open NSIS
installers, so the uninstaller that NSIS generates stays unsigned on this
route (Smart App Control may block it). The MSI and everything in it is
signed.

### macOS (when Mac users arrive)

Apple Developer Program, USD 99 per year; individuals get no fee waiver.

1. Join at <https://developer.apple.com/programs/>.
2. In Xcode or the developer portal create a **Developer ID Application**
   certificate, export it from Keychain Access as a `.p12` with a password,
   and base64-encode it: `base64 -i cert.p12 | pbcopy`.
3. Environment `desktop-signing` secrets:
   - `APPLE_CERTIFICATE`: the base64 text; `APPLE_CERTIFICATE_PASSWORD`: its
     password. Optional `APPLE_SIGNING_IDENTITY`, e.g.
     `Developer ID Application: Your Name (TEAMID)`.
   - Notarization, one of:
     - `APPLE_ID`, `APPLE_PASSWORD` (an app-specific password from
       appleid.apple.com) and `APPLE_TEAM_ID`; or
     - an App Store Connect API key: `APPLE_API_ISSUER`, `APPLE_API_KEY` (the
       key ID) and `APPLE_API_PRIVATE_KEY` (the contents of
       `AuthKey_<id>.p8`).

A certificate without notarization signs the app, but Gatekeeper still blocks
it; the log says so.

## Verifying a signed build

Windows, on the downloaded installer:
- Right-click → Properties → **Digital Signatures**: the signer is your
  certificate (or SignPath Foundation), with a timestamp.
- PowerShell:
  ```powershell
  Get-AuthenticodeSignature .\desktop-windows-LobbyForge_0.3.0_x64-setup.exe |
    Format-List Status, SignerCertificate, TimeStamperCertificate
  ```
  `Status` must be `Valid`.
- After installing, check `%LOCALAPPDATA%\LobbyForge\lobbyforge-desktop.exe`
  and `uninstall.exe` the same way.
- With the Windows SDK: `signtool verify /pa /v <file>`.
- Compare the SHA-256 with the release's `.sha256` file, as the download page
  explains.

macOS:
```sh
codesign -dv --verbose=4 /Applications/LobbyForge.app
spctl -a -vv /Applications/LobbyForge.app      # "accepted, source=Notarized Developer ID"
xcrun stapler validate /Applications/LobbyForge.app
```

## Secrets and variables reference

All in Settings → Environments → `desktop-signing`. `release.yml` passes them
to the desktop build by name; nothing else receives them.

| Name | Kind | Route | What |
|---|---|---|---|
| `CERTUM_EMAIL` | secret | Certum | SimplySign account e-mail |
| `CERTUM_OTP` | secret | Certum | TOTP seed from the SimplySign activation QR code |
| `SIGNPATH_API_TOKEN` | secret | SignPath | API token of the CI user |
| `SIGNPATH_ORGANIZATION_ID` | variable or secret | SignPath | organization ID |
| `SIGNPATH_PROJECT_SLUG`, `SIGNPATH_POLICY_SLUG`, `SIGNPATH_TEST_POLICY_SLUG`, `SIGNPATH_APP_ARTIFACT_CONFIG`, `SIGNPATH_INSTALLERS_ARTIFACT_CONFIG` | variables | SignPath | optional overrides, defaults above |
| `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD` | secrets | macOS | Developer ID `.p12` (base64) and its password |
| `APPLE_SIGNING_IDENTITY` | secret | macOS | optional identity name |
| `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID` | secrets | macOS | notarization with an Apple ID |
| `APPLE_API_ISSUER`, `APPLE_API_KEY`, `APPLE_API_PRIVATE_KEY` | secrets | macOS | notarization with an API key |

Not related: `LF_RELEASE_SIGNING_KEY` signs the server's release manifest, not
the desktop apps; the future Tauri updater key (`TAURI_SIGNING_PRIVATE_KEY`)
signs update packages, and neither affects SmartScreen.

## Publisher name

`bundle.publisher` in `apps/desktop/src-tauri/tauri.conf.json` is
**"LobbyForge contributors"**. It shows as the publisher in Settings → Apps and
as the company name in the exe's properties; the copyright line uses it too.
It used to be "LobbyForge", the same as the product name, which the Microsoft
Store does not accept.

It does not have to match the certificate; Windows shows the certificate's
name in security prompts either way. Change it if you prefer:
- your own name, matching a Certum certificate (it then also appears in
  Settings → Apps);
- your Microsoft Store developer display name, if you publish there.

Changing it again moves one registry key (`HKCU\Software\<publisher>\LobbyForge`,
which remembers the install folder); the default folder is the same, so
upgrades are not affected.

## Upgrading Tauri

The NSIS template is a copy of Tauri's, so every `@tauri-apps/cli` upgrade
needs a rebase. A unit test in `apps/desktop` fails when the installed CLI
version differs from the one in the template's `TAURI_TEMPLATE_BASE` line.

1. Find the bundler version of the new CLI:
   `https://crates.io/api/v1/crates/tauri-cli/<version>/dependencies`
   (`tauri-bundler`; it is published together with the CLI).
2. Download the old and the new crate
   (`https://static.crates.io/crates/tauri-bundler/tauri-bundler-<v>.crate`, a
   `.tar.gz`) and diff `src/bundle/windows/nsis/installer.nsi` between them.
3. Apply that diff to our template, keeping the four `LOBBYFORGE (n/4)`
   blocks, and update the header and `TAURI_TEMPLATE_BASE` line.
4. Build (`pnpm --filter @lobbyforge/desktop tauri build --bundles nsis`) and
   run the installer: one-click, `/WIZARD`, `/S`, an upgrade over a running
   app, and the uninstaller.