Skip to main content

mcumgr_toolkit/mcuboot/image/
mod.rs

1use std::io;
2
3/// The firmware version
4#[derive(Debug, Clone, Copy, Eq, PartialEq)]
5pub struct ImageVersion {
6    /// Major version
7    pub major: u8,
8    /// Minor version
9    pub minor: u8,
10    /// Revision
11    pub revision: u16,
12    /// Build number
13    pub build_num: u32,
14}
15impl std::fmt::Display for ImageVersion {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "{}.{}.{}", self.major, self.minor, self.revision)?;
18        if self.build_num != 0 {
19            write!(f, ".{}", self.build_num)?;
20        }
21        Ok(())
22    }
23}
24
25/// The hash id of a firmware image
26#[derive(Debug, Clone, Copy, Eq, PartialEq)]
27pub enum ImageHashId {
28    Sha256([u8; SHA256_LEN]),
29    Sha384([u8; SHA384_LEN]),
30    Sha512([u8; SHA512_LEN]),
31}
32
33impl ImageHashId {
34    /// The hash type, as a human readable string
35    pub fn get_hash_type(&self) -> &'static str {
36        match self {
37            ImageHashId::Sha256(_) => "SHA256",
38            ImageHashId::Sha384(_) => "SHA384",
39            ImageHashId::Sha512(_) => "SHA512",
40        }
41    }
42}
43
44impl From<ImageHashId> for Vec<u8> {
45    fn from(hash: ImageHashId) -> Self {
46        match hash {
47            ImageHashId::Sha256(val) => val.into(),
48            ImageHashId::Sha384(val) => val.into(),
49            ImageHashId::Sha512(val) => val.into(),
50        }
51    }
52}
53
54impl From<ImageHashId> for Box<[u8]> {
55    fn from(hash: ImageHashId) -> Self {
56        match hash {
57            ImageHashId::Sha256(val) => val.into(),
58            ImageHashId::Sha384(val) => val.into(),
59            ImageHashId::Sha512(val) => val.into(),
60        }
61    }
62}
63
64impl AsRef<[u8]> for ImageHashId {
65    fn as_ref(&self) -> &[u8] {
66        match self {
67            ImageHashId::Sha256(val) => val,
68            ImageHashId::Sha384(val) => val,
69            ImageHashId::Sha512(val) => val,
70        }
71    }
72}
73
74/// Information about an MCUboot firmware image
75#[derive(Debug, Clone, Copy, Eq, PartialEq)]
76pub struct ImageInfo {
77    /// Firmware version
78    pub version: ImageVersion,
79    /// The identifying hash for the firmware
80    ///
81    /// Note that this will not be the same as the SHA256 of the whole file, it is the field in the
82    /// MCUboot TLV section that contains a hash of the data which is used for signature
83    /// verification purposes.
84    pub hash: ImageHashId,
85}
86
87/// Possible error values of [`get_image_info`].
88#[derive(thiserror::Error, Debug, miette::Diagnostic)]
89pub enum ImageParseError {
90    /// The given image file is not an MCUboot image.
91    #[error("Image is not an MCUboot image")]
92    #[diagnostic(code(mcumgr_toolkit::mcuboot::image::unknown_type))]
93    UnknownImageType,
94    /// The given image file does not contain TLV entries.
95    #[error("Image does not contain TLV entries")]
96    #[diagnostic(code(mcumgr_toolkit::mcuboot::image::tlv_missing))]
97    TlvMissing,
98    /// The given image file does not contain an SHA hash id.
99    #[error("Image does not contain an SHA hash id")]
100    #[diagnostic(code(mcumgr_toolkit::mcuboot::image::hash_id_missing))]
101    HashIdMissing,
102    /// Failed to read from the image
103    #[error("Image read failed")]
104    #[diagnostic(code(mcumgr_toolkit::mcuboot::image::read))]
105    ReadFailed(#[from] std::io::Error),
106}
107
108fn read_u32(data: &mut dyn std::io::Read) -> Result<u32, std::io::Error> {
109    let mut bytes = [0u8; 4];
110    data.read_exact(&mut bytes)?;
111    Ok(u32::from_le_bytes(bytes))
112}
113
114fn read_u16(data: &mut dyn std::io::Read) -> Result<u16, std::io::Error> {
115    let mut bytes = [0u8; 2];
116    data.read_exact(&mut bytes)?;
117    Ok(u16::from_le_bytes(bytes))
118}
119
120fn read_u8(data: &mut dyn std::io::Read) -> Result<u8, std::io::Error> {
121    let mut byte = 0u8;
122    data.read_exact(std::slice::from_mut(&mut byte))?;
123    Ok(byte)
124}
125
126/// The identifying header of an MCUboot image
127const IMAGE_MAGIC: u32 = 0x96f3b83d;
128const IMAGE_TLV_INFO_MAGIC: u16 = 0x6907;
129const IMAGE_TLV_SHA256: u16 = 0x10;
130const IMAGE_TLV_SHA384: u16 = 0x11;
131const IMAGE_TLV_SHA512: u16 = 0x12;
132const SHA256_LEN: usize = 32;
133const SHA384_LEN: usize = 48;
134const SHA512_LEN: usize = 64;
135const TLV_INFO_HEADER_SIZE: u32 = 4;
136const TLV_ELEMENT_HEADER_SIZE: u32 = 4;
137
138/// Extract information from an MCUboot image file
139pub fn get_image_info(
140    mut image_data: impl io::Read + io::Seek,
141) -> Result<ImageInfo, ImageParseError> {
142    let image_data = &mut image_data;
143
144    let ih_magic = read_u32(image_data)?;
145    log::debug!("ih_magic: 0x{ih_magic:08x}");
146    if ih_magic != IMAGE_MAGIC {
147        return Err(ImageParseError::UnknownImageType);
148    }
149
150    let ih_load_addr = read_u32(image_data)?;
151    log::debug!("ih_load_addr: 0x{ih_load_addr:08x}");
152
153    let ih_hdr_size = read_u16(image_data)?;
154    log::debug!("ih_hdr_size: 0x{ih_hdr_size:04x}");
155
156    let ih_protect_tlv_size = read_u16(image_data)?;
157    log::debug!("ih_protect_tlv_size: 0x{ih_protect_tlv_size:04x}");
158
159    let ih_img_size = read_u32(image_data)?;
160    log::debug!("ih_img_size: 0x{ih_img_size:08x}");
161
162    let ih_flags = read_u32(image_data)?;
163    log::debug!("ih_flags: 0x{ih_flags:08x}");
164
165    let ih_ver = ImageVersion {
166        major: read_u8(image_data)?,
167        minor: read_u8(image_data)?,
168        revision: read_u16(image_data)?,
169        build_num: read_u32(image_data)?,
170    };
171    log::debug!("ih_ver: {ih_ver:?}");
172
173    image_data.seek(io::SeekFrom::Start(
174        u64::from(ih_hdr_size) + u64::from(ih_protect_tlv_size) + u64::from(ih_img_size),
175    ))?;
176
177    let it_magic = match read_u16(image_data) {
178        Ok(val) => val,
179        Err(e) => {
180            if e.kind() == std::io::ErrorKind::UnexpectedEof {
181                return Err(ImageParseError::TlvMissing);
182            }
183            return Err(e.into());
184        }
185    };
186    log::debug!("it_magic: 0x{it_magic:04x}");
187    if it_magic != IMAGE_TLV_INFO_MAGIC {
188        return Err(ImageParseError::TlvMissing);
189    }
190
191    let it_tlv_tot = read_u16(image_data)?;
192    log::debug!("it_tlv_tot: 0x{it_tlv_tot:04x}");
193
194    let mut id_hash = None;
195    {
196        let mut tlv_read: u32 = 0;
197        // Loop while at least one tlv header can still be read
198        while tlv_read + TLV_INFO_HEADER_SIZE + TLV_ELEMENT_HEADER_SIZE <= u32::from(it_tlv_tot) {
199            let it_type = read_u16(image_data)?;
200            let it_len = read_u16(image_data)?;
201
202            if it_type == IMAGE_TLV_SHA256 && usize::from(it_len) == SHA256_LEN {
203                let mut sha256_hash = [0u8; SHA256_LEN];
204                image_data.read_exact(&mut sha256_hash)?;
205                id_hash = Some(ImageHashId::Sha256(sha256_hash));
206            } else if it_type == IMAGE_TLV_SHA384 && usize::from(it_len) == SHA384_LEN {
207                let mut sha384_hash = [0u8; SHA384_LEN];
208                image_data.read_exact(&mut sha384_hash)?;
209                id_hash = Some(ImageHashId::Sha384(sha384_hash));
210            } else if it_type == IMAGE_TLV_SHA512 && usize::from(it_len) == SHA512_LEN {
211                let mut sha512_hash = [0u8; SHA512_LEN];
212                image_data.read_exact(&mut sha512_hash)?;
213                id_hash = Some(ImageHashId::Sha512(sha512_hash));
214            } else {
215                image_data.seek_relative(it_len.into())?;
216            }
217
218            log::debug!("- it_type: 0x{it_type:04x}, it_len: 0x{it_len:04x}");
219            tlv_read += u32::from(it_len) + 4;
220        }
221    }
222
223    if let Some(id_hash) = id_hash {
224        Ok(ImageInfo {
225            version: ih_ver,
226            hash: id_hash,
227        })
228    } else {
229        Err(ImageParseError::HashIdMissing)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::{ImageHashId, ImageParseError, ImageVersion, get_image_info};
236    use std::io::{self, Cursor, Read, Seek, SeekFrom};
237
238    const IMAGE_MAGIC: u32 = 0x96f3_b83d;
239    const IMAGE_HEADER_SIZE: usize = 32;
240
241    const IMAGE_TLV_INFO_MAGIC: u16 = 0x6907;
242    const IMAGE_TLV_PROT_INFO_MAGIC: u16 = 0x6908;
243
244    const IMAGE_TLV_KEYHASH: u16 = 0x01;
245    const IMAGE_TLV_SHA256: u16 = 0x10;
246    const IMAGE_TLV_SHA384: u16 = 0x11;
247    const IMAGE_TLV_SHA512: u16 = 0x12;
248    const IMAGE_TLV_ECDSA_SIG: u16 = 0x22;
249    const IMAGE_TLV_SEC_CNT: u16 = 0x50;
250
251    fn tlv(ty: u16, value: &[u8]) -> Vec<u8> {
252        let mut out = Vec::with_capacity(4 + value.len());
253        out.extend_from_slice(&ty.to_le_bytes());
254        out.extend_from_slice(&(value.len() as u16).to_le_bytes());
255        out.extend_from_slice(value);
256        out
257    }
258
259    fn tlv_area(info_magic: u16, entries: &[Vec<u8>]) -> Vec<u8> {
260        let total_len: usize = 4 + entries.iter().map(Vec::len).sum::<usize>();
261        let mut out = Vec::with_capacity(total_len);
262        out.extend_from_slice(&info_magic.to_le_bytes());
263        out.extend_from_slice(&(total_len as u16).to_le_bytes());
264        for entry in entries {
265            out.extend_from_slice(entry);
266        }
267        out
268    }
269
270    fn image_header(
271        image_magic: u32,
272        hdr_size: u16,
273        protect_tlv_size: u16,
274        img_size: u32,
275        version: ImageVersion,
276    ) -> Vec<u8> {
277        let mut out = Vec::with_capacity(IMAGE_HEADER_SIZE);
278        out.extend_from_slice(&image_magic.to_le_bytes()); // ih_magic
279        out.extend_from_slice(&0x1122_3344u32.to_le_bytes()); // ih_load_addr
280        out.extend_from_slice(&hdr_size.to_le_bytes()); // ih_hdr_size
281        out.extend_from_slice(&protect_tlv_size.to_le_bytes()); // ih_protect_tlv_size
282        out.extend_from_slice(&img_size.to_le_bytes()); // ih_img_size
283        out.extend_from_slice(&0x5566_7788u32.to_le_bytes()); // ih_flags
284
285        out.push(version.major);
286        out.push(version.minor);
287        out.extend_from_slice(&version.revision.to_le_bytes());
288        out.extend_from_slice(&version.build_num.to_le_bytes());
289
290        out.extend_from_slice(&0u32.to_le_bytes()); // _pad1
291        assert_eq!(out.len(), IMAGE_HEADER_SIZE);
292        out
293    }
294
295    fn build_image(
296        image_magic: u32,
297        hdr_size: u16,
298        version: ImageVersion,
299        payload: &[u8],
300        protected_tlv_area: Option<Vec<u8>>,
301        regular_tlv_area: Option<Vec<u8>>,
302    ) -> Vec<u8> {
303        let protected_tlv_size = protected_tlv_area
304            .as_ref()
305            .map(|v| v.len() as u16)
306            .unwrap_or(0);
307
308        let mut out = image_header(
309            image_magic,
310            hdr_size,
311            protected_tlv_size,
312            payload.len() as u32,
313            version,
314        );
315
316        if hdr_size as usize > IMAGE_HEADER_SIZE {
317            out.resize(hdr_size as usize, 0xAA);
318        }
319
320        out.extend_from_slice(payload);
321
322        if let Some(protected) = protected_tlv_area {
323            out.extend_from_slice(&protected);
324        }
325        if let Some(regular) = regular_tlv_area {
326            out.extend_from_slice(&regular);
327        }
328
329        out
330    }
331
332    #[test]
333    fn image_version_display_omits_zero_build_number() {
334        let version = ImageVersion {
335            major: 1,
336            minor: 2,
337            revision: 345,
338            build_num: 0,
339        };
340
341        assert_eq!(version.to_string(), "1.2.345");
342    }
343
344    #[test]
345    fn image_version_display_includes_nonzero_build_number() {
346        let version = ImageVersion {
347            major: 1,
348            minor: 2,
349            revision: 345,
350            build_num: 6789,
351        };
352
353        assert_eq!(version.to_string(), "1.2.345.6789");
354    }
355
356    #[test]
357    fn image_hash_id_reports_human_readable_hash_type() {
358        assert_eq!(ImageHashId::Sha256([0x11; 32]).get_hash_type(), "SHA256");
359        assert_eq!(ImageHashId::Sha384([0x22; 48]).get_hash_type(), "SHA384");
360        assert_eq!(ImageHashId::Sha512([0x33; 64]).get_hash_type(), "SHA512");
361    }
362
363    #[test]
364    fn image_hash_id_converts_to_vec_box_and_slice() {
365        let sha256 = ImageHashId::Sha256([0xA5; 32]);
366        let sha384 = ImageHashId::Sha384([0xB6; 48]);
367        let sha512 = ImageHashId::Sha512([0xC7; 64]);
368
369        let sha256_vec: Vec<u8> = sha256.into();
370        let sha384_box: Box<[u8]> = sha384.into();
371
372        assert_eq!(sha256_vec, vec![0xA5; 32]);
373        assert_eq!(&*sha384_box, &[0xB6; 48]);
374        assert_eq!(sha512.as_ref(), &[0xC7; 64]);
375    }
376
377    #[test]
378    fn parses_sha256_image_and_scans_past_other_unprotected_tlvs() {
379        let version = ImageVersion {
380            major: 7,
381            minor: 9,
382            revision: 0x1234,
383            build_num: 0x89AB_CDEF,
384        };
385        let payload = b"payload-bytes";
386        let hash = [0x10; 32];
387
388        let regular_tlv_area = tlv_area(
389            IMAGE_TLV_INFO_MAGIC,
390            &[
391                tlv(IMAGE_TLV_KEYHASH, &[0x01, 0x02, 0x03, 0x04]),
392                tlv(IMAGE_TLV_SHA256, &hash),
393                tlv(IMAGE_TLV_ECDSA_SIG, &[0x55; 8]),
394            ],
395        );
396
397        let image = build_image(
398            IMAGE_MAGIC,
399            IMAGE_HEADER_SIZE as u16,
400            version,
401            payload,
402            None,
403            Some(regular_tlv_area),
404        );
405
406        let info = get_image_info(Cursor::new(image)).expect("valid SHA256 image should parse");
407        assert_eq!(info.version, version);
408        assert_eq!(info.hash, ImageHashId::Sha256(hash));
409    }
410
411    #[test]
412    fn parses_sha384_image_and_respects_hdr_size_for_payload_offset() {
413        let version = ImageVersion {
414            major: 3,
415            minor: 4,
416            revision: 0xBEEF,
417            build_num: 0x0102_0304,
418        };
419        let payload = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
420        let hash = [0x44; 48];
421
422        let regular_tlv_area = tlv_area(IMAGE_TLV_INFO_MAGIC, &[tlv(IMAGE_TLV_SHA384, &hash)]);
423
424        // Use a non-default header size to verify the parser honors ih_hdr_size
425        // instead of assuming the fixed 32-byte struct size.
426        let image = build_image(
427            IMAGE_MAGIC,
428            64,
429            version,
430            &payload,
431            None,
432            Some(regular_tlv_area),
433        );
434
435        let info = get_image_info(Cursor::new(image)).expect("valid SHA384 image should parse");
436        assert_eq!(info.version, version);
437        assert_eq!(info.hash, ImageHashId::Sha384(hash));
438    }
439
440    #[test]
441    fn parses_sha512_image_after_protected_tlv_block() {
442        let version = ImageVersion {
443            major: 5,
444            minor: 6,
445            revision: 0x2468,
446            build_num: 0x1357_9BDF,
447        };
448        let payload = b"firmware";
449        let hash = [0x77; 64];
450
451        let protected_tlv_area = tlv_area(
452            IMAGE_TLV_PROT_INFO_MAGIC,
453            &[tlv(IMAGE_TLV_SEC_CNT, &[0x05, 0x00, 0x00, 0x00])],
454        );
455
456        let regular_tlv_area = tlv_area(
457            IMAGE_TLV_INFO_MAGIC,
458            &[
459                tlv(IMAGE_TLV_KEYHASH, &[0xAA; 16]),
460                tlv(IMAGE_TLV_SHA512, &hash),
461            ],
462        );
463
464        let image = build_image(
465            IMAGE_MAGIC,
466            IMAGE_HEADER_SIZE as u16,
467            version,
468            payload,
469            Some(protected_tlv_area),
470            Some(regular_tlv_area),
471        );
472
473        let info = get_image_info(Cursor::new(image))
474            .expect("valid image with protected TLVs should parse");
475        assert_eq!(info.version, version);
476        assert_eq!(info.hash, ImageHashId::Sha512(hash));
477    }
478
479    #[test]
480    fn rejects_non_mcuboot_magic() {
481        let version = ImageVersion {
482            major: 1,
483            minor: 0,
484            revision: 1,
485            build_num: 1,
486        };
487        let payload = b"x";
488        let regular_tlv_area =
489            tlv_area(IMAGE_TLV_INFO_MAGIC, &[tlv(IMAGE_TLV_SHA256, &[0x11; 32])]);
490
491        let image = build_image(
492            0x0000_0000,
493            IMAGE_HEADER_SIZE as u16,
494            version,
495            payload,
496            None,
497            Some(regular_tlv_area),
498        );
499
500        let err = get_image_info(Cursor::new(image)).unwrap_err();
501        assert!(matches!(err, ImageParseError::UnknownImageType));
502    }
503
504    #[test]
505    fn rejects_image_without_any_tlv_info_header() {
506        let version = ImageVersion {
507            major: 9,
508            minor: 9,
509            revision: 9,
510            build_num: 9,
511        };
512        let payload = b"no tlvs here";
513
514        let image = build_image(
515            IMAGE_MAGIC,
516            IMAGE_HEADER_SIZE as u16,
517            version,
518            payload,
519            None,
520            None,
521        );
522
523        let err = get_image_info(Cursor::new(image)).unwrap_err();
524        assert!(matches!(err, ImageParseError::TlvMissing));
525    }
526
527    #[test]
528    fn rejects_protected_tlv_block_without_following_regular_tlv_info_header() {
529        let version = ImageVersion {
530            major: 2,
531            minor: 1,
532            revision: 0x0102,
533            build_num: 3,
534        };
535        let payload = b"abc";
536
537        let protected_tlv_area = tlv_area(
538            IMAGE_TLV_PROT_INFO_MAGIC,
539            &[tlv(IMAGE_TLV_SEC_CNT, &[1, 0, 0, 0])],
540        );
541
542        // Per the spec, if IMAGE_TLV_PROT_INFO_MAGIC is present, a normal
543        // IMAGE_TLV_INFO_MAGIC block must follow after ih_protect_tlv_size bytes.
544        let image = build_image(
545            IMAGE_MAGIC,
546            IMAGE_HEADER_SIZE as u16,
547            version,
548            payload,
549            Some(protected_tlv_area),
550            None,
551        );
552
553        let err = get_image_info(Cursor::new(image)).unwrap_err();
554        assert!(matches!(err, ImageParseError::TlvMissing));
555    }
556
557    #[test]
558    fn rejects_image_with_wrong_tlv_info_magic() {
559        let version = ImageVersion {
560            major: 1,
561            minor: 2,
562            revision: 3,
563            build_num: 4,
564        };
565        let image = build_image(
566            IMAGE_MAGIC,
567            IMAGE_HEADER_SIZE as u16,
568            version,
569            b"x",
570            None,
571            Some(tlv_area(0xFFFF, &[tlv(IMAGE_TLV_SHA256, &[0x11; 32])])),
572        );
573
574        let err = get_image_info(Cursor::new(image)).unwrap_err();
575        assert!(matches!(err, ImageParseError::TlvMissing));
576    }
577
578    #[test]
579    fn rejects_image_with_tlv_area_but_without_any_supported_hash_tlv() {
580        let version = ImageVersion {
581            major: 8,
582            minor: 1,
583            revision: 2,
584            build_num: 3,
585        };
586        let payload = b"firmware";
587        let regular_tlv_area = tlv_area(
588            IMAGE_TLV_INFO_MAGIC,
589            &[
590                tlv(IMAGE_TLV_KEYHASH, &[0x11; 16]),
591                tlv(IMAGE_TLV_ECDSA_SIG, &[0x22; 8]),
592            ],
593        );
594
595        let image = build_image(
596            IMAGE_MAGIC,
597            IMAGE_HEADER_SIZE as u16,
598            version,
599            payload,
600            None,
601            Some(regular_tlv_area),
602        );
603
604        let err = get_image_info(Cursor::new(image)).unwrap_err();
605        assert!(matches!(err, ImageParseError::HashIdMissing));
606    }
607
608    #[test]
609    fn rejects_sha256_tlv_with_wrong_payload_length() {
610        // Type 0x10 with 16 bytes instead of the required 32 must be skipped,
611        // not parsed as a valid hash → HashIdMissing.
612        let version = ImageVersion {
613            major: 1,
614            minor: 0,
615            revision: 0,
616            build_num: 0,
617        };
618        let regular_tlv_area = tlv_area(
619            IMAGE_TLV_INFO_MAGIC,
620            &[tlv(IMAGE_TLV_SHA256, &[0x42u8; 16])], // wrong length
621        );
622        let image = build_image(
623            IMAGE_MAGIC,
624            IMAGE_HEADER_SIZE as u16,
625            version,
626            b"payload",
627            None,
628            Some(regular_tlv_area),
629        );
630        let err = get_image_info(Cursor::new(image)).unwrap_err();
631        assert!(matches!(err, ImageParseError::HashIdMissing));
632    }
633
634    struct FailingReader;
635
636    impl Read for FailingReader {
637        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
638            Err(io::Error::other("injected read failure"))
639        }
640    }
641
642    impl Seek for FailingReader {
643        fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
644            Ok(0)
645        }
646    }
647
648    #[test]
649    fn propagates_io_failures_as_read_failed() {
650        let err = get_image_info(FailingReader).unwrap_err();
651
652        match err {
653            ImageParseError::ReadFailed(inner) => {
654                assert_eq!(inner.kind(), io::ErrorKind::Other);
655                assert_eq!(inner.to_string(), "injected read failure");
656            }
657            other => panic!("expected ReadFailed, got {other:?}"),
658        }
659    }
660}