Skip to main content

Secure Signing

Security is one of the most important aspects of any IoT solution and securing the device is a key consideration for this. The ESP32 series of SoCs (e.g. ESP32, ESP32-S2, ESP32-C3, ESP32-C6, etc.) support secure boot to ensure that only a valid, signed firmware can run on them. To understand more about secure boot, please refer to the ESP IDF Documentation. The docs here will just focus on steps to enable secure boot using ESP RainMaker's secure signing feature without going into the working details of secure boot itself.

Creating the signing key

The first step for secure signing is to create the key. This can be done from the RainMaker Dashboard. Just browse to the Key Management section and click on "Generate New Key" on top right. Provide a name and description of your choice, select the algorithm and generate. Algorithm would generally be RSA-3072 for secure boot v2. Please check the IDF docs to confirm what is valid for your platform.

Once the key is generated, it will start showing in the list along with some additional details.

Creating the images

To create the images for secure boot, appropriate configuration options have to be enabled in the sdkconfig.

  • Enable hardware secure boot (CONFIG_SECURE_BOOT) using: idf.py menuconfig -> Security features -> Enable hardware Secure Boot in bootloader (READ DOCS FIRST) -> y
  • Disable CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES since the signing will be done later in the cloud. Use idf.py menuconfig -> Security features -> Sign binaries during build -> n
  • You can optionally also enable flash encryption (CONFIG_SECURE_FLASH_ENC_ENABLED) using idf.py menuconfig -> Security features -> Enable flash encryption on boot
  • Change the partition table offset (CONFIG_PARTITION_TABLE_OFFSET) to 0xC000 using idf.py menuconfig -> Partition Table -> Offset of partition table
  • Choose partitions_4mb_optimised.csv as the partition table (CONFIG_PARTITION_TABLE_CUSTOM_FILENAME) using idf.py menuconfig -> Partition Table -> Custom partition CSV file. This is required because the default RainMaker partition sizes are not sufficient. The image sizes increase due to the additional code pulled in for secure boot and the padding and signature being appended.
  • Build the project using idf.py build

Signing Images

For enabling secure boot, the bootloader as well as the firmware image needs to be signed. Thereafter, for OTA firmware upgrades, only the firmware image needs to be signed.

After compiling the project, these can be found in the project folder at

  • Firmware: build/<project_name>.bin
  • Bootloader: build/bootloader/bootloader.bin

Follow these steps to sign the images:

  • On the RainMaker dashboard, browse to the Firmware Images section and click on "Add Image" on top right

  • Give a friendly name and device Type.

  • Click on Advanced to see the secure signing options.

  • Choose the key created in earlier steps from the dropdown list

  • Select the bootloader image (required only while enabling secure boot, not for subsequent OTA jobs)

  • Click on Add Image and wait

  • The firmware and bootloader images will be uploaded and signed. If everything goes well, you will see a message like below.

  • Clicking on "Show signed image details" will show the images which can be downloaded for flashing.

  • These same images can also be downloaded from the Image details page which can be reached by clicking on the Image name under the "Firmware Images" page.

Enabling Secure Boot

Once the signed images are available, flash these along with other binaries like the partition table, ota_data_initial.bin, fctry binary, etc. Sample invocation:

python -m esptool --chip esp32c3 -b 460800 --before default_reset --after no_reset --no-stub -p /dev/tty.usbserial-110 write_flash --flash_mode dio --flash_size keep --flash_freq 80m 0xc000 build/partition_table/partition-table.bin 0x16000 build/ota_data_initial.bin 0x20000 secure_boot/c3/Secure\ Boot\ Light\ C3_signed.bin 0x0 secure_boot/c3/bootloader_signed.bin

Note that secure_boot/c3/Secure\ Boot\ Light\ C3_signed.bin and secure_boot/c3/bootloader_signed.bin are the signed images downloaded in previous steps.

Once the device boots up for the first time, the logs should show something like this, indicating that secure boot v2 is enabled.

I (146) esp_image: segment 0: paddr=00020020 vaddr=3c130020 size=4d7e8h (317416) map
I (207) esp_image: segment 1: paddr=0006d810 vaddr=3fc97000 size=02808h ( 10248) load
I (209) esp_image: segment 2: paddr=00070020 vaddr=42000020 size=1206cch (1181388) map
I (408) esp_image: segment 3: paddr=001906f4 vaddr=3fc99808 size=00f78h ( 3960) load
I (410) esp_image: segment 4: paddr=00191674 vaddr=40380000 size=16e2ch ( 93740) load
I (433) esp_image: segment 5: paddr=001a84a8 vaddr=00000000 size=07b28h ( 31528)
I (438) esp_image: Verifying image signature...
I (439) secure_boot_v2: Secure boot V2 is not enabled yet and eFuse digest keys are not set
I (444) secure_boot_v2: Verifying with RSA-PSS...
I (452) secure_boot_v2: Signature verified successfully!
I (462) boot: Loaded app from partition at offset 0x20000
I (502) boot: Set actual ota_seq=1 in otadata[0]
I (502) secure_boot_v2: enabling secure boot v2...
I (503) efuse: Batch mode of writing fields is enabled
I (508) esp_image: segment 0: paddr=00000020 vaddr=3fcd5990 size=032f4h ( 13044)
I (518) esp_image: segment 1: paddr=0000331c vaddr=403cc710 size=00b9ch ( 2972)
I (524) esp_image: segment 2: paddr=00003ec0 vaddr=403ce710 size=05330h ( 21296)
I (535) esp_image: Verifying image signature...
I (538) secure_boot_v2: Secure boot V2 is not enabled yet and eFuse digest keys are not set
I (547) secure_boot_v2: Verifying with RSA-PSS...
I (555) secure_boot_v2: Signature verified successfully!
I (558) secure_boot_v2: Secure boot digests absent, generating..
I (571) secure_boot_v2: Digests successfully calculated, 1 valid signatures (image offset 0x0)
I (574) secure_boot_v2: 1 signature block(s) found appended to the bootloader.
I (581) secure_boot_v2: Burning public key hash to eFuse
I (590) efuse: Writing EFUSE_BLK_KEY0 with purpose 9
I (741) secure_boot_v2: Digests successfully calculated, 1 valid signatures (image offset 0x20000)
I (741) secure_boot_v2: 1 signature block(s) found appended to the app.
I (747) secure_boot_v2: Application key(0) matches with bootloader key(0).
I (754) secure_boot_v2: Revoking empty key digest slot (1)...
I (761) secure_boot_v2: Revoking empty key digest slot (2)...
I (767) secure_boot_v2: blowing secure boot efuse...
I (773) secure_boot: Enabling Security download mode...
I (779) secure_boot: Disable hardware & software JTAG...
I (785) secure_boot: Prevent read disabling of additional efuses...
I (793) efuse: BURN BLOCK4
I (799) efuse: BURN BLOCK4 - OK (write block == read block)
I (801) efuse: BURN BLOCK0
I (807) efuse: BURN BLOCK0 - OK (all write block bits are set)
I (811) efuse: Batch mode. Prepared fields are committed
I (817) secure_boot_v2: Secure boot permanently enabled

Subsequent boots will show limited prints indicating that secure boot v2 is enabled.

I (180) esp_image: segment 0: paddr=00020020 vaddr=3c130020 size=4d7e8h (317416) map
I (241) esp_image: segment 1: paddr=0006d810 vaddr=3fc97000 size=02808h ( 10248) load
I (244) esp_image: segment 2: paddr=00070020 vaddr=42000020 size=1206cch (1181388) map
I (443) esp_image: segment 3: paddr=001906f4 vaddr=3fc99808 size=00f78h ( 3960) load
I (444) esp_image: segment 4: paddr=00191674 vaddr=40380000 size=16e2ch ( 93740) load
I (467) esp_image: segment 5: paddr=001a84a8 vaddr=00000000 size=07b28h ( 31528)
I (472) esp_image: Verifying image signature...
I (473) secure_boot_v2: Verifying with RSA-PSS...
I (477) secure_boot_v2: Signature verified successfully!
I (487) boot: Loaded app from partition at offset 0x20000
I (487) secure_boot_v2: enabling secure boot v2...
I (492) secure_boot_v2: secure boot v2 is already enabled, continuing..

Secure Boot Key Digest

Once secure boot is enabled, the RainMaker firmware reports a sha256 checksum of the secure boot public key's digest in the node config under info.secure_boot_digest as shown below

{
"node_id": "1091A8AABBCC",
"config_version": "2020-03-20",
"info": {
"name": "ESP RainMaker Device",
"fw_version": "1.1",
"type": "Lightbulb",
"model": "led_light",
"project_name": "led_light",
"platform": "esp32c3",
"secure_boot_digest": {
"k0": "a1d634140fc4922639da85206fe73de6fee9b3c62319318cf7545cf79f200faa",
"k1": null,
"k2": null
}
}
}

This can be used by the backend to find out the signing key used for this node, which is useful for OTA Upgrades.

OTA Upgrades

For OTA Upgrades, the firmware image should be built as per the instructions above, but these can be uploaded without selecting a signing key. Once uploaded, click on the "Start OTA" button on the right in the Firmware Images list. In the popup, give OTA Job name, select the Node/Group and select the "Secure Boot" checkbox.

Thereafter, you can use one of these 2 options:

  1. Auto select key: This can be used only for jobs without "Force Push". In this method, whenever the node queries for an OTA, the backend finds the corresponding signing key for the node (based on the digest reported in node config), signs the image using that key and then sends the URL. This is especially useful since it avoids any issues of wrong keys being used for signing. It can also help in cases wherein different nodes in the job have different signing keys, but the same firmware.

  2. Manual signing: This can be used for any OTA Job, including force push. In this method, choose the signing key from the dropdown. The backend will use this key for signing the image and then send the URL to the nodes. Backend will not check the digest reported by the node in this case.

Choose the correct option based on your use case and start the OTA. Under the OTA details page, you can see Secure boot: yes and also view signed image details. It will also indicate auto select key if applicable.

The OTA status can be seen on the same page, similar to any other OTA Job.