macOS App 签名与公证流程详解及一键自动化

216 阅读4分钟

一、签名证书申请

  •   注册 Apple Developer Program(年费 $99),⚠️务必登录账号所有人的 Apple 账号,才能操作申请这个证书。 image.png
  •    在 Apple Developer 开发证书申请后台,创建以下证书:
    •   Developer ID Application:用于签名应用。
    •   Developer ID Installer:用于签名安装包。
  •   macOS  App 证书 和  iOS App 开发是两套证书,有些证书名字很相似,注意做好区分。
  •   下载并安装相关证书,⚠️证书信任采用默认设置,信任等级要保持一致。 image.png

二、签名前的准备工作

在 macOS 中,应用的代码签名和打包结构有严格要求,不正确的库放置位置会导致签名验证失败或运行时崩溃。以下是苹果对 macOS 应用打包结构的要求及解决方案:

1.确保应用结构符合苹果的 macOS 签名规范

macOS 应用 .app 包格式(本质是目录),标准结构如下:

YourApp.app/
├── Contents/
│   ├── Info.plist               # 应用元数据(必选)
│   ├── PkgInfo                  # 包类型信息(可选)
│   ├── MacOS/                   # 可执行文件目录
│   │   └── YourApp              # 主程序(必选)
│   ├── Resources/               # 资源文件(图标、nib 等)
│   ├── Frameworks/              # 动态框架目录(存放 .framework)
│   │   ├── Framework1.framework
│   │   └── Framework2.framework
│   └── Libraries/               # 动态库目录(存放 .dylib,较少使用)
│       └── libYourLibrary.dylib
└── _CodeSignature/              # 签名信息目录(由 codesign 自动生成)
    └── CodeResources

2.关键目录的作用

2.1  Frameworks/

  •   存放应用依赖的动态框架(.framework),这些框架会被应用动态加载。
  •   签名要求:框架必须单独签名,且其内部的二进制文件、资源文件也需递归签名。

2.2  Libraries/

  •   存放独立的动态库(.dylib),但苹果推荐优先使用 .framework 格式。
  •   注意:若使用 .dylib,需确保路径正确配置(如 @rpath)。

2.3  MacOS/

  •   主程序可执行文件必须位于此目录,否则系统无法找到入口点。

3.检查 YourApp.app 的 Info.plist 配置情况

其中必须包含以下关键字段:

CFBundleName   YourApp
CFBundleExecutable      YourApp
CFBundlePackageType  APP
CFBundleIdentifier    com.yourcompany.aippt

4.检查库依赖

  •   使用 otool -L 查看依赖库
otool -L YourApp.app/Contents/MacOS/YourApp

可以列出可执行文件依赖的所有库及其路径。检查输出中是否存在指向 /usr/lib/usr/local/lib、或其他非应用目录的路径。如果有,说明有库没有正确包含在应用包中。 ⚠️注意:这里只是初步扫描,并不是很准确,是否引用三方库,需要代码确认。

  •   检查嵌入框架和插件:对应用包内的每个可执行文件(如 Contents/Frameworks 和 Contents/PlugIns 中的 .dylib/.so/*.node 等),同样使用 otool -L 检查。例如:
otool -L YourApp.app/Contents/Frameworks/*.dylib

确认所有依赖都指向包内路径(通常以 @rpath@loader_path 或 @executable_path 开头)并且实际存在于应用包中。

5.检查网络配置,尽量关闭VPN,代理等

因代码签名时,系统会向苹果的时间戳服务器(timestamp.apple.com)请求时间戳,用于验证签名的有效性和时间。可能被防火墙、代理或网络环境拦截,无法访问苹果的时间戳服务器。

三、签名流程—脚本自动化

在账号目录下,创建一个签名管理的文件夹MacSign,将待签名【.app】文件放进去该目录。

entitlements.plist 配置

在MacSign目录下,创建名为 entitlements.plist文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>
</plist>

签名脚本(升级版)

在MacSign目录下,通过文本编辑器,创建名为 sign.command(格式不可变)文件:

#!/bin/bash
printf "\033c"
{ set +e; unset SHELL_SESSION_HISTORY; } >/dev/null 2>&1

# ===================== 签名配置 =============================
CERT_NAME="Developer ID Application: Shenzhen Pixel Bloom Technology Co., Ltd. (xxxxxxxxx)"
APPLE_ID="wyk125@163.com"
TEAM_ID="xxxxxxxx"
NOTARY_PASSWORD="xxxx-xxxx-xxxx-xxxx"

APP_DIR="$HOME/MacSign"
ENTITLEMENTS="$APP_DIR/entitlements.plist"

# 扫描目录下唯一 .app 文件,自动赋值名称
# 查找所有 .app 文件
APP_LIST=("$APP_DIR"/*.app)
# 校验:未找到 .app 文件
if [ ${#APP_LIST[@]} -eq 0 ] || [ ! -e "${APP_LIST[0]}" ]; then
    echo -n -e "\n\n"
    echo -e "\033[31m==========================================================\033[0m"
    echo -e "\033[31m❌  错误:未在目录中找到任何 .app 文件!\033[0m"
    echo -e "\033[33m📂 请检查路径:$APP_DIR\033[0m"
    echo -e "\033[33m⚠️  已终止签名打包流程,请添加.app文件后重试\033[0m"
    echo -e "\033[31m==========================================================\033[0m"
    echo -n -e "\n\n 按 Enter 退出终端"
    read -rs -t 3600
    osascript -e 'tell application "Terminal" to quit window 1' >/dev/null 2>&1 & exit 0  
    exit 1
fi
# 校验:找到多个 .app 文件
if [ ${#APP_LIST[@]} -gt 1 ]; then
    echo -n -e "\n\n"
    echo -e "\033[31m==========================================================\033[0m"
    echo -e "\033[31m❌  错误:目录中存在多个 .app 文件!\033[0m"
    # 👇 核心修改:循环逐行展示每个文件
    echo -e "\033[33m📂 找到的文件:\033[0m"
    for app_file in "${APP_LIST[@]}"; do
        echo -e "\033[33m   ✦ $app_file\033[0m"
    done
    echo -e "\033[33m⚠️  请只保留一个需要签名的.app文件\033[0m"
    echo -e "\033[31m==========================================================\033[0m"
    echo -n -e "\n\n 按 Enter 退出终端"
    read -rs -t 3600
    osascript -e 'tell application "Terminal" to quit window 1' >/dev/null 2>&1 & exit 0  
    exit 1
fi


# 自动获取 APP 文件名
FULL_APP_PATH="${APP_LIST[0]}"
INPUT_APP=$(basename "$FULL_APP_PATH")          # 例如:aippt.app
APP_BASE_NAME="${INPUT_APP%.app}"               # 提取纯名称:aippt
OUTPUT_DMG="${APP_BASE_NAME}.dmg"               # 自动生成:aippt.dmg

# ✅ 自动获取APP可执行文件
APP_EXECUTABLE="$INPUT_APP/Contents/MacOS/$APP_BASE_NAME"
# ==============================================================

cd "$APP_DIR" || exit 1

# 主界面
echo "=========================================================="
echo "🚀 macOS App 签名与公证一键自动化"
echo "📦 输入:${INPUT_APP}"
echo "📀 输出:${OUTPUT_DMG}"
echo "=========================================================="

echo -n -e "\n1️⃣  清理系统隔离权限..."
xattr -rc "$INPUT_APP" >/dev/null 2>&1
echo "✅"

echo -n "2️⃣  修复应用权限..."
chmod -R 755 "$INPUT_APP" >/dev/null 2>&1
chmod +x "$APP_EXECUTABLE" >/dev/null 2>&1
echo "✅"

echo -n "3️⃣  清理原有签名..."
codesign --remove-signature "$INPUT_APP" >/dev/null 2>&1
echo "✅"

echo -n "4️⃣  签名框架与子进程..."
for helper in "$INPUT_APP"/Contents/Frameworks/*Helper*.app; do
  [ -e "$helper" ] && codesign --force --deep --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERT_NAME" "$helper" >/dev/null 2>&1
done
for framework in "$INPUT_APP"/Contents/Frameworks/*.framework; do
  [ -e "$framework" ] && codesign --force --deep --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERT_NAME" "$framework" >/dev/null 2>&1
done
echo "✅"

echo -n "5️⃣  主程序深度签名..."
codesign --force --deep --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERT_NAME" "$INPUT_APP" >/dev/null 2>&1
echo "✅"

echo -n "6️⃣  验证签名有效性..."
codesign --verify --deep --strict "$INPUT_APP" >/dev/null 2>&1
echo "✅"

echo -n "7️⃣  生成 DMG 安装包..."
hdiutil create -volname "$APP_BASE_NAME" -srcfolder "$INPUT_APP" -ov -format UDZO "$OUTPUT_DMG" >/dev/null 2>&1
echo "✅"

echo -n "8️⃣  给 DMG 签名..."
codesign --sign "$CERT_NAME" "$OUTPUT_DMG" >/dev/null 2>&1
echo "✅"

# 🔥 公证提交 + 实时上传进度(保留原样)
echo -e "9️⃣  提交苹果公证(处理中)..."

# ===================== 保留:实时显示上传进度 =====================
xcrun notarytool submit "$OUTPUT_DMG" \
  --apple-id "$APPLE_ID" \
  --team-id "$TEAM_ID" \
  --password "$NOTARY_PASSWORD" \
  --wait

# ===================== 稳定提取 Submission ID =====================
SUBMISSION_ID=$(xcrun notarytool history \
  --apple-id "$APPLE_ID" \
  --team-id "$TEAM_ID" \
  --password "$NOTARY_PASSWORD" 2>/dev/null \
  | grep -Eo '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' \
  | head -n 1)

# 失败判断
if [ -z "$SUBMISSION_ID" ]; then
  echo -e "\n\033[31m❌ 公证提交失败!\033[0m"
  exit 1
fi

# ============== 只保留这一行:⏳ 公证结果查询:结果 ==============
status=$(xcrun notarytool info "$SUBMISSION_ID" \
  --apple-id "$APPLE_ID" \
  --team-id "$TEAM_ID" \
  --password "$NOTARY_PASSWORD" 2>&1 | grep -i "status" | head -1 | awk '{print $2}')

echo -n "⏳ 公证结果查询:"
if [ "$status" = "Accepted" ]; then
  echo -e "\033[32m✅ 成功\033[0m"
else
  echo -e "\033[31m❌ 失败\033[0m"
  exit 1
fi

echo -n -e "🔒 绑定公证凭证..."
xcrun stapler staple "$OUTPUT_DMG" >/dev/null 2>&1
echo "✅"

# 完成提示
echo -e "\n=========================================================="
echo " ✅ 全部操作完成!"
echo " 📀 成品文件:${APP_DIR}/${OUTPUT_DMG}"
echo "=========================================================="

echo -n -e "\n 按 Enter 退出终端"
read -rs -t 3600
osascript -e 'tell application "Terminal" to quit window 1' >/dev/null 2>&1 & exit 0

⚠️配置信息需要改成自己公司的,才可正常签名。

双击sign.command文件,弹起终端窗口,脚本自动化处理所有流程。

执行结果

image.png

四、签名流程详解

创建一个签名管理的文件夹MacSign,将【AiPPT.app】文件放进去该目录。

1.先对【AiPPT.app】进行代码签名

1.1清理旧签名(可选)

codesign --remove-signature AiPPT.app

1.2递归签名依赖项(签名内部文件)

Developer ID Application 需要换成自己公司的

sudo codesign -f -o runtime -s "Developer ID Application: Shenzhen Pixel Bloom Technology Co., Ltd. (7UXDYFGRA9)" -v AiPPT.app --deep  --verbose=4

三方库一般不用独立签名,重复签名可能会引起签名冲突等不可控的异常问题。 特殊情况下,才单独对内部关键文件签名,后对.app 签名

sudo codesign -f -o runtime -s "Developer ID Application: Shenzhen Pixel Bloom Technology Co., Ltd. (7UXDYFGRA9)" -v AiPPT.app/Contents/Libraries/bydm.dylib  --verbose=4

1.3验证代码签名

codesign --verify --deep --strict --verbose=4 AiPPT.app

提示如下,则代码签名成功

AiPPT.app: valid on disk

AiPPT.app: satisfies its Designated Requirement

2.再对【AiPPT.dmg】进行包签名

2.1创建 .dmg 文件

hdiutil create -volname "AiPPT" -srcfolder "AiPPT.app" -ov -format UDZO "AiPPT.dmg"

2.2对dmg签名

codesign --sign "Developer ID Application: Shenzhen Pixel Bloom Technology Co., Ltd. (7UXDYFGRA9)" AiPPT.dmg

2.3验证dmg代码签名

codesign --verify --deep --strict --verbose=4 AiPPT.dmg

提示如下,则代码签名成功

AiPPT.app: valid on disk AiPPT.app: satisfies its Designated Requirement

3.对【AiPPT.dmg】进行签名公证

3.1对 .dmg 本身进行公证

⚠️公证信息需要改成自己公司的,才可正常公证。

xcrun notarytool submit AiPPT.dmg \
  --apple-id "wyk125@163.com" \
  --team-id "xxxxxxxxx" \
  --password "xxxx-xxxx-xxxx-xxxx" \
  --wait

3.2装订公证票据

将已公证的 .dmg 文件装订票据,以标记该文件已被公证,可离线使用:

xcrun stapler staple AiPPT.dmg

4.对签名进行验证

4.1验证APP签名合法性,Gatekeeper验公证结果


 spctl --assess --type execute --verbose=4 AiPPT.dmg

正确的提示信息如下:

AiPPT.app: accepted source=Notarized Developer ID

4.2获取签名的信息

spctl -a -vv /Users/wangyongkang/MacSign/AiPPT.dmg

成功获取信息,提示如下:

/Users/wangyongkang/MacSign/AiPPT.app: accepted source=Notarized Developer ID origin=Developer ID Application: Shenzhen Pixel Bloom Technology Co., Ltd. (7UXDYFGRA9)

5.完成以上签名流程,就可分发 .dmg 安装包

✅ 签名有效  

✅ 已通过 Apple 公证  

✅ 已正确 stapled(嵌入公证票据)  

✅ Gatekeeper 认可为安全应用

点击签名后的 .dmg 文件,按流程安装即可。

image.png

参考文档

苹果官方文档:developer.apple.com/developer-i…