# 安全启动 (secure boot) v2

[English]

重要

本文档介绍了安全启动 v2，ESP32-C2 支持该启动模式。

安全启动 v2 使用基于 ECDSA 的应用程序和 二级引导加载程序 验证。若需要使用 ECDSA 方案对应用程序签名，且无需对引导加载程序签名，同样可以参考本文档。

备注

在本指南中，最常用的命令形式为 `idf.py secure-<command>`，这是对应 `espsecure.py <command>` 的封装。基于 `idf.py` 的命令能提供更好的用户体验，但与基于 `espsecure.py` 的命令相比，可能会损失一部分高级功能。

## 背景

安全启动通过检查每个启动的软件是否已签名来确保设备不会运行任何未经授权（即未签名）的代码。在 ESP32-C2 上，这些软件包括二级引导加载程序和每个应用程序的二进制文件。注意，一级 (ROM) 引导加载程序是无法更改的 ROM 代码，因此不需要签名。

ESP32-C2 引入了基于 ECC 的安全启动验证方案，即安全启动 v2。

ESP32-C2 的安全启动包括以下步骤：

1. 一级 (ROM) 引导加载程序加载二级引导加载程序，并验证二级引导加载程序的 ECDSA 签名。验证通过后，方可运行二级引导加载程序。

2. 二级引导加载程序加载特定应用程序镜像，并验证应用程序的 ECDSA 签名。若验证通过，则执行应用程序镜像。

## 优势

- ECDSA 的公钥存储在设备上，而相应的 ECDSA 私钥存储在私密位置，设备无法访问。

- 芯片在量产时只能生成并存储一个公钥。

- 应用程序和二级引导加载程序采用相同的镜像格式和签名验证方法。

- 设备不存储任何机密信息，因此可以免受被动侧通道攻击的影响，如时序分析或功耗分析。

## 使用安全启动 v2

以下为使用安全启动 v2 流程的概述。有关如何启用安全启动，请参阅 启用安全启动 v2。

安全启动 v2 使用专用的 签名块 验证引导加载程序镜像和应用程序二进制镜像，每个镜像末尾都附加了一个单独生成的签名块。

在 ESP32-C2 中，引导加载程序或应用程序镜像只能附加一个签名块。

每个签名块包含前一个镜像的签名和相应的 ECDSA-256 或 ECDSA-192 公钥。有关格式详情，请参阅 签名块格式。ECDSA-256 或 ECDSA-192 公钥的摘要存储在 eFuse 中。

应用程序镜像不仅在每次启动时验证，也会在每次空中升级 (OTA) 时验证。如果当前所选 OTA 应用程序镜像无法验证，引导加载程序将回退，并寻找其他正确签名的应用程序镜像。

安全启动 v2 流程遵循以下步骤：

1. 启动时，ROM 代码检查 eFuse 中的安全启动 v2 位。如果禁用了安全启动，则执行普通启动；如果启用了安全启动，将继续以下步骤。

2. ROM 代码验证引导加载程序的签名块，请参阅 验证签名块。如果验证失败，启动过程将中止。

3. ROM 代码使用原始镜像数据、相应的签名块以及 eFuse 验证引导加载程序镜像，请参阅 验证镜像。如果验证失败，启动过程将中止。

4. ROM 代码执行引导加载程序。

5. 引导加载程序验证应用程序镜像的签名块，请参阅 验证签名块。如果验证失败，启动过程将中止。

6. 引导加载程序使用原始镜像数据、相应的签名块以及 eFuse 验证引导加载程序镜像，请参阅 验证镜像。如果验证失败，启动过程将中止。如果验证失败，但发现了其他应用程序镜像，引导加载程序将使用步骤 5 到 7 验证另一个镜像。该过程将重复，直至找到有效镜像，或所有镜像验证完毕。

7. 引导加载程序执行经验证的应用程序镜像。

## 签名块格式

签名块以 4 KB 的整数倍为起始位置，拥有独立 flash 扇区。签名计算覆盖了镜像中的所有字节，包括填充字节，请参阅 安全填充。

各签名块内容如下表所示：

ECDSA 签名块的内容
偏移量 | 大小（字节） | 描述
--- | --- | ---
0 | 1 | 魔法字节。
1 | 1 | 版本号字节，当前为 0x03。
2 | 2 | 填充字节。保留，应设置为 0。
4 | 32 | 仅针对镜像内容的 SHA-256 哈希值，不包括签名块。
36 | 1 | 曲线 ID。1 代表 NIST192p 曲线，2 代表 NIST256p 曲线。
37 | 64 | ECDSA 公钥：32 字节的 X 坐标，后跟 32 字节的 Y 坐标。
101 | 64 | 对镜像内容的 ECDSA 签名结果（RFC6090 中的 5.3.2 节）：32 字节的 R 组件，后跟 32 字节的 S 组件。
165 | 1031 | 保留。
1196 | 4 | 前面 1196 字节的 CRC32。
1200 | 16 | 长度填充为 1216 字节的零填充。

签名扇区的其余部分是已擦除的 flash (0xFF)，支持在前一个签名块之后写入其他签名块。

## 安全填充

在安全启动 v2 方案中，应用程序镜像经过处理，会填充到与 flash MMU 页面大小边界对齐，确保只有经过验证的内容会映射到内部地址空间，这称为安全填充。填充后会进行镜像签名计算，随后将签名块 (4 KB) 附加到镜像上。

- 默认 flash MMU 页面大小为 64 KB

- ESP32-C2 支持配置 flash MMU 页面大小，`CONFIG_MMU_PAGE_SIZE` 根据 CONFIG_ESPTOOLPY_FLASHSIZE 设置

- 在进行由 `esptool.py` 执行的 `elf2image` 转换时，可以通过使用选项 `--secure-pad-v2` 应用安全填充

带有安全填充和签名块的安全启动 v2 签名镜像的内容如下表所示：

已签名应用程序的内容
偏移量 | 大小 (KB) | 描述
--- | --- | ---
0 | 580 | 未签名的应用程序大小，作为示例
580 | 60 | 安全填充，与下一个 64 KB 边界对齐
640 | 4 | 签名块

备注

注意，应用程序镜像始终从下一个 flash MMU 页面大小的边界开始，默认为 64 KB。因此使用上述签名块之后剩余的空间可以存储其他数据分区，如 `nvs`。

## 验证签名块

如果签名块的第一个字节是 `0xe7`，并且偏移量 1196 处存储了有效的 CRC32，则签名块有效，否则无效。

## 验证镜像

如果存储在某个签名块中的公钥是适用于当前设备的有效公钥，且该签名块中存储的签名与从 flash 中读取的镜像数据计算出的签名匹配，则该镜像通过验证。

1. 将嵌入在引导加载程序签名块中的公钥生成的 SHA-256 哈希摘要与存储在 eFuse 中的摘要进行比较，如果公钥的哈希摘要无法与 eFuse 中的任何哈希摘要匹配，则验证失败。

2. 生成应用程序镜像摘要，将其与签名块中的镜像摘要进行匹配，如果无法匹配，则验证失败。

3. 使用公钥，采用 ECDSA（RFC6090 的第 5.3.3 节）算法，验证引导加载程序镜像的签名，并与步骤 (2) 中计算的镜像摘要比较。

## bootloader 大小

启用安全启动和/或 flash 加密都会增加引导加载程序的大小，因此可能需要更新分区表偏移量，请参阅 引导加载程序大小。

禁用 CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES 时，引导加载程序将使用 `esptool` 的 `elf2image` 命令中的 `--pad-to-size` 选项进行扇区填充，每个扇区大小为 4 KB。

## 使用 eFuse

- SECURE_BOOT_EN - 在启动时启用安全启动保护。

密钥必须为可读密钥，以便软件访问。如果密钥设置了读保护，软件只能读取到全为零的数据，导致签名验证失败，启动中止。

## 启用安全启动 v2

1. 打开 项目配置菜单，在 `Security features` 下设置 `Enable hardware Secure Boot in bootloader` 以启用安全启动模式。

2. 选择 `Secure Boot v2` 选项，并默认将 `App Signing Scheme` 设置为 ECDSA (v2)。

3. 在项目目录的基础上，明确指定安全启动签名密钥的路径。

4. 在 `UART ROM download mode` 中选择所需 UART ROM 选项。默认情况下，通常建议将其设置为 `Permanently switch to Secure mode`。对于生产设备，最安全的选项是将其设置为 `Permanently disabled`。

5. 按需设置其他 menuconfig 选项，随后退出 menuconfig 并保存配置。

6. 初次运行 `idf.py build` 时，如果未找到签名密钥，将打印错误消息，并提供通过 `idf.py secure-generate-signing-key` 生成签名密钥的命令。

重要

通过此方法生成的签名密钥将使用操作系统和其 Python 安装中提供的最佳随机数源，在 OSX/Linux 上为 /dev/urandom，在 Windows 上为 CryptGenRandom()。如果此随机数源不足以提供足够的安全性，那么生成的私钥也不足以提供足够的安全性。

重要

在生产环境下，建议使用 OpenSSL 或其他行业标准的加密程序生成密钥对，详情请参阅 生成安全启动签名密钥。

7. 运行 `idf.py bootloader` 构建启用了安全启动的引导加载程序，构建输出中会包含一个烧录命令的提示，使用 `esptool.py write_flash` 烧录。

8. 烧录引导加载程序前，请运行指定命令并等待烧录完成。注意，此处的指定命令需要手动输入，构建系统不会执行此过程。

9. 运行 `idf.py flash` 构建并烧录分区表以及刚刚构建的应用程序镜像，该镜像使用步骤 6 中生成的签名密钥进行签名。

备注

如果启用了安全启动，`idf.py flash` 不会烧录引导加载程序。

10. 重置 ESP32-C2，它将启动你烧录的二级引导加载程序。该二级引导加载程序会在芯片上启用安全启动，然后验证应用程序镜像签名，并启动应用程序。请查看 ESP32-C2 的串行控制器输出，确保已启用安全启动，且没有因构建配置发生错误。

备注

在烧录了有效的分区表和应用程序镜像之前，安全启动不会启用，避免在系统完全配置前发生意外情况。

备注

如果在初次启动过程中重置或关闭了 ESP32-C2，它会在下次启动时重新开始上述步骤。

11. 在后续启动过程中，安全启动硬件会验证二级引导加载程序是否更改，二级引导加载程序会使用其附加的签名块中经验证的公钥部分，验证已签名的应用程序镜像。

## 启用安全启动后的限制

- 任何更新过的引导加载程序或应用程序都需要使用与已存储在 eFuse 中的摘要相匹配的密钥来签名。

- 注意，启用安全启动或 flash 加密会禁用 ROM 中的 USB-OTG USB 栈，阻止通过该端口进行串行仿真或设备固件更新 (DFU)。

- 一旦启用安全启动，就无法再对 eFuse 密钥进行读保护，这可以避免攻击者对存储公共密钥摘要的 eFuse 块进行读保护，进而导致系统无法验证和处理签名，系统服务无法正常运行。有关读保护密钥的更多信息，请参阅下方详细说明。

### 烧录读保护密钥

读保护密钥： 以下密钥受到读保护后，相应的硬件将直接访问这些密钥（软件无法读取）：

- flash 加密密钥

不受读保护的密钥： 因软件访问需要（软件可读取），以下密钥不受读保护：

- 安全启动公共密钥摘要

- 用户数据

启用安全启动后，默认禁用 eFuses 读保护功能。如后续需在应用程序中对某个 eFuse（例如上述读保护密钥列表中的密钥）进行读保护，请在启用安全启动的同时启用配置项 CONFIG_SECURE_BOOT_V2_ALLOW_EFUSE_RD_DIS。

建议在启用安全启动之前，完成全部密钥的烧录。如需启用配置项 CONFIG_SECURE_BOOT_V2_ALLOW_EFUSE_RD_DIS，请在所有读保护 eFuse 密钥烧录后，使用 `esp_efuse_write_field_bit()` 烧录 eFuse ESP_EFUSE_WR_DIS_RD_DIS。

备注

如果在启用安全启动时，第二阶段引导加载程序启用了 flash 加密，则首次启动时生成的 flash 加密密钥已经受到读保护。

## 生成安全启动签名密钥

根据构建系统提示，使用 `idf.py secure-generate-signing-key` 命令生成新签名密钥。

传递 `--version 2 --scheme ecdsa256` 或 `--version 2 --scheme ecdsa192` 选择 ECDSA 方案，生成相应的 ECDSA 私钥。

签名密钥的强度取决于 (a) 系统的随机数源和 (b) 所用算法的正确性。对于生产设备，建议从具有高质量熵源的系统生成签名密钥，并使用最佳的可用 ECDSA 密钥生成工具。

例如，使用 OpenSSL 命令行生成签名密钥时：

生成 ECC NIST192p 曲线密钥

    openssl ecparam -name prime192v1 -genkey -noout -out my_secure_boot_signing_key.pem

生成 ECC NIST256p 曲线密钥

    openssl ecparam -name prime256v1 -genkey -noout -out my_secure_boot_signing_key.pem

注意，安全启动系统的强度取决于能否保持签名密钥的私密性。

## 远程镜像签名

### 使用 `idf.py` 进行签名

对于生产构建，将签名密钥存储在远程签名服务器上，而不是本地构建机器上，是一种比较好的方案，这也是默认的 ESP-IDF 安全启动配置。可以使用命令行工具 `espsecure.py` 在远程系统上为应用程序镜像和分区表数据签名，供安全启动使用。

使用远程签名时，请禁用选项 CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES，并构建固件。此时，私钥无需存在于构建系统中。

构建完应用程序镜像和分区表后，构建系统会使用 `idf.py` 打印签名步骤：

    idf.py secure-sign-data BINARY_FILE --keyfile PRIVATE_SIGNING_KEY

上述命令将镜像签名附加到现有的二进制文件中，可以使用 --output 参数将签名后的二进制文件写入单独的文件：

    idf.py secure-sign-data --keyfile PRIVATE_SIGNING_KEY --output SIGNED_BINARY_FILE BINARY_FILE

### 使用预计算的签名进行签名

如果存在为镜像生成的有效预计算签名及相应公钥，可以使用这些签名生成一个签名扇区，并将其附加到镜像中。注意，预计算的签名应计算在镜像中的所有字节，包括安全填充字节。

在此情况下，应禁用选项 CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES 来构建固件镜像。该镜像将进行安全填充，并使用以下命令，生成带签名的二进制文件：

    idf.py secure-sign-data --pub-key PUBLIC_SIGNING_KEY --signature SIGNATURE_FILE --output SIGNED_BINARY_FILE BINARY_FILE

上述命令会验证签名，生成签名块（请参阅 签名块格式），并将其附加到二进制文件中。

### 使用外部硬件安全模块 (HSM) 进行签名

为了提高安全性，可能需要使用外部硬件安全模块 (HSM) 存储私钥，该私钥无法直接访问，但具备一个接口，可以生成二进制文件及其相应公钥的签名。

在此情况下，请禁用选项 CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES 并构建固件。随后，可以将已进行安全填充的镜像提供给外部硬件安全模块来生成签名。请参阅 使用外部 HSM 签名 生成已签名镜像。

备注

在上述三种远程签名工作流程中，已签名的二进制文件将写入提供给 `--output` 参数的文件名中。

## 使用安全启动的建议

- 在具备高质量熵源的系统上生成签名密钥。

- 时刻对签名密钥保密，泄漏此密钥将危及安全启动系统。

- 不允许第三方使用 `idf.py secure-` 命令来观察密钥生成或签名过程的任何细节，这两个过程都容易受到定时攻击或其他侧信道攻击的威胁。

- 在安全启动配置中启用所有安全启动选项，包括 flash 加密、禁用 JTAG、禁用 BASIC ROM 解释器和禁用 UART 引导加载程序的加密 flash 访问。

- 结合 flash 加密 使用安全启动，防止本地读取 flash 内容。

## 技术细节

以下章节包含安全启动元件的详细参考描述。

安全启动已集成到 ESP-IDF 构建系统中，因此 `idf.py build` 将进行应用程序镜像签名。启用 CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES 后，`idf.py bootloader` 将生成一个已签名的引导加载程序。

另外，也可以使用 `idf.py` 或 `openssl` 工具生成独立签名并进行验证。建议使用 `idf.py`，但如果需在非 ESP-IDF 环境中生成或验证签名，也可以使用 `openssl` 命令，因为安全引导 v2 生成签名符合标准的签名算法。

### 用 `idf.py` 生成和验证签名

1. 二进制镜像签名：

    idf.py secure-sign-data --keyfile ./my_signing_key.pem --output ./image_signed.bin image-unsigned.bin

Keyfile 是包含 ECDSA-256 或 ECDSA-192 签名私钥的 PEM 文件。

2. 验证签名的二进制镜像：

>     idf.py secure-verify-signature --keyfile ./my_signing_key.pem image_signed.bin

Keyfile 是包含 ECDSA-256 或 ECDSA-192 公钥/私钥签名的 PEM 文件。

### 用 OpenSSL 生成和验证签名

最好使用 `idf.py` 工具生成和验证签名，但如果需要使用 OpenSSL 执行这些操作，可参考以下命令：

1. 生成需要计算签名的二进制文件镜像的摘要。

   >     openssl dgst -sha256 -binary BINARY_FILE  > DIGEST_BINARY_FILE

2. 使用上一步计算出的摘要生成该镜像的签名。

   > 用于生成 ECDSA 签名：

   > >     openssl pkeyutl -sign \
   > >         -in  DIGEST_BINARY_FILE \
   > >         -inkey PRIVATE_SIGNING_KEY \
   > >         -out SIGNATURE_FILE

3. 验证生成的签名。

   > 用于验证 ECDSA 签名：

   > >     openssl pkeyutl -verify \
   > >         -in DIGEST_BINARY_FILE \
   > >         -pubin -inkey PUBLIC_SIGNING_KEY \
   > >         -sigfile SIGNATURE_FILE

## 安全启动 & flash 加密

如果使用安全启动时没有启用 flash 加密，可能会发生 `time-of-check to time-of-use` 攻击，即在验证并运行镜像后交换 flash 内容。因此，建议同时使用这两个功能。

重要

ESP32-C2 只有一个 eFuse 密钥块，用于存储两种密钥：安全启动和 flash 加密，但 eFuse 密钥块只能烧录一次，因此建议同时烧录这两种密钥。注意，`Secure Boot` 和 `Flash Encryption` 无法分别启用，否则后续写入 eFuse 密钥块将返回错误。

## 在未启用硬件安全启动时对应用程序进行签名校验

无需启用硬件安全启动选项，即可在 OTA 更新时验证应用程序的安全启动 v2 签名。这种方法采用了与安全启动 v2 相同的应用程序签名方案，但不同于硬件安全启动，软件安全启动无法阻止能够写入 flash 的攻击者绕过签名验证。

如果在启动时无法接受安全启动验证的延迟，和/或威胁模型不包括物理访问或攻击者在 flash 中写入引导加载程序或应用程序分区，则适合使用未启用硬件安全启动的验证。

在此模式下，当前运行的应用程序签名块中的公钥将用于验证新更新的应用程序签名。更新时，不会验证运行中的应用程序签名，而是假定它有效。通过这种方式，系统建立了从当前运行的应用程序到新更新的应用程序之间的信任链。

因此，请务必确保烧录到设备的初始应用程序已签名。应用程序启动时会进行检查，如果没有找到签名，应用程序将中止，并且将无法再进行任何更新。若应用程序在未找到签名时仍继续更新，则可能导致设备损坏，后续任何更新都无法得到应用。应用程序应只包含一个位于第一位置的有效签名块。注意，不同于安全启动 v2，系统在启动时不会验证运行中的应用程序的签名，只会验证位于第一位置的签名块，并忽略其他附加的签名块。

虽然使用硬件安全启动时支持多个受信任的密钥，但如果配置了无需安全启动的签名检查，则仅使用签名块中的第一个公钥验证更新。如果需要多个受信任的公钥，必须启用完整的安全启动功能。

备注

若非确信未启用硬件安全启动的验证已满足应用程序的安全需要，建议使用完整的硬件安全启动。

### 启用已签名的应用程序验证

1. 打开 项目配置菜单 > `Security features`。

1. 确保 `App Signing Scheme` 设置为 `ECDSA (v2)`。

3. 启用 CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT。

4. 默认情况下，选择 `Sign binaries during build` 选项将启用 `Require signed app images` 功能，该功能会在构建过程中自动对二进制文件签名，在 `Secure Boot private signing key` 中指定的文件将用于镜像签名。

5. 如果禁用了 `Sign binaries during build` 选项，则必须按照 远程镜像签名 中的说明，手动签名所有应用程序二进制文件。

警告

注意，所有烧录的应用程序都必须经过签名，可以在构建过程中签名，也可以在构建后签名。

## 进阶功能

### JTAG 调试

启用安全启动模式时，eFuse 会默认禁用 JTAG。初次启动时，引导加载程序即禁用 JTAG 调试功能，并启用安全启动模式。

有关在启用安全启动或已签名应用程序验证的情况下使用 JTAG 调试的更多信息，请参阅 JTAG 与 flash 加密和安全引导。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。