使用 EAS 更新进行端到端代码签名

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


信息 EAS 更新代码签名仅对订阅了 EAS 生产版或企业版计划的账户可用。了解更多

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

🌐 The expo-updates library supports end-to-end code signing using public-key cryptography. Code signing allows developers to cryptographically sign their updates with their own keys. The signatures are then verified on the client before the update is applied, which ensures ISPs, CDNs, cloud providers, and even EAS itself cannot tamper with updates run by apps.

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

🌐 The following steps will guide you through the process of generating a private key and corresponding certificate, configuring your project to use code signing, and publishing a signed update for your app.

1

生成私钥和相应的证书

🌐 Generate a private key and corresponding certificate

在此步骤中,我们将为你的应用生成一对密钥及相应的代码签名证书。请为 --key-output-directory 标志指定一个在你的源代码管理之外的目录,以确保生成的私钥不会被意外添加到源代码管理中。

🌐 In this step, we will generate a key pair and corresponding code signing certificate for your app. Specify a directory outside of your source control for the --key-output-directory flag to ensure the generated private key isn't accidentally added to source control.

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"

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

🌐 This command generated a key pair along with a code signing certificate to be included in the app:

  • ../keys/private-key.pem:密钥对的私钥。
  • ../keys/public-key.pem:密钥对的公钥。
  • certs/certificate.pem:代码签名证书配置为有效期为 10 年。此文件应添加到源代码管理中(如果适用)。
  • 生成的私钥必须保持私密和安全。上面的命令建议在源代码管理之外的目录中生成和存储这些密钥,以确保它们不会被意外地提交到源代码管理中。我们建议以存储其他敏感信息(如 KMS、密码管理器等)的方式来存储私钥,而存储方式的不同会影响在步骤(3)中发布更新所需的操作步骤。
  • 公钥可以与私钥一起存储,但它不敏感。
  • 证书应包含在项目中(签入到源代码管理)。它包含公钥和用于验证代码签名的方法。当下载签名的更新时,将使用此证书来验证更新的签名。
  • 证书有效期是一个设置,可能会根据你的应用的安全需求而有所不同。
    • 较短的有效期将需要更频繁地进行密钥轮换,但这被认为是更好的做法,因为一旦私钥被泄露,其过期时间更早,从而限制了风险暴露。
    • 较短的有效期会增加应用发布流程的开销,因为密钥需要更频繁地轮换。证书过期的二进制文件将无法应用新的更新。
    • 例如,Expo 对公共的 Expo Go 应用将此值设置为 20 年,但对于内部应用(其二进制文件更新更频繁)则仅设置为 1 年。我们计划每 10 年更换一次密钥。

2

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

🌐 Configure your project to use code signing

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

如果你使用持续原生生成(CNG)来生成你的原生项目,那么由 npx expo-updates codesigning configure 命令生成的 app.json 配置就是你所需要的一切。更改将在下一次生成原生项目时应用。

在 app.json 中配置代码签名

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

🌐 After running the above command, your app.json will include additional configuration for code signing:

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

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

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

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

🌐 You will need to add two fields to the <application> element in android/app/src/main/AndroidManifest.xml

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

🌐 Before doing that, we need to generate an XML-escaped version of your certificate. You can either copy the contents of certs/certificate.pem and replace all of the \r characters with &#xD; and \n with &#xA; manually, or run the following script to do that for you:

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 条目的值。

🌐 Now add the following two fields, and replace the android:value for the expo.modules.updates.CODE_SIGNING_CERTIFICATE field with the XML-escaped certificate. You do not need to modify the value for expo.modules.updates.CODE_SIGNING_METADATA entry.

android/app/src/main/AndroidManifest.xml
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="(insert XML-escaped certificate here)" /> <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> 元素添加两个字段。

🌐 You will need to add two fields to the <dict> element in ios/project-name/Supporting/Expo.plist.

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

🌐 Before doing that, we need to generate an XML-escaped version of your certificate. You can either copy the contents of certs/certificate.pem and replace all of the \r characters with &#xD;, or run the following script to do that for you:

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

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

🌐 Now add the following two fields, and replace the certificate value with the XML-escaped certificate. You do not need to update the EXUpdatesCodeSigningMetadata field.

ios/project-name/Supporting/Expo.plist
<key>EXUpdatesCodeSigningCertificate</key> <string>-----BEGIN CERTIFICATE-----&#xD; (insert XML-escaped certificate, it should look something like this)&#xD; (spanning multiple lines with \r escaped but \n not escaped)&#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>

配置代码签名后,使用新的运行时版本创建一个新的构建。代码签名证书将嵌入到这个新构建中。

🌐 With code signing configured, create a new build with a new runtime version. The code signing certificate will be embedded in this new build.

3

发布已签名的应用更新

🌐 Publish a signed update for your app

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

在使用 eas update 发布 EAS 更新时,EAS CLI 会自动检测到你的应用已配置代码签名。然后,它会验证更新的完整性,并使用你的私钥创建数字签名。此过程在本地执行,因此你的私钥不会离开你的计算机。生成的签名会自动发送到 EAS,以便与更新一起存储。

🌐 During an EAS Update publish using eas update, the EAS CLI automatically detects that code signing is configured for your app. It then verifies the integrity of the update and creates a digital signature using your private key. This process is performed locally so that your private key never leaves your machine. The generated signature is automatically sent to EAS to store alongside the update.

4

确认更新已加载

🌐 Verify that the update is loaded

在客户端下载更新(这一步由库自动补齐)。从步骤(2)构建的用于代码签名的版本会检查是否有可用的新更新。服务器会返回在步骤(3)发布的更新及其生成的签名。在下载更新之后但在应用之前,会根据嵌入的证书和包含的签名对更新进行验证。如果证书和签名有效,则应用更新;否则,更新会被拒绝。

🌐 Download the update on the client (this step is done automatically by the library). The build from step (2) that is configured for code signing checks if there is a new update available. The server responds with the update published in step (3) and its generated signature. After being downloaded but before being applied, the update is verified against the embedded certificate and included signature. The update is applied if the certificate and signature are valid, and rejected otherwise.

附加信息

🌐 Additional information

密钥轮换

🌐 Key rotation

密钥轮换是指用于签署更新的密钥对被更换的过程。这通常在以下几种情况下进行:

🌐 Key rotation is the process by which the key pair used for signing updates is changed. This is most commonly done in a few cases:

  • 密钥过期。在上述部分的步骤(1)中,我们将 certificate-validity-duration-years 设置为 10 年(虽然可以配置为任意值)。这意味着在 10 年后,由与证书对应的私钥签署的更新将在应用下载后不再应用。在签署证书到期之前下载的更新将继续正常运行。在证书过期之前提前更换密钥有助于预防潜在的密钥过期问题,并有助于确保所有用户在旧证书过期之前使用新证书。
  • 私钥泄露。如果用来签署更新的私钥意外地暴露给公众,它将不再被视为安全的,因此使用该私钥签署的更新的完整性也无法得到保证。例如,恶意行为者可能会制作一个恶意更新,并使用泄露的私钥进行签署。
  • 出于安全最佳实践的考虑进行密钥轮换。定期轮换密钥是最佳做法,以确保系统能够应对手动密钥轮换,从而应对上述其他原因中的某一个。

在任何这些情况下,程序都是相似的:

🌐 In any of these cases, the procedure is similar:

  1. 备份上述步骤 (1) 中生成的旧密钥和证书。
  2. 按照上述步骤从步骤(1)开始生成一个新密钥。为了帮助调试,你可能希望通过修改应用配置(app.json)中的 updates.codeSigningMetadata.keyid 字段来更改新密钥的 keyid
  3. 代码签名证书是应用运行时的一部分,因此应为使用此证书的构建设置新的运行时版本,以确保只有使用新密钥签名的更新才会在新构建中运行。
  4. 按照上述步骤 (3) 使用新密钥发布签名更新。

删除代码签名

🌐 Removing code signing

从应用中移除代码签名的过程类似于密钥轮换,可以将其视为对 null 密钥的轮换。

🌐 The process of removing code signing from an app is similar to key rotation and can be thought of as a key rotation to a null key.

  1. 备份在上述步骤 (1) 中生成的旧密钥和证书。
  2. 从你的应用配置(app.json)中删除 updates.codeSigningMetadata 字段。
  3. 新的无证书应用是一个新的独特运行时,因此应为构建设置新的运行时版本,以确保在新构建中仅运行未签名的更新。