ci: Desktop-Pakete mit dem Updater-Schluessel signieren, Signatur und updateVersion ins Manifest

- ci.yml: Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD als env nur an den
  beiden tauri-build-Schritten des Jobs desktop (kein Job-env, kein
  --no-sign); alles andere strukturgleich
- desktop-collect.sh: liest <bundle>.sig (genau eine Base64-Zeile) als
  files.<p>.signature ins Manifest, schreibt updateVersion (X.Y.Z bzw.
  X.Y.Z-beta.g<sha7>); fehlende .sig bricht auf main/Tag oder bei
  gesetztem Schluessel ab, warnt sonst (dev, lokal --no-sign)
- desktop-stamp.sh check: kein Cache-Stand ohne updateVersion und
  Signaturen beider Plattformen
- .gitignore: *.key

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 15:34:45 +02:00
parent de81c74e53
commit 7004b5b020
4 changed files with 97 additions and 5 deletions
+69 -5
View File
@@ -20,6 +20,21 @@
# Die Version kommt aus tauri.conf.json (von desktop-version.sh geschrieben
# oder als eingecheckte Basislinie vorhanden) -- dieses Skript liest sie nur,
# es schreibt sie nicht. Dieses Skript kennt kein Secret.
#
# Signatur (quick-260917-kgc): Die Tauri-CLI legt beim Bau mit
# `createUpdaterArtifacts` neben jedem Bundle eine `<bundle>.sig` ab
# (minisign, eine Base64-Zeile). Dieses Skript traegt deren INHALT als
# `files.<plattform>.signature` ins Manifest -- die `.sig`-Datei selbst wird
# nicht kopiert (die API liefert JSON). Pflichtregel: fehlt die `.sig`,
# bricht das Skript ab, wenn TAURI_SIGNING_PRIVATE_KEY gesetzt ist ODER der
# Kanal nicht `dev` ist (main/Tag -- dort sind Signaturen Pflicht,
# `--no-sign` gibt es nur lokal); sonst Warnung und das Feld entfaellt
# (lokaler Bau mit `tauri build --no-sign` bleibt moeglich, die API antwortet
# auf /desktop/update dann 204). Neues Feld `updateVersion` = die SemVer-Form
# fuer den Updater: X.Y.Z bei live/dev, X.Y.Z-beta.g<sha7> bei beta (das
# `g` ist Pflicht, ein rein numerischer SHA mit fuehrender Null waere kein
# gueltiger SemVer-Identifier). Der Schluessel wird nur auf Gesetztsein
# geprueft, nie gelesen oder ausgegeben.
set -eu
REQUIRE=""
@@ -79,6 +94,43 @@ esac
COMMIT="$(git rev-parse --short=7 HEAD)"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# SemVer-Form fuer den Updater (siehe Kopfkommentar): erst NACH der
# Versionspruefung bilden, SUFFIX/SHA_SHORT sind aus der Kanalentscheidung
# bekannt.
UPDATE_VERSION="$VERSION"
if [ "$CHANNEL" = beta ]; then
UPDATE_VERSION="$VERSION-beta.g$SHA_SHORT"
fi
SIGN_REQUIRED=0
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ "$CHANNEL" != dev ]; then
SIGN_REQUIRED=1
fi
# read_signature <bundle-pfad>: schreibt den Inhalt von <bundle-pfad>.sig
# nach stdout (genau eine Base64-Zeile). Fehlt die Datei: bei
# SIGN_REQUIRED=1 Fehler und exit 1, sonst Warnung und leere Ausgabe.
read_signature() {
SIG_FILE="$1.sig"
if [ -f "$SIG_FILE" ]; then
SIG_CONTENT="$(cat "$SIG_FILE")"
# grep prueft zeilenweise -- die Zeilenzahl deshalb getrennt erzwingen.
SIG_LINES="$(printf '%s\n' "$SIG_CONTENT" | wc -l | tr -d ' ')"
if [ "$SIG_LINES" -ne 1 ] || ! printf '%s' "$SIG_CONTENT" | grep -qE '^[A-Za-z0-9+/=]+$'; then
echo "Signaturdatei $SIG_FILE hat nicht die erwartete Form (genau eine Base64-Zeile)." >&2
exit 1
fi
printf '%s' "$SIG_CONTENT"
return 0
fi
if [ "$SIGN_REQUIRED" = 1 ]; then
echo "Signaturdatei $SIG_FILE fehlt. Im CI muessen TAURI_SIGNING_PRIVATE_KEY und TAURI_SIGNING_PRIVATE_KEY_PASSWORD an den tauri-build-Schritten gesetzt sein; --no-sign ist nur lokal erlaubt." >&2
exit 1
fi
echo "Warnung: Signaturdatei $SIG_FILE fehlt (Bau ohne Schluessel, Kanal dev) -- Feld signature entfaellt, kein Update in der App." >&2
return 0
}
mkdir -p "$DESKTOP_DIST"
# Alte Pakete/Manifest entfernen, Platzhalter (.gitkeep) bleibt erhalten.
rm -f "$DESKTOP_DIST"/*.AppImage "$DESKTOP_DIST"/*.exe "$DESKTOP_DIST/manifest.json"
@@ -90,9 +142,11 @@ trap 'rm -f "$TMP_MANIFEST"' EXIT INT TERM
LINUX_NAME=""
LINUX_SIZE=""
LINUX_SHA=""
LINUX_SIG=""
WINDOWS_NAME=""
WINDOWS_SIZE=""
WINDOWS_SHA=""
WINDOWS_SIG=""
case ",$REQUIRE," in
*,linux,*)
@@ -107,7 +161,10 @@ case ",$REQUIRE," in
cp "$APPIMAGE_SRC" "$DESKTOP_DIST/$LINUX_NAME"
LINUX_SIZE="$(stat -c %s "$DESKTOP_DIST/$LINUX_NAME")"
LINUX_SHA="$(sha256sum "$DESKTOP_DIST/$LINUX_NAME" | cut -d' ' -f1)"
echo "linux: $LINUX_NAME (${LINUX_SIZE} Bytes, sha256 $LINUX_SHA)"
LINUX_SIG="$(read_signature "$APPIMAGE_SRC")"
[ -n "$LINUX_SIG" ] || [ "$SIGN_REQUIRED" = 0 ] || exit 1
if [ -n "$LINUX_SIG" ]; then LINUX_SIGNED="signiert"; else LINUX_SIGNED="ohne Signatur"; fi
echo "linux: $LINUX_NAME (${LINUX_SIZE} Bytes, sha256 $LINUX_SHA, $LINUX_SIGNED)"
;;
esac
@@ -124,7 +181,10 @@ case ",$REQUIRE," in
cp "$NSIS_SRC" "$DESKTOP_DIST/$WINDOWS_NAME"
WINDOWS_SIZE="$(stat -c %s "$DESKTOP_DIST/$WINDOWS_NAME")"
WINDOWS_SHA="$(sha256sum "$DESKTOP_DIST/$WINDOWS_NAME" | cut -d' ' -f1)"
echo "windows: $WINDOWS_NAME (${WINDOWS_SIZE} Bytes, sha256 $WINDOWS_SHA)"
WINDOWS_SIG="$(read_signature "$NSIS_SRC")"
[ -n "$WINDOWS_SIG" ] || [ "$SIGN_REQUIRED" = 0 ] || exit 1
if [ -n "$WINDOWS_SIG" ]; then WINDOWS_SIGNED="signiert"; else WINDOWS_SIGNED="ohne Signatur"; fi
echo "windows: $WINDOWS_NAME (${WINDOWS_SIZE} Bytes, sha256 $WINDOWS_SHA, $WINDOWS_SIGNED)"
;;
esac
@@ -132,25 +192,29 @@ esac
# manuelles String-Zusammenbauen von JSON).
jq -n \
--arg version "$VERSION" \
--arg updateVersion "$UPDATE_VERSION" \
--arg channel "$CHANNEL" \
--arg commit "$COMMIT" \
--arg buildTime "$BUILD_TIME" \
--arg linuxName "$LINUX_NAME" \
--argjson linuxSize "${LINUX_SIZE:-null}" \
--arg linuxSha "$LINUX_SHA" \
--arg linuxSig "$LINUX_SIG" \
--arg windowsName "$WINDOWS_NAME" \
--argjson windowsSize "${WINDOWS_SIZE:-null}" \
--arg windowsSha "$WINDOWS_SHA" \
--arg windowsSig "$WINDOWS_SIG" \
'{
version: $version,
updateVersion: $updateVersion,
channel: $channel,
commit: $commit,
buildTime: $buildTime,
files: (
{}
+ (if $linuxName != "" then { linux: { name: $linuxName, size: $linuxSize, sha256: $linuxSha } } else {} end)
+ (if $windowsName != "" then { windows: { name: $windowsName, size: $windowsSize, sha256: $windowsSha } } else {} end)
+ (if $linuxName != "" then { linux: ({ name: $linuxName, size: $linuxSize, sha256: $linuxSha } + (if $linuxSig != "" then { signature: $linuxSig } else {} end)) } else {} end)
+ (if $windowsName != "" then { windows: ({ name: $windowsName, size: $windowsSize, sha256: $windowsSha } + (if $windowsSig != "" then { signature: $windowsSig } else {} end)) } else {} end)
)
}' > "$DESKTOP_DIST/manifest.json"
echo "Manifest geschrieben: $DESKTOP_DIST/manifest.json (Version $VERSION, Kanal $CHANNEL)"
echo "Manifest geschrieben: $DESKTOP_DIST/manifest.json (Version $VERSION, updateVersion $UPDATE_VERSION, Kanal $CHANNEL)"