use super::{FormatType, SignableArtifact};
use crate::WSError;
use sha2::{Digest, Sha256};
use std::io::Write;
const MCUBOOT_MAGIC: [u8; 4] = [0x3d, 0xb8, 0xf3, 0x96];
const MCUBOOT_HEADER_SIZE: usize = 32;
const MAX_MCUBOOT_SIZE: usize = 16 * 1024 * 1024;
const TLV_TYPE_ED25519: u16 = 0x24;
const TLV_INFO_MAGIC: u16 = 0x6907;
#[derive(Debug, Clone)]
pub struct McubootArtifact {
data: Vec<u8>,
pub header_img_size: u32,
verified_img_size: u32,
pub is_little_endian: bool,
signature: Option<Vec<u8>>,
}
impl McubootArtifact {
pub fn from_bytes(data: Vec<u8>) -> Result<Self, WSError> {
if data.len() > MAX_MCUBOOT_SIZE {
return Err(WSError::InternalError(format!(
"MCUboot image too large: {} bytes (max: {} bytes)",
data.len(),
MAX_MCUBOOT_SIZE,
)));
}
if data.len() < MCUBOOT_HEADER_SIZE {
return Err(WSError::InternalError(
"File too small for MCUboot header".into(),
));
}
if data[0..4] != MCUBOOT_MAGIC {
return Err(WSError::InternalError(
"Not a valid MCUboot image: magic bytes mismatch".into(),
));
}
let is_little_endian = true;
let header_img_size = u32::from_le_bytes(
data[12..16].try_into().map_err(|_| WSError::ParseError)?,
);
let hdr_size = u16::from_le_bytes(
data[8..10].try_into().map_err(|_| WSError::ParseError)?,
) as u32;
let declared_total = hdr_size as usize + header_img_size as usize;
if declared_total > data.len() {
return Err(WSError::InternalError(format!(
"MCUboot header declares image size {} + header {} = {} bytes, \
but file is only {} bytes (SC-13 violation: header manipulation detected)",
header_img_size,
hdr_size,
declared_total,
data.len(),
)));
}
let trailing_bytes = data.len() - declared_total;
const MAX_TLV_OVERHEAD: usize = 8192; if trailing_bytes > MAX_TLV_OVERHEAD {
return Err(WSError::InternalError(format!(
"MCUboot image has {} bytes beyond declared content ({} bytes). \
Maximum expected TLV trailer is {} bytes. This may indicate a \
partial-image attack where ih_img_size was reduced to exclude \
payload from signing (SC-36 / H-38)",
trailing_bytes,
declared_total,
MAX_TLV_OVERHEAD,
)));
}
let verified_img_size = header_img_size;
Ok(McubootArtifact {
data,
header_img_size,
verified_img_size,
is_little_endian,
signature: None,
})
}
pub fn from_file(path: &str) -> Result<Self, WSError> {
let data = std::fs::read(path)?;
Self::from_bytes(data)
}
pub fn payload(&self) -> &[u8] {
let hdr_size = u16::from_le_bytes(
self.data[8..10].try_into().unwrap_or([0; 2]),
) as usize;
let end = hdr_size + self.verified_img_size as usize;
&self.data[..end.min(self.data.len())]
}
}
impl SignableArtifact for McubootArtifact {
fn format_type(&self) -> FormatType {
FormatType::Mcuboot
}
fn compute_hash(&self) -> Result<[u8; 32], WSError> {
let mut hasher = Sha256::new();
hasher.update(self.payload());
Ok(hasher.finalize().into())
}
fn attach_signature(&mut self, signature_data: &[u8]) -> Result<(), WSError> {
self.signature = Some(signature_data.to_vec());
Ok(())
}
fn detach_signature(&self) -> Result<Option<Vec<u8>>, WSError> {
Ok(self.signature.clone())
}
fn serialize(&self, writer: &mut dyn Write) -> Result<(), WSError> {
writer.write_all(self.payload())?;
if let Some(ref sig) = self.signature {
writer.write_all(&TLV_INFO_MAGIC.to_le_bytes())?;
let tlv_entry_size = 4 + sig.len(); let total_tlv_size = 4 + tlv_entry_size;
writer.write_all(&(total_tlv_size as u16).to_le_bytes())?;
writer.write_all(&TLV_TYPE_ED25519.to_le_bytes())?;
writer.write_all(&(sig.len() as u16).to_le_bytes())?;
writer.write_all(sig)?;
}
Ok(())
}
fn content_bytes(&self) -> &[u8] {
self.payload()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_mcuboot() -> Vec<u8> {
let mut img = vec![0u8; 64];
img[0..4].copy_from_slice(&MCUBOOT_MAGIC);
img[8] = 32;
img[9] = 0;
img[12] = 32;
img[13] = 0;
img[14] = 0;
img[15] = 0;
img
}
#[test]
fn test_mcuboot_parse_valid() {
let img = minimal_mcuboot();
let artifact = McubootArtifact::from_bytes(img).unwrap();
assert_eq!(artifact.header_img_size, 32);
assert_eq!(artifact.verified_img_size, 32);
assert!(artifact.signature.is_none());
}
#[test]
fn test_mcuboot_parse_bad_magic() {
let mut img = minimal_mcuboot();
img[0] = 0x00;
assert!(McubootArtifact::from_bytes(img).is_err());
}
#[test]
fn test_mcuboot_parse_size_mismatch() {
let mut img = minimal_mcuboot();
img[12] = 0xFF;
img[13] = 0xFF;
assert!(McubootArtifact::from_bytes(img).is_err());
}
#[test]
fn test_mcuboot_hash_deterministic() {
let img = minimal_mcuboot();
let artifact = McubootArtifact::from_bytes(img).unwrap();
let hash1 = artifact.compute_hash().unwrap();
let hash2 = artifact.compute_hash().unwrap();
assert_eq!(hash1, hash2);
}
#[test]
fn test_mcuboot_format_type() {
let img = minimal_mcuboot();
let artifact = McubootArtifact::from_bytes(img).unwrap();
assert_eq!(artifact.format_type(), FormatType::Mcuboot);
}
#[test]
fn test_mcuboot_too_large() {
let data = vec![0u8; MAX_MCUBOOT_SIZE + 1];
assert!(McubootArtifact::from_bytes(data).is_err());
}
#[test]
fn test_mcuboot_too_small() {
let data = vec![0u8; 10];
assert!(McubootArtifact::from_bytes(data).is_err());
}
#[test]
fn test_mcuboot_payload_extraction() {
let img = minimal_mcuboot();
let artifact = McubootArtifact::from_bytes(img.clone()).unwrap();
assert_eq!(artifact.payload().len(), 64);
}
}