From f39bf4c0717ec185bad7bf2dbbddde53b820b718 Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Tue, 7 Jul 2026 15:31:00 -0700 Subject: [PATCH] fix(signing): pin a team-based designated requirement for dev builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit #65 switched dev-build signing from a pinned Developer ID cert to the generic "Apple Development" identity on the reasoning "same team, so TCC stays stable." That holds for Developer ID (team-based DR) but NOT for Apple Development: codesign's default DR for a development cert pins the specific leaf cert's Common Name ("Apple Development: … (ID)"), which changes on cert rotation. So the designated requirement silently changed, TCC's stored csreq (from the old Developer-ID grant) no longer matched the new signature, and Accessibility read "toggle on, still denied." Pin an explicit team-based DR on the /Applications re-sign step: identifier "dev.alex.blurt" and anchor apple generic and certificate leaf[subject.OU] = "Y54ZB9JF63" It's satisfied by both Apple Development and Developer ID signatures and survives cert rotation, so the grant stays stable. Built via the DEVELOPMENT_TEAM / PRODUCT_BUNDLE_IDENTIFIER build settings so it tracks the project config. Verified: clean Debug-Local build installs to /Applications with the team-based DR; codesign --verify --strict passes; hotkey/injection works after a one-time re-grant. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/Blurt/Blurt.xcodeproj/project.pbxproj | 2 +- App/Blurt/project.yml | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/App/Blurt/Blurt.xcodeproj/project.pbxproj b/App/Blurt/Blurt.xcodeproj/project.pbxproj index 3429bcd..a0ed52b 100644 --- a/App/Blurt/Blurt.xcodeproj/project.pbxproj +++ b/App/Blurt/Blurt.xcodeproj/project.pbxproj @@ -1949,7 +1949,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n# Debug-only: installs a re-signed copy at a stable path so TCC will\n# register Blurt in the Accessibility / Input Monitoring lists.\n# Release builds skip this — the release-build.sh script handles\n# staging, secure-timestamp signing, DMG creation, and notarization\n# separately.\ncase \"$CONFIGURATION\" in\n Debug | Debug-Local) ;; # local dev + test builds install\n *)\n echo \"Skipping Install to /Applications for $CONFIGURATION builds.\"\n exit 0\n ;;\nesac\nif [ \"${CODE_SIGNING_ALLOWED:-YES}\" = \"NO\" ]; then\n echo \"Skipping Install to /Applications: codesigning disabled (CI / health check).\"\n exit 0\nfi\nif [ \"${CODE_SIGN_IDENTITY:-}\" = \"-\" ]; then\n # Ad-hoc identity — the UI-test / CI build (scripts/uitest.sh and\n # check.sh sign ad-hoc so the XCUITest runner satisfies library\n # validation). The Developer ID re-sign below can't run without the\n # maintainer's cert, and the tests launch the app straight from\n # DerivedData, so there's nothing to install.\n echo \"Skipping Install to /Applications: ad-hoc signing (UI test / CI).\"\n exit 0\nfi\n# Copy + re-sign into /Applications so TCC sees a permanent path.\n# (Xcode/CLI builds otherwise live in DerivedData / /tmp — paths\n# macOS treats as transient and refuses to add to the Privacy\n# & Security toggle list.) Falls back to ~/Applications if the\n# system folder isn't writable on this machine.\nif [ -w /Applications ]; then\n DEST=\"/Applications/${PRODUCT_NAME}.app\"\nelse\n DEST=\"$HOME/Applications/${PRODUCT_NAME}.app\"\n mkdir -p \"$HOME/Applications\"\nfi\nrm -rf \"$DEST\"\ncp -R \"$BUILT_PRODUCTS_DIR/${PRODUCT_NAME}.app\" \"$DEST\"\n\n# Re-sign with the Apple Development cert (login keychain), matching the\n# build above — NOT the Developer ID release key (dedicated locked\n# keychain, release-only). Same team, so TCC stays stable.\n# Errors are NOT swallowed.\nIDENTITY=\"Apple Development\"\nENTS=\"${SRCROOT}/Blurt/Blurt.entitlements\"\n# Inside-out signing: Debug builds emit unsigned dylibs\n# (__preview.dylib, Blurt.debug.dylib). Signing the bundle\n# alone doesn't recurse, so verify fails. Sign nested code first.\nfind \"$DEST\" -type f \\( -name \"*.dylib\" -o -name \"*.so\" \\) \\\n -print0 | while IFS= read -r -d '' f; do\n codesign --force --sign \"$IDENTITY\" --options runtime \"$f\"\ndone\ncodesign --force --sign \"$IDENTITY\" \\\n --entitlements \"$ENTS\" \\\n --options runtime \\\n \"$DEST\"\ncodesign --verify --strict --verbose=1 \"$DEST\"\necho \"Installed: $DEST\"\n\n# Keep LaunchServices pointing only at the stable install. This build\n# also leaves a copy in DerivedData ($BUILT_PRODUCTS_DIR) registered\n# under the same bundle id; if it lingers, macOS can resolve the id to\n# that transient path and Blurt disappears from the Accessibility\n# list. Drop the DerivedData copy and (re)register the install.\nLSREGISTER=\"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister\"\n\"$LSREGISTER\" -u \"$BUILT_PRODUCTS_DIR/${PRODUCT_NAME}.app\" >/dev/null 2>&1 || true\n\"$LSREGISTER\" -f \"$DEST\" >/dev/null 2>&1 || true\n"; + shellScript = "set -e\n# Debug-only: installs a re-signed copy at a stable path so TCC will\n# register Blurt in the Accessibility / Input Monitoring lists.\n# Release builds skip this — the release-build.sh script handles\n# staging, secure-timestamp signing, DMG creation, and notarization\n# separately.\ncase \"$CONFIGURATION\" in\n Debug | Debug-Local) ;; # local dev + test builds install\n *)\n echo \"Skipping Install to /Applications for $CONFIGURATION builds.\"\n exit 0\n ;;\nesac\nif [ \"${CODE_SIGNING_ALLOWED:-YES}\" = \"NO\" ]; then\n echo \"Skipping Install to /Applications: codesigning disabled (CI / health check).\"\n exit 0\nfi\nif [ \"${CODE_SIGN_IDENTITY:-}\" = \"-\" ]; then\n # Ad-hoc identity — the UI-test / CI build (scripts/uitest.sh and\n # check.sh sign ad-hoc so the XCUITest runner satisfies library\n # validation). The Developer ID re-sign below can't run without the\n # maintainer's cert, and the tests launch the app straight from\n # DerivedData, so there's nothing to install.\n echo \"Skipping Install to /Applications: ad-hoc signing (UI test / CI).\"\n exit 0\nfi\n# Copy + re-sign into /Applications so TCC sees a permanent path.\n# (Xcode/CLI builds otherwise live in DerivedData / /tmp — paths\n# macOS treats as transient and refuses to add to the Privacy\n# & Security toggle list.) Falls back to ~/Applications if the\n# system folder isn't writable on this machine.\nif [ -w /Applications ]; then\n DEST=\"/Applications/${PRODUCT_NAME}.app\"\nelse\n DEST=\"$HOME/Applications/${PRODUCT_NAME}.app\"\n mkdir -p \"$HOME/Applications\"\nfi\nrm -rf \"$DEST\"\ncp -R \"$BUILT_PRODUCTS_DIR/${PRODUCT_NAME}.app\" \"$DEST\"\n\n# Re-sign with the Apple Development cert (login keychain), matching the\n# build above — NOT the Developer ID release key (dedicated locked\n# keychain, release-only).\n# Errors are NOT swallowed.\nIDENTITY=\"Apple Development\"\nENTS=\"${SRCROOT}/Blurt/Blurt.entitlements\"\n# Pin an explicit, TEAM-based designated requirement. TCC keys the\n# Accessibility / Input Monitoring grant to the app's designated\n# requirement, and codesign's DEFAULT DR for an Apple Development cert\n# pins the specific leaf cert's Common Name (\"Apple Development: … (ID)\")\n# — which changes every time that cert rotates (they expire ~yearly and\n# Xcode reissues them), silently orphaning the grant (\"toggle on, still\n# denied\"). A team-based DR stays stable across cert rotation AND is\n# satisfied by both Apple Development and Developer ID signatures (both\n# carry OU = the team id), so dev and release builds share one identity\n# as far as TCC is concerned.\nREQ='designated => identifier \"'\"${PRODUCT_BUNDLE_IDENTIFIER}\"'\" and anchor apple generic and certificate leaf[subject.OU] = \"'\"${DEVELOPMENT_TEAM}\"'\"'\n# Inside-out signing: Debug builds emit unsigned dylibs\n# (__preview.dylib, Blurt.debug.dylib). Signing the bundle\n# alone doesn't recurse, so verify fails. Sign nested code first.\nfind \"$DEST\" -type f \\( -name \"*.dylib\" -o -name \"*.so\" \\) \\\n -print0 | while IFS= read -r -d '' f; do\n codesign --force --sign \"$IDENTITY\" --options runtime \"$f\"\ndone\ncodesign --force --sign \"$IDENTITY\" \\\n --entitlements \"$ENTS\" \\\n --options runtime \\\n -r=\"$REQ\" \\\n \"$DEST\"\ncodesign --verify --strict --verbose=1 \"$DEST\"\necho \"Installed: $DEST\"\n\n# Keep LaunchServices pointing only at the stable install. This build\n# also leaves a copy in DerivedData ($BUILT_PRODUCTS_DIR) registered\n# under the same bundle id; if it lingers, macOS can resolve the id to\n# that transient path and Blurt disappears from the Accessibility\n# list. Drop the DerivedData copy and (re)register the install.\nLSREGISTER=\"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister\"\n\"$LSREGISTER\" -u \"$BUILT_PRODUCTS_DIR/${PRODUCT_NAME}.app\" >/dev/null 2>&1 || true\n\"$LSREGISTER\" -f \"$DEST\" >/dev/null 2>&1 || true\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/App/Blurt/project.yml b/App/Blurt/project.yml index 7296b2c..8a7198c 100644 --- a/App/Blurt/project.yml +++ b/App/Blurt/project.yml @@ -151,10 +151,21 @@ targets: # Re-sign with the Apple Development cert (login keychain), matching the # build above — NOT the Developer ID release key (dedicated locked - # keychain, release-only). Same team, so TCC stays stable. + # keychain, release-only). # Errors are NOT swallowed. IDENTITY="Apple Development" ENTS="${SRCROOT}/Blurt/Blurt.entitlements" + # Pin an explicit, TEAM-based designated requirement. TCC keys the + # Accessibility / Input Monitoring grant to the app's designated + # requirement, and codesign's DEFAULT DR for an Apple Development cert + # pins the specific leaf cert's Common Name ("Apple Development: … (ID)") + # — which changes every time that cert rotates (they expire ~yearly and + # Xcode reissues them), silently orphaning the grant ("toggle on, still + # denied"). A team-based DR stays stable across cert rotation AND is + # satisfied by both Apple Development and Developer ID signatures (both + # carry OU = the team id), so dev and release builds share one identity + # as far as TCC is concerned. + REQ='designated => identifier "'"${PRODUCT_BUNDLE_IDENTIFIER}"'" and anchor apple generic and certificate leaf[subject.OU] = "'"${DEVELOPMENT_TEAM}"'"' # Inside-out signing: Debug builds emit unsigned dylibs # (__preview.dylib, Blurt.debug.dylib). Signing the bundle # alone doesn't recurse, so verify fails. Sign nested code first. @@ -165,6 +176,7 @@ targets: codesign --force --sign "$IDENTITY" \ --entitlements "$ENTS" \ --options runtime \ + -r="$REQ" \ "$DEST" codesign --verify --strict --verbose=1 "$DEST" echo "Installed: $DEST"