max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
6 files changed,
+141 insertions,
-4 deletions
| @@ -4,6 +4,8 @@ | |||
| 4 | 4 | dist/*.dmg | |
| 5 | 5 | dist/*.AppImage | |
| 6 | 6 | dist/*.deb | |
| 7 | + | # Detached signatures follow the artifacts they sign, and are never committed. | |
| 8 | + | dist/*.minisig | |
| 7 | 9 | # Release/build scripts are source — keep them version-controlled even if a | |
| 8 | 10 | # broader dist/ ignore is ever added. | |
| 9 | 11 | !dist/*.sh |
| @@ -96,6 +96,35 @@ | |||
| 96 | 96 | | Device export profiles | `crates/audiofiles-rhai/plugins/bundled/` | | |
| 97 | 97 | | Architecture | `docs/architecture.md` | | |
| 98 | 98 | ||
| 99 | + | ## Verifying a release | |
| 100 | + | ||
| 101 | + | Every Linux release artifact (`.AppImage`, `.deb`) is published with a detached | |
| 102 | + | [minisign](https://jedisct1.github.io/minisign/) signature beside it, named | |
| 103 | + | `<artifact>.minisig`. audiofiles does not update itself, so running this check is | |
| 104 | + | the only thing that tells you a download is the build we published. | |
| 105 | + | ||
| 106 | + | Download both files, then: | |
| 107 | + | ||
| 108 | + | ```bash | |
| 109 | + | minisign -Vm Audiofiles-<version>-<arch>.AppImage -p makecreative.pub | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | The public key is `dist/makecreative.pub` in this repository: | |
| 113 | + | ||
| 114 | + | ``` | |
| 115 | + | untrusted comment: Make Creative, LLC release key (minisign) | |
| 116 | + | RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9 | |
| 117 | + | ``` | |
| 118 | + | ||
| 119 | + | You can also pass it inline instead of saving the file: | |
| 120 | + | ||
| 121 | + | ```bash | |
| 122 | + | minisign -Vm Audiofiles-<version>-<arch>.AppImage -P 'RWSMMbsBuZY5GfFRHPd19bZAVcyFAI4zsUlPjC5RaS/5tL7zT45SpjD9' | |
| 123 | + | ``` | |
| 124 | + | ||
| 125 | + | Expect `Signature and comment signature verified`. Any other result means the | |
| 126 | + | file is not what we published. Do not run it. | |
| 127 | + | ||
| 99 | 128 | ## License | |
| 100 | 129 | ||
| 101 | 130 | [PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/) |
| @@ -56,5 +56,19 @@ | |||
| 56 | 56 | # Cleanup | |
| 57 | 57 | rm -rf "$APPDIR" | |
| 58 | 58 | ||
| 59 | + | # Step 5: Sign the AppImage, when a release key is available. | |
| 60 | + | # | |
| 61 | + | # A release build must be signed, but a developer building locally has no key | |
| 62 | + | # and should not be blocked, so this is skipped rather than failed when the key | |
| 63 | + | # is absent. The Bento recipe (dist/recipes/linux.rhai) signs unconditionally, | |
| 64 | + | # which is what makes signing mandatory for anything we publish. | |
| 65 | + | MINISIGN_KEY="${MINISIGN_KEY:-$HOME/.minisign/makecreative.key}" | |
| 66 | + | if [ -f "$MINISIGN_KEY" ]; then | |
| 67 | + | echo "==> Signing AppImage..." | |
| 68 | + | MINISIGN_KEY="$MINISIGN_KEY" "$DIST_DIR/sign-artifacts.sh" "$APPIMAGE_PATH" | |
| 69 | + | else | |
| 70 | + | echo "==> No signing key at $MINISIGN_KEY, skipping signature (unreleasable build)" | |
| 71 | + | fi | |
| 72 | + | ||
| 59 | 73 | echo "" | |
| 60 | 74 | echo "Done: $APPIMAGE_PATH" |
| @@ -24,9 +24,26 @@ | |||
| 24 | 24 | sh_ok(h, "cd " + r + " && ./dist/build-appimage.sh"); | |
| 25 | 25 | sh_ok(h, "cd " + r + " && ./dist/build-deb.sh"); | |
| 26 | 26 | ||
| 27 | - | step("collect"); | |
| 27 | + | // audiofiles has no updater, so a download from the dist host is the only way a | |
| 28 | + | // user gets a build and nothing verifies it for them. Every artifact we publish | |
| 29 | + | // carries a detached minisign signature against the key in dist/makecreative.pub. | |
| 30 | + | // build-appimage.sh already signs the AppImage when a key is on the host; this | |
| 31 | + | // re-signs it so the guarantee comes from the recipe rather than from the key | |
| 32 | + | // happening to be there, and it is the only thing that signs the .deb. | |
| 33 | + | // sign-artifacts.sh verifies each signature against the public key before it | |
| 34 | + | // returns, so a build host holding the wrong key fails here instead of shipping. | |
| 35 | + | step("sign"); | |
| 28 | 36 | let appimage = resolve_artifact(h, r + "/dist/*.AppImage"); | |
| 29 | - | collect(h, appimage, "audiofiles", v); | |
| 30 | 37 | let deb = resolve_artifact_opt(h, r + "/dist/*.deb"); | |
| 31 | - | if deb != "" { collect(h, deb, "audiofiles", v); } | |
| 32 | - | log("audiofiles " + target() + " v" + v + " collected."); | |
| 38 | + | let artifacts = "'" + appimage + "'"; | |
| 39 | + | if deb != "" { artifacts = artifacts + " '" + deb + "'"; } | |
| 40 | + | sh_ok(h, "cd " + r + " && ./dist/sign-artifacts.sh " + artifacts); | |
| 41 | + | ||
| 42 | + | step("collect"); | |
| 43 | + | collect(h, appimage, "audiofiles", v); | |
| 44 | + | collect(h, resolve_artifact(h, appimage + ".minisig"), "audiofiles", v); | |
| 45 | + | if deb != "" { | |
| 46 | + | collect(h, deb, "audiofiles", v); | |
| 47 | + | collect(h, resolve_artifact(h, deb + ".minisig"), "audiofiles", v); | |
| 48 | + | } | |
| 49 | + | log("audiofiles " + 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 |