返回博客

Ad-hoc 签名让 macOS 隐私弹窗每次构建都重新弹出

正在开发一个需要访问 ~/Desktop 目录的 macOS 桌面工具。每次 xcodebuild 重新编译后启动,系统都会弹出:

"YourApp 想访问"桌面"文件夹中的文件"

点「允许」,下次 rebuild 又弹。反复五六次之后你开始怀疑人生:是不是 entitlements 没配对、是不是 Info.plist 漏写了 usage description,把能排查的地方翻了个遍,依然如故。

现象

  • 每次 rebuild + relaunch 后,相同的 TCC 权限弹窗必然出现
  • 系统设置 → 隐私与安全 → 桌面文件夹,里面显示的授权在下次构建后消失或失效
  • tccutil reset 手动清掉再试,问题依旧
  • NSDesktopFolderUsageDescription 已经写在 Info.plist 里

根因

macOS TCC(Transparency, Consent, and Control)把一次权限授予绑定到 App 的 designated requirement(DR),不是 bundle ID。

对于 ad-hoc 签名codesign -s - 或 Xcode 默认本地签名),DR 是二进制的 cdhash

codesign -dvvv YourApp.app 2>&1 | grep -E "Signature|TeamIdentifier"
# Signature=adhoc
# TeamIdentifier=not set

cdhash 由二进制内容决定,任何代码改动都会产生新的 hash,等于新的 App。所以 macOS 每次 rebuild 后都认为这是一个从未授权过的全新应用,自然重新弹窗。

点「允许」只给这一个构建版本的 hash 授权,下次 build 出来的 hash 不同,授权就没了。

坑:ad-hoc 签名让 TCC 授权绑定到易变的 cdhash

ad-hoc 签名 (Signature=adhoc) 没有 TeamIdentifier,macOS 用 cdhash 作为 designated requirement。每次 rebuild 二进制改变 → cdhash 改变 → TCC 认为是新 App → 重新弹窗。com.apple.security.app-sandbox = false 也无法豁免这个机制,即使你没有沙盒,TCC 保护的路径(Desktop、Documents、Photos 等)照样需要授权。

正解

改用稳定的代码签名身份,让 DR 锚定到 team + bundle ID,而非 cdhash。

第 1 步:找到机器上可用的签名身份

security find-identity -v -p codesigning
# 1) AABBCC... "Apple Development: you@example.com (TU4BZ5833F)"
# 2) DDEEFF... "Developer ID Application: Your Name (TXL4HHDJW8)"

优先选 Developer ID Application:不需要 provisioning profile、本地调试和发布用同一张证书、DR 最稳定。

第 2 步:在 xcodegen project.yml 或 Xcode Build Settings 中配置

# project.yml (settings.base 节)
CODE_SIGN_STYLE: Manual
CODE_SIGN_IDENTITY: "Developer ID Application"
DEVELOPMENT_TEAM: TXL4HHDJW8
ENABLE_HARDENED_RUNTIME: YES

注意:CODE_SIGN_IDENTITY: "Apple Development" 在 Manual 模式下会让 xcodebuild 去找 "Mac Development" 证书并报错。要么用 "Developer ID Application",要么贴完整字符串 "Apple Development: name (TEAMID)"

第 3 步:重新生成并构建

xcodegen generate
xcodebuild -scheme YourApp -configuration Debug build

验证签名是否生效:

codesign -dvvv YourApp.app 2>&1 | grep -E "Signature|Authority|TeamIdentifier"
# Authority=Developer ID Application: Your Name (TXL4HHDJW8)
# TeamIdentifier=TXL4HHDJW8   ← 不再是 "not set"
 
codesign -d -r- YourApp.app 2>&1 | grep designated
# designated => identifier "com.you.App" and ... subject.OU = TXL4HHDJW8
#               ^^^^ 基于 team 的 DR,不再是 cdhash

切换签名后,弹窗会再出现最后一次(新 DR 首次授权),点「允许」,之后所有 rebuild 都不再弹。

正解:Developer ID 签名让 TCC 授权跨构建持久

改用 Developer ID Application(或 Apple Development)签名后,designated requirement 变为基于 team + bundle ID 的稳定描述符。TCC 授权一次,所有后续 rebuild 共享同一份授权,弹窗从此消失。

附:嵌入式终端多次弹窗的变体

如果你的 App 内嵌了终端模拟器(如 Ghostty/libghostty),可能遇到同一次启动内弹窗出现 4-5 次的不同问题。原因是终端通过 /usr/bin/login 启动 shell,login 会重置 TCC 的"责任进程"归属,使 shell 和它的每个子进程(nodepnpm 等)访问受保护目录时各自独立触发授权。

解决方法:在系统设置 → 隐私与安全 → 完全磁盘访问权限 中为 App 开启全盘访问。单独授权 Desktop/Documents 文件夹不足以覆盖 login 衍生的子进程。

一句话外卖

ad-hoc 签名的 App 在 macOS TCC 眼里每次 rebuild 都是新身份,要让权限持久,必须给它一个跨构建不变的 team-based designated requirement。