From 452dba4a8a215b62ac555034cabe0073e5bff137 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Thu, 17 Sep 2026 16:25:08 +0200 Subject: [PATCH] Add Developer ID signing and notarization to the macOS build. Co-authored-by: Cursor --- .github/workflows/build.yml | 32 ++++++++++ README.md | 37 ++++++++--- macos/Maturita.entitlements | 14 +++++ scripts/bundle-macos.sh | 118 +++++++++++++++++++++++++++++++++++- 4 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 macos/Maturita.entitlements diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49ebaeb..e319cdc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,9 +94,41 @@ jobs: run: | brew install gtk4 pkg-config + # Developer ID lives in repo secrets. Without it the bundle is only + # ad-hoc signed and Gatekeeper still blocks downloaded copies. + - name: Import Developer ID certificate + if: ${{ secrets.MACOS_CERTIFICATE != '' }} + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + cert="$RUNNER_TEMP/certificate.p12" + keychain="$RUNNER_TEMP/signing.keychain-db" + pass="$(openssl rand -base64 32)" + echo "$MACOS_CERTIFICATE" | base64 --decode > "$cert" + security create-keychain -p "$pass" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$pass" "$keychain" + security import "$cert" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s \ + -k "$pass" "$keychain" + security list-keychains -d user -s "$keychain" \ + $(security list-keychains -d user | tr -d '"') + rm -f "$cert" + - name: Build .app zip env: COMMIT: ${{ github.sha }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: | chmod +x scripts/bundle-macos.sh ./scripts/bundle-macos.sh diff --git a/README.md b/README.md index 86d20be..adc4eb9 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ build, takže soubor nikdy nezestárne. Kam se to nainstaluje: `/Applications` nebo `~/Applications` (macOS), `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). -Buildy nejsou podepsané, proto je při **prvním** spuštění potřeba výše uvedený -krok navíc. Aplikace se pak udržuje sama: po startu porovná svůj commit se -souborem `VERSION` ve větvi `builds` a aktualizaci nabídne v pruhu nahoře. -Ruční kontrola je v **Nastavení → Aktualizace**. +Buildy zatím nejsou notarizované (chybí Apple Developer ID), proto je při +**prvním** spuštění potřeba výše uvedený krok navíc. Aplikace se pak udržuje +sama: po startu porovná svůj commit se souborem `VERSION` ve větvi `builds` +a aktualizaci nabídne v pruhu nahoře. Ruční kontrola je +v **Nastavení → Aktualizace**. #### Bez instalátoru @@ -95,6 +96,16 @@ make bundle # AppImage / .app zip / Windows složka s DLL Volitelně CMake: `cmake -S . -B build && cmake --build build`. +Aby stažená aplikace na macOS šla otevřít bez potvrzení v Nastavení, CI +potřebuje **Apple Developer Program** a GitHub Secrets: + +- `MACOS_CERTIFICATE` — Developer ID Application `.p12` v base64 +- `MACOS_CERTIFICATE_PASSWORD` +- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` — notarizace + (nebo `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID`) + +Bez toho zůstane jen ad-hoc podpis a Gatekeeper stažený build zablokuje. + #### Windows (MSYS2 UCRT64, bez Visual Studia) ```cmd @@ -154,10 +165,10 @@ the current build when you run it, so the file never goes stale. Install locations: `/Applications` or `~/Applications` (macOS), `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). -The builds are unsigned, so the first launch needs the extra step above. -After that the app stays current: it compares its commit to `VERSION` on -`builds` and offers an update in a banner. Manual check: -**Settings → Updates**. +The builds are not notarized yet (no Apple Developer ID), so the first +launch still needs the extra step above. After that the app stays current: +it compares its commit to `VERSION` on `builds` and offers an update in a +banner. Manual check: **Settings → Updates**. #### Packages without the installer @@ -216,6 +227,16 @@ make bundle # AppImage / .app zip / Windows DLL folder Optional CMake: `cmake -S . -B build && cmake --build build`. +Skipping Gatekeeper on a downloaded macOS build needs an +**Apple Developer Program** membership and these GitHub Secrets: + +- `MACOS_CERTIFICATE` — Developer ID Application `.p12`, base64-encoded +- `MACOS_CERTIFICATE_PASSWORD` +- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` for notarization + (or `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID`) + +Without them the bundle stays ad-hoc signed and Gatekeeper still blocks it. + #### Windows (MSYS2 UCRT64, no Visual Studio) ```cmd diff --git a/macos/Maturita.entitlements b/macos/Maturita.entitlements new file mode 100644 index 0000000..a917201 --- /dev/null +++ b/macos/Maturita.entitlements @@ -0,0 +1,14 @@ + + + + + + com.apple.security.cs.allow-dyld-environment-variables + + + com.apple.security.cs.disable-library-validation + + + diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh index 0269111..3e23f6f 100755 --- a/scripts/bundle-macos.sh +++ b/scripts/bundle-macos.sh @@ -243,8 +243,120 @@ if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true fi -# Ad-hoc sign the .app so Gatekeeper is less likely to kill dyld loads. -codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true +# Developer ID + notarization is what lets a downloaded app open without +# System Settings. Without a certificate the bundle stays ad-hoc signed, which +# is enough for local runs but Gatekeeper still blocks internet downloads. +ENTITLEMENTS="$ROOT/macos/Maturita.entitlements" + +pick_sign_identity() { + if [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then + printf '%s\n' "$MACOS_SIGN_IDENTITY" + return + fi + local found + found="$(security find-identity -v -p codesigning 2>/dev/null \ + | awk -F'"' '/Developer ID Application/ { print $2; exit }')" + if [[ -n "$found" ]]; then + printf '%s\n' "$found" + return + fi + printf '%s\n' "-" +} + +sign_one() { + local path="$1" + local use_entitlements="${2:-}" + local args=(--force --sign "$IDENTITY") + + if [[ "$IDENTITY" != "-" ]]; then + args+=(--options runtime --timestamp) + fi + if [[ "$use_entitlements" == "entitlements" && -f "$ENTITLEMENTS" ]]; then + args+=(--entitlements "$ENTITLEMENTS") + fi + codesign "${args[@]}" "$path" +} + +sign_bundle() { + local f + + echo "==> Signing $APP_NAME" + IDENTITY="$(pick_sign_identity)" + if [[ "$IDENTITY" == "-" ]]; then + echo " identity: ad-hoc (downloaded copies will still need Gatekeeper approval)" + else + echo " identity: $IDENTITY" + fi + + shopt -s nullglob + for f in "$FW"/*.dylib; do + sign_one "$f" + done + shopt -u nullglob + + sign_one "$MACOS/maturita-bin" entitlements + sign_one "$MACOS/maturita" entitlements + sign_one "$APP" entitlements + + codesign --verify --strict --verbose=2 "$APP" +} + +can_notarize() { + [[ "$IDENTITY" != "-" ]] || return 1 + if [[ -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_ID:-}" && + -n "${APPLE_API_ISSUER:-}" ]]; then + return 0 + fi + if [[ -n "${APPLE_ID:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" && + -n "${APPLE_TEAM_ID:-}" ]]; then + return 0 + fi + return 1 +} + +notarize_bundle() { + local zip notary_args keyfile + + if [[ "$IDENTITY" == "-" ]]; then + return 0 + fi + if ! can_notarize; then + if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then + echo "error: Developer ID is present but notarization secrets are missing." >&2 + echo "Set APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or" >&2 + echo "APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID." >&2 + exit 1 + fi + echo " warning: signed, but not notarized — Gatekeeper will still block downloads" + return 0 + fi + + echo "==> Notarizing $APP_NAME" + zip="$ROOT/dist/.notarize-$OUT_ZIP" + rm -f "$zip" + ditto -c -k --keepParent "$APP" "$zip" + + notary_args=(submit "$zip" --wait) + keyfile="" + if [[ -n "${APPLE_API_KEY:-}" ]]; then + keyfile="$(mktemp)" + printf '%s\n' "$APPLE_API_KEY" > "$keyfile" + notary_args+=(--key "$keyfile" --key-id "$APPLE_API_KEY_ID" + --issuer "$APPLE_API_ISSUER") + else + notary_args+=(--apple-id "$APPLE_ID" + --password "$APPLE_APP_SPECIFIC_PASSWORD" + --team-id "$APPLE_TEAM_ID") + fi + + xcrun notarytool "${notary_args[@]}" + [[ -n "$keyfile" ]] && rm -f "$keyfile" + rm -f "$zip" + xcrun stapler staple "$APP" + echo " stapled notarization ticket" +} + +sign_bundle echo "==> Smoke-testing launch" if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then @@ -270,6 +382,8 @@ else echo " smoke test OK (process stayed up)" fi +notarize_bundle + echo "==> Zipping" rm -f "$ROOT/dist/$OUT_ZIP" (