Files
hash-wallet/scripts/release/sign-release.sh
T
jwinterm c1f618176c ci(desktop): build macOS, and publish every desktop artifact to the release page
Three desktop targets, three different states, one destination.

macOS did not build at all. There was no workflow, only Cake's from-source
scripts and an Xcode project. It turns out most of the work was already done:
monero_c ships aarch64-apple-darwin and x86_64-apple-darwin, and
Runner.xcodeproj already references both dylibs with CodeSignOnCopy. So this
fetches the same prebuilt bundle the iOS and Android legs use, lipos the two
slices into universal libraries, and builds. One download serves Apple Silicon
and Intel, because a single-arch build looks correct on the machine that made
it and fails on half of everyone else's.

It signs with a Developer ID Application certificate, which is NOT the Apple
Distribution certificate the TestFlight workflow uses. They are not
interchangeable: an App Store cert on a directly downloaded app is refused by
Gatekeeper. Signing is inside-out by hand rather than --deep, which Apple
deprecated and which skips nested code it does not recognise. Then notarytool
with the App Store Connect key this repository already has, then stapler, so
Gatekeeper clears the app without a network round trip.

Every credential is optional and the preflight says, before spending forty
minutes, exactly which stages the run can reach and what the user will see if
it cannot reach them. Absent secrets downgrade the build loudly; they do not
fail it and they do not pass silently.

Windows and Linux already built. Windows already Authenticode-signs through the
Fleet broker. What both lacked was a way to get the artifact to anybody:
upload-artifact is the only publication and Gitea Actions artifacts here upload
and then list as zero, so the signed Windows zip has never been reachable. Both
now publish to the GitHub release, on the smirk pattern: tag-triggered, the
release must already exist so a half-populated one never goes public, and a
same-named asset from an earlier attempt is replaced rather than duplicated.
Windows does its API work in PowerShell because git-bash there has neither
python nor node guaranteed and ConvertFrom-Json is native.

Tags now build automatically on all three. A tag is the thing a release is cut
from, and the artifact that gets signed should come from it rather than from
whatever was dispatched by hand.

scripts/release/sign-release.sh detach-signs the collected artifacts and signs
SHA256SUMS, which is the signature that matters when a download reaches someone
from a mirror without its .asc. It verifies even immediately after signing: a
signature written but never checked invites trust it has not earned.

Not done here, and both need a human: GH_RELEASE_TOKEN does not exist in this
repository's Gitea secrets, and the release key's user ID still says Smirk.
2026-09-23 08:13:10 -04:00

100 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# sign-release.sh: detach-sign every shipped desktop artifact, and verify.
#
# Same shape as smirk's, deliberately: one person verifies downloads from both
# products and should not have to learn two procedures.
#
# windows : HashBags-windows-<tag>.zip
# macos : HashBags-macos-<version>.zip
# linux : Hash_Bags-x86_64.AppImage
#
# Each artifact gets a detached ASCII signature beside it (<file>.asc), and
# SHA256SUMS is written and signed too. Signing the sums file is the one that
# matters most: it is what lets someone verify a download that reached them
# from a mirror or a store page, where the .asc never travelled with the file.
#
# Usage:
# scripts/release/sign-release.sh 1.0.3 --dir ~/release-1.0.3
# scripts/release/sign-release.sh 1.0.3 --dir ~/release-1.0.3 --verify
# HASHBAGS_SIGNING_KEY=<keyid!> scripts/release/sign-release.sh 1.0.3 --dir ...
#
# HASHBAGS_SIGNING_KEY should name the RELEASE SIGNING SUBKEY, not the primary,
# and should carry a trailing exclamation mark so gpg uses exactly that subkey
# rather than choosing one itself. The primary never needs to be present on a
# build machine, and is not needed here.
set -euo pipefail
fail() { echo "sign-release: $1" >&2; exit 1; }
VERSION="${1:-}"
if [ -z "$VERSION" ] || [ "$VERSION" = "-h" ] || [ "$VERSION" = "--help" ]; then
sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'
exit 2
fi
shift
DIR=""
VERIFY_ONLY=0
while [ $# -gt 0 ]; do
case "$1" in
--dir) DIR="${2:-}"; shift 2 ;;
--verify) VERIFY_ONLY=1; shift ;;
*) fail "unknown argument: $1" ;;
esac
done
[ -n "$DIR" ] || fail "no --dir. Where are the downloaded artifacts?"
[ -d "$DIR" ] || fail "no such directory: $DIR"
# The company release key. One key covers Hash Bags and Smirk, which is a
# deliberate choice: the fingerprint a user is asked to trust belongs to Such
# Software rather than to a product, so it does not have to be re-learned per
# app. The cost is that rotating it affects everything, which is why the
# primary stays offline and only this subkey signs.
KEY="${HASHBAGS_SIGNING_KEY:-96F6836D5110C2CB!}"
command -v gpg >/dev/null || fail "gpg is not installed"
cd "$DIR"
shopt -s nullglob
FILES=( HashBags-windows-*.zip HashBags-macos-*.zip Hash_Bags-*.AppImage *.dmg *.deb )
[ ${#FILES[@]} -gt 0 ] || fail "no release artifacts found in $DIR"
echo "artifacts in $DIR:"
printf ' %s\n' "${FILES[@]}"
echo
if [ "$VERIFY_ONLY" -eq 0 ]; then
# SHA256SUMS first, so the signature covers the final set rather than a set
# that grew afterwards.
: > SHA256SUMS
for f in "${FILES[@]}"; do
if command -v sha256sum >/dev/null; then sha256sum "$f" >> SHA256SUMS
else shasum -a 256 "$f" >> SHA256SUMS; fi
done
cat SHA256SUMS
echo
for f in "${FILES[@]}" SHA256SUMS; do
rm -f "$f.asc"
gpg --local-user "$KEY" --armor --detach-sign --output "$f.asc" "$f"
echo "signed $f"
done
echo
fi
# Verify unconditionally, including right after signing. A signature that was
# written but cannot be checked is worth less than none, because it invites
# trust it has not earned.
rc=0
for f in "${FILES[@]}" SHA256SUMS; do
[ -f "$f.asc" ] || { echo "MISSING SIGNATURE: $f.asc"; rc=1; continue; }
if gpg --verify "$f.asc" "$f" 2>/dev/null; then
echo "verified $f"
else
echo "VERIFY FAILED: $f"; rc=1
fi
done
[ "$rc" -eq 0 ] || fail "one or more artifacts did not verify"
echo
echo "all artifacts signed and verified for $VERSION"