From 6bef9d339b51354acb85987f94b3b97c2812cc13 Mon Sep 17 00:00:00 2001 From: joeVenner Date: Wed, 10 Jun 2026 15:29:15 +0100 Subject: [PATCH 1/5] build(macos): Developer ID + hardened-runtime signing with ad-hoc fallback When SIGN_IDENTITY is set, sign the binary with hardened runtime, the audio- input entitlement, and a secure timestamp (the prerequisites for notarization and for a Gatekeeper-clean install), then seal the bundle. Without it, keep the ad-hoc path for local dev. Adds scripts/holler.entitlements. --- scripts/bundle-macos.sh | 21 ++++++++++++++++++--- scripts/holler.entitlements | 11 +++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 scripts/holler.entitlements diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh index f41aff3..af5319c 100755 --- a/scripts/bundle-macos.sh +++ b/scripts/bundle-macos.sh @@ -72,9 +72,24 @@ cat > "$APP_DIR/Contents/Info.plist" < PLIST -echo "==> Ad-hoc code signing" -codesign --force --sign - --timestamp=none "$APP_DIR/Contents/MacOS/holler" -codesign --force --sign - --timestamp=none "$APP_DIR" +# Sign with a real Developer ID when SIGN_IDENTITY is set (CI/release), so the +# app passes Gatekeeper for everyone and the TCC grant survives rebuilds. +# Otherwise fall back to ad-hoc for local dev (Gatekeeper will warn). +ENTITLEMENTS="$(dirname "$0")/holler.entitlements" +if [[ -n "${SIGN_IDENTITY:-}" ]]; then + echo "==> Developer ID signing + hardened runtime ($SIGN_IDENTITY)" + # Entitlements + hardened runtime go on the executable; the bundle wrapper is + # then sealed over it. A secure --timestamp is required for notarization. + codesign --force --options runtime --timestamp \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGN_IDENTITY" "$APP_DIR/Contents/MacOS/holler" + codesign --force --options runtime --timestamp \ + --sign "$SIGN_IDENTITY" "$APP_DIR" +else + echo "==> Ad-hoc code signing (no SIGN_IDENTITY — Gatekeeper will warn on other Macs)" + codesign --force --sign - --timestamp=none "$APP_DIR/Contents/MacOS/holler" + codesign --force --sign - --timestamp=none "$APP_DIR" +fi codesign --verify --verbose "$APP_DIR" echo diff --git a/scripts/holler.entitlements b/scripts/holler.entitlements new file mode 100644 index 0000000..1ca21b5 --- /dev/null +++ b/scripts/holler.entitlements @@ -0,0 +1,11 @@ + + + + + + com.apple.security.device.audio-input + + + From 3aa3fb0d00c0972e6f17a39aab00185bb105c287 Mon Sep 17 00:00:00 2001 From: joeVenner Date: Wed, 10 Jun 2026 15:31:34 +0100 Subject: [PATCH 2/5] ci(macos): sign + notarize the universal DMG on release Import the Developer ID cert into a throwaway keychain, pass SIGN_IDENTITY to the bundler (hardened-runtime Developer ID signing), then notarize the DMG with notarytool and staple the ticket. All steps are gated on their secrets, so builds without the secrets configured fall back to the existing ad-hoc path and stay green. --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94ed18b..d5e88a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,9 +102,29 @@ jobs: target/x86_64-apple-darwin/release/holler \ -output target/universal-apple-darwin/release/holler + # Import the Developer ID cert into a throwaway keychain so codesign can + # find it. Skipped (→ ad-hoc signing) until the secret is configured. + - name: Import signing certificate + if: ${{ secrets.MACOS_CERT_P12_BASE64 != '' }} + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + run: | + KEYCHAIN="$RUNNER_TEMP/build.keychain-db" + KEYCHAIN_PW="$(openssl rand -base64 24)" + security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + echo "$MACOS_CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PW" "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | sed s/\"//g) + - name: Bundle universal .app env: BINARY_PATH: target/universal-apple-darwin/release/holler + # Empty when unconfigured → bundle-macos.sh falls back to ad-hoc. + SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} run: | if [ "$GITHUB_REF_TYPE" = "tag" ]; then export VERSION="${GITHUB_REF_NAME#v}"; fi bash scripts/bundle-macos.sh @@ -116,6 +136,20 @@ jobs: -ov -format UDZO \ Holler-macOS-universal.dmg + # Notarize the DMG with Apple and staple the ticket so it installs without + # a Gatekeeper warning, even offline. Skipped until secrets are configured. + - name: Notarize & staple + if: ${{ secrets.APPLE_ID != '' }} + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + run: | + xcrun notarytool submit Holler-macOS-universal.dmg \ + --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_PASSWORD" --wait + xcrun stapler staple Holler-macOS-universal.dmg + - name: Upload universal DMG uses: actions/upload-artifact@v4 with: From f1da146a18507cb806884d531f9accceda73e1dd Mon Sep 17 00:00:00 2001 From: joeVenner Date: Wed, 10 Jun 2026 15:32:41 +0100 Subject: [PATCH 3/5] docs: macOS signing/notarization setup guide Add docs/SIGNING.md (Developer ID cert, the six GitHub secrets and where to get each, release + local flows). Update the README signing note and the DECISIONS distribution row to point at it. --- README.md | 7 ++--- docs/DECISIONS.md | 2 +- docs/SIGNING.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 docs/SIGNING.md diff --git a/README.md b/README.md index d2b8349..10d4456 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,10 @@ cargo run # run from the terminal (see logs); injection/keychain For logs while using the bundle’s stable identity, run the inner binary: `./Holler.app/Contents/MacOS/holler`. -> Note: ad-hoc signing ties permissions to the exact binary, so after -> rebuilding the bundle you may need to re-approve Accessibility. A Developer ID -> signature (Phase 3) makes the grant permanent. +> Note: local/ad-hoc signing ties permissions to the exact binary, so after +> rebuilding you may need to re-approve Accessibility. Release DMGs are signed +> with a **Developer ID** and notarized once the signing secrets are configured +> (see [`docs/SIGNING.md`](docs/SIGNING.md)), which also makes the grant stick. ### Troubleshooting diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1a9d4d1..0f5a0a6 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -18,7 +18,7 @@ Captured during the planning session on **2026-06-08** with Yassir. These are ** | Copy memory | Clipboard set **and** searchable SQLite history | Yassir chose "Both". | | AI providers | Provider-agnostic/BYOK behind traits — Claude, OpenAI, Deepgram, local OpenAI-compatible (Ollama) | Flexibility. **Key storage REVISED 2026-06-10:** moved from the OS keychain (`keyring`) to a `0600` `secrets.toml` in the config dir (separate from `config.toml`). Ad-hoc-signed macOS bundles change identity each rebuild, so the keychain TCC grant never stuck and macOS re-prompted on every run. `HOLLER__KEY` env var overrides the file. | | TTS | Deferred to phase 3 | De-risk v1; design traits now, build later. | -| Distribution | Public eventually → plan signing/notarization + permission onboarding from the start | Stable Developer ID; clean macOS TCC flow. | +| Distribution | Public eventually → plan signing/notarization + permission onboarding from the start | Stable Developer ID; clean macOS TCC flow. **2026-06-10:** repo now public; Developer ID signing + notarization wired into CI (gated on secrets, ad-hoc fallback) — see `docs/SIGNING.md`. | ## Recommendations I made on Yassir's behalf (open to challenge) - **Defer GUI to phase 2**, ship Phase 1 with a TOML config file. Removes the tray+hotkey+egui main-thread-loop integration risk from the MVP critical path. diff --git a/docs/SIGNING.md b/docs/SIGNING.md new file mode 100644 index 0000000..04fa20a --- /dev/null +++ b/docs/SIGNING.md @@ -0,0 +1,67 @@ +# macOS code signing & notarization + +The release pipeline signs the universal macOS DMG with a **Developer ID** +certificate, notarizes it with Apple, and staples the ticket — so it installs +without a Gatekeeper warning on any Mac, and the Accessibility/Microphone grants +survive app rebuilds (a stable signing identity). Until the secrets below are +set, CI falls back to **ad-hoc** signing (Gatekeeper still warns). + +## One-time setup + +### 1. Create a "Developer ID Application" certificate +Xcode → Settings → Accounts → your Apple ID → **Manage Certificates** → + → +**Developer ID Application**. (Requires the paid Apple Developer Program.) + +Find its full identity string and your Team ID: +```bash +security find-identity -v -p codesigning +# → "Developer ID Application: Your Name (ABCDE12345)" ← TEAMID is the (...) +``` + +### 2. Export the cert as a `.p12` and base64 it +Keychain Access → My Certificates → right-click the *Developer ID Application* +cert → **Export** → `.p12` (set an export password). Then: +```bash +base64 -i Certificate.p12 | pbcopy # base64 now on the clipboard +``` + +### 3. Create an app-specific password +[appleid.apple.com](https://appleid.apple.com) → Sign-In & Security → +**App-Specific Passwords** → generate one for "Holler notarization". + +### 4. Add these GitHub repo Secrets +Repo → Settings → Secrets and variables → **Actions** → New repository secret: + +| Secret | Value | +|---|---| +| `MACOS_CERT_P12_BASE64` | the base64 string from step 2 | +| `MACOS_CERT_PASSWORD` | the `.p12` export password | +| `MACOS_SIGN_IDENTITY` | `Developer ID Application: Your Name (TEAMID)` (exact string from step 1) | +| `APPLE_ID` | your Apple ID email | +| `APPLE_TEAM_ID` | the Team ID, e.g. `ABCDE12345` | +| `APPLE_APP_PASSWORD` | the app-specific password from step 3 | + +> Never put these in the repo or paste them in chat — repo Secrets only. + +### 5. Release +Tag as usual — the next release DMG is signed, notarized, and stapled: +```bash +git tag -a v0.1.1 -m "..." && git push origin v0.1.1 +``` +Verify a downloaded DMG would pass Gatekeeper on a clean Mac: +```bash +spctl -a -t open --context context:primary-signature -v Holler-macOS-universal.dmg # → accepted +``` + +## Signing locally (optional) +On your own Mac with the cert in your keychain: +```bash +SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bash scripts/bundle-macos.sh +``` +Notarization then mirrors the CI step (`xcrun notarytool submit … --wait` → +`xcrun stapler staple …`). Entitlements live in `scripts/holler.entitlements` +(hardened runtime needs `com.apple.security.device.audio-input` for the mic). + +## Windows +Windows signing (an OV/EV Authenticode cert to avoid SmartScreen) is a separate, +still-deferred task — the current Windows ZIP is unsigned. From 9513e953a137f4a28c293c2dabfe03fb8572d2ac Mon Sep 17 00:00:00 2001 From: joeVenner Date: Wed, 10 Jun 2026 15:35:53 +0100 Subject: [PATCH 4/5] ci: gate macOS signing on env, not the secrets context The 'secrets' context isn't allowed in step if: conditions (caused an immediate workflow-validation failure). Surface the signing secrets as job-level env and gate the import/notarize steps on env.* instead. --- .github/workflows/build.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5e88a4..9ac7208 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,15 @@ jobs: name: macOS Universal (fat binary) needs: build-macos runs-on: macos-15 + # Surface signing secrets as env so steps can gate on them (the `secrets` + # context is not allowed in `if:`). Empty when unconfigured → ad-hoc path. + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} steps: - uses: actions/checkout@v4 @@ -105,10 +114,7 @@ jobs: # Import the Developer ID cert into a throwaway keychain so codesign can # find it. Skipped (→ ad-hoc signing) until the secret is configured. - name: Import signing certificate - if: ${{ secrets.MACOS_CERT_P12_BASE64 != '' }} - env: - MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} - MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + if: env.MACOS_CERT_P12_BASE64 != '' run: | KEYCHAIN="$RUNNER_TEMP/build.keychain-db" KEYCHAIN_PW="$(openssl rand -base64 24)" @@ -124,7 +130,7 @@ jobs: env: BINARY_PATH: target/universal-apple-darwin/release/holler # Empty when unconfigured → bundle-macos.sh falls back to ad-hoc. - SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + SIGN_IDENTITY: ${{ env.MACOS_SIGN_IDENTITY }} run: | if [ "$GITHUB_REF_TYPE" = "tag" ]; then export VERSION="${GITHUB_REF_NAME#v}"; fi bash scripts/bundle-macos.sh @@ -139,11 +145,7 @@ jobs: # Notarize the DMG with Apple and staple the ticket so it installs without # a Gatekeeper warning, even offline. Skipped until secrets are configured. - name: Notarize & staple - if: ${{ secrets.APPLE_ID != '' }} - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + if: env.APPLE_ID != '' run: | xcrun notarytool submit Holler-macOS-universal.dmg \ --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \ From 74cb5bbb783e25aef9a519ff8599042300722dcc Mon Sep 17 00:00:00 2001 From: joeVenner Date: Wed, 10 Jun 2026 15:57:31 +0100 Subject: [PATCH 5/5] ci(macos): target the ENV environment for signing secrets The signing secrets are stored as Environment secrets (env 'ENV'); a job only sees them if it opts into that environment. Add environment: ENV to the universal-DMG job so the cert import + notarization steps can read them. --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ac7208..8d234d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,9 @@ jobs: name: macOS Universal (fat binary) needs: build-macos runs-on: macos-15 + # The signing secrets live in the "ENV" environment, so this job must opt + # into it to read them. + environment: ENV # Surface signing secrets as env so steps can gate on them (the `secrets` # context is not allowed in `if:`). Empty when unconfigured → ad-hoc path. env: