use aes::Aes128;
use cmac::{Cmac, Mac};
pub const CMAC_KEY: [u8; 16] = [
0xBD, 0x20, 0x94, 0x08, 0xE3, 0x5E, 0x52, 0x6A, 0x36, 0x23, 0x52, 0x34, 0x43, 0x4F, 0xE8, 0xAB,
];
pub const TABLE_OFFSET: usize = crate::layout::CMAC_TABLE.start;
pub const ENTRY_COUNT: usize = 16;
pub const ENTRY_SIZE: usize = 28;
pub const ENABLED: u32 = 0x0000_0001;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CmacEntry {
pub index: usize,
pub entry_off: usize,
pub enabled: u32,
pub start: u32,
pub end: u32,
pub stored: [u8; 16],
}
impl CmacEntry {
pub fn is_active(&self) -> bool {
self.enabled == ENABLED
}
pub fn covers_table(&self) -> bool {
(self.start as usize) <= TABLE_OFFSET && (self.end as usize) >= TABLE_OFFSET
}
}
fn read_u32_le(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
pub fn parse_table(image: &[u8]) -> Result<Vec<CmacEntry>, CmacError> {
let table_end = TABLE_OFFSET + ENTRY_COUNT * ENTRY_SIZE;
if image.len() < table_end {
return Err(CmacError::ImageTooSmall {
need: table_end,
got: image.len(),
});
}
let mut out = Vec::with_capacity(ENTRY_COUNT);
for index in 0..ENTRY_COUNT {
let entry_off = TABLE_OFFSET + index * ENTRY_SIZE;
let enabled = read_u32_le(image, entry_off);
let start = read_u32_le(image, entry_off + 4);
let end = read_u32_le(image, entry_off + 8);
let mut stored = [0u8; 16];
stored.copy_from_slice(&image[entry_off + 12..entry_off + 28]);
out.push(CmacEntry {
index,
entry_off,
enabled,
start,
end,
stored,
});
}
Ok(out)
}
fn reverse_blocks(data: &mut [u8]) {
for chunk in data.chunks_mut(16) {
chunk.reverse();
}
}
pub fn compute_stored_digest(image: &[u8], start: u32, end: u32) -> Result<[u8; 16], CmacError> {
let start = start as usize;
let end = end as usize;
if end < start || end >= image.len() {
return Err(CmacError::BadRange { start, end });
}
let mut data = image[start..=end].to_vec();
reverse_blocks(&mut data);
let mut mac = <Cmac<Aes128> as Mac>::new_from_slice(&CMAC_KEY)
.expect("AES-128 key length is always valid for CMAC");
mac.update(&data);
let tag = mac.finalize().into_bytes();
let mut digest = [0u8; 16];
digest.copy_from_slice(&tag);
digest.reverse(); Ok(digest)
}
#[derive(Debug, Clone, Copy)]
pub struct EntryVerdict {
pub entry: CmacEntry,
pub computed: [u8; 16],
pub matches: bool,
}
pub fn verify_detailed(image: &[u8]) -> Result<Vec<EntryVerdict>, CmacError> {
let table = parse_table(image)?;
let mut verdicts = Vec::new();
for entry in table {
if !entry.is_active() {
continue;
}
let computed = compute_stored_digest(image, entry.start, entry.end)?;
verdicts.push(EntryVerdict {
matches: computed == entry.stored,
entry,
computed,
});
}
Ok(verdicts)
}
pub fn verify(image: &[u8]) -> bool {
match verify_detailed(image) {
Ok(v) => !v.is_empty() && v.iter().all(|e| e.matches),
Err(_) => false,
}
}
pub fn resign(image: &[u8]) -> Result<Vec<u8>, CmacError> {
let mut img = image.to_vec();
let table = parse_table(&img)?;
for entry in table.iter().filter(|e| e.is_active() && !e.covers_table()) {
let digest = compute_stored_digest(&img, entry.start, entry.end)?;
img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
}
for entry in table.iter().filter(|e| e.is_active() && e.covers_table()) {
let digest = compute_stored_digest(&img, entry.start, entry.end)?;
img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
}
Ok(img)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CmacError {
ImageTooSmall {
need: usize,
got: usize,
},
BadRange {
start: usize,
end: usize,
},
}
impl std::fmt::Display for CmacError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CmacError::ImageTooSmall { need, got } => {
write!(f, "image too small: need {need} bytes, got {got}")
}
CmacError::BadRange { start, end } => {
write!(f, "bad CMAC range: start=0x{start:x} end=0x{end:x}")
}
}
}
}
impl std::error::Error for CmacError {}