audience: [contributor] source_files:
- crates/chatty-gpui/src/auto_updater/signature.rs
- crates/chatty-gpui/src/auto_updater/release_keys.txt
- crates/chatty-gpui/src/auto_updater/platform.rs
- .github/workflows/release.yml related:
- RELEASE_PROCESS.md
Release signing and update authenticity
When to read this: you are setting up or rotating the release key, a release
failed at sign-checksums, or a user’s update was refused.
Security note (SEC-10, AGE-817)
checksums.txt proves a download is intact, but it comes from the same GitHub
release as the binary. Anyone who can write the release (a leaked token, a
compromised workflow) can replace both. Two checks close that gap:
- Signed checksums, every platform.
release.ymlsignschecksums.txtwith an Ed25519 release key in minisign format and uploadschecksums.txt.sig. The updater downloads both and verifies the signature against the public keys compiled into it (auto_updater/release_keys.txt) before it trusts any hash. A missing, malformed or foreign signature refuses the update; there is no unsigned fallback. - macOS Team ID before the swap. After the DMG is mounted and before anything is
copied out of it, the install helper requires: the DMG passes Gatekeeper as
Notarized Developer ID(its stapled ticket), the app inside passescodesign --verify --deep --strict, and itsTeamIdentifierequals the Team ID compiled into the running app (CHATTY_MACOS_TEAM_ID, set byrelease.ymlfrom theNOTARIZE_TEAM_IDsecret). On refusal the helper leaves the installed app untouched and relaunches it; the reason is in~/Library/Logs/chatty_update.log.
Not covered: Windows has no code-signing certificate and releases carry no build provenance (decided in AGE-736). An attacker holding the release private key can still ship a signed update; the key therefore lives only in a protected environment that a single job can read.
The one unpinned state
While release_keys.txt lists no key, the updater skips the signature check and logs
No release signing key is pinned in this build. That is the state before the
runbook below has run. A build without a compiled Team ID (local builds, or releases
with MACOS_SIGNING_ENABLED off) skips the Team-ID check the same way: its own
releases carry no Team ID.
Where things live
| Thing | Where |
|---|---|
| Private key | RELEASE_SIGNING_KEY secret (base64 of the minisign secret-key file) in the release-signing GitHub environment; offline copy in your password manager |
| Public keys | crates/chatty-gpui/src/auto_updater/release_keys.txt, one RW... line per key |
| Switch | repo variable RELEASE_SIGNING_ENABLED (true = sign every release) |
| Signing job | sign-checksums in .github/workflows/release.yml |
| Key inventory | vault fabric-security §7 |
release.yml enforces the pairing both ways: with the switch on, a missing secret or
a signature that no pinned key accepts fails the release; with the switch off, a
pinned key fails the release. Either way no unsigned release reaches pinned clients.
Runbook: first key
Run on your own machine with minisign installed (brew install minisign or
apt install minisign). None of these commands prints the private key.
-
Generate the key in a private scratch directory, without a password (the CI job cannot type one; the protected environment is the access control):
umask 077 && mkdir -p ~/chatty-release-key && cd ~/chatty-release-key minisign -G -W -p release.pub -s release.key -
Create the environment and set the secret. Restrict the environment to the refs releases run from (
mainforprepare-release.yml,v*tags for the published-release and dispatch paths). Do not add required reviewers: releases are cut automatically and would wait forever.gh api -X PUT repos/boersmamarcel/chatty2/environments/release-signing \ -F 'deployment_branch_policy[protected_branches]=false' \ -F 'deployment_branch_policy[custom_branch_policies]=true' gh api -X POST repos/boersmamarcel/chatty2/environments/release-signing/deployment-branch-policies \ -f name=main -f type=branch gh api -X POST repos/boersmamarcel/chatty2/environments/release-signing/deployment-branch-policies \ -f name='v*' -f type=tag base64 < release.key | tr -d '\n' \ | gh secret set RELEASE_SIGNING_KEY --env release-signing --repo boersmamarcel/chatty2 -
Flip the switch. From now on every release carries
checksums.txt.sig(current installs ignore it; the signing job warns that no key is pinned yet).gh variable set RELEASE_SIGNING_ENABLED --body true --repo boersmamarcel/chatty2 -
Pin the public key. Add the second line of
release.pub(the one starting withRW) torelease_keys.txtin a PR. Merge it after step 3, never before: a build that pins a key must never be followed by an unsigned release. -
Release. The signing job now also checks the signature against the pinned key. Installs that update to this release enforce the signature from then on.
-
Store and clean up. Put
release.keyandrelease.pubin your password manager, thenrm -P(macOS) orshred -u(Linux) both files. Ask the coordinator to add the key ID (minisign -Vprints it) to the vault key inventory.
Rotation (two-key window, SEC-3)
- Generate the new key (step 1) and add its public line to
release_keys.txtnext to the old one. Release: installs now trust both. - Replace the
RELEASE_SIGNING_KEYsecret with the new key (step 2, last command). Release: signed by the new key, accepted by every install that took step 1’s release. - Remove the old line from
release_keys.txt. Release.
On a suspected private-key leak, skip the window: pin only the new key, set the new secret, release at once. Installs still on old builds trust the leaked key until they update, so revoke the leaked secret immediately.
When a release fails at signing
RELEASE_SIGNING_ENABLED is true but the RELEASE_SIGNING_KEY secret is not set: step 2 did not run, or the secret was set at repo level instead of in therelease-signingenvironment.does not verify against any key in release_keys.txt: the secret and the pinned public key are from different pairs. Fix whichever is wrong; never remove the pin to get a release out.release_keys.txt pins a release key but RELEASE_SIGNING_ENABLED is off: turn the switch back on.