iOS App 签名的原理

0x00 前言

iOS 签名机制比较复杂,涉及到一堆证书,Provisioning Profile,entitlements,CertificateSigningRequest,p12,AppID 等等概念繁多,也很容易出错,下面我们就来看下这些概念到底是什么?是怎么和 iOS App 的签名机制一起工作的?了解这些会有助于理解 iOS 这套复杂的签名机制。

0x01 签名机制的目的

首先要知道苹果为什么要退出这一套把人搞晕的签名机制。众所周知 iOS 是一个封闭的生态体系,苹果希望能取得对平台上的所有应用的绝对控制权。所以 iOS 不会像 PC 上那样,无需任何签名验证即可随意安装任何地方获取到的软件,而这种不受控的软件安装也是导致盗版盛行和生态混乱的元凶。那么苹果要怎样做才能保证每一个安装到 iOS 设备上的 App 都是经过苹果官方许可的呢?这就要靠签名和签名的验证机制来实现。

0x02 非对称加密

Asymmetric Key System

RSA

具体的原理可以参考这两篇文章:RSA 算法原理(一)(二)以及相关的数论知识:欧拉定理费马小定理欧拉函数同余

非对称加密算法是数字签名的基石。它通过使用两份密钥对数据进行加解密操作的。这两个密钥分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。

0x03 数字签名

现在知道了有非对称加密这东西,那什么是数字签名呢?

数字签名的作用是我对某一份数据作个标记,表示我认可了这份数据(签名),然后我再发送给其他人,其他人通过验证签名就能知道这个数据是不是被我认可的,数据有没有被篡改过。

为了保证签名在传输过程中的安全性需要对签名进行加密,基于非对称加密算法,就可以实现上述的方案:

Alt text

  1. 首先用一种算法,算出原始数据的摘要。需满足:
    a. 若原始数据有任何变化,计算出来的摘要值都会变化。
    b. 摘要要够短。
    这里最常用的算法是MD5。
  2. 生成一份非对称加密的公钥和私钥,私钥我自己拿着,公钥公布出去。
  3. 对一份数据,算出摘要后,用私钥加密这个摘要,得到一份加密后的数据,称为原始数据的签名。把它跟原始数据一起发送给用户。
  4. 用户收到数据和签名后,用公钥解密得到摘要。同时用户用同样的算法计算原始数据的摘要,对比这里计算出来的摘要和用公钥解密签名得到的摘要是否相等,若相等则表示这份数据中途没有被篡改过,因为如果篡改过,摘要会变化。

之所以要有第一步计算摘要,是因为非对称加密的原理限制可加密的内容不能太大(不能大于上述 n 的位数,也就是一般不能大于 1024 位 / 2048 位),于是若要对任意大的数据签名,就需要改成对它的特征值签名,效果是一样的。

有了以上的铺垫,现在来看看怎样通过这个数字签名的机制保证每一个安装到 iOS 上的 APP 都是经过苹果允许的。

0x04 最简单的签名方案

200%

我们先来讨论一种最直接的验证方式。那就是,苹果官方自己生成一对公私钥对。将公钥内置在 iOS 系统之中,私钥则由苹果的后台保存。开发者将 App 上传到苹果 AppStore 进行发布的时候,苹果用私钥将 App 的数据进行签名,iOS 系统下载这个 App 的时候,用系统中内置的公钥来验证这个 App 的签名,如果签名正确,就可以认为这个 App 的数据是没有被篡改过的,是经过苹果认证的版本。这样就满足了苹果的要求:保证 iOS 上安装的 App 都是经过苹果官方允许的。

如果 iOS 上的 App 安装渠道只有 AppStore 一个的话,那现在这个方案已经可以满足需求了。但实际上因为除了从 AppStore 下载,我们还可以有其他三种方式安装 App:

  1. 开发 App 时可以直接使用 Xcode 将应用直接编译安装进手机进行调试。
  2. In-House 企业开发者证书内部分发,iOS 设备可以直接下载安装企业 In-House 证书签名后的 App。
  3. Ad-Hoc 相当于开发者分发的限制版,限制安装设备数量,较少用。

所以苹果同样要对用这三种方式安装的 App 进行控制,那么在新的场景之下,方案就无法像上面这样简单了。

0x05 新场景

a. XCode 安装

我们先来看第一个,开发时安装APP,它有两个问题:

  1. 安装包不需要传到苹果服务器,可以直接安装到手机上。如果你编译一个 APP 到手机前要先传到苹果服务器签名,这显然是不能接受的。
  2. 苹果必须对这里的安装有控制权,包括
    a. 经过苹果允许才可以这样安装。
    b. 不能被滥用导致非开发app也能被安装。

为了解决这两个问题,iOS 签名的复杂度也就开始增加了。
苹果的方案是使用双层签名,比较繁琐,流程大概是这样的:

110%

① 首先在你的 Mac 开发机上生成一对公私钥,我们称为公钥L,私钥L(L: Local)。
② 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A (A:Apple)。
把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书
③ 在开发时,App 编译通过之后,用本地的私钥 L 对这个 App 进行签名,同时把第三步得到的证书一起打包进 App 里,安装到手机上。所以 App 的 IPA 安装包中同时包含了 可执行文件,可执行文件的签名以及证书
④ 在安装 App 时,iOS 系统从 App 包中取得证书,通过系统内置的公钥 A,去验证证书的数字签名是否正确。
⑤ 验证证书后确保了公钥 L 是苹果认证过的,没有被篡改,再用公钥 L 去验证 App 的签名,这里就间接验证了这个 App 安装行为是否经过苹果官方允许。(这个过程只验证安装行为,不验证 App 是否被修改,因为开发阶段 APP 内容总是不断变化的,苹果不需要管。)

0x06 进一步研究

上面讲的这一堆流程只解决了上面第一个问题,也就是需要经过苹果允许才可以安装 App,还未解决第二个避免证书被滥用的问题。针对这一点,苹果又添加加了两个限制,一是限制在苹果后台注册过的设备(UDID)才可以安装,二是限制签名只能针对某一个具体的 App 起作用。

那么具体是怎么做的呢?在上面的 ③,苹果用私钥 A 签名我们本地公钥 L 时,实际上除了签名公钥 L,还可以添加他需要的任何数据,这些数据都可以保证 App 是经过苹果官方认证的,不会有被篡改的可能。

120%

可以想到把 允许安装的设备 ID 列表 和 App对应的 AppID 等数据,都在 ③ 这里跟公钥L一起组成证书,再用苹果私钥 A 对这个证书签名。在 ⑤ 的验证时就可以拿到设备 ID(UDID) 列表,判断当前设备是否符合要求。根据数字签名的原理,只要数字签名通过验证,第 5 步这里的UDID 列表 / AppID / 公钥 L 就都是经过苹果认证的,无法被修改,苹果就可以限制可安装的设备和 APP,避免滥用。

0x07 最终方案

现在这个证书已经变得很复杂了,有很多额外信息,实际上除了 UDID 列表 / AppID,还有其他信息也需要在这里用苹果签名,App 里 iCloud / push / App Group / 后台运行 等权限和能力苹果都想控制,苹果称这些权限开关为 Entitlements,它也需要通过签名去授权。

另外,一个“证书”本来就有规定的格式规范,上面我们把各种额外信息塞入证书里是不合适的,其实在真正的生产环境下,苹果也没有这么做,他们另外搞了个叫 Provisioning Profile 的东西,一个 Provisioning Profile 里就包含了证书以及上述提到的所有额外信息,以及所有信息的签名。

所以流程就变成了这样:

Alt text

现在我们来总结一下完整的最终流程

在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。(L: Local)
苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。(A: Apple)

  1. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  2. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  3. 在开发时,编译完一个 App 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  4. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  5. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 App 里的 Entitlements 对应等。

开发者证书从签名到认证最终苹果采用的流程大致是这样。

0x08 概念和操作

  1. 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  1. 第 2 步苹果处理,不用管。
  1. 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  1. 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  1. 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在 _CodeSignature 目录下。
  1. 第 6 – 7 步的打包和验证都是 Xcode 和 iOS 系统自动做的事。

然后,在总结下上面说到的概念:

概念小结

证书:内容是公钥或私钥,由其他机构对其签名组成的数据包。
Entitlements:包含了 App 权限开关列表。
CertificateSigningRequest:本地公钥。
p12:本地私钥,可以导入到其他电脑。
Provisioning Profile:包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

0x09 其他发布方式

  1. In-House, Ad-Hoc

  2. AppStore

前面以开发包为例子说了签名和验证的流程,另外两种方式 In-House 企业签名和 AD-Hoc 流程也是差不多的,只是企业签名不限制安装的设备数,另外需要用户在 iOS 系统设置上手动点击信任这个企业才能通过验证。

而 AppStore 的签名验证方式有些不一样,前面我们说到最简单的签名方式,苹果在后台直接用私钥签名 App 就可以了,实际上苹果确实是这样做的,如果去下载一个 AppStore 的安装包,会发现它里面是没有 embedded.mobileprovision 文件的,也就是它安装和启动的流程是不依赖这个文件,验证流程也就跟上述几种类型不一样了。

据猜测,因为上传到 AppStore 的包苹果会重新对内容加密,原来的本地私钥签名就没有用了,需要重新签名,从 AppStore 下载的包苹果也并不打算控制它的有效期,不需要内置一个 embedded.mobileprovision 去做校验,直接在苹果用后台的私钥重新签名,iOS 安装时用本地公钥验证 App 签名就可以了。

那为什么发布 AppStore 的包还是要跟开发版一样搞各种证书和 Provisioning Profile?猜测因为苹果想做统一管理,Provisioning Profile 里包含一些权限控制,AppID 的检验等,苹果不想在上传 AppStore 包时重新用另一种协议做一遍这些验证,就不如统一把这部分放在 Provisioning Profile 里,上传 AppStore 时只要用同样的流程验证这个 Provisioning Profile 是否合法就可以了。

所以 App 上传到 AppStore 后,就跟你的 证书 / Provisioning Profile 都没有关系了,无论他们是否过期或被废除,都不会影响 AppStore 上的安装包。

上述内容就是 iOS App 签名机制的原理和主流程了,希望能够通过这样简单的梳理让看到这篇文章的 iOS 开发者能够对这个复杂的流程有所认识和了解。