use anyhow::{anyhow, bail, Result};
use der_parser::der::{parse_der, Class, DerObject};
use serde::Serialize;
use log::{debug, warn};
#[derive(Copy, Clone, Debug, Serialize)]
pub enum ContainerKind {
Img4,
Im4pStandalone,
Im4mStandalone,
}
#[derive(Debug)]
pub struct Parsed {
pub kind: ContainerKind,
pub im4p: Option<Im4p>,
pub im4m: Option<Im4m>,
pub im4r: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct KbagEntry {
pub kclass: u64, pub iv: Vec<u8>, pub key: Vec<u8>, }
#[derive(Debug)]
pub struct Im4p {
pub r#type: String,
pub description: String,
pub data: Vec<u8>,
pub kbag_der: Option<Vec<u8>>,
pub kbag_summary: Option<Vec<KbagEntry>>,
}
#[derive(Debug)]
pub struct Im4m {
pub raw: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum Im4mValue {
Integer(u128), Boolean(bool), Ia5String(String), OctetString(String), BitString(String), Null, SequenceLen(usize), SetLen(usize), Unknown { class_id: u8, tag: u32, len: usize },
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4mProperty {
pub key: String, pub value: Im4mValue,
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4mImageManifest {
pub fourcc: String, pub properties: Vec<Im4mProperty>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4pInfo {
pub r#type: String,
pub description: String,
pub data_len: usize,
pub kbag: Option<Vec<KbagEntry>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4mInfoSummary {
pub version: Option<u64>,
pub manifest_property_tags: Vec<String>,
pub images_present: Vec<String>,
pub cert_chain_len: Option<usize>,
pub signature_len: Option<usize>,
}
pub fn parse_img4_like(bytes: &[u8]) -> Result<Parsed> {
debug!("parse_img4_like: starting, input length {}", bytes.len());
let (_, obj) = parse_der(bytes).map_err(|e| anyhow!("DER: {}", e))?;
let seq = obj.as_sequence().map_err(|_| anyhow!("top-level not SEQUENCE"))?;
let label_str = seq
.get(0)
.and_then(ia5str)
.ok_or_else(|| anyhow!("missing label"))?;
debug!("top-level label: {}", label_str);
if label_str == "IMG4" {
debug!("Detected IMG4 container");
let im4p = parse_im4p_from_der_obj(
seq.get(1)
.ok_or_else(|| anyhow!("IMG4 missing IM4P"))?,
)?;
debug!("Parsed IM4P successfully");
let mut im4m = None;
let mut im4r = None;
for child in &seq[2..] {
let hdr = child.header.clone();
if hdr.class() == Class::ContextSpecific {
let t = hdr.tag().0;
if t == 0 {
debug!("Found context-specific [0] (IM4M)");
if let Ok(inner_der) = child.as_slice() {
im4m = Some(im4m_from_bytes(inner_der)?);
debug!("Parsed IM4M from context-specific tag");
}
} else if t == 1 {
debug!("Found context-specific [1] (IM4R)");
if let Ok(bytes) = child.as_slice() {
im4r = Some(bytes.to_vec());
debug!("Captured IM4R payload, {} bytes", bytes.len());
}
}
}
}
Ok(Parsed {
kind: ContainerKind::Img4,
im4p: Some(im4p),
im4m,
im4r,
})
} else if label_str == "IM4P" {
debug!("Detected standalone IM4P");
let im4p = parse_im4p_from_der_obj(&obj)?;
Ok(Parsed {
kind: ContainerKind::Im4pStandalone,
im4p: Some(im4p),
im4m: None,
im4r: None,
})
} else if label_str == "IM4M" {
debug!("Detected standalone IM4M");
let im4m = im4m_from_bytes(bytes)?;
Ok(Parsed {
kind: ContainerKind::Im4mStandalone,
im4p: None,
im4m: Some(im4m),
im4r: None,
})
} else {
warn!("Unknown top-level label: {}", label_str);
bail!("unknown top-level label: {label_str}");
}
}
fn parse_im4p_from_der_obj(obj: &DerObject) -> Result<Im4p> {
debug!("parse_im4p_from_der_obj: entering");
let seq = obj.as_sequence().map_err(|_| anyhow!("IM4P not SEQUENCE"))?;
let s0 = seq
.get(0)
.and_then(ia5str)
.ok_or_else(|| anyhow!("IM4P label missing"))?;
if s0 != "IM4P" {
bail!("IM4P[0] != \"IM4P\"");
}
debug!("IM4P label confirmed");
let ty = seq
.get(1)
.and_then(ia5str)
.ok_or_else(|| anyhow!("IM4P type missing"))?
.to_string();
debug!("IM4P type: {}", ty);
let desc = seq
.get(2)
.and_then(ia5str)
.ok_or_else(|| anyhow!("IM4P description missing"))?
.to_string();
debug!("IM4P description length: {}", desc.len());
let data = seq
.get(3)
.and_then(as_bytes)
.ok_or_else(|| anyhow!("IM4P data missing"))?
.to_vec();
debug!("IM4P payload size: {} bytes", data.len());
let kbag_der_vec = seq.get(4).and_then(as_bytes).map(|b| b.to_vec());
if kbag_der_vec.is_some() {
debug!("IM4P contains KBAG");
} else {
debug!("IM4P does not contain KBAG");
}
let kbag_summary = if let Some(ref kraw) = kbag_der_vec {
match parse_kbag_summary(kraw) {
Ok(summary) => {
debug!("Parsed KBAG with {} entries", summary.len());
Some(summary)
}
Err(e) => {
warn!("Failed to parse KBAG: {}", e);
None
}
}
} else {
None
};
Ok(Im4p {
r#type: ty,
description: desc,
data,
kbag_der: kbag_der_vec,
kbag_summary,
})
}
fn parse_kbag_summary(kbag_der: &[u8]) -> Result<Vec<KbagEntry>> {
debug!(
"parse_kbag_summary: entering, KBAG DER size {} bytes",
kbag_der.len()
);
let (_, obj) = parse_der(kbag_der).map_err(|e| anyhow!("KBAG DER: {e}"))?;
let seq = obj.as_sequence().map_err(|_| anyhow!("KBAG not SEQUENCE"))?;
let mut out = Vec::new();
for (idx, entry) in seq.iter().enumerate() {
let es = entry
.as_sequence()
.map_err(|_| anyhow!("KBAG entry not SEQUENCE"))?;
if es.len() != 3 {
bail!("KBAG entry malformed");
}
let kclass = es[0].as_u64().map_err(|_| anyhow!("KBAG class not INTEGER"))?;
let iv = es[1].as_slice().map_err(|_| anyhow!("KBAG iv not OCTET STRING"))?.to_vec();
let key = es[2].as_slice().map_err(|_| anyhow!("KBAG key not OCTET STRING"))?.to_vec();
debug!(
"KBAG entry {}: class={}, iv_len={}, key_len={}",
idx,
kclass,
iv.len(),
key.len()
);
out.push(KbagEntry { kclass, iv, key });
}
debug!("parse_kbag_summary: finished, total {} entries", out.len());
Ok(out)
}
fn im4m_from_bytes(bytes: &[u8]) -> Result<Im4m> {
debug!(
"im4m_from_bytes: entering, candidate DER size {} bytes",
bytes.len()
);
let (_, obj) = parse_der(bytes).map_err(|e| anyhow!("IM4M DER: {e}"))?;
let seq = obj.as_sequence().map_err(|_| anyhow!("IM4M not SEQUENCE"))?;
let lbl = seq
.get(0)
.and_then(ia5str)
.ok_or_else(|| anyhow!("IM4M label missing"))?;
if lbl != "IM4M" {
bail!("label != IM4M");
}
debug!("IM4M label confirmed");
Ok(Im4m { raw: bytes.to_vec() })
}
pub fn summarize_im4m(im4m: &Im4m) -> Result<Im4mInfoSummary> {
debug!(
"summarize_im4m: start, raw DER size {} bytes",
im4m.raw.len()
);
let (_, obj) = parse_der(&im4m.raw).map_err(|e| anyhow!("IM4M DER: {e}"))?;
let seq = obj.as_sequence().map_err(|_| anyhow!("IM4M not SEQUENCE"))?;
let lbl = seq
.get(0)
.and_then(ia5str)
.ok_or_else(|| anyhow!("IM4M label missing"))?;
if lbl != "IM4M" {
bail!("label != IM4M");
}
debug!("IM4M label validated");
let version = seq.get(1).and_then(|o| o.as_u64().ok());
debug!("IM4M version: {:?}", version);
let signature_len = seq
.get(3)
.and_then(|o| o.as_slice().ok())
.map(|b| b.len());
debug!("IM4M signature length: {:?}", signature_len);
let cert_chain_len = seq
.get(4)
.and_then(|o| o.as_sequence().ok())
.map(|s| s.len());
debug!("IM4M cert chain length: {:?}", cert_chain_len);
let mut tokens = Vec::<String>::new();
scan_der_collect_ia5_fourccs(&im4m.raw, &mut tokens)?;
debug!("Collected {} IA5 tokens before dedup", tokens.len());
dedup_stable(&mut tokens);
debug!("Token count after dedup: {}", tokens.len());
let manifest_property_tags = tokens
.iter()
.filter(|s| s.as_str() == "MANB" || s.as_str() == "MANP")
.cloned()
.collect::<Vec<_>>();
debug!(
"Manifest property tags identified: {:?}",
manifest_property_tags
);
const NON_IMAGE_KEYS: &[&str] = &[
"IM4M", "MANB", "MANP", "DGST", "CEPO", "CPRO", "SDOM", "augs", "clas", "fchp", "pave", "srvn",
"styp", "type", "upcl", "vnum", "gdmg", "ginc", "ginf", "gtcd", "gtgv",
];
let images_present = tokens
.iter()
.filter(|s| {
s.len() == 4
&& !NON_IMAGE_KEYS.contains(&s.as_str())
&& s.chars().all(|c| c.is_ascii_lowercase())
})
.cloned()
.collect::<Vec<_>>();
debug!("Images present identified: {:?}", images_present);
Ok(Im4mInfoSummary {
version,
manifest_property_tags,
images_present,
cert_chain_len,
signature_len,
})
}
pub fn extract_im4m_cert_chain(raw: &[u8]) -> anyhow::Result<Vec<Vec<u8>>> {
let mut out: Vec<Vec<u8>> = Vec::new();
let mut pos = 0usize;
let (tag_len, _, _, _) = der_read_tag(&raw[pos..]).map_err(|e| anyhow::anyhow!("IM4M tag: {e}"))?;
pos += tag_len;
let (len_len, _) = der_read_len(&raw[pos..]).map_err(|e| anyhow::anyhow!("IM4M len: {e}"))?;
pos += len_len;
for idx in 0..5 {
let elem_start = pos;
let (tag_len, class, constructed, tag_no) = der_read_tag(&raw[pos..])
.map_err(|e| anyhow::anyhow!("elem[{idx}] tag: {e}"))?;
pos += tag_len;
let (len_len, elem_len) = der_read_len(&raw[pos..])
.map_err(|e| anyhow::anyhow!("elem[{idx}] len: {e}"))?;
pos += len_len;
if idx == 4 {
if class != 0 || tag_no != 16 {
return Ok(Vec::new()); }
let chain_end = pos + elem_len;
while pos < chain_end {
let cert_start = pos;
let (cert_tag_len, cert_class, cert_constructed, cert_tag) = der_read_tag(&raw[pos..])
.map_err(|e| anyhow::anyhow!("cert tag: {e}"))?;
pos += cert_tag_len;
let (cert_len_len, cert_len) = der_read_len(&raw[pos..])
.map_err(|e| anyhow::anyhow!("cert len: {e}"))?;
pos += cert_len_len;
let cert_full_len = cert_tag_len + cert_len_len + cert_len;
debug!("cert[{}]: @{:04x} class={}, tag={}, len={}", out.len(), cert_start, cert_class, cert_tag, cert_len);
match (cert_class, cert_tag) {
(0, 4) => {
out.push(raw[pos..pos + cert_len].to_vec());
}
(0, 16) => {
out.push(raw[cert_start..cert_start + cert_full_len].to_vec());
}
_ => {
let content = &raw[pos..pos + cert_len];
if !content.is_empty() && content[0] == 0x30 {
out.push(content.to_vec());
} else {
out.push(encode_der_sequence(content));
}
}
}
pos += cert_len;
}
break;
} else {
pos += elem_len;
}
}
Ok(out)
}
fn encode_der_sequence(content: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(1 + der_len_encoded_bytes(content.len()) + content.len());
v.push(0x30); write_der_len(&mut v, content.len());
v.extend_from_slice(content);
v
}
fn der_len_encoded_bytes(len: usize) -> usize {
if len < 128 { 1 } else {
let mut n = 0usize;
let mut tmp = len;
while tmp > 0 { n += 1; tmp >>= 8; }
1 + n
}
}
fn write_der_len(buf: &mut Vec<u8>, len: usize) {
if len < 128 {
buf.push(len as u8);
} else {
let mut bytes = [0u8; 8]; let mut n = 0usize;
let mut tmp = len;
while tmp > 0 {
bytes[7 - n] = (tmp & 0xFF) as u8;
tmp >>= 8;
n += 1;
}
buf.push(0x80 | (n as u8));
buf.extend_from_slice(&bytes[8 - n..]);
}
}
pub fn extract_im4m_properties(raw: &[u8]) -> Result<Vec<Im4mProperty>> {
let (_, obj) = parse_der(raw).map_err(|e| anyhow!("IM4M DER: {e}"))?;
let mut out = Vec::<Im4mProperty>::new();
collect_props_from_obj(&obj, &mut out)?;
Ok(out)
}
fn collect_props_from_obj(o: &DerObject, out: &mut Vec<Im4mProperty>) -> Result<()> {
if let Ok(seq) = o.as_sequence() {
if seq.len() >= 2 {
if let Some(k) = ia5str(&seq[0]) {
if k.len() == 4 && k.chars().all(|c| c.is_ascii_graphic()) {
let v = decode_any_value(&seq[1])?;
out.push(Im4mProperty { key: k.to_string(), value: v });
}
}
}
for ch in seq {
collect_props_from_obj(ch, out)?;
}
return Ok(());
}
if let Ok(set) = o.as_set() {
for ch in set {
collect_props_from_obj(ch, out)?;
}
return Ok(());
}
let h = &o.header;
if h.is_constructed() {
if let Ok(bytes) = o.as_slice() {
let mut off = 0usize;
while off < bytes.len() {
let (rem, child) = parse_der(&bytes[off..]).map_err(|e| anyhow!("inner DER: {e}"))?;
let consumed = bytes[off..].len() - rem.len();
collect_props_from_obj(&child, out)?;
off += consumed;
}
}
}
Ok(())
}
fn decode_any_value(o: &DerObject) -> Result<Im4mValue> {
let h = &o.header;
let class_num = h.class() as u8;
let tag = h.tag().0;
if class_num == 0 {
match tag {
1 => {
let b = o.as_bool().map_err(|_| anyhow!("BOOLEAN decode"))?;
return Ok(Im4mValue::Boolean(b));
}
2 => {
if let Ok(u) = o.as_u64() {
return Ok(Im4mValue::Integer(u as u128));
}
if let Ok(bytes) = o.as_slice() {
let mut acc: u128 = 0;
for &b in bytes {
acc = (acc << 8) | (b as u128);
}
return Ok(Im4mValue::Integer(acc));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
3 => {
if let Ok(bytes) = o.as_slice() {
return Ok(Im4mValue::BitString(hex::encode(bytes)));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
4 => {
if let Ok(bytes) = o.as_slice() {
return Ok(Im4mValue::OctetString(hex::encode(bytes)));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
5 => return Ok(Im4mValue::Null),
16 => {
if let Ok(seq) = o.as_sequence() {
return Ok(Im4mValue::SequenceLen(seq.len()));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
17 => {
if let Ok(set) = o.as_set() {
return Ok(Im4mValue::SetLen(set.len()));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
22 => {
if let Some(s) = ia5str(o) {
return Ok(Im4mValue::Ia5String(s.to_string()));
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
_ => {
if let Ok(bytes) = o.as_slice() {
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: bytes.len() });
}
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? });
}
}
} else {
if let Ok(bytes) = o.as_slice() {
return Ok(Im4mValue::Unknown { class_id: class_num, tag, len: bytes.len() });
}
Ok(Im4mValue::Unknown { class_id: class_num, tag, len: o.length().definite()? })
}
}
fn dedup_stable(v: &mut Vec<String>) {
v.sort();
v.dedup();
}
fn scan_der_collect_ia5_fourccs(input: &[u8], out: &mut Vec<String>) -> Result<()> {
let mut off = 0usize;
while off < input.len() {
let (tag_len, class, constructed, tag_no) = der_read_tag(&input[off..])?;
let len_off = off + tag_len;
let (len_len, content_len) = der_read_len(&input[len_off..])?;
let hdr_len = tag_len + len_len;
let val_start = off + hdr_len;
let val_end = val_start
.checked_add(content_len)
.ok_or_else(|| anyhow!("overflow"))?;
if val_end > input.len() {
bail!("IM4M: element exceeds buffer");
}
if class == 0 && tag_no == 22 {
let s = &input[val_start..val_end];
if let Ok(su) = std::str::from_utf8(s) {
if su.len() == 4 && su.chars().all(|c| c.is_ascii_graphic()) {
out.push(su.to_string());
debug!("Found IA5 token: {}", su);
}
}
}
if constructed {
debug!(
"Recursing into constructed tag (class={}, tag_no={}) at offset {}",
class, tag_no, off
);
scan_der_collect_ia5_fourccs(&input[val_start..val_end], out)?;
}
off = val_end;
}
Ok(())
}
fn der_read_tag(i: &[u8]) -> Result<(usize, u8, bool, u32)> {
if i.is_empty() {
bail!("short tag");
}
let b0 = i[0];
let class = (b0 & 0b1100_0000) >> 6; let constructed = (b0 & 0b0010_0000) != 0;
let mut tag_no = (b0 & 0b0001_1111) as u32;
let mut idx = 1usize;
if tag_no == 0b1_1111 {
tag_no = 0;
loop {
if idx >= i.len() {
bail!("short high-tag-number");
}
let b = i[idx];
idx += 1;
tag_no = (tag_no << 7) | (b & 0x7F) as u32;
if (b & 0x80) == 0 {
break;
}
}
}
Ok((idx, class, constructed, tag_no))
}
fn der_read_len(i: &[u8]) -> Result<(usize, usize)> {
if i.is_empty() {
bail!("short length");
}
let b0 = i[0];
if (b0 & 0x80) == 0 {
Ok((1, (b0 & 0x7F) as usize))
} else {
let n = (b0 & 0x7F) as usize;
if n == 0 {
bail!("indefinite length not allowed in DER");
}
if i.len() < 1 + n {
bail!("short long-form length");
}
let mut len: usize = 0;
for &b in &i[1..=n] {
len = (len << 8) | (b as usize);
}
Ok((1 + n, len))
}
}
fn as_bytes<'a>(o: &'a DerObject<'a>) -> Option<&'a [u8]> {
o.as_slice().ok()
}
fn ia5str<'a>(o: &'a DerObject<'a>) -> Option<&'a str> {
o.as_slice().ok().and_then(|s| std::str::from_utf8(s).ok())
}