Skip to content

macOS: sign and notarize the Workshop DMG (Gatekeeper reports it as damaged) #53

Description

@gregjkal

Problem

The macOS Workshop DMG is not codesigned. Tauri never runs codesign because no signingIdentity is configured, so the bundle carries only the linker's default ad hoc signature with no sealed resources:

$ codesign -dv PromptForge.app
Signature=adhoc
flags=0x20002(adhoc,linker-signed)
Sealed Resources=none
$ spctl --assess --type execute PromptForge.app
code has no resources but signature indicates they must be present

Gatekeeper treats that as a corrupt app rather than an unsigned one. A Safari-downloaded copy opened from Finder shows "PromptForge is damaged and can't be opened. You should move it to the Trash." with only Move to Trash / Cancel. There is no Open Anyway path in this state. The only way past it is xattr -d com.apple.quarantine, which no doc mentions.

Gatekeeper dialog: PromptForge is damaged and cannot be opened

Verified 2026-09-17 on Apple Silicon, macOS 26, with both the published PromptForge-arm64.dmg (0.2.0) and a DMG built from master (3121cdd). Both are in the same state.

Step 1: ad hoc sign the bundle (no certificate needed)

Set bundle.macOS.signingIdentity to "-" in crates/workshop/tauri.macos.conf.json. Tauri then ad hoc signs the whole bundle, including the externalBin gateway sidecar, with the hardened runtime and the existing Entitlements.plist.

Verified locally with the equivalent codesign --force --deep --sign - --options runtime --entitlements Entitlements.plist PromptForge.app:

  • The dialog becomes "PromptForge Not Opened. Apple could not verify..." with a Done button, and System Settings > Privacy & Security offers Open Anyway. This is the standard path for unsigned open source apps.

    Gatekeeper dialog: PromptForge Not Opened, Apple could not verify

  • The signed bundle boots, spawns the sidecar gateway, and loads the whisper speech engine normally, so the hardened runtime and the mic entitlement are safe for this build.

Pair it with a README Downloads paragraph describing the Open Anyway step.

Step 2: Developer ID signing plus notarization in CI

  • Enroll in the Apple Developer Program and create a Developer ID Application certificate. Export it as a base64 .p12.
  • Add the secrets to the two macOS matrix entries in .github/workflows/release-workshop.yml, where the # SIGNING (macOS) placeholder already sits. Tauri and tauri-action read these and sign, notarize, and staple the app and DMG:
APPLE_CERTIFICATE
APPLE_CERTIFICATE_PASSWORD
APPLE_SIGNING_IDENTITY
APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID
  (or APPLE_API_ISSUER + APPLE_API_KEY + APPLE_API_KEY_PATH)
  • Set bundle.macOS.minimumSystemVersion while there; the bundle currently claims the Tauri default 10.13.

Library validation risk. The gateway downloads whisper.cpp and llama.cpp dylibs at runtime. Under a real Team ID, hardened runtime library validation may refuse that third-party code. If speech or local inference fails to load on the signed build, add com.apple.security.cs.disable-library-validation to Entitlements.plist. Ad hoc signing skips team matching, so this cannot be tested before the certificate exists.

Step 3: make the release test catch this

The macOS "Install and check" step copies the app out of the DMG with cp and runs the binary directly, so it never touches Gatekeeper, LaunchServices, or quarantine. It passes today's broken state. Add:

  • codesign --verify --deep --strict PromptForge.app
  • spctl --assess --type execute PromptForge.app (expected to pass once Step 2 lands)
  • an open of a copy stamped with xattr -w com.apple.quarantine, checking that the process comes up
  • a speech-load check on the signed build, for the library validation risk above

Note for anyone reproducing

A bundle that Finder has already refused once stays wedged on macOS 26: any binary inside it hangs at _dyld_start when executed directly, even after clearing quarantine, while a fresh copy of the same bytes runs. Diagnose from a fresh copy, not from a bundle that has been through the dialog.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions