macOS:在脚本中添加公证?

26
因为用Xcode进行代码签名和归档耗费时间、乏味且存在问题,因此我一直通过命令行工具xcodebuildcodesign等自己的脚本来完成Developer ID签名的macOS应用程序的代码签名、归档和发布。看起来,将要做的是主要的疼点:实现公证。能否在我的脚本中添加公证?
4个回答

39

是的。不幸的是,官方答案仍有一些漏洞,例如来自Quinn "the Eskimo"的重要信息。以下是如何操作:

一次性设置

获取应用特定密码

为您的“应用”决定一个名称以进行应用程序公证。 我使用我的产品发货脚本的名称SSYShipProduct.pl,因为这是将使用此密码的“应用”。 我们将引用您所编写的任何名称为your-notarizing-name.

浏览至https://appleid.apple.com/account/manage,滚动到 Security > App-Specific Password,并生成名为your-notarizing-nameapp-specific密码。 复制它给你的密码。 我们将称之为 app-specific-password

在macOS钥匙串中添加应用特定密码

运行此命令将刚创建的密码添加到您的键链中:

security add-generic-password -a "your-apple-ID-email" -w "app-specific-password" -s "your-notarizing-name"

-s参数是此项在您的Keychain中的名称。 我认为实际上可以使用不同的名称,但在我的脑海中,在这里使用your-notarizing-name是有意义的。

您可以通过在应用程序中搜索来验证它是否起作用。 但是,请注意,新项目在退出并重新启动之前不会列在中。

可能获取相关的 itc-provider

如果您的Apple ID与多个Apple Developer Connection团队相关联(例如您从事合同工作),则需要该应用程序应获得公证的团队的itc_provider

要查找您团队的itc_provider,请执行以下命令:

/Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u "your-apple-ID-email" -p "app-specific-password"

滚动到此命令打印的输出末尾,并查看所需团队的Provider listing表格。 复制所需团队的Short Name。 我们将其称为“developer-team-itc-provider”。

每次发货(可脚本化!)

如果您使用/usr/bin/codesign命令行工具对应用程序的组件进行签名,则每个codesign调用必须具有以下新的参数值,该参数值告诉codesign使用所谓的hardened runtime进行签名:

 `--options runtime`

如果您的应用程序在Xcode中已签名,您必须在所有可执行组件目标中设置Xcode 10或更高版本中提供的"强化运行时"构建设置为"是"。

除此之外,您的脚本应创建一个发布配置的应用程序构建,并对其进行代码签名,就像在无需经过公证之前一样。

上传到Apple公证服务

然后,您的脚本应将您的应用程序打包成.zip或.dmg。请注意,这是一个仅会上传到苹果公证服务而不会发货的中间文件。

接下来,您的脚本应该组成一个“主要捆绑标识值”,它将是您的应用程序的捆绑标识符,后面附加上.zip或.dmg。例如:your-pbid-value = com.mycompany.YourApp.zip

在接下来的步骤中,您的脚本将使用altool,这是苹果的应用程序加载工具的名称。

然后,您的脚本应运行以下命令来获得您的.zip或.dmg公证:

/usr/bin/xcrun altool --notarize-app --primary-bundle-id "your-pbid-value" --username "your-apple-id-email" --password "@keychain:your-notarizing-name" -itc_provider "developer-team-itc-provider" --file /path/to/YourApp.zip/or/YourApp.dmg --output-format "xml"

(请注意,在上面的命令中,奇怪的是,所有参数名称都以两个短划线开头,除了-itc_provider之前只有一个短划线。此外,如果您正在使用的脚本语言在字符串中插入@字符,请编写代码以防止插值@keychain)。

一分钟左右后,xcrun将退出并打印一些XML到stdout,如果您的提交被接受(注意:尚未获得批准),则会像此示例一样:

<?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>notarization-upload</key>
    <dict>
        <key>RequestUUID</key>
        <string>2ab59b26-19ec-4a30-84cf-6d2cb8d3c97e</string>
    </dict>
    <key>os-version</key>
    <string>10.15.0</string>
    <key>success-message</key>
    <string>No errors uploading 'path/to/YourApp.zip'.</string>
    <key>tool-path</key>
    <string>/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework</string>
    <key>tool-version</key>
    <string>1.1.1138</string>
</dict>
</plist>



你只需要从中获取RequestUUID的值。然而,由于我经常发布四个应用程序,当我的发货脚本失败而没有提供有用的错误信息时,它会破坏我的一天,并且因为(请参见以下内容)您将进行另一个调用,该调用还返回有趣的XML,所以我花费了一些时间添加到我的脚本中一个子程序,它接受两个参数,XML和关键路径,并返回给定键路径处的XML的值。 在上面的情况下,我调用此子例程以获取RequestUUID,然后再次以获取success-message

(我的脚本是用Perl编写的。虽然CPAN中有一个名为XML::Simple的模块可以在一两行中执行此解析,但维护者将其标记为不适用于新设计。因此,为避免需要安装和处理真正的 XML解析器,我选择使用PlistBuddy,如@khuttun的注释建议的那样。这也有点痛苦,因为不幸的是,altool没有选项将其输出写入文件,而PlistBuddy未记录接受stdin的方法。 因此,我的子程序将来自altool的stdout写入临时文件,然后将该临时文件的路径传递给PlistBuddy。 有点恶心,但它可以工作。)

删除未装订的zip包

此时,我建议您的脚本删除已上传的.zip.dmg文件。原因:该文件是从尚未附加您的验章票据的产品中存档的。在脚本结束时,您将从具有附加票证的修改后的应用程序创建新的.zip.dmg。立即删除文件可以防止您错误地发布未经装订的应用程序。

循环等待Apple的响应

然后,您的脚本可以通过运行此命令并伴随一些延迟来开始不断向Apple服务器请求最终结果:

`/usr/bin/xcrun altool --notarization-info --username "your-apple-id-email" --password "@keychain:your-notarizing-name" --output-format "xml"

如果您的脚本立即运行此命令,则会在stdout中返回一些XML,其外观类似于此示例:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>notarization-info</key>
    <dict>
        <key>Date</key>
        <date>2019-08-07T01:17:37Z</date>
        <key>RequestUUID</key>
        <string>4ba71353-9d99-4b52-b579-37f384717130</string>
        <key>Status</key>
        <string>in progress</string>
    </dict>
    <key>os-version</key>
    <string>10.15.0</string>
    <key>success-message</key>
    <string>No errors getting notarization info.</string>
    <key>tool-path</key>
    <string>/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework</string>
    <key>tool-version</key>
    <string>1.1.1138</string>
</dict>
</plist>


其中重要的关键路径是 notarization-info:Status,其值为in progress表示苹果仍在处理您的提交。通常几分钟后(苹果表示“应该少于一小时”,但我曾经历过在2019年7月4日美国假期下午长达三个半小时的情况),altool将在stdout中返回一个不同的XML,类似于以下内容:

<?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>notarization-info</key>
    <dict>
        <key>Date</key>
        <date>2019-08-06T23:28:25Z</date>
        <key>LogFileURL</key>
        <string>https://osxapps-ssl.itunes.apple.com/itunes-assets/Enigma113/v4/f6/09/be/f609bee3-b031-323a-0987-d1f620a78758/developer_log.json?accessKey=1565410613_1722173034418364591_TvycjBAzd6FRTYGKZEFU6EwDfsws8Wa1MV%2FYnTiJ1zyOZamc%2FoeO5RMeIzZN669ZQJgO2Q4W48ipKNFO%2BQGuq%2FITXN8MQAetbNe90w9ogzqXbrzTHg%2FgYK89yvEFmiiRxhaVlZqLI93NBpY0hwBqXv2bvvlg%2FRCc%2BVaCNRJ%2BrnE%3D</string>
        <key>RequestUUID</key>
        <string>07fc3745-b0ff-4d1a-9b15-37f384717130</string>
        <key>Status</key>
        <string>success</string>
        <key>Status Code</key>
        <integer>0</integer>
        <key>Status Message</key>
        <string>Package Approved</string>
    </dict>
    <key>os-version</key>
    <string>10.15.0</string>
    <key>success-message</key>
    <string>No errors getting notarization info.</string>
    <key>tool-path</key>
    <string>/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework</string>
    <key>tool-version</key>
    <string>1.1.1138</string>
</dict>
</plist>
在进行了一些逆向工程后,你会发现在每次循环迭代中,你的脚本应该解析XML并在Status的值为in progress之外的某些情况下退出循环,或者如果你愿意的话,当定义了LogFileURL时退出循环。或者如果你喜欢电子邮件触发器,你的脚本可以查找来自苹果的标题为“您现在可以分发您的Mac软件”的电子邮件。

2019-11-02更新:

在我的最近几次装运中遇到了这个问题,在今天又遇到了,我现在确认了一个Apple Notary Service的错误。 错误在于altool --notarization-info命令将失败1-5小时,返回非零退出代码,并在标准输出中出现错误代码1519“无法找到RequestUUID”,例如以下示例stdout:

<?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>os-version</key>
    <string>10.15.1</string>
    <key>product-errors</key>
    <array>
        <dict>
            <key>code</key>
            <integer>1519</integer>
            <key>message</key>
            <string>Could not find the RequestUUID.</string>
            <key>userInfo</key>
            <dict>
                <key>NSLocalizedDescription</key>
                <string>Could not find the RequestUUID.</string>
                <key>NSLocalizedFailureReason</key>
                <string>Apple Services operation failed.</string>
                <key>NSLocalizedRecoverySuggestion</key>
                <string>Could not find the RequestUUID.</string>
            </dict>
        </dict>
    </array>
    <key>tool-path</key>
    <string>/Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework</string>
    <key>tool-version</key>
    <string>4.00.1181</string>
</dict>
</plist>


这是一个bug,因为我的脚本已经提交了它从苹果公证服务中接收到的请求UUID,苹果应该能够找到它,并且当我手动发送命令时,在大约2小时后,突然,命令返回Success,并继续返回Success与随后的命令,我也从苹果那里收到了 Success 电子邮件。今天,7个不同的好的请求UUID都出现了这种延迟,最长的时间为5个小时。可能,在此期间,苹果公证服务创建和发送请求UUID与其用于响应notarization-info请求的数据库之间存在1-5小时的延迟,因此您会收到这个虚假错误。非常遗憾。

由于我无法控制苹果何时分配人员来修复错误,因此我修改了脚本的这个阶段,以解析来自Apple的响应,并仅在命令返回非零退出状态并且第一个(索引= 0)product-errors数组条目的code不是1519时才停止。如果像我一样使用PlistBuddy解析XML,则该键路径为code应为product-errors:0:code。我的脚本循环打印每次收到错误1519的时间,因此我可以看到发生了什么,当然,我已修改其while条件,以便如果错误代码为1519,则不退出。

在修复脚本后,我有几个应用要发布。苹果公证服务很好地处理了第一个:没有错误1519,并且在大约两分钟后出现了Success。然而,下一个需要我的脚本的这个新功能。在09:54(HH:mm)时,我的脚本从苹果那里接收到了请求UUID。20秒钟后,它发送了第一个altool --notarization-info查询。响应是虚假错误1519。随后的查询也返回了虚假错误1519,持续了将近3个小时,直到12:44。然后,在12:45,突然收到了一个in progress响应。经过5次in progress响应,在12:47,最终Success

在离开这个话题之前还有一件事:在那个请求成功且没有错误1519的一个小时后,一个小时前的先前请求突然开始返回in progress,几分钟后变成Success。结论:被转移到错误1519陷阱的请求UUID不会按FIFO排队,而是与后来可能通过偶然避免错误1519的请求UUID分开。因此,更好的解决方法可能是在收到另一个Error 1519响应之后放弃一个请求UUID,并重新上传应用程序到苹果公证服务并获取另一个您希望表现更好的请求UUID。当然,在接下来的几个小时内,您会收到许多电子邮件,因为您放弃的所有请求UUID最终都会成功。

无论如何,现在进入脚本的下一步...

检查苹果的日志文件

您的脚本应该解析LogFileURL的值,以便可以检查日志,因为即使公证成功,苹果创建的日志文件可能包含警告。当

curl <LogFileURL-Value>

日志文件显然是JSON格式的。警告或错误被作为数组呈现,并作为键 issues 的值。因此,您的脚本应该使用JSON解析器解析 curl 输出,并且如果键 issues 的值为JSON null或空数组,则继续发货。

将门票固定在您的应用程序上

这一步非常简单...

xcrun stapler staple /path/to/YourApp.app

运行此命令将向您的应用程序包中添加一个新文件:YourApp.app/Contents/CodeResources。这显然是您的公证票。请注意,此文件除了仍然存在的文件YourApp.app/Contents/_CodeSignature/CodeResources外,还包含代码签名,与公证之前的情况相同。

验证票据钉扎

但有一种更好的方法来验证您的应用程序现在是否拥有良好的票据。您的脚本现在应该运行(或重新运行)Gatekeeper检查:

spctl -a -v /path/to/YourApp.app

结果应该在stderr中,

/path/to/YourApp.app: accepted
source=Notarized Developer ID

该结果与预审批相同,仅在其中插入已公证。敏锐的脚本将解析stderr,如果未检测到上述单词,则会中止发货。

压缩和发送

现在,已添加了票证,您的脚本可以再次压缩或dmg您的.app文件,并将其发送出去。


1
altool有一个--output-format选项,可以用于获取结构化输出,可以使用PlistBuddy解析,例如对于notarize-app和notarization-info命令。 - khuttun
1
我的 dmg 文件已经经过公证,但我没有看到 YourApp.app/Contents/CodeResources 文件。/Volumes/DMG_Name/YourApp.app:已接受 source=Notarized Developer ID。 - Parag Bafna
@ParagBafna:很有趣。我只用zip测试过我的,没有用磁盘映像。当你说“我的dmg文件已经公证”时,你是指在最后一步中,“spctl”测试返回“已公证的开发者ID”吗?如果是这样,我想知道“stapler”操作做了什么。它肯定做了些什么——我猜是在某个地方添加了一个文件。 - Jerry Krinock
@JerryKrinock 你可以使用 --output-format xml 命令来获取 plist 输出。 - Parag Bafna
无法使用“查找您团队的itc_provider”的命令,因为Xcode工具“Application Loader”不存在。它必须已被删除。 - Oscar
显示剩余3条评论

5
这是一个可重复使用且自由许可的公证和装订脚本,用于自动化构建:

https://github.com/rednoah/notarize-app/blob/master/notarize-app

它将运行并等待,只有在所有操作完成后才会退出:

  1. 运行 altool --notarize-app
  2. 定期运行 altool --notarization-info 直到认证完成
  3. 运行 stapler staple

我们应该把那个脚本放在哪里? - Ahmadreza
在创建了你的 *.pkg 部署工件之后,你需要在构建脚本中运行 notarize-app - rednoah
抱歉,我不太明白。您的意思是我应该将那个脚本放在构建设置中的构建脚本中,然后归档应用程序吗?(我想要对屏幕保护程序进行公证) - Ahmadreza
1
https://dev59.com/bVMI5IYBdhLWcg3wJ4Oz#56890758 全面解释了应用程序公证的工作原理。最好手动逐步完成操作,以便您了解事物的运作方式,然后再使用 notarize-app 自动化该过程。 - rednoah

2

苹果已更新公证流程以自动化此脚本... 公证 myapp.app:

ditto -c -k --sequesterRsrc --keepParent myapp.app myapp.zip
sudo xcrun notarytool submit myapp.zip --apple-id drwho@bbc.com --team-id ABCDEFGHIJ --password <token>

对 myapp.dmg 进行公证:

zip myapp.dmg.zip myapp.dmg
sudo xcrun notarytool submit myapp.dmg.zip --apple-id drwho@bbc.com --team-id ABCDEFGHIJ --password <token> --wait

本答案中的先前示例使用了已弃用且现在已过时的方法。


2
谢谢!对于将来的用户,请记住,链接的代码是GPL许可证(在不弹回到主存储库的情况下很难确定)--在复制之前确保您可以在您的软件中使用GPL代码。 - Deadpikle

0

altool现已弃用。您可以使用此脚本进行代码签名,使用新的notarytool进行公证,然后将应用程序加上订书钉:https://github.com/thezik/notarizer


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接