Skip to main content

max / balanced_breakfast

Sign published Linux artifacts with minisign The Tauri updater key already signs the OTA bundle, so an update arriving through the updater is verified. A manual download of the .AppImage or .deb got none of that. Every published Linux artifact now carries a detached .minisig against the Make Creative release key, which is deliberately a different key: this one is checked by a person, not by the app, and nothing has its public half compiled in. sign-artifacts.sh verifies each signature against dist/makecreative.pub before returning, so a build host holding the wrong key fails the release instead of shipping artifacts nobody can check. The dist/* ignore rule covered both new files, so they are negated by name rather than by reopening dist/*.sh, keeping the rule in force for whatever else lands in dist/.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-28 15:39 UTC
Signed with PGP, not checked
Commit: 2b3e37ba4b66496b1c2c8655401017d780fd735c
Parent: a428e60
5 files changed, +133 insertions, -4 deletions
M .gitignore +7
@@ -25,6 +25,13 @@
25 25 # recipes are non-secret and tracked.
26 26 dist/*
27 27 !dist/recipes/
28 + # Artifact signing. sign-artifacts.sh holds no secret (the key and its password
29 + # live outside the repo, under ~/.minisign), and the public key is public by
30 + # definition: README.md tells people to verify downloads against it, so both have
31 + # to arrive with a checkout. Named individually rather than as `!dist/*.sh` to
32 + # keep the rule above in force for anything else that lands in dist/.
33 + !dist/sign-artifacts.sh
34 + !dist/makecreative.pub
28 35
29 36 # Tauri generated
30 37 src-tauri/gen/
M README.md +30
@@ -103,6 +103,36 @@
103 103 | Plugin authoring guide | `docs/plugin_authoring.md` |
104 104 | Architecture | `docs/architecture.md` |
105 105
106 + ## Verifying a release
107 +
108 + Every Linux release artifact (`.AppImage`, `.deb`) is published with a detached
109 + [minisign](https://jedisct1.github.io/minisign/) signature beside it, named
110 + `<artifact>.minisig`. Updates that arrive through the in-app updater are checked
111 + automatically against a separate key; this covers the manual download path,
112 + which nothing checks for you.
113 +
114 + Download both files, then:
115 +
116 + ```bash
117 + minisign -Vm balanced-breakfast_<version>_<arch>.AppImage -p makecreative.pub
118 + ```
119 +
120 + The public key is `dist/makecreative.pub` in this repository:
121 +
122 + ```
123 + untrusted comment: Make Creative, LLC release key (minisign)
124 + RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9
125 + ```
126 +
127 + You can also pass it inline instead of saving the file:
128 +
129 + ```bash
130 + minisign -Vm balanced-breakfast_<version>_<arch>.AppImage -P 'RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9'
131 + ```
132 +
133 + Expect `Signature and comment signature verified`. Any other result means the
134 + file is not what we published. Do not run it.
135 +
106 136 ## License
107 137
108 138 PolyForm Noncommercial 1.0.0
@@ -31,9 +31,26 @@
31 31 "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$BB_TAURI_PASSWORD " +
32 32 "cargo tauri build " + feature_flags());
33 33
34 - step("collect");
34 + // The Tauri updater key above signs the OTA bundle, so an update that arrives
35 + // through the updater is already verified. A user who downloads the AppImage or
36 + // the .deb by hand gets none of that, which is what this covers: a detached
37 + // minisign signature against the key in dist/makecreative.pub, published in
38 + // README.md. It is deliberately a different key from the updater one, because
39 + // this signature is checked by a person rather than by the app.
40 + // sign-artifacts.sh verifies each signature before it returns, so a build host
41 + // holding the wrong key fails here.
42 + step("sign");
35 43 let appimage = resolve_artifact(h, r + "/target/release/bundle/appimage/*.AppImage");
36 - collect(h, appimage, "balanced_breakfast", v);
37 44 let deb = resolve_artifact_opt(h, r + "/target/release/bundle/deb/*.deb");
38 - if deb != "" { collect(h, deb, "balanced_breakfast", v); }
39 - log("Balanced Breakfast " + target() + " v" + v + " collected.");
45 + let artifacts = "'" + appimage + "'";
46 + if deb != "" { artifacts = artifacts + " '" + deb + "'"; }
47 + sh_ok(h, "cd " + r + " && ./dist/sign-artifacts.sh " + artifacts);
48 +
49 + step("collect");
50 + collect(h, appimage, "balanced_breakfast", v);
51 + collect(h, resolve_artifact(h, appimage + ".minisig"), "balanced_breakfast", v);
52 + if deb != "" {
53 + collect(h, deb, "balanced_breakfast", v);
54 + collect(h, resolve_artifact(h, deb + ".minisig"), "balanced_breakfast", v);
55 + }
56 + log("Balanced Breakfast " + target() + " v" + v + " collected (signed).");
@@ -1,0 +1,2 @@
1 + untrusted comment: Make Creative, LLC release key (minisign)
2 + RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9
@@ -1,0 +1,73 @@
1 + #!/usr/bin/env bash
2 + # Sign release artifacts with the Make Creative release key (minisign).
3 + #
4 + # Every Linux artifact we publish gets a detached `.minisig` beside it so a
5 + # download can be checked against the public key in dist/makecreative.pub (also
6 + # printed in README.md). Nothing in the app auto-applies an update, so a user
7 + # running the verify command is the only thing between a compromised dist host
8 + # and a bad binary on their machine.
9 + #
10 + # dist/sign-artifacts.sh <artifact> [<artifact>...]
11 + #
12 + # Key material, present on every Linux build host (fw13, astra):
13 + #
14 + # ~/.minisign/makecreative.key encrypted secret key, mode 0600
15 + # ~/.minisign/password.env exports MINISIGN_PASSWORD
16 + #
17 + # Neither is in git. Generation, storage, and rotation are in
18 + # _private/docs/meta/ota-release-runbook.md.
19 + #
20 + # Signatures are verified against dist/makecreative.pub before this script
21 + # exits, so a build host holding the wrong key fails the release rather than
22 + # shipping artifacts nobody can verify.
23 +
24 + set -euo pipefail
25 +
26 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27 +
28 + KEY="${MINISIGN_KEY:-$HOME/.minisign/makecreative.key}"
29 + PASSWORD_ENV="${MINISIGN_PASSWORD_ENV:-$HOME/.minisign/password.env}"
30 + PUBKEY="${MINISIGN_PUBKEY:-$SCRIPT_DIR/makecreative.pub}"
31 +
32 + die() { echo "sign-artifacts: $*" >&2; exit 1; }
33 +
34 + [ "$#" -gt 0 ] || die "usage: $0 <artifact> [<artifact>...]"
35 +
36 + command -v minisign >/dev/null 2>&1 \
37 + || die "minisign is not on PATH (Debian/Ubuntu: apt install minisign)"
38 +
39 + [ -f "$KEY" ] || die "no signing key at $KEY (see the OTA release runbook)"
40 + [ -f "$PUBKEY" ] || die "no public key at $PUBKEY"
41 + if grep -q '^PLACEHOLDER' "$PUBKEY"; then
42 + die "$PUBKEY is still a placeholder: install the real public key (see the OTA release runbook)"
43 + fi
44 +
45 + if [ -z "${MINISIGN_PASSWORD:-}" ]; then
46 + [ -f "$PASSWORD_ENV" ] \
47 + || die "MINISIGN_PASSWORD is unset and $PASSWORD_ENV does not exist"
48 + # shellcheck disable=SC1090
49 + . "$PASSWORD_ENV"
50 + fi
51 + [ -n "${MINISIGN_PASSWORD:-}" ] || die "MINISIGN_PASSWORD is empty"
52 +
53 + for artifact in "$@"; do
54 + [ -f "$artifact" ] || die "no such artifact: $artifact"
55 + done
56 +
57 + # One invocation for the whole set: the password is read once, and minisign
58 + # still writes a per-file trusted comment (timestamp, filename, prehashed), so
59 + # a signature cannot be transplanted onto a different artifact.
60 + printf '%s\n' "$MINISIGN_PASSWORD" | minisign -S \
61 + -s "$KEY" \
62 + -c "Make Creative, LLC release artifact" \
63 + -m "$@" >/dev/null
64 +
65 + for artifact in "$@"; do
66 + if ! minisign -V -q -p "$PUBKEY" -m "$artifact" >/dev/null; then
67 + # Leave nothing behind that a later collect could mistake for a good
68 + # signature.
69 + rm -f "${artifact}.minisig"
70 + die "signature for $artifact does not verify against $PUBKEY"
71 + fi
72 + echo "signed: ${artifact}.minisig"
73 + done