Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一文读懂,如何工程化实现 macOS App 公证过程 #41

Open
WJCHumble opened this issue Oct 21, 2023 · 0 comments
Open

一文读懂,如何工程化实现 macOS App 公证过程 #41

WJCHumble opened this issue Oct 21, 2023 · 0 comments
Labels

Comments

@WJCHumble
Copy link
Owner

前言

对于 macOS App 开发者来说,我们通常情况下可能会选择在网络上分发 App。但是,站在使用者的角度,如果下载的 App 没有经过 Apple Notary Service 公证(Notarizate)过,这在安装的时候系统则会提示“无法打开 xxx App,因为无法验证开发者”:

那么,这个时候的解决方法就是修改系统偏好——>安全性与隐私的设置,选择仍要打开该 App:

虽然,这样可以让使用者安装 App,但是,这并不是真正在解决这个问题的本质。从根上解决应该是让我们要分发到网络上的 App 通过 Apple Notary Service 公证(Notarizate),这样一来他人下载安装我们应用的时候则不会出现无法打开的提示,而是:

所以,今天本文也将围绕「macOS App 公证」展开如何通过手动或者自动化(Shell、工具)实现公证(Notarizate)过程。

1. 手动公证

首先,我们先了解下如何手动公证?手动公证过程可以通过 Xcode 提供的 GUI 界面操作完成。同样地,首先我们需要构建 .xarchive 文件:

构建完后,Xcode 会弹出窗口让你选择 Distribute App 或 Validate App,这里我们选择前者:

接着,不同于之前分发 App Store,我们需要选择 Developer ID,它表示的是在 App Store 之外分发 App:

然后,我们需要选择 Upload ——> Development Team -> Manually manage signing,此时需要选择 Develop ID 对应的 Distribution 证书和 Provisioning Profile:

最后,则选择 Next ——> Upload,然后则会将我们的 App 上传到 Apple Notary Service 进行公证,通过则会提示我们可以分发 App。

如果,了解过 App Store 分发相关的同学应该熟悉这个手动操作的过程,因为使用 Xcode 公证 App 的过程和 App Store 分发大同小异。所以,在通过简单了解使用 Xcode 手动实现公证过程后,接下来,我们来认识下如何自动化实现公证的过程?

2. 自动化公证

自动化公证则指的是我们通过代码实现前面介绍到的使用 GUI 手动公证的过程。那么,这里我列出了 4 种现在社区中实现的可以对 macOS App 自动化公证的方式:

  • altool --notarize-app 使用的旧的 Apple Notary Service,适用于 Xcode 12 以及更早的版本,但是需要注意的是Apple 将会在 2023 年秋季停止对它的支持
  • notarytool 使用的新的 Apple Notary Service,只适用于 Xcode 13 及以上的版本,相比较 altool --notarize-app 快了 10 倍
  • electron-notarize 用 JavaScript 实现的用于公证的工具,支持 notarytoolaltool --notarize-app 等 2 种公证方式
  • fastlane 用 Ruby 实现的一个可以便捷地帮你完成证书管理、代码签名和发布等相关的工具,适用于 iOS、macOS 和 Android 应用

当然,更进一步的话我们可以把这个自动化公证也加入到 CI/CD 过程中,有兴趣的同学可以自行了解相关实现。所以,接着下面将会对这 4 个的使用做对应的展开介绍,首先是 altool --notarize-app

2.1 altool --notarize-app

altool 是一个内置于 Xcode 中的命令行工具,用于验证 App 的二进制文件并将其上传至 App Store 或者对 App 进行公证(Notarize)。而 altool --notarize-app 也是最早大家使用的实现自动化公证应用的方式,这在社区中也可以看到大量基于它的实践。

altool --notarize-app 则主要是将应用上传到 Apple Notary Service,但是并不会告知你公证成功与否,所以通常需要结合 altool --notarization-info 命令一起使用(核对公证成功与否),整个公证的过程会是这样:

可以看到,首先我们需要使用 altool --notarize-app 将应用上传公证,然后获取本次公证的 UUID:

xcrun altool --notarize-app \
# .app 的压缩包或 .dmg
--file ./Output/Apps/FEKit.zip \
--primary-bundle-id "com.xxxx.xxxx" \
# Apple ID
--username "xxxxx" \
# 应用专用密码,这可以在 https://appleid.apple.com/account/manage 申请
--password "xxxxx" \

然后,终端中则会输出本次公证上传操作的信息和 RequestUUID,前者用于表示本次操作执行是否成功,后者可以用于 altool --notarization-info 命令查询本次公证过程的信息:

No errors uploading 'Output/Apps/FEKit.zip'.
RequestUUID = xxxxxxx-xxx-xxxx-xxxx-xxxxx

进行完公证上传操作后,Apple Notary Service 则会对本次公证执行相关的操作,而这需要一定的时间,所以我们需要(定时)轮询执行 altool --notarization-info 命令实时地获取公证成功失败与否的信息

# 加载 Apple ID($user)和 App 专用密码($pwd)
source "./Build/app_store_user_pwd.sh"

# 标识公证执行过程成功失败与否,失败 1,0 成功
success=1
i=0
while true; do
    let i+=1
    echo "Checking notarize progress...$i"
    # 获取 altool --notarization-info 执行的输出信息
    process=$(xrun altool --notarization-info $uuid --username $user --password $pwd 2>&1)
    echo "${progress}"
    # 如果上次命令执行结果 $? 不等于 0(表示失败),或者命令输出信息中包含 Invalid
    if [ $? -ne 0 ] || [[ "${progress}" =~ "Invalid"]] then
        echo "Error with notarization. Exiting"
        break
    if
    # 如果命令输出信息中包含 success 表示成功
    if [[ "${progress}" =~ "success"]]; then
        success=0
        break
    else
        echo "Not completed yet. Sleeping for 30 seconds.\n"
    fi
    sleep 30
done

if [ $success -eq 0 ]; then
    echo "Notarize successed."
if

其中,$uuid 则是执行 altool --notarize-app 命令后获取的返回结果:

echo xcrun altool --notarize-app \
--file xxxx \
--primary-bundle-id "com.xxxx.xxxx" \
--username $user \
--password $pwd \
2>&1 | grep RequestUUID |  awk '{print $3}'

这里我们来看下大家比较陌生的 2>&1grep RequestUUIDawk '{print $3}' 等 3 个命令的作用:

  • 2>&1 是为了将标准错误 stderr 输出重定向到标准输出 stdout
  • grep RequestUUID 匹配标准输出中所有包含 RequestUUID 的行
  • awk '{print $3}' 打印出第 3 列的结果,在 RequestUUID = xxxxxxx-xxx-xxxx-xxxx-xxxxx 就是 RequestUUID 的值 xxxxxxx-xxx-xxxx-xxxx-xxxxx
  • | 管道,用于将上个命令的输出通过管道输入到下一个命令

2.2 notarytool

相比较 altool --notarization-info 而言,notarytool 使用起来心智负担少一些,并且快于前者很多,我们只需要记忆一些 Option,使用一行命令 xcrun notarytool 则可以实现上传公证和过程信息获取的过程:

xcrun notarytool submit ./Output/Apps/FEKit.zip \
# Apple ID
--apple-id $user \
--team-id $teamId \
# 应用专用密码
--password $pwd \
-v \
-f "json"

其中,--team-id 指的是用户 ID(由数字和字母组成),这可以在本地 KeyChain 的证书中查看(或者 Apple 证书后台),-v--verbose 的缩写,指的是输出公证过程的信息,-f "json" 则是表示最终结果以 JSON 的格式输出,例如:

{
  "path":"\/Users\/wujingchang\/Documents\/project\/demo\/FEKit\/Output\/Apps\/FEKit.zip",
  "message":"Successfully uploaded file",
  "id":"xxxxxxxxxxxxxxxxxxxx"
}

需要注意的是这里使用的是 Appple ID 和应用专用密码的方式做与公证服务的请求认证 Authentication,此外你还可以通过以下 3 种 Option 来进行认证:

  • --keychain-profile <keychain-profile>,使用 xcrun notarytool store-credentials 预先在本地钥匙串中新建一个应用程序密码,例如叫 AC_PASSWORD,那么在使用 notarytool submit 命令的时候则可以直接使用 --keychain-profile AC_PASSWORD 代替之前的 --apple-id $user --team-id $teamId --password $pwd
  • --keychain <keychain>,不同于前者 --keychain-profile 这里是输入的 AC_PASSWORD 文件所在的位置
  • --key <key-id> --key-id <key-id> --issuer <issuer>,这使用的是 App Store Connect API keys 的方式进行认证,本质上是生成一个和 App Store Connect 约定好的 JWT,然后每次请求的时候携带上它,从而通过认证

2.3 electron-notarize

electron-notarize 则是一个用 JavaScript 实现的公证工具,它的原理则是使用的 child_process 执行前面我们提及的 altool --notarization-infonotarytool 这 2 个命令。

electron-notarize 具名导出了 notarize 函数,我们只需要使用它以及指定的 Option 则可以完成公证的过程,这里我们来看下其函数的实现(伪代码):

// src/index.ts
export async function notarize({ appPath, ...otherOptions }: NotarizeOptions) {
  if (otherOptions.tool === 'notarytool') {
    // ...
    await notarizeAndWaitForNotaryTool({
      appPath,
      ...otherOptions,
    });
  } else {
    // ....
    const { uuid } = await startLegacyNotarize({
      appPath,
      ...otherOptions,
    });
    // ...
    await delay(10000);
    // 获取公证过程信息
    await waitForLegacyNotarize({ uuid, ...otherOptions });
  }

  await stapleApp({ appPath });
}

可以看到,notarize 函数会根据 Option 中传入的 toolnotarytoollegacy 来执行不同的命令来完成公证,这里前者是 notarytool 后者则是 altool --notarization-info。所以,如果我们要用 notarytool 的方式进行公证会是这样:

import { notarize } from "electron-notarize"

await notarize({
  appPath: "./Output/Apps/FEKit.zip",
  // Apple ID
  appleId: "xxxxxx",
  // 应用专用密码
  appleIdPassword: "xxxxxxxx",
  teamId: "xxxxxxxxxxx",
  tool: "notarytool"
})

其中,如果你不希望密码直接明文暴露在代码中的话,electron-notarize 也支持了前面我们说的 --keychain--keychain-profile 等 3 个 Option,你可以根据自己的需要选择对应的认证方式。

2.4 fastlane

fastlane 是一个可以便捷地帮你完成证书管理、代码签名和发布等相关的工具,适用于 iOS、macOS 和 Android 应用。那么,我们也就可以使用 fastlane 来完成 macOS App 的公证。

首先,肯定是安装 fastlane,关于这方面的介绍官方文档讲解的很是详尽,这里就不重复论述。而当你安装好 fastlane,则可以在应用项目的根目录执行 fastlane init 来初始化它相关的配置,在初始化的过程会让你选择使用 fastlane 的方式,这里我们选择手动配置即可。

然后,它会在项目根目录下创建一个 fastlane/Fastfile 目录和文件,后续我们在执行 fastlane xxx 命令的时候则会根据该文件的代码实现执行具体的操作,默认生成的 Fastfile 文件的配置会是这样:

default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :custome_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end

其中,default_platform 用于定义一个默认的平台 Platform,例如当我们有 2 个平台(iOS 和 macOS)的时候,它的的配置需要这样:

default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :custome_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end
platform :mac do
  desc "Description of what the lane does"
  lane :custome_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end

此时,如果我们执行 fastlane custome_lane,由于这里平台默认为 ios,所以则会执行 platorm:ios 下的 custome_lane,反之执行 fastlane mac custome_lane,则是 platform :mac 下的 custome_lane。那么,对于前面我们这个例子而言只需要 platform:mac

default_platform(:ios)

platform :mac do
  desc "Description of what the lane does"
  lane :custome_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end

接着,则可以在 platform:mac 写我们需要实现的自动化分发 App Store 相关的代码。fastlane 便捷之处在于它实现了很多开箱即用的 Action,这里我们需要使用 notarize 这个 Action,它可以用于完成 macOS App 的公证:

default_platform(:mac)

platform :mac do
  desc "Notarizes a macOS app"
  lane :notarize_app do
    notarize(
      package: "./Output/Apps/FEKit.zip",
      use_notarytool: "xcrun notarytool",
      bundle_id: "com.xxxx.xxxx",
      username: "xxxxxxxxxxxxxx",
      verbose: true,
    )
  end
end

其中,在使用 notarize 的时候需要注意的是,这里只是声明了你 App Store 的用户名 username,而专用密码需要预先在系统环境变量中添加 FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD(其他认证方式,有兴趣的同学可以自行了解),然后 fastlane 在执行 notarize_app Action 时会去读取该环境变量,从而进行并完成后续的公证过程。

结语

看到这里,我想可能会有同学会问:“这几种实现公证的工具,选择哪个比较好?”,这里比较建议的是选择 fastlane,因为,除开前面提及它的使用方式非常便捷的优点,它具备的能力也很多,不仅仅可以做 App 公证,还可以做 App Store 分发、证书和版本管理等,所以,选择 fastlane 将来也可以支持我们别的诉求,何乐不为呢?

最后,如果文中存在表达不当或错误的地方,欢迎各位同学提 Issue ~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant