用免费 Personal Team 给 Tauri macOS 应用签名,避免 TCC 权限反复失效

作者 mcx 日期 2026-07-26
用免费 Personal Team 给 Tauri macOS 应用签名,避免 TCC 权限反复失效

最近给一个带屏幕截图、录屏、辅助功能和全局快捷键的 Tauri macOS 应用做本机测试时,遇到了一个很烦的问题:每次重新编译后,系统都像看到了一个新应用,需要重新到“系统设置 → 隐私与安全性”里授权屏幕录制。

真正的问题不是 TCC 本身“随机失忆”,而是开发构建一直在使用 ad-hoc 签名。每次构建产生的代码身份不稳定,macOS 就可能把它当作另一个应用。

解决办法是:使用免费的 Apple 账户登录 Xcode,创建 Personal Team 的 Apple Development 证书,然后让本机测试包始终使用同一个签名身份。

先说结论

本机开发测试可以使用免费 Apple 账户的 Apple Development 证书,不需要购买 Apple Developer Program。

它适合:

  • 本机运行和调试 macOS 应用;
  • 保持同一个应用代码身份;
  • 减少屏幕录制、麦克风、辅助功能等 TCC 权限的重复授权。

它不适合:

  • Developer ID 分发;
  • 公证(Notarization);
  • 面向其他用户的正式安装包发布。

发布和本机开发应该分开:本机使用 Apple Development,CI/Release 继续使用项目原来的 ad-hoc 签名策略。

1. 在 Xcode 中登录免费 Apple 账户

打开 Xcode,进入:

1
Xcode → Settings → Apple Accounts

点击 Add Apple Account...,登录 Apple 账户。登录成功后,账户下应该能看到:

1
Personal Team

进入 Personal Team,点击 Manage Certificates...,通过 Create Certificate 创建 Apple Development 证书。

免费账户的 Personal Team 足够用于本机测试,但不会提供 Developer ID Application 证书,也不会让你绕过公证要求。

2. 检查代码签名身份

用下面的命令检查本机是否已经有可用签名身份:

1
security find-identity -v -p codesigning

正常情况下应该看到类似输出:

1
2
3
1) ABCDEF0123456789ABCDEF0123456789ABCDEF01
"Apple Development: [email protected] (TEAMID1234)"
1 valid identities found

注意这里的 Apple Development 才是我们想要的开发签名。下面这些都不是同一回事:

  • Developer ID Application:付费开发者账号用于外部分发;
  • Apple Distribution:分发或提交 App Store;
  • -:ad-hoc 签名,免费但代码身份不适合解决 TCC 反复授权。

3. 处理 WWDR G3 证书链

这一步是最容易被忽略的地方。

某些 macOS 环境里,钥匙串中只有已经过期的旧版 WWDR 中间证书。此时 Xcode 可能已经创建了 Apple Development 证书,但命令行签名会报:

1
2
Warning: unable to build chain to self-signed root
errSecInternalComponent

Apple Development 证书现在通常由 Worldwide Developer Relations - G3 中间证书签发。可以从 Apple 官方 PKI 页面下载:

命令行安装方式如下:

1
2
3
4
5
6
7
8
9
10
11
tmp_dir="$(mktemp -d /tmp/apple-wwdr-g3.XXXXXX)"
curl -fsSL \
"https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer" \
-o "$tmp_dir/AppleWWDRCAG3.cer"

security import "$tmp_dir/AppleWWDRCAG3.cer" \
-k "$HOME/Library/Keychains/login.keychain-db" \
-T /usr/bin/codesign \
-T /usr/bin/security

rm -R "$tmp_dir"

然后再次检查:

1
security find-identity -v -p codesigning

如果从 0 valid identities found 变成出现 Apple Development 身份,说明证书链已经恢复。

4. Tauri 应用的签名思路

Tauri 的 macOS 应用不是只有一个可执行文件。典型 bundle 结构大致如下:

1
2
3
4
5
Qx.app/
└── Contents/
└── MacOS/
├── qx
└── qx-ffmpeg

主程序和嵌套的 helper、外部二进制都需要保持可验证的签名。对于 Tauri bundle,本机测试可以使用:

1
2
3
4
5
6
7
8
codesign \
--force \
--deep \
--sign "Apple Development: [email protected] (TEAMID1234)" \
--timestamp=none \
--identifier "com.example.myapp" \
--entitlements "src-tauri/Entitlements.plist" \
"path/to/MyApp.app"

这里几个参数的作用是:

参数 作用
--force 替换 bundle 中已有的 ad-hoc 或旧签名
--deep 递归处理 bundle 内的嵌套代码
--sign 指定 Apple Development 签名身份
--timestamp=none 本机开发签名不依赖分发时间戳
--identifier 固定代码签名标识,通常与 Bundle ID 一致
--entitlements 使用应用声明的 macOS 权限配置

--deep 适合这里的 Tauri 本机 bundle 快速签名。正式分发时,应该根据嵌套代码的层级逐个签名,再签最外层 bundle,并使用 Developer ID 和公证流程。

5. 一个可复用的本机签名脚本

下面是这次本机测试实际采用的脚本模板。把它保存为项目外的本地脚本,或者放到不提交的个人工具目录中,避免把本机证书身份写进公共配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env bash
set -euo pipefail

APP_PATH="${1:?用法:sign-macos-dev.sh /path/to/MyApp.app}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-src-tauri/Entitlements.plist}"
IDENTITY="${APPLE_DEVELOPMENT_IDENTITY:?请设置 APPLE_DEVELOPMENT_IDENTITY}"
BUNDLE_ID="${BUNDLE_ID:-com.example.myapp}"

if [[ ! -d "$APP_PATH" ]]; then
echo "找不到应用 bundle:$APP_PATH" >&2
exit 1
fi

echo "使用签名身份:$IDENTITY"
echo "签名应用:$APP_PATH"

codesign \
--force \
--deep \
--sign "$IDENTITY" \
--timestamp=none \
--identifier "$BUNDLE_ID" \
--entitlements "$ENTITLEMENTS_PATH" \
"$APP_PATH"

codesign \
--verify \
--deep \
--strict \
--verbose=2 \
"$APP_PATH"

echo "签名验证通过:$APP_PATH"

使用时:

1
2
3
4
5
export APPLE_DEVELOPMENT_IDENTITY='Apple Development: [email protected] (TEAMID1234)'
export BUNDLE_ID='com.example.myapp'

./sign-macos-dev.sh \
src-tauri/target/release/bundle/macos/MyApp.app

以 Qx 为例,本机实际签名命令类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
identity='Apple Development: [email protected] (TEAMID1234)'
entitlements='src-tauri/Entitlements.plist'

for app in \
src-tauri/target/release/bundle/macos/Qx.app \
/Applications/Qx.app
do
codesign \
--force \
--deep \
--sign "$identity" \
--timestamp=none \
--identifier 'com.mcx.qx' \
--entitlements "$entitlements" \
"$app"

codesign \
--verify \
--deep \
--strict \
--verbose=2 \
"$app"
done

Qx bundle 内的 qx-ffmpeg 也会被递归处理。签名后可以看到类似结果:

1
2
3
4
5
Authority=Apple Development: ...
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
TeamIdentifier=...
Identifier=com.mcx.qx

6. xcodeutil 不是关键

这次没有依赖名为 xcodeutil 的系统命令。macOS/Xcode 的标准工具链是:

  • security:查看钥匙串、证书和签名身份;
  • codesign:对 Mach-O 文件和 .app bundle 签名;
  • xcodebuild:构建 Xcode 工程;
  • spctl:检查 Gatekeeper 评估结果。

如果某个第三方工具叫 xcodeutil,只要它最终调用的是同一个 Apple Development 身份,效果可以等价;但工具名字本身不会让 TCC 授权稳定,真正起作用的是最终的代码签名身份和 Bundle ID。

如果目标应用安装在 /Applications,也可以对安装后的 bundle 再签一次:

1
./sign-macos-dev.sh /Applications/MyApp.app

签名之前最好先退出正在运行的应用,避免进程仍然使用旧 bundle 内容。

7. 验证签名是否真的生效

验证 bundle 完整性

1
codesign --verify --deep --strict --verbose=2 /Applications/MyApp.app

看到下面的结果就说明 bundle 结构完整:

1
2
MyApp.app: valid on disk
MyApp.app: satisfies its Designated Requirement

查看签名身份

1
codesign -dvvv /Applications/MyApp.app 2>&1

重点确认:

1
2
3
4
5
Identifier=com.example.myapp
Authority=Apple Development: ...
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
TeamIdentifier=...

其中最重要的是:

  • Authority 不再是 ad-hoc;
  • TeamIdentifier 已出现;
  • Identifier 与 Bundle ID 稳定一致。

查看实际 entitlements

1
codesign -d --entitlements :- /Applications/MyApp.app

不要只看源码中的 Entitlements.plist,最终以签名后的 bundle 输出为准。

8. 为什么这样可以减少 TCC 重复授权

macOS 的 TCC 不只是按照应用文件名判断身份。它会结合应用的代码签名要求、签名标识和相关代码身份来判断“这是不是之前授权过的那个应用”。

如果每次构建都使用 ad-hoc 签名,代码身份可能随构建变化:

1
2
旧构建:ad-hoc identity A
新构建:ad-hoc identity B

即使 Bundle ID 没变,TCC 也可能把新版本当作新应用,于是重新要求屏幕录制或辅助功能授权。

固定使用同一 Apple Development 证书后,身份变成:

1
2
旧构建:Apple Development + Team ID + Bundle ID
新构建:Apple Development + Team ID + Bundle ID

这会显著提高授权记录的复用概率。不过首次从 ad-hoc 切换到 Apple Development 时,仍然建议重新授权一次;这是代码身份改变后的正常行为。

还需要注意,改变下面这些内容也可能导致 macOS 重新评估授权:

  • Bundle ID;
  • 签名证书或 Team ID;
  • 关键 entitlements;
  • 主程序实际路径或应用结构。

9. Tauri 项目中发布和本机测试要分开

本机使用 Personal Team 签名,不代表 GitHub Release 也应该使用 Personal Team。

例如项目的 Release Action 仍然可以独立执行:

1
2
3
4
5
6
7
8
codesign \
--force \
--deep \
--sign - \
--timestamp=none \
--identifier "com.example.myapp" \
--entitlements src-tauri/Entitlements.plist \
path/to/MyApp.app

CI 机器上没有你的 Apple Development 私钥,也不应该把个人证书提交到仓库或 GitHub Secrets。这样做的好处是:

  • 本机开发签名可以稳定 TCC 身份;
  • Release Action 仍然保持原来的免费 ad-hoc 流程;
  • 不会把 Apple 账户、私钥或 provisioning profile 带进仓库;
  • CI 不依赖某一台开发机的钥匙串状态。

唯一要记住的是:如果 CI 产物下载到本机后运行,它使用的是另一个签名身份,可能仍然需要单独授权。这是预期行为,不是本机开发签名失效。

10. 常见错误排查

0 valid identities found

通常表示:

  • Xcode 没有登录 Apple 账户;
  • Personal Team 没有创建 Apple Development 证书;
  • 证书的私钥不在当前用户钥匙串;
  • WWDR 中间证书缺失或过期。

先执行:

1
security find-identity -v -p codesigning

再检查 Xcode 的 Apple Accounts 页面和 WWDR G3 中间证书。

errSecInternalComponent

这个错误信息很笼统,但在开发签名场景里经常与证书链有关。重点看签名时是否同时出现:

1
unable to build chain to self-signed root

如果出现,优先补齐与证书签发者匹配的 WWDR 中间证书。不要盲目删除所有钥匙串证书。

签名成功但 TCC 仍然重新询问

确认以下几点:

  1. 你运行的确实是刚签名的 bundle;
  2. 没有被旧的 ad-hoc 构建覆盖;
  3. Bundle ID 没有变化;
  4. 应用已完全退出后重新启动;
  5. 第一次从 ad-hoc 切换到 Apple Development 后,已经在系统设置中重新授权。

可以用下面的命令确认实际运行文件:

1
ps aux | grep '[M]yApp.app'

spctl 拒绝本机开发包

Apple Development 签名主要用于开发测试,不等于 Developer ID 分发签名。不要把本机开发包的 spctl 结果和正式分发包的 Gatekeeper 结果混为一谈。

正式分发需要 Developer ID Application、Hardened Runtime、Notarization 和 stapling;这些不属于免费 Personal Team 的能力范围。

总结

这次问题的关键不是给应用“多加几个 entitlement”,而是让 macOS 每次看到的代码身份保持稳定:

1
2
3
4
5
6
7
8
9
固定 Apple Development 证书
+
固定 Team ID
+
固定 Bundle ID
+
正确的 WWDR G3 证书链

本机开发构建更容易复用 TCC 授权

对于 Tauri 项目,最实用的分工是:本机开发包使用 Apple Development,Release Action 继续使用独立的 CI 签名流程。这样既能改善本机体验,也不会把个人开发证书和发布流水线耦合在一起。