前言
看完下面的内容后,你将对Apple的产物签名机制和流程有初步了解。
同时也能弄清楚iOS应用打包涉及的一些内容,比如:.P12
文件是怎么来的,ExportOptions.plist
的作用,苹果开发者后台下载的Provisioning Profile的作用等等。
这篇译文对于理解iOS编译签名打包工作流程中涉及的签名文件的由来和关联关系很有帮助,可以让你在设计CICD流程时更好地运用这些知识。
本篇译文整合了两篇原文内容,篇幅较长,请慢慢阅读~ 😝
TN3161: 深入代码签名: 证书
了解代码签名如何使用证书来识别代码作者。
概述
TN3125: 深入代码签名: Provisioning Profiles 解释了Apple平台如何使用配置文件来授权执行第三方代码。一个配置文件将五个标准绑定在一起:谁、什么、在哪里、什么时候以及如何。就"谁"而言,TN3125描述了每个配置文件如何包含该配置文件所涵盖的每个开发者的证书。然而,它并没有详细说明证书是什么。本技术说明旨在填补这些细节。
关于代码签名身份的日常管理建议,请参阅发布和开发者账户帮助。
关于本技术说明系列
代码签名是所有Apple平台的基础技术。许多讨论代码签名的文档都集中在解决特定问题上。深入代码签名技术说明系列揭示了代码签名的内部工作原理,让你更好地理解它是如何工作的。有关本系列所有技术说明的列表,请参阅TN3125: 深入代码签名: Provisioning Profiles中的介绍。
深入代码签名技术说明讨论的代码签名细节并不被视为API。代码签名的结构在过去已经多次改变,将来也可能继续改变。不要在你的产品中编码这些信息。在签署代码时,请使用Xcode(所有平台)或codesign工具(仅macOS)。要获取信息或验证代码签名,请使用codesign工具或代码签名服务 API。Apple会更新这些工具以适应代码签名结构的任何变更。
公钥基础设施
要理解证书,你首先必须了解一些关于公钥密码学及其相关公钥基础设施(PKI)的知识。
本节描述的许多Apple特定流程都在Apple PKI页面上有正式文档。
公钥密码学
现代密码学使用两种不同的密钥系统。对称密钥密码学对加密和解密使用相同的密钥。你用一个密钥加密消息,任何拥有该密钥的人都可以解密它。公钥密码学使用巧妙的数学来创建非对称密钥对。你广泛发布你的公钥。任何人都可以使用该公钥加密消息发送给你。你保留私钥并用它来解密消息。只要你保管好私钥,只有你才能读取这些消息。
数字签名
非对称密钥对也允许进行数字签名。你自己保管私钥,并公开你的公钥。如果你用私钥签名一条消息,任何人都可以用你的公钥验证该签名。
对于某些公钥算法,签署消息等同于加密该消息的哈希值,但这并不是普遍适用的。最好将加密和签名视为两个概念上不同的任务。
数字签名是代码签名的核心。可以将你的代码视为一条消息,你对其进行签名,操作系统在执行前进行验证。这种验证有两个步骤:
- 验证签名本身,即自你用私钥签名以来没有发生任何改变。
- 评估你的公钥在此上下文中是否受信任。
第二步使用证书。
数字证书
在现实世界中,证书是由颁发机构对主体某些事实进行认证的文件。例如,在你的出生证明中:
- 颁发机构是你出生地的管理部门
- 主体是你
- 事实包括你的姓名、出生日期、父母等信息
这个系统依赖于事实上现实世界的证书难以伪造:它们使用特殊纸张、特殊印章、蜡封等。
数字证书与实体证书有着相同的目标:颁发机构对主体的某些事实进行认证。但它不能使用特殊纸张来防止伪造。相反,数字证书依赖于公钥密码学。
Apple代码签名使用X.509标准的数字证书。一个X.509证书包含五个部分信息:
- 颁发机构的详细信息
- 主体的详细信息
- 主体的公钥
- 必需的事实,如有效期范围
- 可选的事实,称为扩展
颁发机构使用其私钥对这些信息进行签名,然后将所有内容打包在一起形成证书。
关于X.509证书格式的详细说明,请参阅RFC 5280。
例如,如果你从开发者网站下载代码签名证书,你可以像这样导出.cer文件的内容:
具体分析如下:
- 第7行显示了颁发机构的详细信息,在这里是开发者ID认证机构。
- 第11行显示了主体的详细信息。这个示例显示的是用于直接分发的Mac代码的开发者ID证书。对于代码签名证书,Apple将开发者的Team ID放入主体的组织单位(OU)字段中。
- 第12到13行是主体的公钥。
- 第3到5行和第8到10行是必需的事实,即证书格式版本、序列号和有效期范围。
- 第14到15行是可选的事实。代码签名证书包含多个扩展。其中一些是行业标准扩展,而另一些是Apple特有的。后者在Apple PKI页面上有文档说明。
- 第16到18行是颁发机构的签名。
用简单的话说,这个证书表明"Apple证实该开发者与此公钥相关联,且匹配的私钥可用于签名Mac代码。"这显然是一个简化说明——它没有涉及有效期范围、序列号,甚至Apple最初是如何识别开发者的——但这是一个合理的入门模型。
Apple发布多种不同类型的代码签名证书。完整列表请参见证书类型。
证书通常以两种格式存储:
- DER
- 它直接存储二进制证书。Apple工具和API偏好这种格式。这种格式的文件通常使用.cer或.der扩展名。
- PEM
- 这是二进制证书的文本呈现形式。非Apple工具和库(尤其是OpenSSL)偏好这种格式。这种格式的文件通常使用.pem扩展名。
要在这些格式之间转换,可以使用带x509子命令的openssl命令行工具:
信任链
证书通常会形成一个信任链:验证者使用证书中的颁发者信息来查找颁发者的证书,然后使用其颁发者信息找到链中的下一个证书,依此类推,直到遇到一个根证书(由于政策原因信任的证书)。这个过程可能非常复杂,但对于Apple的代码签名证书来说通常相当简单。要查看应用的证书链,请运行以下命令:
关于构建信任链的标准算法的详细描述,请参见 RFC 5280。
在这个例子中,Developer ID Application叶子证书由Developer ID Certification Authority中间证书颁发,而中间证书由Apple Root CA根证书颁发。这种三级信任链是Apple代码签名的标准模式,其中:
- 叶子证书由Apple开发者网站颁发给开发者。
- 中间证书是用于代码签名的一组非常有限的证书之一,包括Apple Worldwide Developer Relations Certification Authority和Developer ID Certification Authority。Apple在Apple PKI页面上发布这些中间证书。
- 根证书是Apple Root CA。Apple操作系统对此有隐含信任。
不要依赖这个信任链的具体细节;它可能在将来发生变化。
上面示例中的Authority字段显示了链中每个证书的简短摘要。要获取实际的证书,请使用--extract-certificates
选项运行codesign:
这将为链中的每个证书创建一个证书文件,叶子证书使用codesign0,其颁发者使用codesign1,以此类推。因此,要查看叶子证书,请运行以下命令:
数字身份
前一节关于证书的讨论完全集中在公钥上。证书是用于评估公钥信任的公共数据结构,而公钥又用于验证签名。
要签署代码,你需要一个证书和与证书中的公钥匹配的私钥。这种组合称为数字身份,如果用于签署代码,则称为代码签名身份。
由于证书只包含公钥,你不能用它来签署代码。
许多人在表示数字身份时使用"证书"一词。这种行业范围内的混淆延伸到了Apple生态系统。例如,Xcode使用术语"签名证书",Keychain Access使用"我的证书",而Apple Mail使用"个人证书"。
处理数字身份的Apple工具和API通常偏好PKCS#12格式。PKCS#12文件通常使用.p12
扩展名,尽管.pfx
也是常见的替代选择。
要了解更多关于PKCS#12的信息,请阅读RFC 7292。
非Apple工具和库(最著名的是OpenSSL)使用PEM格式处理数字身份。在这种情况下,PEM文件包含两个单独的项:一个用于证书,一个用于私钥。这类文件可以有各种扩展名,但一个常见的是.cer
,这进一步增加了证书和数字身份之间的混淆。
要在PKCS#12和PEM格式之间转换,请使用带pkcs12子命令的openssl命令行工具。
证书签名请求
Apple通过三种不同的方式颁发代码签名证书:
- 手动证书签名请求(CSR)流程
- Xcode的证书管理
- 云管理证书
理解这些颁发流程如何与私钥相关联很重要,因为私钥与证书结合后形成你的代码签名身份。
Developer Account Help描述了手动CSR流程。该流程的一个关键步骤是使用Keychain Access创建CSR。这会做两件事:
- 在你的登录钥匙串中创建一个公钥/私钥对。
- 将公钥包装在CSR结构中,并提示你将其保存为
.certSigningRequest
文件。
然后你将CSR提交给开发者网站,它会颁发一个包含你的公钥的证书。当你下载此证书并将其导入到你的钥匙串时,它会与步骤1中创建的私钥形成一个代码签名身份。
要了解更多关于CSR的一般信息,请阅读RFC 2986。
这个过程有一些关键优势:
- 你的私钥永远不会离开你的Mac。只要你保管好那个密钥,没有人能以你的身份签署代码。
- 你可以选择使用存储在智能卡或其他类型的硬件令牌中的私钥。
然而,它确实有一个值得注意的缺点:很容易忽视你最重要的代码签名资产(你的私钥)藏在你的登录钥匙串中。如果你忽视了这一点,你可能会丢失你的私钥,例如,当你迁移到新Mac时。如果发生这种情况,请按照Developer > Support > Certificates中的建议操作。
Xcode的证书管理遵循与手动CSR流程相同的总体路径。例如,当你使用Synchronizing your code signing identities with Apple Developer Portal中描述的流程创建新的代码签名身份时,Xcode实际上会创建一个CSR,将其提交给开发者网站,下载生成的证书,并将其添加到你的钥匙串中。它很容易使用,但有相同的陷阱:Xcode会自动生成一个私钥并将其存储在你的登录钥匙串中。
当你使用云管理证书时(例如,使用Xcode Cloud构建时),私钥和证书都由Apple的云签名基础设施管理。你无法直接访问它们中的任何一个。
密码消息语法
代码签名在内部使用密码消息语法(CMS)。这完全是一个实现细节,但探索它可能会很有趣。要提取代码签名的CMS结构,用--dump-cms
选项运行codesign:
要了解更多关于CMS的信息,请阅读RFC 5652。
代码签名的PKI操作
代码签名有三个核心操作:
- 签署代码
- 显示代码签名
- 验证代码签名
以下各节解释了这些操作如何与Apple的代码签名基础设施交互。
签署代码
当你签署代码时,你需要使用--sign
子命令向codesign传递代码签名身份的名称。例如:
这个例子使用了codesign命令行工具,但这些概念也适用于Xcode。要查看Xcode如何调用codesign
,请转到Reports导航器,找到你的Build报告,并查看构建记录中的CodeSign步骤。
默认情况下,codesign会搜索所有钥匙串,寻找证书匹配所提供名称的代码签名身份。如果多个身份匹配,codesign会报告歧义。要解决这个问题,可以传入完整名称或传入身份证书的SHA-1哈希值。
codesign的man页详细解释了这个搜索过程。关于man页的一般信息,请参阅读取UNIX手册页。
在搜索代码签名身份时,codesign会检查每个身份证书的某些方面:
- 检查是否可以从证书构建到受信任根证书的信任链。
- 检查当前时间是否在证书的有效范围内。
- 检查证书是否支持代码签名。具体来说,它会在证书的扩展密钥用途扩展中查找代码签名:
要查看所有可用的代码签名身份,运行以下命令:
注意有三个代码签名身份,但其中一个已过期,只剩下两个有效身份。
每个身份旁边的十六进制字符串是其证书的SHA-1哈希值,这是解决名称歧义的好方法。
除非使用--keychain
选项指定钥匙串文件,否则codesign会搜索所有钥匙串,包括数据保护钥匙串。如果你提供了钥匙串文件,codesign只会搜索该钥匙串。
如果你不熟悉数据保护钥匙串,请参阅TN3137: Mac钥匙串API和实现说明。
codesign工具能够找到并使用数据保护钥匙串中的代码签名身份,包括存储在智能卡或其他类型硬件令牌中的身份。然而:
security find-identity
命令只搜索钥匙串文件;它不会显示数据保护钥匙串中的身份。- 如果你想使用存储在数据保护钥匙串中的代码签名身份,不要使用
--keychain
选项指定钥匙串文件,因为这会告诉codesign只搜索该文件。 - 如果你使用Xcode签署代码,请使用Xcode 13或更高版本。早期版本的Xcode仅适用于基于文件的代码签名身份。
macOS内置支持PIV智能卡。第三方开发者可以通过创建CryptoTokenKit应用扩展来添加对其他类型硬件令牌的支持。
一旦codesign确定要使用的代码签名身份,它会在身份证书和受信任锚之间建立信任链。如果无法建立该链,它会失败并显示错误"无法构建到自签名根的链"。这种失败的最常见原因是缺少中间证书。codesign使用的中间证书由Xcode自动安装。如果你不使用Xcode,请从Apple PKI页面下载这些中间证书并自行安装。
codesign工具将完整的信任链添加到代码签名内的CMS结构中。当你在设备上运行代码时,系统使用这个CMS结构中的中间证书来建立其信任链。这意味着用户设备不必安装代码签名中间证书。
显示代码签名
要显示代码签名的信任链,请使用--display
子命令和两个-v
选项运行codesign命令:
每个Authority字段代表信任链中的一个环节。这只是一个简短的摘要。要查看完整的证书,请使用--extract-certificates
选项:
这个功能的一个奇怪之处在于,codesign并不是简单地打印存储在代码签名内CMS结构中的证书列表。相反,它通过对CMS叶子证书执行标准信任评估来从头构建信任链。它使用trust对象来完成这个操作。在大多数情况下,这会产生与CMS结构中匹配的信任链,但这并不能保证一定是这种情况。如果codesign显示的信任链看起来很奇怪(例如,它可能只显示一个Authority字段),请提取CMS结构并查看其证书。
有关如何提取CMS结构的说明,请参阅密码消息语法。
验证代码签名
要验证代码签名,请使用带有--verify
子命令运行codesign命令:
这个命令会静默成功,只设置状态,所以添加几个-v
选项来增加详细程度:
要进行更深入的检查,添加--strict
和--deep
选项:
请谨慎解读这个输出。它并不是说这个代码适合某个特定用途,比如在你的Mac上运行或者安装到你的iPhone上。相反,它表明代码在内部是一致的,即:
- 所有预期的文件都存在。
- 没有额外的文件。
- 没有文件被修改。要了解代码签名如何检查更改,请阅读TN3126: Inside Code Signing: Hashes。
- 叶子证书的基本X.509信任评估成功。
- 代码满足其自身的指定要求(DR)。要了解更多关于代码签名要求的一般信息,以及DR的重要性,请参阅TN3127: Inside Code Signing: Requirements。
要检查代码是否适合特定用途,你需要其他机制:
- 对于要提交到App Store的应用,使用带有
--validate-app
子命令的altool。更多信息请参阅altool的man页面。这使用与Xcode管理器中的验证应用按钮相同的基础设施。 - 对于使用Developer ID代码签名直接分发的Mac应用,使用
syspolicy_check
工具。要了解更多关于这个工具的信息,请参阅其man页面。
非Apple证书
Apple的代码签名支持是在Apple开始向第三方开发者颁发代码签名证书之前创建的。这导致了一些在今天看来很奇怪的行为:
- 在签署代码时,
codesign
不检查代码签名身份的证书是否由Apple颁发。 - 在验证代码时,
codesign
不检查代码是否会被目标平台接受。
从历史上看,使用非Apple颁发证书的代码签名身份来签署代码可能是有意义的。但现在不再是这样了。大多数Apple平台只运行带有Apple颁发证书的代码。唯一的例外是macOS,它可以运行带有其他证书或者根本没有证书的代码。然而,这在实践中并不实用,因为Gatekeeper会阻止带有非Apple证书的代码。
关于Gatekeeper的一般信息,请参阅在Mac上安全地打开应用程序。
App Store 重新签名
当你在 App Store 发布应用时,App Store 会在发布过程中重新签名你的应用。这在 macOS 上很容易确认。假设你有一个叫 MyAppStoreApp 的应用可以从 Mac App Store 下载。安装该应用后运行以下命令显示其代码签名:
叶子证书是"Apple Mac OS Application Signing"。这与你在提交前用于签名应用的代码签名身份的证书不同,后者命名为"Apple Distribution: <团队>"或"3rd Party Mac Developer Application: <团队>",其中"<团队>"标识你的团队。
虽然这个例子来自 macOS,但这种重新签名发生在所有平台的所有 App Store 应用上。
在大多数情况下你不会注意到你的应用已被重新签名,但在几个地方它很重要:
- App Store 应用使用不会过期的凭证签名。相比之下,你的发布证书和配置文件可能过期。但是,这种重新签名意味着你的凭证只需在提交应用时有效即可。
- App Store 必须能够重新签名你的所有代码。如果它看不到特定的代码项,就无法重新签名该代码,代码就无法运行。这个问题的一个常见例子是嵌入在归档文件中的代码,比如 .zip 或 .jar 文件。为了获得最佳结果,请遵循在 Bundle 中放置内容中的规则。
- 试图进行自我篡改检测的应用必须考虑这种重新签名。当 Apple 重新设计 App Store 发布流程时,这类代码经常会失败。为避免此类问题,请使用 DeviceCheck 框架的 App Attest 功能来建立应用的完整性。
- 你在 App Store 提交过程中为发布签名你的应用。这是 App Store 发布签名应用的预期用途。你自己无法运行这样的应用。
关于最后一点,唯一的例外是 macOS。在某些情况下,macOS 能够运行 App Store 发布签名的应用。但是,这不受支持,而且如果应用声明受限权利,目前会失败。有关受限权利的更多信息,请参阅 TN3125: Inside Code Signing: Provisioning Profiles。
因为你无法可靠地运行你的 App Store 发布签名应用,所以你需要其他方式在发布前测试它。一个很好的选择是 TestFlight。TestFlight 有很多不错的功能,但在这个上下文中一个关键优势是它会重新签名你的应用,就像 App Store 一样。
证书过期
所有 X.509 证书都有一个有效日期范围。对于 Apple 代码签名证书,通常是从发布之日起一年,尽管具体有效期因证书类型而异。
对于 App Store 应用,证书过期并不复杂。当你向 App Store 提交应用时,它会检查应用的发布证书当前是否有效。然后 App Store 重新签名你的应用,使其独立于你的发布证书的过期日期。
这种机制仅适用于 App Store 应用,这就提出了一个问题:当你使用 Developer ID 签名直接发布 Mac 软件时会发生什么。安装你产品的人希望它能继续工作,无论你的证书是否过期。Apple 通过在你的代码签名中嵌入安全时间戳来实现这一点。这记录了你的代码签名时间。当你运行代码时,macOS 会检查其 Developer ID 证书在签名时是否有效。
当 Xcode 使用 Developer ID 代码签名身份签名代码时,会自动添加安全时间戳。如果你使用 codesign 签名代码,传入 --timestamp 选项来添加安全时间戳。Apple 公证服务确保所有代码都有安全时间戳,所以如果你做错了,在公证代码时就会知道。有关公证的更多信息,请参阅发布前对 macOS 软件进行公证。
要检查你的代码是否有安全时间戳,查看 --display 子命令的输出。例如:
Timestamp字段代表安全时间戳。
不要混淆Timestamp和Signed Time字段。后者并不由Apple时间戳服务保护。相反,codesign基于你Mac的当前时间设置此字段。你通常在开发签名的代码中看到此字段。例如:
安全时间戳要求有一个重要结果:你必须能够访问互联网才能使用Developer ID代码签名身份签名代码。具体来说,codesign当前使用基于X.509时间戳协议(如RFC 3161所述)与Apple时间戳服务(timestamp.apple.com)通信。如果你在特定网络上无法签名代码,请检查其防火墙是否允许这些连接。
Apple时间戳服务仅供代码签名使用。其名称和行为被视为实现细节。不要发布依赖这些细节的产品。
证书不是唯一会过期的代码签名资产。配置文件也有过期日期。要了解更多相关信息,请参阅TN3125: Inside Code Signing: Provisioning Profiles。
修订历史
- 2024-02-13 做了一个小的编辑更改。
- 2024-02-06 首次发布。
证书签名请求详解
我发现很多人对代码签名请求(CSR)的工作原理感到困惑,这会导致后续出现问题。这篇文章尝试解释这个过程以避免这些问题的发生。
本文介绍了Developer Account Help > Create certificates > Create a certificate signing request中描述的"经典"证书创建流程。
如果你使用Xcode创建证书,或者使用云管理证书,流程会有所不同。
建议在阅读本章节之前,先熟悉前面章节的内容 TN3161: 深入代码签名: 证书
以下是CSR流程的基本概述:
- 你运行钥匙串访问并选择证书助理 > 从证书颁发机构请求证书。
- 你按照Developer Account Help > Create certificates > Create a certificate signing request中描述的工作流程操作。
- 这会做两件事:
- 在你的钥匙串中生成一个公钥/私钥对。要查看这些密钥,运行钥匙串访问并在左侧选择"登录",在顶部选择"密钥"。查找名称与你在第2步中输入的通用名称匹配的密钥。
- 提示你保存一个
.certSigningRequest
文件(CSR)。该文件包含公钥的副本。
- 你将CSR文件上传到开发者网站。
- 开发者网站向你颁发证书。从人类角度来说,这个证书表明"Apple证实该证书的主体持有与嵌入此证书中的公钥匹配的私钥。"
Note
开发者网站根据你的开发者账户设置证书中的主体信息。它会忽略CSR中的主体信息。所以,你可以在第2步中输入任何信息。这是一个在钥匙串中区分不同密钥的好方法。例如,你可以在第2步中设置通用名称字段,包含一个独特的标识符,以便轻松识别在第3步生成的公钥/私钥对。
- 你下载证书并将其添加到你的钥匙串中。
此时你的钥匙串包含了一个数字身份,即一个证书和与该证书中嵌入的公钥匹配的私钥。要在钥匙串访问中查看这个,在左侧选择"登录",在顶部选择"我的证书"。
"我的证书"是什么?
这里存在一个行业范围的术语问题。人们用"证书"这个词来表示两种不同的东西:
- 数字身份,即一个证书及其匹配的私钥
- 实际的证书
这种行业范围的混淆延伸到了Apple生态系统中。例如:
- Security框架在这方面是正确的,明确区分了数字身份(
SecIdentity
)和证书(SecCertificate
)。 - 钥匙串访问使用"我的证书"来表示数字身份。
- 其他面向用户的应用使用不同的术语。例如,Apple Configurator使用签名身份(为他们点赞!)。另一方面,Apple Mail的帮助文档使用"个人证书"这个术语。
- Xcode及其文档使用"签名证书"一词来表示可用于代码签名的数字身份。
这种术语上的不准确导致了各种问题。例如,假设你正在设置一台新Mac。你从开发者网站下载了证书,然后不明白为什么无法签名你的代码。这是因为开发者网站给你的是一个证书,而不是数字身份。实际上,开发者网站无法给你一个数字身份,因为它从未获得你的私钥的副本[1]。
[1] 再次说明,我们这里讨论的是经典的证书创建流程;对于云管理证书,这个说法不适用。
数字身份的形成
Apple平台通过以下步骤形成数字身份:
- 从证书中提取公钥。
- 计算该公钥的SHA-1摘要。
- 查找
kSecAttrApplicationLabel
属性与该SHA-1哈希值匹配的私钥。
要了解更多背景信息,请参阅我的密钥的SecItem属性文章。
注意,多个证书可以匹配同一个私钥是完全有效的,每个证书都会产生一个数字身份。当你更新证书时经常会看到这种情况。
深入了解CSR
CSR是一个带有CERTIFICATE REQUEST标签的PEM文件(PEM是Privacy-Enhanced Mail的简写):
要查看内部结构,运行openssl工具如下所示:
要了解更多细节,将文件转换为DER
格式然后作为ASN.1
解析:
我使用dumpasn1
工具,该工具可在这里下载。
要从CSR
中提取公钥,运行以下命令:
要进一步探索该密钥,请参考我的关于密码学密钥格式一文中的技术。
修订历史
- 2024-07-23 更新以添加TN3161 深入代码签名:证书的链接。
- 2022-11-03 首次发布。