使用 EAS Update 的端到端代码签名

编辑页面

了解代码签名和密钥轮换在 EAS Update 中的工作原理。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

EAS Update 代码签名仅对订阅了 EAS Production 或 Enterprise 方案的账户可用。了解更多

expo-updates 库支持使用公钥密码学进行端到端代码签名。代码签名允许开发者使用自己的密钥对更新进行加密签名。随后会在客户端应用更新之前验证这些签名,这确保了 ISP、CDN、云服务提供商,甚至 EAS 本身都无法篡改由应用运行的更新。

以下步骤将引导你完成生成私钥及对应证书、配置项目以使用代码签名,以及为应用发布已签名更新的过程。

1

生成私钥及对应证书

在这一步中,我们将为你的应用生成一对密钥和对应的代码签名证书。请为 --key-output-directory 参数指定一个位于源代码管理之外的目录,以确保生成的私钥不会意外被加入源代码管理。

Terminal
npx expo-updates codesigning:generate \
--key-output-directory ../keys \ --certificate-output-directory certs \ --certificate-validity-duration-years 10 \ --certificate-common-name "Your Organization Name"

此命令会生成一对密钥以及一个要包含在应用中的代码签名证书:

  • ../keys/private-key.pem:该密钥对的私钥。

  • ../keys/public-key.pem:该密钥对的公钥。

  • certs/certificate.pem:配置为有效期 10 年的代码签名证书。此文件应加入源代码管理(如果适用)。

  • 生成的私钥必须妥善保密并安全存放。上面的命令建议在源代码管理之外的目录中生成并存储这些密钥,以确保它们不会被意外提交到源代码管理中。我们建议以与你存储其他敏感信息相同的方式(KMS、密码管理器等)存储私钥,而你如何存储它将会影响你在第 (3) 步发布更新所需的步骤。

  • 公钥可以与私钥一起存放,但它并不敏感。

  • 证书应包含在项目中(提交到源代码管理)。它包含公钥以及用于验证代码签名的方法。当下载已签名更新时,会使用此证书验证更新的签名。

  • 证书有效期是一个可根据应用安全需求而变化的设置。

    • 较短的有效期会要求更频繁地进行key rotation,但通常被认为是更好的做法,因为一旦私钥泄露,其过期时间会更早,从而限制影响范围。
    • 较短的有效期会增加应用发布流程的开销,因为密钥需要更频繁地轮换。证书过期的二进制文件将无法应用新的更新。
    • 例如,Expo 将此值为公开的 Expo Go 应用设置为 20 年,但对于更频繁分发二进制文件的内部应用则仅设置为 1 年。我们计划每 10 年轮换一次密钥。

2

配置你的项目以使用代码签名

Terminal
npx expo-updates codesigning:configure \
--certificate-input-directory certs \ --key-input-directory ../keys

如果你正在使用 Continuous Native Generation (CNG) 来生成原生项目,那么 npx expo-updates codesigning configure 命令生成的 app.json 配置就是你所需要的全部内容。相关更改会在下次生成原生项目时应用到这些项目中。

在 app.json 中配置代码签名

运行上述命令后,你的 app.json 将包含额外的代码签名配置:

app.json
{ "expo": { "updates": { "codeSigningCertificate": "./certs/certificate.pem", "codeSigningMetadata": { "keyid": "main", "alg": "rsa-v1_5-sha256" } } } }

如果你没有使用 Continuous Native Generation (CNG) 来生成原生项目,那么你需要在应用的 AndroidManifest.xml 和/或 Expo.plist 文件中配置代码签名。

在 Android 原生项目中配置代码签名

你需要在 android/app/src/main/AndroidManifest.xml 中的 <application> 元素添加两个字段。

在此之前,我们需要先生成证书的 XML 转义版本。你可以手动复制 certs/certificate.pem 的内容,并将所有 \r 字符替换为 &#xD;,将 \n 替换为 &#xA;,也可以运行以下脚本来自动完成:

Terminal
node -e "console.log(require('fs').readFileSync('./certs/certificate.pem', 'utf8')\
.replace(/\r/g, '&#xD;').replace(/\n/g, '&#xA;'));"

现在添加以下两个字段,并将 expo.modules.updates.CODE_SIGNING_CERTIFICATE 字段的 android:value 替换为 XML 转义后的证书。你不需要修改 expo.modules.updates.CODE_SIGNING_METADATA 条目的值。

android/app/src/main/AndroidManifest.xml
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="(在此处插入 XML 转义后的证书)" /> <meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{&quot;keyid&quot;:&quot;main&quot;,&quot;alg&quot;:&quot;rsa-v1_5-sha256&quot;}" />
在 iOS 原生项目中配置代码签名

你需要在 ios/project-name/Supporting/Expo.plist 中的 <dict> 元素添加两个字段。

在此之前,我们需要先生成证书的 XML 转义版本。你可以手动复制 certs/certificate.pem 的内容,并将所有 \r 字符替换为 &#xD;,也可以运行以下脚本来自动完成:

Terminal
node -e "console.log(require('fs').readFileSync('./certs/certificate.pem', 'utf8')\
.replace(/\r/g, '&#xD;'));"

现在添加以下两个字段,并将证书值替换为 XML 转义后的证书。你不需要更新 EXUpdatesCodeSigningMetadata 字段。

ios/project-name/Supporting/Expo.plist
<key>EXUpdatesCodeSigningCertificate</key> <string>-----BEGIN CERTIFICATE-----&#xD; (插入 XML 转义后的证书,它看起来应该类似这样)&#xD; (跨越多行,\r 已转义但 \n 未转义)&#xD; +-----END CERTIFICATE-----&#xD; </string> <key>EXUpdatesCodeSigningMetadata</key> <dict> <key>keyid</key> <string>main</string> <key>alg</key> <string>rsa-v1_5-sha256</string> </dict>

配置好代码签名后,使用新的 runtime version 创建一个新构建。代码签名证书将被嵌入到这个新构建中。

3

为你的应用发布已签名更新

Terminal
eas update --private-key-path ../keys/private-key.pem

在使用 eas update 发布 EAS Update 期间,EAS CLI 会自动检测你的应用是否已配置代码签名。随后它会验证更新的完整性,并使用你的私钥创建数字签名。此过程在本地执行,因此你的私钥永远不会离开你的机器。生成的签名会自动发送给 EAS,以便与更新一同存储。

4

验证更新已加载

在客户端上下载该更新(这一步由库自动完成)。在第 (2) 步中配置了代码签名的构建会检查是否有可用的新更新。服务器会返回第 (3) 步发布的更新及其生成的签名。更新在下载后、应用前,会使用嵌入的证书和包含的签名进行验证。如果证书和签名有效,则应用该更新;否则会被拒绝。

其他信息

密钥轮换

密钥轮换是指更改用于签名更新的密钥对。最常见于以下几种情况:

  • 密钥过期。在上文第 (1) 步中,我们将 certificate-validity-duration-years 设置为 10 年(尽管它可以配置为任意值)。这意味着 10 年后,使用与证书对应的私钥签名的更新,在被应用下载后将不再被应用。在证书过期之前下载的更新将继续正常运行。在证书过期前提前轮换密钥,有助于预先避免任何潜在的证书过期问题,并有助于确保所有用户在旧证书过期前都已使用新证书。
  • 私钥泄露。如果用于签名更新的私钥意外暴露给公众,那么它将不再能被视为安全,因此用它签名的更新的完整性也无法再得到保证。例如,恶意行为者可能构造一个恶意更新并使用泄露的私钥对其签名。
  • 为安全最佳实践而进行的密钥轮换。定期轮换密钥是最佳实践,以确保系统能够在面对上述其他原因之一时,具备手动轮换密钥的韧性。

在任何这些情况下,流程都类似:

  1. 备份上面第 (1) 步生成的旧密钥和证书。
  2. 按照上述步骤从第 (1) 步开始生成新密钥。为了便于调试,你可能希望通过修改应用配置(app.json)中的 updates.codeSigningMetadata.keyid 字段来更改新密钥的 keyid
  3. 代码签名证书属于应用的 runtime,因此使用此证书的构建应设置新的 runtime version,以确保新构建中只运行使用新密钥签名的更新。
  4. 按照上面的第 (3) 步,使用新密钥发布已签名更新。

移除代码签名

从应用中移除代码签名的过程与key rotation类似,可以将其视为将密钥轮换到一个 null 密钥。

  1. 备份上面第 (1) 步生成的旧密钥和证书。
  2. 从应用配置(app.json)中移除 updates.codeSigningMetadata 字段。
  3. 不含证书的新应用是一个新的独立 runtime,因此应为构建设置新的 runtime version,以确保新构建中只运行未签名的更新。