iOS app 重签名及发布至 AppStore

原文转载自:https://evanxlh.github.io/2020/05/28/ios-app-resigning/

iOS app 重签名及发布至 AppStore

由于公司有个项目请了外包团队来开发,但不提供源码,只提供 xcarchive 或 ipa 包,而做为公司又不可能将证书及私钥发给外包团队,让他们来打包。所以重签名就是绕不过的坎了,只能自己来做这一工作了。刚开始直接从网上找资料,直接用现成的脚本作些修改来进行重签名,可都会出现各种问题。无耐之下,只能翻阅苹果官网文档以及 Troubleshooting,最终完成重签名,并上传至 AppStore。现在将重签名过程重新整理下,记录下来方便以后查阅,也希望能帮助到有需要的同学们。

核心概念

代码签名(Code Signing)是操作系统应用的一种安全技术,用来证明 app 是你创建的。一旦 app 被签名后,操作系统就可以检测出 App 是否被意外或恶意地修改过,是否来自一个受信任的发布机构或组织,或个人。代码签名最基础、最核心部分就是数据的加密哈希函数和非对称加密,然后在此基础上,定义一些规则与策略,来完成代码签名。接下来,我们先来了解下签名相关的一些核心概念。

加密哈希函数

加密哈希函数可以接收任意大小的数据,然后应用特定算法处理输入数据,并输出固定大小的哈希数据(如128 / 256 位等)。一些常见的哈希算法有 SHA-1, SHA-2 等等。在代码签名过程中,我们用它来生成代码数据的信息摘要(message digest, Apple 称它为 seal, 即封条,用来验证数据是否被更改)。

非对称加密

非对称加密(public key cryptography, 或者 asymmetric cryptography) 提供两组密钥:公钥和私钥。公钥可以公开并分享给他人,而私钥只能自己拥有。公称和私钥都可以用来加解密,但在一次加密解密过程中,需要配对使用。不管是用公钥加密,还是私钥加密,最后通过解密后,都能还原出明文,正如下图所示:
Security Asymmetric

信息摘要

信息摘要(message digest)是由代码签名软件创建的代码各个部分的校验和或哈希的集合,可以用来检测数据是否被更改。 拿写书信为例,写好的书信放入信封,然后进行密封。如果收信人发现密封损坏,他就会知道这封信肯定被打开过或被他人动过手脚。所以 Apple 也称它为 seal,意为密封或封条。通过私钥加密后,就成了代码签名中的加密信息摘要(encrypted message digest, 或者 encrypted seal)。

数字证书

数字证书(Digital Certificate) 是用于验证数字证书 持有者或发送者 的身份的数据集合,来证明签名者公钥的真实性。比如 X.509 证书包含以下一些信息:

  • 结构信息: 版本,序列号,用于创建签名的消息摘要算法等
  • 证书颁发机构的数字签名
  • 有关证书持有者的信息: 名称,电子邮件地址,公司名称,所有者的公钥等
  • 有效期: 证书在此期限之前或之后无效
  • 证书扩展: 包含其他信息的属性,如证书的允许用途
    Digital Certificate
    下图为 Apple Worldwide Developer Relations Certification Authority 证书中所包含的信息:
    Apple Worldwide Developer Relations Certification Authority

数字身份

数字身份(Digital Identity) 是公钥-私钥对以及对应的数字证书的组合。当我们向苹果申请到签名证书后,导入到 KeyChain Access 中,我们就获取到了数字身份,如下图所示:
Digital Identity

证书颁发机构

证书颁发机构(Certification Authority) 是颁发数字证书的人员或组织,它确保数字证书没有被更改,并且表明颁发数字证书者的身份。如 Apple, 我们可以从 Apple 提供的工具来创建数字身份(Digital Identity),并申请数字证书。

数字签名

数字签名(Digital Signatures) 就是加密信息摘要及数字证书的组合,是一种使用非对称加密来保证数据完整性的方法。它可以用来验证数据签名者的身份,跟我们使用传统签名(用墨水在纸上写上自己的名字)方法一样,都是用来表明及验证身份的。但数字签名除了身份验证外,还能检查出数据是否被修改过。

规范要求

Code Requirements,我就译成规范要求吧,是操作系统用来评估代码签名的规则。进行评估的系统根据其目标决定在评估时要应用哪些要求。比如 macos 的 Gatekeeper 有一个规则,即在首次允许启动应用程序之前,必须由Mac App Store或开发人员ID证书签名。再比如一个应用程序可以强制执行一个规范要求,即该应用程序使用的所有插件均应由 Apple 签名。有关规范的详细描述,可以查看苹果官方 Code Requirements

数字签名原理

数字签名过程

接下来,我们通过图例来阐述单个数据(这里指一个文件)的数字签名的过程:

  1. 签名者使用加密哈希算法,生成信息摘要
  2. 使用私钥对信息摘要进行加密
  3. 将加密的信息摘要、有关签名者数字证书的信息,以及数据本身打包在一起,完成数字签名
    Digital Signatures Process

a

验证数字签名过程

比如我们对一个文档进行了数据签名,那接收者如何去验证数字签名的正确性呢?下面我们结合图例来阐述下验证的过程:

  1. 接收者从签名者的证书中获取签名者的公钥
  2. 使用公钥对加密信息摘要进行解密
  3. 接收者使用证书中指定的算法创建数据的新的信息摘要
  4. 将新的信息摘要与数字签名中传递的信息摘要的解密副本进行比较。 如果它们匹配,则接收到的数据肯定于签名者创建的原始数据相同。
    Verify digital signature

iOS App 代码签名

iOS 代码签名比之前提到的数字签名过程会复杂一些,在签名过程中,还需要加入 Provisioning Profile,并且要对各种代码文件分别进行数字签名。

需要对哪些文件签名

iOS app 有三类文件需要签名:

  • 内嵌代码 (Nested code)
    包括所有的打包在 App 内的 libraries, frameworks, helpers, tools, 以及 app 依赖的其它组件。注意静态库请不要内嵌,否则重签名后的 ipa 文件上传到 iTunes Connect 会报错。
  • Mach-O 可执行文件 (Mach-O executables)
    签名软件会将签名信息直接写入可执行文件中。可执行文件包括 App 可执行文件 和 内嵌的 Framework 中的可执行文件。
  • 资源文件 (Resources)
    除了代码及可执行文件以外的文件都属于资源文件,都需要进行签名。每个资源文件签完名后,签名信息会被记录 CodeResoures 中,存放在 _CodeSignature 目录下。App 中可能会有多个 _CodeSignature 目录,因为 App 的 main bundle 中可能包含有内嵌 framework bundle, 或者其它的 resource bundle

Entitlements

权限文件是一个特殊的 plist 文件,可以被认为是写入应用签名中的字符串,该字符串允许特定功能或使该应用选择特定服务。 操作系统在允许应用访问某些功能之前会检查这些字符串。 比如,在允许应用程序在运行时访问 Wi-Fi 信息之前,该应用程序必须具有 Wi-Fi Info 权利。 关于 Entitlements 更多的详细信息,请点这里

<?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>application-identifier</key>
	<string>AA11BB22CC.com.company.appresignature.test</string>
	<key>beta-reports-active</key>
	<true/>
	<key>com.apple.developer.networking.wifi-info</key>
	<true/>
	<key>com.apple.developer.team-identifier</key>
	<string>AA11BB22CC</string>
	<key>get-task-allow</key>
	<false/>
	<key>keychain-access-groups</key>
	<array>
		<string>AA11BB22CC.*</string>
	</array>
</dict>
</plist>

Provisioning Profile

Provisioning Profile 是一种系统配置文件,可让您在iOS设备或Mac上安装应用程序,用于在设备上启动一个或多个应用程序并使用某些服务。

Provisioning Profile 文件包含签名证书、app bundle ID、Entitlements 信息,或者 deivce ids,不过 Provisioning Profile 可以分为 Development / Distribution 类型,而 Distribution 又分为 AppStore / AdHoc / Inhouse / Developer ID。每种类型的 Provisioning Profile 包含的信息都有些不一样,下图为 AppStore Provisioning Profile 包含的信息:

AppStore Provisioning Profile

CodeResources

CodeResources 是一个特殊的 Plist 文件,bundle 中的每个资源文件的加密信息摘要都记录在这个 Plist 文件中。除了这些加密信息摘要以外,它还包括一些签名规则,可用于接收端进行签名验证。下图为 CodeResources 文件所包含的内容:

CodeResources

App 签名

大多数时候,Xcode可以帮我们自动完成所有的签名操作。但如果是手动来签名的话,我们需要使用 Apple 提供的 codesign 程序来签名。codesign 的签名规则如下:

  • codesign 可以直接给一个 Bundle 进行签名,这里的 Bundle 可以是 app bundle (.app) ,framework bundle (.framework)dynamic library (.dylib) 等。
  • 它采用递归策略对 Bundle 下的每个文件进行签名。Bundle 实际上就是一个文件夹。
  • 可执行文件的加密信息摘要会被直接写入可执行文件的本身。这里说的可执行文件可以是 executable, Mach-O 文件类型,比如 app 的可执行文件,framework 中的可执行文件。
  • 先从最底层 Bundle ,再签上层 Bundle。比如先签 app bundle 中的内嵌的每个 framework bundle ,然后签 app bundle

上图为 iOS app 代码签名的概览图,现在我们结合这张图来介绍 app 的签名流程。

  • 准备好签名要用到的所有资源:ipa 或 xcarchive,provisioning profile,签名证书。
  • 将 App 中的各类代码文件(如库、可执行文件、脚本、资源文件,以及其他一些像代码的数据)按照数字签名过程并结合签名规则进行签名,并把签名得到的 encryted seal 记录在 CodeResource 文件中。可执行文件 (Mach-O文件) 的 encryted seal 不会记录在 CodeResource, 而是直接写入可执行文件本身。
  • 签完名后,重新打包 IPA。

App 重签名实战 

接下来,我们用苹果提供的命令行程序 codesign 来实现手动签名。如想了解 codesign 命令的具体用法,可以查看这篇文章的 Signing Code Manually 小节。

重签名环境

  • MacOS: 10.15.4
  • Xcode: version 11.5

App 重签名参数配置文件

App 重签名需要提供一些参数,我们把这些参数都集中放到这个参数配置文件中。以下为参数列表:

  • RootWorkingDirectory (必须)
    Shell 脚本工作的根目录,.ipa 或 .xcarchive,provisioning profile 必需放在这个目录下。 .ipa 或者 .xcarchive,两者不能同时存在。
  • SignIdentity (必须)
    签名用的身份信息,可以在命令控制台终端输入 security find-identity ,列出 KeyChain Access 中的所有 identities,然后选择你要用的 identity.
  • NewNameForIPA (可选)
    重签名后输出的 ipa 文件名,不包括 .ipa 后缀。不提供则使用默认名。
  • AppleID & AppleIDPassword (可选)
    Apple ID 和 password 可选。如果提供,可以将重签名好的 ipa 直接上传到 iTunes Connect。

所以在开始签名前,你的工作目录大概是这样,不过 脚本文件随便放哪都行:

重签名 ipa 的工作目录重签名 xcarchive 的工作目录
Working Directoryxcarchive working directory

Shell 脚本重签名

Shell 脚本签名分为 10 个步骤,10 个步骤需要按照先后顺序执行, 你也可以在直接查看完整的 shell 脚本脚本要求所有路径中不能有空格,所以请注意您 App 中的所有文件或文件夹名都不能带空格。

1. 读取重签名参数配置文件

执行脚本后,终端会提示你将重签名用的参数配置文件拖到控制台(这样就可以得到文件的绝对路径)。点我获取参数配置的样例文件

Read resigning params

2. 准备需要重签名的 app

脚本支持对 ipa, xcarchive 两种包类型进行重签名。如果是 ipa,我们需要先将其解压;如果是 xcarchive,我们需要自己创建 IPA 的内容目录结构,并将 xcarchive 里面的 app bundle 拷贝到创建的 Payload 目录中去。如果 Xcode 工程开启 EMBEDDED_CONTENT_CONTAINS_SWIFT,我们还需要将 SwiftSupport 放入 IPA

最后 IPA 中的内容就如下图所示(SwiftSupport 需视实际情况来决定):

App Contents

3. 删除原有的 _CodeSignature 目录

先递归查找 app bundle 中所有的 _CodeSignature 目录,然后将它们逐一删除。

oldSignatures=`find $app_contents_root_path -name "_CodeSignature"`

for signature in $oldSignatures; do
  rm -rf $signature
done

 

4. 生成 entitlements.plist

entitlements.plist 在签名 app bundle 时需要使用,我们可以从提供的 Provisioning Profile 中提取出相关信息,并生成 entitlements.plist

# 从 Provisioning Profile 中提取出来的 entitlements 信息存储路径
entitlements_plist_path="${root_working_dir_path}/entitlements.plist"

# 将 *.mobileprovision 文件中的信息输出到一个临时plist
security cms -D -i $new_profile_path > tempProfile.plist

# 从临时plist中提取出 entitlements 信息并写入 entitlements.plist
/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' tempProfile.plist > $entitlements_plist_path

5. 替换 Provisioning Profile

将新的 Provisioning Profile 拷贝到 app bundle 中,替换原有的 Provisioning Profile

cp $new_profile_path $app_profile_path

6. 替换 Bundle ID

新的 bundle id,我们可以从之前生成的 entitlements.plist 中的 application-identifier 提取,但它包含了 Team ID, 需要将其移除。然后用新的 bundle id 替换 app bundle 中的 Info.plist 中的 bundle id

# E6ABDGA.com.company.appresignature.test
local app_identifier=`/usr/libexec/PlistBuddy -c "Print :application-identifier" $entitlements_plist_path`

# https://stackoverflow.com/questions/10586153/split-string-into-an-array-in-bash
IFS='.' read -r -a components <<< "${app_identifier}"

# Remove `E6ABDGA`: https://askubuntu.com/questions/435996/how-can-i-remove-an-entry-from-a-list-in-a-shells-script
unset components[0]
new_bundle_id=`joinStringComponents "." "${components[@]}"`

# Replace the bundle id in Info.plist with new bundle id.
plutil -replace CFBundleIdentifier -string $new_bundle_id $app_infoplist_path

7. 对内嵌 Frameworks 签名

万事俱备只欠东风,我们现在正式开始签名。首先找出 app bundle 中的内嵌 Frameworks 和动态库,然后使用签名身份逐一进行签名。

local frameworks=`find $app_frameworks_path -name "*.framework" -o -name "*.dylib"`

for framework in $frameworks; do
  codesign -f -s "${sign_identity}" $framework
done

8. 对 App Bundle 签名

对 app bundle 签名除了需要签名身份, 还需要使用 entitlements.plist

codesign -f -s "${sign_identity}" --entitlements "${entitlements_plist_path}" $app_bundle_path

 

9. 验证签名

到目前为止,app 的签名工作已完成,现在我们来验证签名是否有效:

#https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html#//apple_ref/doc/uid/TP40005929-CH4-SW9
codesign --verify --deep --strict --verbose=2 $app_bundle_path

 

如果验证失败,控制台会输出详细的错误。如果验证成功,控制台会输出:

/path/to/Test.app: valid on disk
/path/to/Test.app: satisfies its Designated Requirement

 

10. 制作已重签名的 IPA

签名验证也成功了,最后我们将所有的 App 内容重新压缩成 .ipa 文件,然后输出到指定目录(ResignedIPAs目录)。在压缩文件时,注意要忽略 .DS_Store

local root_content_names=`ls $app_contents_root_path`
local contents_will_zipped=""

# 列出 app contents 根目录下的所有内容
for name in $root_content_names; do
  contents_will_zipped+="$name "
done

cd $app_contents_root_path
zip -qr $new_ipa_name $contents_will_zipped -x "*.DS_Store"
mv $new_ipa_name $ipa_output_directory

 

11. 上传至 iTunes Connect (可选)

这一步为可选的,我们也可以将重签名的 ipa 直接通过命令行上传至 iTunes Connect。如果在重签名参数配置文件中有填写 Apple ID 和 Apple ID Password,Shell 脚本将执行这一步。

xcrun altool --upload-app -f $reigned_ipa_path -t iOS -u $apple_id -p $apple_id_password

 

常见错误解决

常见的签名错误你都可以在这两个地方找到 Troubleshooting Failed Signature Verification ,Entitlements Troubleshooting

脚本及参数配置文件获取

以下是完整的重签名 shell 脚本和参数配置文件:
ios-app-resigning.sh
resigning-params-configuration.plist
写了这么久,总算完成了。可以好好休息下了!🍹

参考资料

Asymmetric Cryptography
Code Signing Guide
Cryptographic Services Guide
Software Security Overview
Technical Note TN2318: Troubleshooting Failed Signature Verification
Technical Note TN2415: Entitlements Troubleshooting

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注