Files
jiu/docs/macos-signing.md
T
wangjia 48e282dd97 feat(devops): macOS 构建接入 Developer ID 签名 + 公证
- compile-macos.sh:flutter build 后 codesign(hardened runtime + timestamp
  + Release.entitlements)→ notarytool 公证(复用 App Store Connect API key)
  → stapler staple → 重新打包,产出已签名+公证+stapled 的 .app
- 强制策略:缺 MACOS_DEVELOPER_ID_CERT_P12_BASE64 / APPSTORE_API_KEY_P8_BASE64
  时在 flutter build 前 fail-fast,绝不产出未签名包
- deploy-client.yml:build-macos 步骤注入证书与 API key secrets
- 新增 docs/macos-signing.md:Developer ID Application 证书创建与 secret 配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:56:08 +08:00

4.6 KiB
Raw Blame History

macOS 分发(Developer ID + 公证)— 一次性配置

macOS 客户端通过官网下载 zip + 应用内自动更新分发(非 Mac App Store)。要让用户双击/自更新重启时不被 Gatekeeper 拦截(「已损坏」「无法验证开发者」),CI 构建出的 .app 必须 用 Developer ID Application 证书签名 + 提交 Apple 公证(Notarization+ staple 票据

所有凭证通过 Forgejo Secrets 注入,不入库。macOS 强制签名+公证:未配置证书 secret 时 build-macos job 会直接报错、流水线红,绝不产出未签名包(避免把会被 Gatekeeper 拦截的包发出去)。

与 iOS 的关系:公证复用 iOS 那套 App Store Connect API key,无需新建。只需额外做一张 macOS 专用的 Developer ID Application 证书(iOS 的 Apple Distribution 证书在这里不可用)。

前置

  • Apple Developer Program 账号(与 iOS 同一个,Team ID BYL4KQHMTN)。
  • 已按 docs/ios-signing.md 配好 APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64(公证直接复用,本文不再重复)。

1. 创建 Developer ID Application 证书

为什么不是 Apple DistributionApple Distribution 证书只用于 App Store / TestFlight;官网自分发必须用 Developer ID Application,否则公证会失败。

两种方式任选:

  • Xcode(推荐,最省事)Xcode → Settings → Accounts → 选中团队 → Manage Certificates → 左下 +Developer ID Application。生成后证书带私钥落在「钥匙串访问」的「登录」里。
  • 开发者后台Certificates, Identifiers & Profiles → Certificates → +Developer ID Application → 用钥匙串「证书助理」生成的 CSR 上传 → 下载 .cer 双击导入钥匙串。

2. 导出为 .p12

「钥匙串访问」→「我的证书」里找到 Developer ID Application: …(BYL4KQHMTN连同私钥一起右键导出为 .p12,设一个导出密码:

base64 -i devid.p12 | pbcopy        # 复制 base64 到剪贴板

3. 在 Forgejo 仓库配置 Secrets

仓库 → Settings → Actions → Secrets新增 2 项

Secret 名
MACOS_DEVELOPER_ID_CERT_P12_BASE64 第 2 步 .p12 的 base64
MACOS_DEVELOPER_ID_CERT_PASSWORD .p12 导出密码

公证用的 APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64 已在 iOS 配置时存在,复用即可

4. 验证

client-v* tag 触发流水线,确认 build-macos job

  • 日志出现 signing + notarizing(非 SKIP);
  • 出现 signed + notarized + stapled
  • 在一台没装过本 app 的干净 Mac 上双击官网下载的 jiu-macos-x64.zip 解出的 .app不弹 Gatekeeper 警告直接打开;
  • 断网再双击仍能打开(票据已 staple 进 bundle)。

本地手动验证已签名包:

codesign --verify --deep --strict --verbose=2 Jiu.app   # 应无报错
xcrun stapler validate Jiu.app                          # The validate action worked!
spctl -a -vvv -t install Jiu.app                        # source=Notarized Developer ID, accepted

故障排查

  • 公证失败xcrun notarytool log <submission-id> --key … --key-id … --issuer … 看详细原因。最常见三类:
    1. 没用 --options runtimehardened runtime 未启用);
    2. 没带 --timestamp(安全时间戳缺失);
    3. 用了 Apple Distribution 而非 Developer ID Application 证书。
  • security import 报私钥缺失:导出 .p12 时没勾选私钥,重新从「我的证书」节点(不是单独的证书条目)导出。
  • spctl 显示 rejected:通常是 staple 没成功或公证未通过——先确认 notarytool 返回 Accepted

工作原理

  • scripts/ci/compile-macos.sh:开头先校验证书/API key secret,缺失立即 exit 1fail-fast,不浪费一次 build);flutter build macos 后建临时 keychain 导入 Developer ID 证书 → inside-out codesignhardened runtime + timestamp + Release.entitlements)→ notarytool submit --wait(复用 App Store Connect API key)→ stapler staple → 重新 dittodist/jiu-macos-x64.zip强制签名+公证,不产出未签名包。
  • .gitea/workflows/deploy-client.ymlbuild-macos job:通过 env 注入上述 secrets。
  • 自更新链路:client/lib/core/update/app_updater_io.dart 下载该 zip → ditto 解压 → 替换 .appopen 重启;因票据已 staple,重启的新 app 离线也通过 Gatekeeper。