Repository: esp-rs/espflash (language: rust)

Issue:
`espflash save-image` does not update the App Image SHA256 in the correct location.
When generating a project with secure boot v2, the bootloader is padded: https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32c3/security/secure-boot-v2.html#secure-padding

Using a padded image with `espflash save-image --bootloader path-to-bootloader ...` will read the file into a buffer:

`flasher/mod.rs`:
```
impl FlashData {
    pub fn new(...) -> Result<Self, Error> {
        // If the '--bootloader' option is provided, load the binary file at the
        // specified path.
        let bootloader = if let Some(path) = bootloader {
            let data = fs::canonicalize(path)
                .and_then(fs::read)
                .map_err(|e| Error::FileOpenError(path.display().to_string(), e))?;

            Some(data)
        } else {
            None
        };
```

Then that bootloader buffer gets passed to `image_format.rs` `IdfBootloaderFormat`. From there, the header is updated to match the args so the sha256 needs updating. However, the location of the sha256 is naively calculated as being the last 32 bytes of the bootloader file. However, it's actually the last 32 bytes of the bootloader without padding. 

`image_format.rs`:
```
        // re-calculate hash of the bootloader - needed since we modified the header
        let bootloader_len = bootloader.len(); // <- size of the file including padding
        let mut hasher = Sha256::new();
        hasher.update(&bootloader[..bootloader_len - 32]); //  <-- wrong location of sha256
        let hash = hasher.finalize();
        bootloader.to_mut()[bootloader_len - 32..].copy_from_slice(&hash);
```

One approach, which I'm not sure is the correct one due to all the different environments but worked in my specific case, was to walk through the bootloader segments while adding up the length. Then adding width the 16-byte aligned 1-byte checksum at the end. Maybe a better approach can be derived by referencing `esptool/bin_image.py` directly.

https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32c3/api-reference/system/app_image_format.html#application-image-structures

> The image has a single checksum byte after the last segment. This byte is written on a sixteen byte padded boundary, so the application image might need padding.

Without this, secure-boot-enabled ESPs will not boot because the first-stage bootloader will detect the mismatch between the actual and old unchanged SHA256.

This appears to be related but not the same as #713


Discussion / hints:
more details:

```
$ python -m esptool image_info --version 2 image.bin
esptool.py v4.8.1
File size: 45056 (bytes)
Detected image type: ESP32-C3

ESP32-C3 image header
=====================
Image version: 1
Entry point: 0x403cc710
Segments: 3
Flash size: 4MB
Flash freq: 40m
Flash mode: DIO

ESP32-C3 extended image header
==============================
WP pin: 0xee (disabled)
Flash pins drive settings: clk_drv: 0x0, q_drv: 0x0, d_drv: 0x0, cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0
Chip ID: 5 (ESP32-C3)
Minimal chip revision: v0.3, (legacy min_rev = 3)
Maximal chip revision: v1.99

Segments information
====================
Segment   Length   Load addr   File offs  Memory types
-------  -------  ----------  ----------  ------------
      0  0x03a84  0x3fcd5988  0x00000018  DRAM, BYTE_ACCESSIBLE
      1  0x00968  0x403cc710  0x00003aa4  IRAM
      2  0x05c00  0x403ce710  0x00004414  IRAM

ESP32-C3 image footer
=====================
Checksum: 0xb8 (valid)
Validation hash: a3e7ee9e006567278ecb31b65bfb2927138879a4c9b3b693d0f36815f2acf6be (valid)
```

snippet from `xxd` for a valid image file:
```
...
00009ff0: aa85 3766 cd3f 3795 cd3f 1306 86d8 1305  ..7f.?7..?......
0000a000: 4519 97c0 c2ff e780 a0d4 0565 1305 4560  E..........e..E`
0000a010: 45b7 8509 c9b7 9107 45bf 0000 0000 00b8  E.......E....... # <-- last byte of 16-byte aligned is 0xb8, the checksum
0000a020: a3e7 ee9e 0065 6727 8ecb 31b6 5bfb 2927  .....eg'..1.[.)' # <-- first half of SHA256
0000a030: 1388 79a4 c9b3 b693 d0f3 6815 f2ac f6be  ..y.......h..... # <-- second half of SHA256
0000a040: ffff ffff ffff ffff ffff ffff ffff ffff  ................ # <-- start of padding
0000a050: ffff ffff ffff ffff ffff ffff ffff ffff  ................
0000a060: ffff ffff ffff ffff ffff ffff ffff ffff  ................
0000a070: ffff ffff ffff ffff ffff ffff ffff ffff  ................
...
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 445ee4ca..fef4ab93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 
+- Update the app image SHA in the correct location for padded images (#715)
+
 ### Removed
 
 ## [3.3.0] - 2025-01-13
diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml
index 9dea8f38..39a3d614 100644
--- a/espflash/Cargo.toml
+++ b/espflash/Cargo.toml
@@ -43,7 +43,7 @@ directories = { version = "5.0.1", optional = true }
 env_logger = { version = "0.11.6", optional = true }
 esp-idf-part = "0.5.0"
 flate2 = "1.0.35"
-hex = { version = "0.4.3", features = ["serde"], optional = true }
+hex = { version = "0.4.3", features = ["serde"]}
 indicatif = { version = "0.17.9", optional = true }
 lazy_static = { version = "1.5.0", optional = true }
 log = "0.4.22"
@@ -79,7 +79,6 @@ cli = [
     "dep:dialoguer",
     "dep:directories",
     "dep:env_logger",
-    "dep:hex",
     "dep:indicatif",
     "dep:lazy_static",
     "dep:parse_int",
diff --git a/espflash/src/image_format.rs b/espflash/src/image_format.rs
index 2af83ece..96e2caf2 100644
--- a/espflash/src/image_format.rs
+++ b/espflash/src/image_format.rs
@@ -4,6 +4,7 @@ use std::{borrow::Cow, io::Write, iter::once, mem::size_of};
 
 use bytemuck::{bytes_of, from_bytes, Pod, Zeroable};
 use esp_idf_part::{Partition, PartitionTable, Type};
+use log::debug;
 use sha2::{Digest, Sha256};
 
 use crate::{
@@ -135,11 +136,22 @@ impl<'a> IdfBootloaderFormat<'a> {
         };
 
         // fetch the generated header from the bootloader
-        let mut header: ImageHeader = *from_bytes(&bootloader[0..size_of::<ImageHeader>()]);
+        let mut calc_bootloader_size = 0;
+        let bootloader_header_size = size_of::<ImageHeader>();
+        calc_bootloader_size += bootloader_header_size;
+        let mut header: ImageHeader = *from_bytes(&bootloader[0..bootloader_header_size]);
         if header.magic != ESP_MAGIC {
             return Err(Error::InvalidBootloader);
         }
 
+        for _ in 0..header.segment_count {
+            let segment: SegmentHeader = *from_bytes(
+                &bootloader
+                    [calc_bootloader_size..calc_bootloader_size + size_of::<SegmentHeader>()],
+            );
+            calc_bootloader_size += segment.length as usize + size_of::<SegmentHeader>();
+        }
+
         // update the header if a user has specified any custom arguments
         if let Some(mode) = flash_settings.mode {
             header.flash_mode = mode as u8;
@@ -156,12 +168,26 @@ impl<'a> IdfBootloaderFormat<'a> {
             bytes_of(&header).iter().copied(),
         );
 
-        // re-calculate hash of the bootloader - needed since we modified the header
-        let bootloader_len = bootloader.len();
+        // The header was modified so we need to recalculate the hash of the
+        // bootloader. The hash is at the end of the bootloader segments and
+        // 1-byte checksum at the end of a 16-byte padded boundary.
+        //
+        // Source: Point 3 of https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32c3/api-reference/system/app_image_format.html
+        calc_bootloader_size += 1; // add checksum size
+        calc_bootloader_size = calc_bootloader_size + ((16 - (calc_bootloader_size % 16)) % 16);
+        let bootloader_sha_start = calc_bootloader_size;
+        calc_bootloader_size += 32; // add sha256 size
+        let bootloader_sha_end = calc_bootloader_size;
+
         let mut hasher = Sha256::new();
-        hasher.update(&bootloader[..bootloader_len - 32]);
+        hasher.update(&bootloader[..bootloader_sha_start]);
         let hash = hasher.finalize();
-        bootloader.to_mut()[bootloader_len - 32..].copy_from_slice(&hash);
+        debug!(
+            "Updating bootloader SHA256 from {} to {}",
+            hex::encode(&bootloader[bootloader_sha_start..bootloader_sha_end]),
+            hex::encode(hash)
+        );
+        bootloader.to_mut()[bootloader_sha_start..bootloader_sha_end].copy_from_slice(&hash);
 
         // write the header of the app
         // use the same settings as the bootloader


Test patch: