使用 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/private-key.pem: the private key of the key pair.

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

    ¥../keys/public-key.pem: the public key of the key pair.

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

    ¥certs/certificate.pem: the code signing certificate configured to be valid for 10 years. This file should be added to source control (if applicable).

  • 生成的私钥必须保密且安全。上面的命令建议在源代码控制之外的目录中生成和存储这些密钥,以确保不会意外将它们签入源代码控制。我们建议以与存储其他敏感信息(KMS、密码管理器等)相同的方式存储私钥,存储方式将改变在步骤 (3) 中发布更新所需的步骤。

    ¥The generated private key must be kept private and secure. The command above suggests generating and storing these keys in a directory outside of your source control to ensure they are not accidentally checked into source control. We recommend storing the private key in the same way you would store other sensitive information (KMS, password manager, and so on), and how you store it will vary the steps needed to publish an update in step (3).

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

    ¥The public key may be stored alongside the private key, but isn't sensitive.

  • 证书应包含在项目中(签入源代码管理)。它包含公钥和验证代码签名的方法。下载签名更新时,使用此证书验证更新的签名。

    ¥The certificate should be included in the project (checked into source control). It contains the public key and a method for verifying code signatures. When a signed update is downloaded, the update's signature is verified using this certificate.

  • 证书有效期的设置可能会根据应用的安全需求而有所不同。

    ¥The certificate validity duration is a setting that may vary based on the security needs of your app.

    • 较短的有效期将更频繁地需要 密钥轮换,但被认为是更好的做法,因为受损的私钥将更快到期,从而限制暴露。

      ¥A shorter validity duration will require key rotation more frequently but is considered better practice since a compromised private key will have a sooner expiration which limits exposure.

    • 较短的有效期会增加应用发布过程的开销,因为密钥必须更频繁地轮换。证书过期的二进制文件将不会应用新的更新。

      ¥Shorter validity durations add overhead to your app's release process as the key must be rotated more frequently. Binaries with expired certificates won't apply new updates.

    • 例如,对于公共 Expo Go 应用,Expo 将此值设置为 20 年,但对于具有更频繁分发的二进制文件的内部应用,仅为 1 年。我们计划每 10 年轮换一次密钥。

      ¥For example, Expo sets this value to 20 years for the public Expo Go app, but only 1 year for internal apps with binaries that are distributed more frequently. We plan to rotate our keys every 10 years.

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 配置就是你所需要的。更改将在下次生成时应用于原生项目。

¥If you are using Continuous Native Generation (CNG) to generate your native projects, then the app.json configuration generated by the npx expo-updates codesigning configure command is all that you need. The changes will be applied to the native projects the next time they are generated.

Configure code signing in 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 文件中配置代码签名。

¥If you are not using Continuous Native Generation (CNG) to generate your native projects, then you will need to configure code signing in your app AndroidManifest.xml and/or Expo.plist files.

Configure code signing in an Android native project

你需要在 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;'));"

现在添加以下两个字段,并用 XML 转义证书替换 expo.modules.updates.CODE_SIGNING_CERTIFICATE 字段中的 android:value。你无需修改​​ 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;}"
/>
Configure code signing in an iOS native project

你需要在 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

发布到 Apple App Store

¥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 年后,使用证书对应的私钥签名的更新在被应用下载后将不再应用。在签名证书到期之前下载的更新将继续正常运行。在证书过期之前轮换密钥有助于预防任何潜在的密钥过期问题,并有助于保证所有用户在旧证书过期之前使用新证书。

    ¥Key expiration. In step (1) from the section above, we set certificate-validity-duration-years to 10 years (though it can be configured to any value). This means that after 10 years, updates signed with the private key corresponding to the certificate will no longer be applied after being downloaded by the app. Updates downloaded before the expiration of their signing certificate will continue to function normally. Rotating keys well before the certificate expires helps to preempt any potential key expiration issues and helps to guarantee all users are using the new certificate before the old certificate expires.

  • 私钥泄露。如果用于签署更新的私钥意外暴露给公众,则不再被视为安全,因此无法再保证用它签名的更新的完整性。例如,恶意行为者可以制作恶意更新并使用泄露的私钥对其进行签名。

    ¥Private key compromise. If the private key used to sign updates is accidentally exposed to the public, it can no longer be considered secure and therefore the integrity of updates signed with it can no longer be guaranteed. For example, a malicious actor could craft a malicious update and sign it with the leaked private key.

  • 安全最佳实践的密钥轮换。最佳实践是定期轮换密钥,以确保系统能够适应上述其他原因之一的手动密钥轮换。

    ¥Key rotation for security best practices. It is best practice to rotate keys periodically to ensure that a system is resilient to manual key rotation in response to one of the other reasons above.

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

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

  1. 备份上述步骤 (1) 中生成的旧密钥和证书。

    ¥Back up the old keys and certificate that were generated in step (1) above.

  2. 按照上述步骤从步骤 (1) 开始生成新密钥。为了帮助调试,你可能希望通过修改应用配置 (app.json) 中的 updates.codeSigningMetadata.keyid 字段来更改新密钥的 keyid

    ¥Generate a new key by following the steps above starting from step (1). To assist in debugging, you may wish to change the keyid of the new key by modifying the updates.codeSigningMetadata.keyid field in your app config (app.json).

  3. 代码签名证书是应用运行时的一部分,因此应为使用此证书的构建设置新的运行时版本,以确保只有使用新密钥签名的更新才会在新构建中运行。

    ¥The code signing certificate is part of the app's runtime, so a new runtime version should be set for builds using this certificate to ensure that only updates signed with the new key run in the new build.

  4. 按照上述步骤 (3) 使用新密钥发布签名更新。

    ¥Publish signed updates using the new key by following step (3) above.

删除代码签名

¥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) 中生成的旧密钥和证书。

    ¥Backup the old key and certificate that were generated in step (1) above.

  2. 从应用配置 (app.json) 中删除 updates.codeSigningMetadata 字段。

    ¥Remove the updates.codeSigningMetadata field from your app config (app.json).

  3. 新的无证书应用是一个新的独特运行时,因此应为构建设置新的运行时版本,以确保在新构建中仅运行未签名的更新。

    ¥The new certificate-less app is a new distinct runtime, so a new runtime version should be set for builds to ensure that only unsigned updates run in the new build.