Skip to main content

max / goingson

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 would have silently kept the public key out of the repo while README.md told people to verify against it, so it is negated explicitly.
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: bb46e03032f3185a5e8ee854c1c3e5e0217325c9
Parent: 26d6e80
5 files changed, +130 insertions, -4 deletions
M .gitignore +3
@@ -22,6 +22,9 @@
22 22 dist/*
23 23 !dist/recipes/
24 24 !dist/*.sh
25 + # The release-signing public key is public by definition: README.md tells people
26 + # to verify downloads against it, so it has to arrive with a checkout.
27 + !dist/makecreative.pub
25 28
26 29 # Archive
27 30 _archive/
M README.md +30
@@ -68,6 +68,36 @@
68 68 - **Keyboard shortcuts**: vim-style navigation throughout
69 69 - **Platforms**: macOS (primary), Windows, Linux; iOS in development
70 70
71 + ## Verifying a release
72 +
73 + Every Linux release artifact (`.AppImage`, `.deb`) is published with a detached
74 + [minisign](https://jedisct1.github.io/minisign/) signature beside it, named
75 + `<artifact>.minisig`. Updates that arrive through the in-app updater are checked
76 + automatically against a separate key; this covers the manual download path,
77 + which nothing checks for you.
78 +
79 + Download both files, then:
80 +
81 + ```bash
82 + minisign -Vm goingson_<version>_<arch>.AppImage -p makecreative.pub
83 + ```
84 +
85 + The public key is `dist/makecreative.pub` in this repository:
86 +
87 + ```
88 + untrusted comment: Make Creative, LLC release key (minisign)
89 + RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9
90 + ```
91 +
92 + You can also pass it inline instead of saving the file:
93 +
94 + ```bash
95 + minisign -Vm goingson_<version>_<arch>.AppImage -P 'RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9'
96 + ```
97 +
98 + Expect `Signature and comment signature verified`. Any other result means the
99 + file is not what we published. Do not run it.
100 +
71 101 ## License
72 102
73 103 PolyForm Noncommercial 1.0.0. Personal, research, and non-commercial use are free. For commercial use, contact `info@makenot.work` to discuss a commercial license.
@@ -30,11 +30,29 @@
30 30 "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$GOINGSON_TAURI_PASSWORD " +
31 31 "cargo tauri build");
32 32
33 - step("collect");
33 + // The Tauri updater key above signs the OTA bundle, so an update that arrives
34 + // through the updater is already verified. A user who downloads the AppImage or
35 + // the .deb by hand gets none of that, which is what this covers: a detached
36 + // minisign signature against the key in dist/makecreative.pub, published in
37 + // README.md. It is deliberately a different key from the updater one, because
38 + // this signature is checked by a person rather than by the app.
39 + // sign-artifacts.sh verifies each signature before it returns, so a build host
40 + // holding the wrong key fails here.
41 + //
34 42 // Resolve concrete paths (collect quotes its source, so a glob would not expand
35 43 // for a local host, fw13 is the daemon's own host).
44 + step("sign");
36 45 let appimage = resolve_artifact(h, r + "/target/release/bundle/appimage/*.AppImage");
37 - collect(h, appimage, "goingson", v);
38 46 let deb = resolve_artifact_opt(h, r + "/target/release/bundle/deb/*.deb");
39 - if deb != "" { collect(h, deb, "goingson", v); }
40 - log("GoingsOn " + target() + " v" + v + " collected.");
47 + let artifacts = "'" + appimage + "'";
48 + if deb != "" { artifacts = artifacts + " '" + deb + "'"; }
49 + sh_ok(h, "cd " + r + " && ./dist/sign-artifacts.sh " + artifacts);
50 +
51 + step("collect");
52 + collect(h, appimage, "goingson", v);
53 + collect(h, resolve_artifact(h, appimage + ".minisig"), "goingson", v);
54 + if deb != "" {
55 + collect(h, deb, "goingson", v);
56 + collect(h, resolve_artifact(h, deb + ".minisig"), "goingson", v);
57 + }
58 + log("GoingsOn " + 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