iOS App 签名的原理
-
代码签名的概念
代码签名(Code Signing)是对可执行文件或脚本进行数字签名以确认软件作者及保证软件在签名后未被修改或损坏的措施
-
iOS App 签名的目的
在 iOS 出来之前,在主流操作系统(macOS / Windows / Linux)上开发和运行软件是不需要进行代码签名的,软件随便从哪里下载都能安装和运行,导致平台对第三方软件难以控制,盗版流行
苹果希望解决上述问题,在 iOS 平台对第三方 App 有绝对的控制权:一定要保证每一个安装到 iOS 上的 App 都是经过苹果官方允许的。怎样保证呢?就是通过代码签名机制
-
最简单的 iOS App 签名(AppStore 例子)
要保证每一个安装到 iOS 上的 App 都是经过苹果官方允许的,最简单直接的方式就是:
苹果官方生成一对非对称加密的公私钥:公钥A 内置在 iOS 里,私钥A 由苹果后台保存
当开发者上传 App 到 AppStore 时,苹果后台用 私钥A 对 App 的数据进行签名
当 iOS 系统下载 App 时,用内置的 公钥A 验证 App 的签名
若签名正确,则这个 App 肯定是经过苹果后台认证过的,并且没有被修改
这也就达到了苹果的需求:保证 iOS 安装的每一个 App 都是经过苹果官方允许的
如果 iOS 设备安装 App 只有从 AppStore 下载这一种方式的话,这件事就圆满解决了,没有任何复杂的东西,只用一个数字签名,就能非常简单地解决问题但实际上 iOS 设备安装一个 App,除了从 AppStore 下载外,还有以下 3 种安装方式:
- 开发 App 时可以直接把开发中的 App 安装到手机上进行真机调试
- In-House 分发通道(企业内部分发通道),可以直接安装企业证书签名后的 App
- AD-Hoc 分发通道(程序发布测试通道),相当于企业分发通道的限制版,限制安装设备数量,较少使用
苹果要对用这 3 种方式安装到 iOS 设备上的 App 进行控制,就有了新的需求,无法像上面这样简单了
-
新的需求:iOS 设备需要支持真机调试
上面讲到, iOS 设备安装一个 App,除了从 AppStore 下载外,还有其他的 3 种安装方式。以下详细剖析苹果为支持第一种安装方式所采取的 iOS App 签名流程(第一种安装方式即: 开发 App 时可以直接把开发中的 App 安装到手机上进行真机调试)
苹果如果要做到:在把控 iOS 平台 App 的同时,又要比较方便地支持开发者进行真机调试。需要解决下面两点问题:
- 真机调试方便性问题:App 真机调试的安装包不需要传到苹果服务器进行签名,可以直接安装到 iOS 设备上。试想,开发过程中 App 的改动非常的频繁,如果每编译一次 App 到 iOS 设备前,都需要将安装包先传到苹果服务器进行 App 签名,这显然是不能接受的
- 真机调试授权问题:为了方便开发者进行真机调试,苹果会授予开发者安装 App 到 iOS 设备的权限。但是同时,苹果必须对开发者的 App 安装权限进行控制,包括:
2.1 开发者需要经过苹果官方允许才可以使用该权限安装 App 到 iOS 设备上
2.2 开发者不能滥用苹果给予的权限,导致非真机调试的 App 也能被安装到其他 iOS 设备上
为了解决上面的两点问题,iOS App 签名的复杂度也就开始增加了。苹果这里给出的方案是使用双层签名
-
解决真机调试方便性问题
为了方便开发者进行真机调试,苹果授予开发者安装 App 到 iOS 设备的权限。因此,当开发者安装 App 到 iOS 设备时,苹果需要验证 App 安装行为的合法性:
- 在开发者的 macOS 开发机器上生成一对非对称加密的公私钥,这里称为 公钥L 和 私钥L(L:Local)
- 苹果自己有固定的一对非对称加密的公私钥(跟上面 AppStore 例子一样),这里称为 公钥A 和 私钥A(A:Apple)。公钥A 在每个 iOS 设备上,私钥A 在苹果后台
- 开发者把 公钥L 传到苹果后台,苹果后台用保存的 私钥A 去签名 公钥L,得到一份数据,包含了:公钥L 以及 私钥A对公钥L的签名,把这份数据称为证书
- 开发者在进行真机调试时,编译完一个 App 后,用本地的 私钥L 对这个 App 进行签名,同时把 步骤③ 得到的证书一起打包进 App 里,再把 App 安装到 iOS 设备上
- iOS 设备在安装 App 时,iOS 系统取得 App 里面的证书,通过 iOS 系统内置的 公钥A,去验证证书的数字签名是否正确
- iOS 系统验证证书后,确保了 公钥L 是经过苹果官方认证的,再用 公钥L 去验证 App 的签名,这样就间接验证了这个 App 的安装行为是否经过苹果官方允许(注意:这里只验证 App 的安装行为,不验证 App 是否被改动,因为开发阶段 App 的内容总是不断变化的,苹果不需要管)
-
解决真机调试授权问题
上述流程只解决了 真机调试方便性问题,并没有解决真机调试授权问题。即:
如果只有上述流程,那么开发者只要申请到一个证书,就可以安装任意 App 到任意 iOS 设备,这与苹果想管控 iOS 平台 App 的目的背道而驰。苹果为了防止给予开发者的 App 安装权限被滥用,加了下面 2 个限制:- 限制只有在苹果后台注册过的 iOS 设备才可以进行真机调试
- 限制真机调试时的应用签名,只能针对某一个具体的 App
这些限制是怎么加的呢?思考在上述第 ③ 步,苹果后台用 私钥A 签名本地 公钥L 时,实际上除了签名 公钥L,还可以签名无限多的数据,而且这些数据都可以保证是经过苹果官方认证的,不会有被篡改的可能
可以想到:把 允许真机调试的设备 ID 列表 和 App 对应的 AppID 等数据,都在上述第 ③ 步中跟 公钥L 一起组成证书,再用苹果后台的 私钥A 对这个证书进行签名。在最后第 ⑤ 步验证时就可以:- 通过拿到允许真机调试的设备 ID 列表,判断当前设备是否符合安装要求
- 通过拿到 App 对应的 AppID,判断当前 App 是否符合安装要求
根据数字签名的原理,只要证书的数字签名验证通过,就可以保证第 ⑤ 步这里的:设备 IDs、AppID、公钥L,都是经过苹果官方认证过的,没有被修改。苹果也就能限制可以真机调试的 设备 和 App,避免给予开发者的 App 安装权限被滥用
-
iOS App 签名 最终流程
到这里这个证书的内容已经变得很复杂了,有很多额外的信息。实际上除了 设备 ID / AppID 外,还有其他信息也需要在这里用苹果后台的 私钥A 去签名,像 App 里:iCloud / push / 后台运行 等权限,苹果都想控制,它们也需要通过签名去授权(苹果把这些权限开关统一称为 Entitlements)
实际上一个证书(Certificate)本来就有规定的格式规范,像上面那样把各种额外信息都塞入证书里是不合适的,于是苹果另外搞了个东西,叫 Provisioning Profile 。一个 Provisioning Profile 里就包含了:证书 以及 上述提到的所有额外信息,以及所有信息的签名
所以整个 iOS App 签名 的流程稍微变一下,就变成这样了:
因为步骤有所变动,这里重新再列一遍整个流程:- 在开发者的 macOS 开发机器上生成一对非对称加密的公私钥,这里称为 公钥L 和 私钥L(L:Local)
- 苹果自己有固定的一对非对称加密的公私钥(跟上面 AppStore 例子一样),这里称为 公钥A 和 私钥A(A:Apple)。公钥A 在每个 iOS 设备上,私钥A 在苹果后台
- 开发者把 公钥L 传到苹果后台,苹果后台用保存的 私钥A 去签名 公钥L,得到一份数据,包含了:公钥L 以及 私钥A对公钥L的签名,把这份数据称为证书
- 开发者在苹果后台申请 AppID,配置好允许真机调试的设备 ID 列表和 App 可使用的权限,再加上第 ③ 步的证书,组成的数据用 私钥A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机
- 开发者在进行真机调试时,编译完一个 App 后,用本地的 私钥L 对这个 App 进行签名,同时把 步骤④ 得到的 Provisioning Profile 文件一起打包进 App 里,再把 App 安装到 iOS 设备上(在打包时,Provisioning Profile 文件会被重新命名为 embedded.mobileprovision)
- iOS 设备在安装 App 时,iOS 系统取得 App 里面的 embedded.mobileprovision 文件,通过 iOS 系统内置 的公钥A,去验证 embedded.mobileprovision 文件的数字签名是否正确,同时 embedded.mobileprovision 文件里面的证书签名也会再验证一遍
- iOS 系统验证 embedded.mobileprovision 文件后,确保了 embedded.mobileprovision 文件里面的所有数据都是经过苹果授权, 就可以取出里面的数据进行以下验证:
7.1 使用 公钥L 去验证 App 的签名
7.2 使用 设备 ID 列表 去验证 iOS 设备是否符合调试要求
7.3 验证当前 AppID 是否与开发者在苹果后台申请的 AppID 一致
7.4 验证当前 App 的权限开关列表是否与开发者在苹果后台申请的权限开关列表 一致
7.5 …(除了上述主要的几点外,还会验证证书有效期等内容)
iOS App 签名,从证书申请到签名验证,苹果最终采用的流程大致是这样。还有一些细节像证书有效期 / 证书类型等就不细说了
-
iOS App 签名 最终流程 所对应的实际操作
上面的 iOS App 签名 最终流程对应到实际开发中的具体操作是这样的:
- 第 ① 步对应的是钥匙串访问(Keychain Access)里的 -从证书颁发机构请求证书-,这里会在本地生成一对非对称加密的公私钥,导出的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里
- 第 ② 步由苹果处理,不用管
- 第 ③ 步对应把 CertificateSigningRequest 文件传到苹果后台生成证书,并下载到本地
这时本地有两个证书:一个是第 ① 步生成的(私钥+公钥),一个是这里下载回来的(经苹果签名的公钥)
Keychain Access 会把这两个证书关联起来,因为它们的公私钥是对应的
在 XCode 中选择下载回来的证书时,实际上会找到 Keychain Access 里该证书对应的私钥去签名
这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译和签名这个 App,就把私钥导出给其他 Mac 用,在 Keychain Access 里导出私钥,默认会存成 .p12 文件,其他 Mac 打开 .p12 文件后就导入了这个私钥 - 第 ④ 步都是在苹果开发者网站上操作:选择证书列表、配置 AppID 和 权限开关、选择设备列表,最后生成并下载 Provisioning Profile 文件
- 第 ⑤ 步 XCode 会通过第 ③ 步下载回来的证书(存着公钥),在本地找到对应的私钥(第 ① 步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件重命名为 embedded.mobileprovision 一起打包进 App
这里对 App 的签名数据保存分两部分:
5.1 Mach-O 可执行文件会把签名直接写入这个文件里
5.2 其他资源文件的签名则会保存在 _CodeSignature 目录下 - 打包和验证过程由 XCode 和 iOS 系统自动完成
- 打包和验证过程由 XCode 和 iOS 系统自动完成
-
iOS 设备安装 App 的方式总结
① 开发 App 时可以直接把开发中的 App 安装到手机上进行真机调试,其签名和验证流程,如上所述
② In-House 分发通道(企业内部分发通道),可以直接安装企业证书签名后的 App。其签名和验证流程 与 真机调试的签名和验证流程 差不多,只是企业证书签名的 App 不限制安装设备的数量。另外,企业证书签名的 App 在安装时需要用户在 iOS 系统设置中手动点击信任这个企业证书才能通过 iOS 系统的验证
③ AD-Hoc 分发通道(程序发布测试通道),相当于企业分发通道的限制版,限制安装设备的数量(100 台)。其签名和验证流程 与 真机调试的签名和验证流程 差不多,超级签名技术基于此分发通道
④ AppStore 下载(主要方式)
AppStore 中 App 的签名和验证流程与上面的 3 种方式有些不一样
AppStore 中 App 的签名和验证流程请参考本文开头 最简单的 iOS App 签名(AppStore 例子) 中的介绍(实际上苹果确实是这么做的)
如果去 AppStore 下载 App 的安装包,会发现它里面是没有 embedded.mobileprovision 文件的,也就是 AppStore 下载的 App 它安装和启动的流程是不依赖 embedded.mobileprovision 文件的
据猜测,因为上传到 AppStore 的 IPA 包苹果会重新对包的内容进行加密,因为加密后 IPA 包的内容发生改变,所以原来的本地私钥签名就失效了。因此,需要对 IPA 包重新签名。而且,上架到 AppStore 的包,苹果也并不打算控制它的有效期,不需要内置一个 embedded.mobileprovision 去做校验。所以,直接在苹果后台用 私钥A 对 App 进行重新签名,iOS 安装时用本地内置的 公钥A 去验证 App 签名就可以了
那为什么发布 AppStore 的包还是要跟开发版一样搞各种证书和 Provisioning Profile 呢?猜测是因为苹果想做统一管理,Provisioning Profile 里包含了 权限控制,AppID 的检验等,苹果不想在 IPA 包上传到 AppStore 时重新用另一种协议做一遍这些验证,于是就统一把这部分放在 Provisioning Profile 里,上传 AppStore 时只要用同样的流程验证这个 Provisioning Profile 是否合法就可以了
所以 App 上传到 AppStore 后,就跟 发布证书 、Provisioning Profile 都没有关系了,无论 发布证书、Provisioning Profile 是否过期或被废除,都不会影响用户下载和安装已经在 AppStore 上架的 IPA 包 -
一些疑问
-
关于企业证书
Question:企业证书的签名因为限制少,在国内被广泛用于测试和盗版:
① fir.im / 蒲公英等测试平台 App 都是通过企业证书分发
② 国内一些第三方应用市场像 PP 助手、爱思助手,一部分安装手段也是通过对 App 使用企业证书重签名
通过企业证书签名安装的 App,启动时都会验证企业证书的有效期,并且会不定期请求苹果服务器查看证书是否被吊销,若企业证书已过期或被吊销,则会无法启动 App
对于这种第三方应用市场的盗版安装手段,苹果想打击只能是一个个吊销企业证书,并没有太好的办法
这里我的疑问是,苹果做了那么多签名和验证机制去把控和限制在 iOS 设备安装的 App,为什么又要出这样一个限制很少的方式让盗版钻空子呢?若 App 真的是因为企业用途不适合上架 AppStore,也完全可以在 AppStore 中单独开辟一个的私密版块,供企业用户安装。这样做的话,App 还是通过 AppStore 去安装,就不会有这个问题了
Answer:苹果的企业发布通道可以构建出绝对的 私有网络下载和使用环境,避免一些极其机密内容在公网传播 -
关于 AppStore 对 IPA 包的加密
Question:开发者把 IPA 包上传到 AppStore 后,苹果会对 IPA 包进行加密,导致 App 体积增大不少。但是,这个加密实际上没有多大作用,只是让激活成功教程 App 要多做一个步骤罢了:运行 App 去内存 dump 出真正的可执行文件。无论怎样加密 IPA 包,都可以用这种方式拿出加密前的可执行文件。所以 AppStore 为什么要对 IPA 包做这样的加密呢?想不到有什么好处
Answer:AppStore 对 IPA 包加壳是为了防止静态分析,否则无需越狱手机即可反汇编 IPA 包的源码 -
关于简化 iOS App 签名流程
Question:iOS App 签名的流程很绕很复杂,经常出现各种问题:
① 有 Provisioning Profile 文件,但是执行代码签名时,证书又对不上
② 本地证书中有公钥但没对应的私钥
③ …
以上这些问题,在不理解 iOS App 签名原理的情况下很容易被绕晕。
我的疑问是:这里为什么不能简化 iOS App 签名的流程呢?
还是以开发证书为例,为什么一定要用本地 Mac 开发机去生成签名用的公私钥对呢?
苹果要的只是开发者对于 App 的签名,私钥不一定是要本地生成的,苹果也可以自己生成一对公私钥给开发者,(比如)放在 Provisioning Profile 里,开发者用里面的私钥去签名 App 就行了,这样就不会有 CertificateSigningRequest 和 p12 的概念,并且跟本地的 Keychain Access 没有关系,也不需要关心证书,只要有 Provisioning Profile 文件就能签名,流程会减少,易用性会提高很多,同时苹果想要的控制一点都不会少,也没有什么安全问题,为什么不这样设计呢?
能想到的一个原因是 Provisioning Profile 文件在非 AppStore 安装时会打包到安装包里面,第三方拿到这个 Provisioning Profile 文件就能直接使用里面的私钥来给他自己的 App 签名了。但这种问题也挺好解决,只需要打包时去掉 Provisioning Profile 文件里的私钥就行了,所以仍不明白为什么这样设计
Provisioning Profile 详解
-
Provisioning Profile 简介
Provisioning Profile:供应配置文件 / 描述文件,简称 PP 文件。开发阶段的供应配置文件包含:
① AppID
② 权限列表
③ 设备列表(发布阶段的 Provisioning Profile 不包含此项)
④ 证书列表
Provisioning Profile 决定了哪些证书可以用来签署哪个应用程序,该应用程序可以在哪些设备上进行调试并获得 iOS 系统的哪些服务
Provisioning Profile 在 App 打包时将嵌入到 IPA 包里,安装 App 时,Provisioning Profile 文件将会被拷贝到 iOS 设备中,并重命名为 embedded.mobileprovision。iOS 设备通过 Provisioning Profile 来认证真机调试的合法性 -
Provisioning Profile 结构
XCode 将全部 Provisioning Profile 文件(包括:开发者手动创建并下载安装的 和 XCode自动创建)都放在目录 ~/Library/MobileDevice/Provisioning Profiles 下:
使用命令行查看 Provisioning Profile 文件的内容:// 进入 XCode 存放 Provisioning Profile 文件的目录 cd '/Users/Airths/Library/MobileDevice/Provisioning Profiles' // 列出目录下所有的 Provisioning Profile 文件 ls 15955a3c-fc9e-414f-8e84-66c3b21db90a.mobileprovision 5ee31646-34f5-44e8-a7e0-b278ff9cbffb.mobileprovision d7b8abc6-8ec5-48f1-b906-310b670eec67.mobileprovision 23b89845-f3e0-4d8d-ae7a-0956de1e19c2.mobileprovision c5be1e26-756f-4190-8a5b-b6486f802731.mobileprovision 583626ca-72ab-4274-9b95-9b0c654014d2.mobileprovision d1032aa3-fe86-4ed2-8f22-b9feb4243220.mobileprovision // 查看名为 23b89845-f3e0-4d8d-ae7a-0956de1e19c2.mobileprovision 的 Provisioning Profile 的内容 // 注意: // 通过命令行 security cms -D -i 只能查看到 Provisioning Profile 中字段的内容 // 通过命令行 security cms -D -i 不能查看到苹果官方关于 Provisioning Profile 文件的签名信息 // 查看不到 Provisioning Profile 文件的签名信息,并不能代表 Provisioning Profile 文件没有被签名!!! security cms -D -i 23b89845-f3e0-4d8d-ae7a-0956de1e19c2.mobileprovision <?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>AppIDName</key> <string>use for test</string> <key>ApplicationIdentifierPrefix</key> <array> <string>8H77R3JUK7</string> </array> <key>CreationDate</key> <date>2020-07-21T07:48:49Z</date> <key>Platform</key> <array> <string>iOS</string> </array> <key>IsXcodeManaged</key> <false/> <key>DeveloperCertificates</key> <array> <data>MIIFmjCCBIKgAwIBAgIITH/M74y6X5wwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAwNzIxMDcyMDE3WhcNMjEwNzIxMDcyMDE3WjCBjTEaMBgGCgmSJomT8ixkAQEMCkUzQ0M5ODk2OFMxNTAzBgNVBAMMLGlQaG9uZSBEZXZlbG9wZXI6IGNoYW9nZW4gaHVhbmcgKFZHUzk1UVE3NzQpMRMwEQYDVQQLDAo4SDc3UjNKVUs3MRYwFAYDVQQKDA1jaGFvZ2VuIGh1YW5nMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALRDciLebLZIqyCGx9sZldswpszSE0Idm2MpVJTk3x3wyuJI7VaAYnJQtEPO44oJQWpH5JKrHhPwTnd6eu4rbWNbLBBoJEPX8uQFlSPSVvgzMeadk+Zbq/vaWvrDohz5hUUiCuF3o5dsHzOMFbzyKEgVFa4j3JnQgVYF9gxLYwOHh6bot1FJsG7jiYJQCNvsQ2KpxBZYHvhnDgQDjSBgF8cF6VprjCFEJ9OuVaxIa1uYyHCnldU5v0ap3ZT8HFIIVxSUUjvfFwzDMD4qQktOQvmjD9fAnKxyhYWrC8EJxSitlukk44SuJzJlKZYKhH+w/h0hZdVEbQ6tmqBk4hHLTvsCAwEAAaOCAfEwggHtMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUiCcXCam2GGCL7Ou69kdZxVJUo7cwPwYIKwYBBQUHAQEEMzAxMC8GCCsGAQUFBzABhiNodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHIwMTCCAR0GA1UdIASCARQwggEQMIIBDAYJKoZIhvdjZAUBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFBb6NlTn+JdhBzTbLHYk9i4/NK8UMA4GA1UdDwEB/wQEAwIHgDATBgoqhkiG92NkBgECAQH/BAIFADANBgkqhkiG9w0BAQsFAAOCAQEAJZ7YPWocRRYY47Vi+vfvkmhvpk1sRhfnyHUe7aBtFv/lRMP/QQO57Y/+FdX/buBBJZUfIUdn4n0wtOlMHW9wc2E1dePf1f5KwN/tk6n65Far4RIZDMAA0jMHOy41lGVbN5/tcO4ll+CSsXwLIx+yEHxETg86AU9+7UQi3vCHjts4zrYdR5YTWdohyMlU3VPYxEtHHxjkW2dBnox6fl2VxydQt4evUATAz++iRpIXTTuzqmX6kdwCA8QObNKvLwiPrkbXmJtrk13uIqmbpEbAqlCP6qCQbrqrOvoXavs2jDJIpATPA5EfR/QIgp/xrixcd29xzKcBTCKR8SHFnuDCOg==</data> <data>MIIFsDCCBJigAwIBAgIIKcdVQxqRSqQwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAwNzIxMDcyMzQ2WhcNMjEwNzIxMDcyMzQ2WjCBjjEaMBgGCgmSJomT8ixkAQEMCkUzQ0M5ODk2OFMxNjA0BgNVBAMMLUFwcGxlIERldmVsb3BtZW50OiBjaGFvZ2VuIGh1YW5nIChWR1M5NVFRNzc0KTETMBEGA1UECwwKOEg3N1IzSlVLNzEWMBQGA1UECgwNY2hhb2dlbiBodWFuZzELMAkGA1UEBhMCQ04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUAKB03flYm8IkYYmMKV0+pSvvqHW4WaD8YyrhWmecAdP/NfHzASK8StNpLBS5rWlzcH0EpfaBq8kLEJJgXLuZPGykiZFT0KAz6yu2xQhjIYvbgSrMmrL9A7R7vTq4FRKufDnnD1VIfgY1u6lAurkH2iKdxB3VVUyR+NgjVuIrRIO+4RPK+UFkTV7/ghfrXag/G1vi1MPhhivZdHVaHLZ4zWs8YuxVu6BwwPcmmah83u10rvwzxMbfXYph1U/oiwbvqkJmvzeXK5Q2q2Izb27NX9MpUpiXo4+V6F8Zu2slNHQbkcjKxyBHmGrr5Sw6HEFVyqyye/BlUF1+0NZD5qWPAgMBAAGjggIGMIICAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MD8GCCsGAQUFBwEBBDMwMTAvBggrBgEFBQcwAYYjaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy13d2RyMTkwggEdBgNVHSAEggEUMIIBEDCCAQwGCSqGSIb3Y2QFATCB/jCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA2BggrBgEFBQcCARYqaHR0cDovL3d3dy5hcHBsZS5jb20vY2VydGlmaWNhdGVhdXRob3JpdHkvMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSEOn1/khXQOwBEDxlTONBAwicHODAOBgNVHQ8BAf8EBAMCB4AwEwYKKoZIhvdjZAYBAgEB/wQCBQAwEwYKKoZIhvdjZAYBDAEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBADaLZ02AcpUsS6PZ8XwrDQfv9D56gPe0+/cJaTXn8SGm0S3UfLZtAtDLJ0cXRVBbqjf1UcM7cFvbc2Luc2QiZTk4vyvANy1bRif64gVtVo80NxV/LeEpmWm64hl7291XndRbhB61Eh5xT8Ux/ThLxpx0ycDCEdn+JkFVHDvhqtRj2+eoGBb5V78xlujwGIMbhVjoW9sL1xfJoNNuihtAgcIwI6AUJO2gmvuncneApjsN5VoaPJ2HMIvC3cRG1cOIv2UIdCz40g0JmmGnML8enin03q5M5A/a8XciWCRK2Krgur29wkD4BWgs72Ndy+CU4yPc+kO4Cf03k1OFAggVmtk=</data> </array> <key>Entitlements</key> <dict> <key>application-identifier</key> <string>8H77R3JUK7.com.hcg.test</string> <key>keychain-access-groups</key> <array> <string>8H77R3JUK7.*</string> <string>com.apple.token</string> </array> <key>get-task-allow</key> <true/> <key>com.apple.developer.team-identifier</key> <string>8H77R3JUK7</string> </dict> <key>ExpirationDate</key> <date>2021-07-21T07:48:49Z</date> <key>Name</key> <string>iOS App Development For Test</string> <key>ProvisionedDevices</key> <array> <string>00008020-0013459E1129002E</string> <string>8f341d167941407de47dbdaefec8f7d23de0f7f8</string> </array> <key>TeamIdentifier</key> <array> <string>8H77R3JUK7</string> </array> <key>TeamName</key> <string>chaogen huang</string> <key>TimeToLive</key> <integer>365</integer> <key>UUID</key> <string>23b89845-f3e0-4d8d-ae7a-0956de1e19c2</string> <key>Version</key> <integer>1</integer> </dict> </plist>%
可以看到 Provisioning Profile 是一个 XML 格式的 PList 文件,其中各个字段的含义如下:
- AppIDName:关于 AppID 的描述(在开发者账号中创建 AppID 时填入的 AppID Description 字段)
- ApplicationIdentifierPrefix:AppID 前缀,即 TeamID
- CreationDate:本 PP 文件的创建时间
- Platform:本 PP 文件适用的平台列表
- IsXcodeManaged:本 PP 文件是否是 XCode 自动管理
- DeveloperCertificates:(开发阶段)用于代码签名的证书列表(证书中不包含私钥)
- Entitlements:本 App 拥有的权限列表
- ExpirationDate:本 PP 文件的过期时间
- Name:本 PP 文件的名称(在开发者账号中创建 PP 文件时填入的 Provisioning Profile Name 字段)
- ProvisionedDevices:本 App 可真机调试的设备列表
- TeamIdentifier:TeamID 列表
- TeamName:团队名称,即开发者账号名称
- TimeToLive:有效时长(单位:天)
- UUID:本 PP 文件的唯一标识符(XCode 通过该标识符管理 PP 文件)
- Version:本 PP 文件的版本
权限列表 Entitlements 默认的字段有:
- beta-reports-active:β 版日志收集
- application-identifier:AppID(TeamID + BundleID)
- keychain-access-groups:共享钥匙串访问分组名称
- get-task-allow:是否允许调试
- com.apple.developer.team-identifier:TeamID
注意:
一般情况下,XCode 会根据:- 当前 XCode 登录的开发者账号
- 当前应用的 Bundle ID
- 真机调试的 Device ID
- macOS 中存储的开发者证书
自动到苹果开发者网站生成下载 Provisioning Profile,进行使用
开发者可以在 XCode 中,通过 ProjectTarget – Build Settings – Signing – Provisioning Profile 手动设置项目所需要的 Provisioning Profile同样地,如果 macOS 中未存储开发者证书(Certificate),XCode 会自动到苹果开发者网站生成下载相应的证书文件
其他概念(WWDR、csr、cer、p12、AppID、Entitlements)
-
WWDR
WWDR 即 Apple Worldwide Developer Relations Certification Authority(苹果全球开发者关系认证中心)的简称,用于充当 iOS App 签名 体系中的 CA(证书颁发机构)
上面 iOS App 签名的原理 中,存放在Apple后台的私钥A 和 iOS系统内置的公钥A 组成的非对称加密的公私钥对,就由 WWDR 提供
第一次启动 XCode 时,XCode 会在 Keychain Access 中添加 WWDR 的证书
-
csr 文件
csr 即 Certificate Signing Request(证书签名请求)的简称,可以通过 KeyChain Access(钥匙串访问)导出 csr 文件:
钥匙串访问 – 证书助理 – 从证书颁发机构请求证书通过 钥匙串访问 导出的 csr 文件,默认被命名为:CertificateSigningRequest.cerSigning
csr 文件中包含了:KeyChain Access 生成的开发者公钥注意:
在通过 KeyChain Access 导出 csr 文件时,实际上 KeyChain Access 是生成了一对非对称加密的公私钥对,私钥保存在 KeyChain Access 里面,公钥则导出为 csr 文件// csr 文件里面保存的是 Base64 编码后的(开发者的身份信息 + 开发者的公钥) cat CertificateSigningRequest.certSigningRequest -----BEGIN CERTIFICATE REQUEST----- MIICpjCCAY4CAQAwYTEiMCAGCSqGSIb3DQEJARYTMTg4NTA1Mjc0MzFAMTM5LmNv bTEuMCwGA1UEAwwlQXBwbGUgRGlzdHJpYnV0aW9uIDogQWlydGhzIExpb25IZWFy dDELMAkGA1UEBhMCQ04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDS GAha84zWG76qf2rVYUpWq7IRd3ZkcKqczteSP6K64qN5shx63fboQkYSe9LFzDml MnmLCtikRgvIigdApUYqRQNnl/pOKbcsWgsMX3dwsJbeCS4+JF0aqQzGGXnqzqPn bK4HQAqQ2DCLDUODC84j3gH0o8DidKJwCSoEIUgRi+kJO5hNN0zeaJp9C11DdqNK h0zQKni5Rj2aFvRVyc5TexTusyumYm7mT/uMBTauVODpYYTGXwGE2fj7oSXIk31Z Wyfx1QqRBh2MaD3hAL+mD9tT03NQUumIwFxglNhZQ39GeNMZAPbwvtlYDMJ6StKg TLDlscTrCg4Roe5k4LXFAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAvYVa1cqW We8zJaCDoDbGMXYKXNtukCtcXyEDubXTvw5Ojcy8uVfSPFIrOQF111dihdzN8yFA TW6eiJIOwF7V86YH00o/vM+kDvg1OwElJYTLN1038BMuluP3DsJ/p/kCAwZZc+b4 tw6KKhqUZXvQIVfgL6EhwOGlrn6hhWVo6dpR/AMgCKLerVteEtL5F3/N3HbPwGyh QFw9Y6XTuFwpjZ2HymC4O3Bdh3WdUCzQzNbP9irrUgSLWJ/aLl3yh9Nniqfd8vsa mFhaOn7D/08EkgMwP35NXAccfoi3mlWuj3+neqYg27HJvVob7h1rHw6KQgjDRmBE qyb6PatXzf4Upg== -----END CERTIFICATE REQUEST----- // 通过 OpenSSL 的 ANS1 结构分析工具查看 csr 文件里面的内容 openssl asn1parse -i -in CertificateSigningRequest.certSigningRequest 0:d=0 hl=4 l= 678 cons: SEQUENCE 4:d=1 hl=4 l= 398 cons: SEQUENCE 8:d=2 hl=2 l= 1 prim: INTEGER :00 11:d=2 hl=2 l= 97 cons: SEQUENCE 13:d=3 hl=2 l= 34 cons: SET 15:d=4 hl=2 l= 32 cons: SEQUENCE 17:d=5 hl=2 l= 9 prim: OBJECT :emailAddress 28:d=5 hl=2 l= 19 prim: IA5STRING :18850527431@139.com 49:d=3 hl=2 l= 46 cons: SET 51:d=4 hl=2 l= 44 cons: SEQUENCE 53:d=5 hl=2 l= 3 prim: OBJECT :commonName 58:d=5 hl=2 l= 37 prim: UTF8STRING :Apple Distribution : Airths LionHeart 97:d=3 hl=2 l= 11 cons: SET 99:d=4 hl=2 l= 9 cons: SEQUENCE 101:d=5 hl=2 l= 3 prim: OBJECT :countryName 106:d=5 hl=2 l= 2 prim: PRINTABLESTRING :CN 110:d=2 hl=4 l= 290 cons: SEQUENCE 114:d=3 hl=2 l= 13 cons: SEQUENCE 116:d=4 hl=2 l= 9 prim: OBJECT :rsaEncryption 127:d=4 hl=2 l= 0 prim: NULL 129:d=3 hl=4 l= 271 prim: BIT STRING 404:d=2 hl=2 l= 0 cons: cont [ 0 ] 406:d=1 hl=2 l= 13 cons: SEQUENCE 408:d=2 hl=2 l= 9 prim: OBJECT :sha256WithRSAEncryption 419:d=2 hl=2 l= 0 prim: NULL 421:d=1 hl=4 l= 257 prim: BIT STRING
-
cer 文件
cer 即 Certificate(证书)的简称,开发者上传 csr 文件到苹果开发者账号(即 Apple 后台),csr 文件中的信息(开发者的公钥)+ 苹果开发者账号中的信息(开发者的身份信息) + Apple 后台的信息(WWDR 的身份信息),经过 WWDR 私钥签名后,生成了 cer 文件
从开发者账号中下载的 cer 文件,默认命名为:ios_development.cer / ios_distribution.cer
cer 文件中包含了:经过 WWDR 私钥签名的(开发者的身份信息、WWDR 的身份信息、开发者的公钥、附加扩展信息)
双击安装从开发者账号下载回来的证书文件后,KeyChain Access 会把证书中的信息与保存在本地的开发者私钥关联起来(因为它们本来就是一对非对称加密的公私钥对)
在 XCode 中选择 开发证书/发布证书 去进行应用签名时,实际上 XCode 会找到 KeyChain Access 里该证书对应的开发者私钥去签名
由于种种原因(比如:证书过期、有证书但是没有对应的专用私钥、…),保存在 Keychain Access 中的所有证书并不一定都能用于代码签名
可以通过以下命令查看 Keychain Access 中能用于代码签名的证书列表:// 查看 钥匙串访问.app 中能用于代码签名的证书列表 // 以下输出结果说明当前有 4 个同时有公钥和私钥的可用证书 security find-identity -v -p codesigning 1) 3898326EF62099FAECE6FB216E5994E1E3D6EC3B "iPhone Distribution: chaogen huang (8H77R3JUK7)" 2) 22E6D19768101973762A6D40657CBBD7DEEFA36E "iPhone Developer: chaogen huang (VGS95QQ774)" 3) 098FD1F90A98DD75B47111B548DEBDBF504F2F46 "Apple Distribution: chaogen huang (8H77R3JUK7)" 4) 3EE3E57F2BEFC0DF9B205935A16F2B5F2890D495 "Apple Development: chaogen huang (VGS95QQ774)" 4 valid identities found
-
p12 文件简介
通过前面的介绍可知:一开始,用于签名 App 的私钥,只有生成它的这台 macOS 计算机有
在进行团队协作开发时,如果其他开发者也需要编译和签名 App,则需要把私钥导出来给其他开发者使用
可以到 KeyChain Access 中,通过右键将 开发者证书 与 对应的专用私钥 导出为 .p12 文件
因为 p12 文件中包含了用于 App 签名的私钥,所以需要输入密码进行保护
其他开发者通过 p12 文件将 开发者证书 与 对应的专用私钥 导入自己的 macOS 计算机
因此,通过 KeyChain Access 导出的 p12 文件中包含:开发者证书 + 对应的专用私钥
注意:
在导出开发者证书与它对应的专用私钥时,除了默认导出为 .p12 格式外,还可以选择导出为 .cer 格式
此时,通过 KeyChain Access 导出的 cer 文件中包含了:开发者证书(公钥) + 对应的专用私钥
需要注意区分的是,通过开发者账号下载的 cer 文件,仅包含 开发者证书(公钥)
-
真机测试所需要的条件
① 开发证书 Development Certificate(开发者公钥 + 开发者私钥)
开发者公钥经 WWDR 私钥签名,用于验证 App 的完整性。开发者私钥用于对 App 进行签名② 供应配置文件 Provisioning Profile(AppID、Entitlements、Devices、Certificates)
Provisioning Profile 经 WWDR 私钥签名,用于验证 App 安装的合法性因此,如果开发团队的其他成员需要进行真机测试,则他需要获得 App 对应的:.p12 文件 + .mobileprovision 文件
-
App ID 简介
Team ID:由苹果官方提供,用于唯一标识一个开发者账号。一个开发者账号对应一个 Team ID,不同的开发者账号的 Team ID 不同。可以在开发者账号的 Membership 中查看到属于该账号的 Team ID
Bundle ID:由开发者提供,用于唯一标识一个 App。一个 App 对应一个 Bundle ID,不同的 App 的 Bundle ID 不同。开发者账号提供两种类型的 Bundle ID :
① Explicit ID(显式 ID),建议使用域名反写样式的字符串(com.domainname.appname),不能包含星号
② Wildcard ID(通配 ID),建议使用域名反写样式的字符串,最后一段包含星号(com.domainname.*)
注意:
① Wildcard ID 的星号部分,在打包时,可以替换成任意字符串。使用同一个 Wildcard ID 的 App,如果最后的星号部分不同,则会打出两个不同的 IPA 包,这两个 IPA 包安装到同一个 iOS 设备上,不会相互覆盖
② Explicit ID 可以使用 iOS 系统提供的所有服务(Capabilities),Wildcard ID 只能使用 iOS 系统提供的部分服务(Capabilities)
③ Explicit ID 适合需要上架到 AppStore 的 IPA 包,Wildcard ID 适合走非官方平台的 IPA 包App ID 由以下 2 部分组成:
① App ID Prefix(App ID 前缀):Team ID,因此,有时候 Team ID 也被称为 App ID 前缀
② App ID Suffix(App ID 后缀):Bundle ID,因此,有时候,Bundle ID 也被称为 App ID 后缀
-
Capabilities && Entitlements
iOS 能为所有 App 提供的所有系统服务的集合叫做 Capabilities(服务列表)
开发者在进行 App 开发时,根据 App 的功能与使用场景,从 Capabilities 中选择需要 iOS 提供的系统服务,组成 Entitlements(授权文件 / 权限列表)
Capabilities 是一个预留模板的概念,Entitlements 则是一个实际使用的概念
Capabilities 与 Entitlements 的关系就像是餐馆菜单与顾客菜单。餐馆(iOS 系统)能提供的所有菜品(系统服务)都列在餐馆菜单(Capabilities )上,顾客(App)根据需求从菜单上选择菜品进行点菜形成顾客菜单(Entitlements)因为 AppID 能唯一标识一个 App,所以在开发者账号中,App 权限的配置,包含于注册 AppID 的步骤中
超级签名原理
-
超级签名简介
目前来说,盗版软件绕过 AppStore 进行线上分发和安装的途径有以下 2 种:
- 使用 In-House 分发通道(企业内部分发通道),即通过企业签名
- 使用 AD-Hoc 分发通道(程序发布测试通道),即通过超级签名
企业签名由于存在诸多便利(App 不需要提交审核就能进行安装、不限制 App 安装的设备数量),被广泛应用于盗版软件、游戏外挂等灰产中。苹果公司意识到了这一点并采取了严格的措施:提高企业开发者账号申请门槛、加大企业证书使用情况的审查力度。这导致现在:企业开发者账号申请困难,使用企业证书签名的 IPA 包会频繁掉签。目前,市面上企业开发者账号的价格,已经被炒到 几十万 RMB,使用企业签名的性价比正在逐渐降低。基于此背景,作为 企业签名 替代方案 的 超级签名,逐渐兴起和成熟。
超级签名的原理,其实就是使用了苹果提供给开发者的 AD-Hoc 分发通道(程序发布测试通道),把 App 安装的目标设备当做开发测试设备进行应用分发和安装。超级签名的特点有:
- 稳定不掉签
- 每个开发者账号最多只能安装 100 台 iOS 设备(使用成本: 688 ¥ / 100 pcs / 1 year)
- 需要在苹果开发者账号上添加设备信息并使用对应的 provisioning profile 对 IPA 包进行重签名后,指定设备才可以安装使用超级签名的 IPA 包
-
超级签名自动化流程简介
基于上述超级签名的特点 ②,如果 超级签名的 IPA 包要实现大面积的分发和安装,需要大量的开发者账号支持(一般是个人账号)。超级签名要实现商用价值,就需要能自动化管理大量的开发者账号
基于上述超级签名的特点 ③,如果 超级签名的 IPA 包要实现大面积的分发和安装,则需要将:获取用户设备信息 → 将用户设备信息添加到开发者账号 → 从开发者账号自动获取证书和 Provisioning Profile 文件 → 对预留的 IPA 包进行重签名 → 用户端安装重签名后的 IPA 包 等流程,实现自动化。
一般通过第三方分发平台实现超级签名流程的自动化:
- 用户 iOS 设备安装第三方分发平台预留的 .mobileconfig 文件,第三方分发平台获取用户的设备信息
- 第三方分发平台通过调用苹果开发者网站的接口,将用户设备信息添加到指定的开发者账号下
- 第三方分发平台通过调用苹果开发者网站的接口,生成并下载此设备信息对应的 provisioning profile 文件,并使用该 provisioning profile 文件对预留的 IPA 包进行重签名
- 第三方分发平台将重签名后的 IPA 包上传到分发服务器,用户通过分发链接下载(一般采用 itms-services 协议)
注意
- iOS的开发者证书分两种:开发证书、发布证书。前者开发时使用,后者发布时使用
- 模拟器调试无需代码签名,真机调试时需开发证书代码签名,发布 App 时需发布证书代码签名
- Provisioning Profile 文件经过了苹果官方签名,无法通过更改 Provisioning Profile 文件来达到:扩充 App 的权限、增加调试设备的数量、修改代码签名使用的证书、修改 App 唯一标识、延长 App 签名的有效时间 等目的
- Entitlements(授权文件)的常见字段如下:
<key>Entitlements</key> <dict> // 00.Beta 版日志收集 <key>beta-reports-active</key> <true/> // 01.获取 WiFi 信息 <key>com.apple.developer.networking.wifi-info</key> <true/> // 02. <key>com.apple.security.application-groups</key> <array></array> // 03.应用内支付 <key>com.apple.developer.in-app-payments</key> <array></array> // 04. <key>com.apple.developer.associated-domains</key> <string>*</string> // 05. <key>com.apple.developer.authentication-services.autofill-credential-provider</key> <true/> // 06. <key>com.apple.developer.ClassKit-environment</key> <array> <string>production</string> </array> // 07.默认的数据保护策略 <key>com.apple.developer.default-data-protection</key> <string>NSFileProtectionComplete</string> // 08.App ID(TeamID + BundleID) <key>application-identifier</key> <string>8H77R3JUK7.com.hcg.capabilities</string> // 09.共享钥匙串访问分组列表 <key>keychain-access-groups</key> <array> <string>8H77R3JUK7.*</string> <string>com.apple.token</string> </array> // 10.是否使用 HealthKit <key>com.apple.developer.healthkit</key> <true/> // 11.是否允许调试 <key>get-task-allow</key> <false/> // 12.使用 HealthKit 时,访问权限列表 <key>com.apple.developer.healthkit.access</key> <array> <string>health-records</string> </array> // 13.开发者的 TeamID <key>com.apple.developer.team-identifier</key> <string>8H77R3JUK7</string> // 14.是否使用 HomeKit <key>com.apple.developer.homekit</key> <true/> // 15.是否可以获取在健康中的医院配置 <key>com.apple.developer.networking.HotspotConfiguration</key> <true/> // 16. <key>com.apple.developer.ubiquity-kvstore-identifier</key> <string>8H77R3JUK7.*</string> // 17.iCloud 服务 <key>com.apple.developer.icloud-services</key> <string>*</string> // 18. <key>com.apple.developer.icloud-container-identifiers</key> <array></array> // 19. <key>com.apple.developer.icloud-container-development-container-identifiers</key> <array></array> // 20. <key>com.apple.developer.ubiquity-container-identifiers</key> <array></array> // 21. <key>inter-app-audio</key> <true/> // 22. <key>com.apple.developer.networking.multipath</key> <true/> // 23. <key>com.apple.developer.networking.networkextension</key> <array> <string>app-proxy-provider</string> <string>content-filter-provider</string> <string>packet-tunnel-provider</string> <string>dns-proxy</string> <string>dns-settings</string> </array> // 24.NFC功能使用配置 <key>com.apple.developer.nfc.readersession.formats</key> <array> <string>NDEF</string> <string>TAG</string> </array> // 25.推送的使用环境 <key>aps-environment</key> <string>production</string> // 26.是否能使用 Siri <key>com.apple.developer.siri</key> <true/> // 27.VPN 相关的 API 的使用权限 <key>com.apple.developer.networking.vpn.api</key> <array> <string>allow-vpn</string> </array> // 28. <key>com.apple.external-accessory.wireless-configuration</key> <true/> // 29. <key>com.apple.developer.pass-type-identifiers</key> <array> <string>8H77R3JUK7.*</string> </array> // 30. <key>com.apple.developer.coremedia.hls.low-latency</key> <true/> // 31. <key>com.apple.developer.devicecheck.appattest-environment</key> <array> <string>development</string> <string>production</string> </array> // 32. <key>com.apple.developer.kernel.extended-virtual-addressing</key> <true/> // 33. <key>com.apple.developer.user-fonts</key> <array> <string>app-usage</string> <string>system-installation</string> </array> // 34. <key>com.apple.developer.associated-domains.mdm-managed</key> <true/> // 35. <key>com.apple.developer.applesignin</key> <array> <string>Default</string> </array> </dict>
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/10929.html