1use std::io;
2
3#[derive(Debug, Clone, Copy, Eq, PartialEq)]
5pub struct ImageVersion {
6 pub major: u8,
8 pub minor: u8,
10 pub revision: u16,
12 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#[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 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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
76pub struct ImageInfo {
77 pub version: ImageVersion,
79 pub hash: ImageHashId,
85}
86
87#[derive(thiserror::Error, Debug, miette::Diagnostic)]
89pub enum ImageParseError {
90 #[error("Image is not an MCUboot image")]
92 #[diagnostic(code(mcumgr_toolkit::mcuboot::image::unknown_type))]
93 UnknownImageType,
94 #[error("Image does not contain TLV entries")]
96 #[diagnostic(code(mcumgr_toolkit::mcuboot::image::tlv_missing))]
97 TlvMissing,
98 #[error("Image does not contain an SHA hash id")]
100 #[diagnostic(code(mcumgr_toolkit::mcuboot::image::hash_id_missing))]
101 HashIdMissing,
102 #[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
126const 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
138pub 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 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()); out.extend_from_slice(&0x1122_3344u32.to_le_bytes()); out.extend_from_slice(&hdr_size.to_le_bytes()); out.extend_from_slice(&protect_tlv_size.to_le_bytes()); out.extend_from_slice(&img_size.to_le_bytes()); out.extend_from_slice(&0x5566_7788u32.to_le_bytes()); 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()); 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(®ular);
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 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 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 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])], );
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}