# Release-binary signing
OpenLatch release binaries are signed with [minisign](https://jedisct1.github.io/minisign/) (Ed25519, pre-hashed mode). The auto-update path verifies signatures against the trusted public keys baked into `signing/openlatch.pub` before swapping in a new binary.
> Phase 1 spike scope: the verifier, sanity-check, and atomic swap mechanics are wired up under the hidden `openlatch __spike-update` subcommand. End-user `openlatch update [--check] [--apply]` lands in Phase 2. See `.local/brainstorms/auto-update/PHASE-1-spike.md`.
## Files in this directory
| `openlatch.pub` | The trust root. 1–3 base64 public keys, one per non-comment line. Baked into release binaries via `include_str!`. |
| `openlatch.key` | **NEVER COMMITTED.** Local working copy of the secret key during rotation. Add to `.gitignore`. |
| `README.md` | This file. |
The matching CI secrets are:
| `MINISIGN_SIGN_BIN_SECRET_KEY` | Full text of `openlatch.key` (the encrypted secret-key blob). |
| `MINISIGN_SIGN_BIN_SECRET_PASSWORD` | The password used during `minisign -G`. |
## First-time setup
```bash
# Generate a new keypair (interactive — sets the encryption password).
minisign -G -p signing/openlatch.pub.tmp -s signing/openlatch.key
# The generated openlatch.pub.tmp has two lines:
# untrusted comment: minisign public key XXXXXX
# <base64 key>
# Copy ONLY the base64 line into signing/openlatch.pub (after the
# placeholder block). The "untrusted comment" line is informational and
# would otherwise be misread as a key candidate.
# Save the secret + password into GitHub repo secrets:
# gh secret set MINISIGN_SIGN_BIN_SECRET_KEY < signing/openlatch.key
# gh secret set MINISIGN_SIGN_BIN_SECRET_PASSWORD --body '<password>'
# Verify locally:
# Wipe the working copy of the secret key.
shred -u signing/openlatch.key
```
## Rotation procedure (dual-sign overlap)
The trick: every binary trusts up to 3 baked keys. Releases during a rotation window are signed by BOTH the old and new active keys, so old clients verify via key A and new clients via key B until everyone has updated through the overlap.
1. Generate a new keypair: `minisign -G -p new.pub -s new.key`.
2. Open a PR adding the new public key as line 1 of `signing/openlatch.pub`. Move the previously-active key to line 2. Merge.
3. Update GitHub Actions secrets to the NEW keypair: replace `MINISIGN_SIGN_BIN_SECRET_KEY` with `new.key` content. ALSO add `MINISIGN_SIGN_BIN_PREV_SECRET_KEY` with the previous secret key (used during the overlap window for dual signing).
4. Modify the publish workflow's signing step to sign with BOTH keys for the overlap window. Each release tarball ships TWO `.minisig` files per binary: `openlatch.minisig` (new key) and `openlatch.prev.minisig` (old key). The verifier accepts whichever matches.
5. Ship at least 2 minor releases with dual signing.
6. After the overlap window, drop the previous-secret-key step. The `*.prev.minisig` files stop being produced. Old clients that never updated through the overlap window are stuck — manual reinstall only.
The verifier in `src/core/update.rs::verify_with_any_trusted_key` already accepts any matching key from the trusted list; no code change is needed for the overlap window itself. The `*.prev.minisig` consumption is wired up when the daemon-side update applier lands in P2.
## Security properties
| Trust root is the source repo | `openlatch.pub` is baked into the binary at compile time (`include_str!`); a runtime env-var override exists only behind `#[cfg(any(test, feature = "insecure-test-keys"))]` and is therefore inert in release builds. |
| Pre-hashed signatures | We sign and verify with `minisign -H` so the signed payload is the BLAKE2b hash of the binary, not the binary itself. Lets the verifier stream-hash a multi-megabyte binary without fully buffering. |
| Multi-key acceptance | Up to 3 keys, accept ANY match. Enables zero-downtime rotation. |
| Secret key never leaves CI | The local `openlatch.key` working copy from initial setup is shredded after upload to GitHub secrets. |
| No live download yet | Phase 1 spike binaries are passed in via CLI args. The HTTP fetch + tarball extraction land in Phase 2. |