diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61e1ee8..ec8b0dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,75 @@ jobs: DATATOOLS_TESS_STAGING: build/_tesseract/${{ matrix.platform }} run: pyinstaller build/datatools.spec --clean --noconfirm + # ---- macOS code signing + notarization (before DMG packaging) - + # Signs dist/DataTools.app with the Developer ID, notarizes it, + # and staples the ticket so Gatekeeper passes offline. Wrapped in + # a guard: if the cert secret is absent the step prints a warning + # and exits 0, so dry-run dispatches still produce an (unsigned) + # build. Secret names match build/README.md "Signing". + - name: Sign & notarize macOS app + if: matrix.os == 'macos-latest' + env: + CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }} + CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_PASSWORD }} + NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${CERT_P12_BASE64:-}" ]; then + echo "::warning::MACOS_DEVELOPER_ID_CERT_P12_BASE64 not set — shipping an UNSIGNED build (Gatekeeper will warn buyers)." + exit 0 + fi + + APP="dist/DataTools.app" + + # 1. Import the Developer ID cert into an ephemeral keychain. + KEYCHAIN="$RUNNER_TEMP/build.keychain-db" + KEYCHAIN_PW="$(uuidgen)" + security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + security set-keychain-settings -lut 3600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + echo "$CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" \ + -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" "$KEYCHAIN" >/dev/null + # Make the ephemeral keychain searchable (preserve the login keychain). + security list-keychains -d user -s "$KEYCHAIN" \ + $(security list-keychains -d user | sed 's/"//g') + + IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \ + | grep 'Developer ID Application' | head -1 | awk -F'"' '{print $2}')" + if [ -z "$IDENTITY" ]; then + echo "::error::No 'Developer ID Application' identity found in the imported cert." + exit 1 + fi + echo "Signing with: $IDENTITY" + + # 2. Sign the bundle (hardened runtime + secure timestamp + entitlements). + # --deep signs the nested dylibs/.so the PyInstaller bundle carries. + codesign --deep --force --options runtime --timestamp \ + --entitlements build/macos/entitlements.plist \ + --sign "$IDENTITY" "$APP" + codesign --verify --strict --verbose=2 "$APP" + + # 3. Notarize the .app (notarytool needs a zip/dmg/pkg, not a bare .app), + # then staple so Gatekeeper validates offline. + if [ -n "${NOTARY_APPLE_ID:-}" ]; then + ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/DataTools.zip" + xcrun notarytool submit "$RUNNER_TEMP/DataTools.zip" \ + --apple-id "$NOTARY_APPLE_ID" \ + --team-id "$NOTARY_TEAM_ID" \ + --password "$NOTARY_PASSWORD" \ + --wait + xcrun stapler staple "$APP" + xcrun stapler validate "$APP" + else + echo "::warning::Notary credentials not set — app is signed but NOT notarized (Gatekeeper will still warn)." + fi + + rm -f "$RUNNER_TEMP/cert.p12" + # ---- Per-platform installer packaging ------------------------ - name: Package macOS DMG (installer) diff --git a/build/README.md b/build/README.md index 314dfcc..d4cbb86 100644 --- a/build/README.md +++ b/build/README.md @@ -112,12 +112,15 @@ pyinstaller build/datatools.spec --clean --noconfirm ## Signing (Phase 2 — needs accounts/credentials) -Both code-signing steps are intentionally not in CI yet because they -require credentials the owner sets up first. +**macOS signing + notarization is now wired into `build.yml`** (the +"Sign & notarize macOS app" step, with `build/macos/entitlements.plist`). +It is guarded: if `MACOS_DEVELOPER_ID_CERT_P12_BASE64` is absent the step +warns and exits 0, so dry-run dispatches still produce an unsigned build. +To activate it, just add the secrets below — no code change needed. +**Windows** code-signing is still not wired (accepted v1 friction). **macOS** — Apple Developer Program enrollment ($99/yr). Once enrolled, -add these GitHub Secrets and uncomment the `codesign` + `notarytool` -steps in `build.yml`: +add these GitHub Secrets to activate the signing step in `build.yml`: | Secret | Value | |---|---| diff --git a/build/macos/entitlements.plist b/build/macos/entitlements.plist new file mode 100644 index 0000000..d833907 --- /dev/null +++ b/build/macos/entitlements.plist @@ -0,0 +1,28 @@ + + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.cs.disable-library-validation + + + com.apple.security.cs.allow-dyld-environment-variables + + +