diff --git a/.gitea/scripts/desktop-collect.sh b/.gitea/scripts/desktop-collect.sh index d8f5d4e..98b1e13 100755 --- a/.gitea/scripts/desktop-collect.sh +++ b/.gitea/scripts/desktop-collect.sh @@ -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 `.sig` ab +# (minisign, eine Base64-Zeile). Dieses Skript traegt deren INHALT als +# `files..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 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 : schreibt den Inhalt von .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)" diff --git a/.gitea/scripts/desktop-stamp.sh b/.gitea/scripts/desktop-stamp.sh index 816ec7b..8ecc2a8 100755 --- a/.gitea/scripts/desktop-stamp.sh +++ b/.gitea/scripts/desktop-stamp.sh @@ -43,6 +43,13 @@ # liest ausserhalb von apps/desktop keine Werkstatt-Datei -- frontendDist ist # ../src innerhalb von apps/desktop, keine Abhaengigkeit auf packages/*. # +# quick-260917-kgc: `check` verlangt zusaetzlich `updateVersion` und je +# Plattform `files.

.signature` im gecachten Manifest -- ein Cache-Stand +# aus der Zeit vor der Update-Funktion (oder aus einem Bau mit --no-sign) +# wird nie uebernommen, sonst lieferte der Update-Endpunkt dauerhaft 204. +# Ein Schluesselwechsel (`pubkey` in tauri.conf.json unter apps/desktop) +# aendert den Stempel automatisch, weil apps/desktop in DESKTOP_PATHS liegt. +# # Dieses Skript kennt kein Secret. set -eu @@ -112,6 +119,10 @@ cmd_check() { if [ "$SHA" != "$EXP_SHA" ]; then no_reuse "Pruefsumme von $2 weicht ab" fi + SIG="$(jq -r ".files.$1.signature // empty" "$MANIFEST")" + if [ -z "$SIG" ]; then + no_reuse "Signatur fuer $2 fehlt im Manifest" + fi } if [ "$CACHE_HIT" != "true" ]; then @@ -132,6 +143,11 @@ cmd_check() { no_reuse "Version $MVERSION statt $STAMP_VERSION" fi + UPDATE_VERSION="$(jq -r '.updateVersion // empty' "$MANIFEST")" + if [ -z "$UPDATE_VERSION" ]; then + no_reuse "updateVersion fehlt im Manifest (Stand vor der Update-Funktion)" + fi + LINUX_NAME="$(jq -r '.files.linux.name // empty' "$MANIFEST")" WINDOWS_NAME="$(jq -r '.files.windows.name // empty' "$MANIFEST")" if [ -z "$LINUX_NAME" ] || [ -z "$WINDOWS_NAME" ]; then diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2c90e76..73ca48b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,6 +14,9 @@ # Desktop-Pfaden, siehe .gitea/scripts/desktop-stamp.sh) bereits fertige # Pakete im Zwischenspeicher des Runners liegen; Tags v* bauen immer neu; # `publish` bleibt unveraendert. +# quick-260917-kgc: Die beiden tauri-build-Schritte signieren die Pakete mit dem +# Updater-Schluessel (Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD, nur an diesen +# zwei Schritten); desktop-collect.sh traegt die .sig-Inhalte ins Manifest. name: Tessera CI/CD on: @@ -178,10 +181,16 @@ jobs: - name: Linux-AppImage bauen if: steps.reuse.outputs.reuse != 'true' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: pnpm --filter @tessera/desktop exec tauri build --bundles appimage - name: Windows-Installer bauen (Cross-Bau) if: steps.reuse.outputs.reuse != 'true' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: pnpm --filter @tessera/desktop exec tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis - name: Pakete einsammeln diff --git a/.gitignore b/.gitignore index d7f9d0d..a12373f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ user-files/ # Desktop-Pakete aus dem Bau (Phase 18) desktop-dist/* !desktop-dist/.gitkeep + +# Privater Updater-Signierschluessel liegt ausserhalb des Repos (~/.tessera/desktop-updater/) -- nie einchecken +*.key