use anyhow::{anyhow, bail, Result};
use der_parser::der::{parse_der, Class, DerObject};
use serde::Serialize;
use log::{debug, warn};
use std::collections::HashMap;
use once_cell::sync::Lazy;
#[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)]
pub struct KbagEntry {
pub kclass: u64, pub iv: Vec<u8>, pub key: Vec<u8>, }
#[derive(Debug, Clone, Serialize)]
pub struct KbagEntryInfo {
pub kclass: u64,
pub iv_len: usize,
pub key_len: usize,
}
impl From<&KbagEntry> for KbagEntryInfo {
fn from(e: &KbagEntry) -> Self {
KbagEntryInfo { kclass: e.kclass, iv_len: e.iv.len(), key_len: e.key.len() }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CompressionInfo {
pub algorithm: String, pub method_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub uncompressed_size: Option<u64>,
}
impl From<&Im4pCompression> for CompressionInfo {
fn from(c: &Im4pCompression) -> Self {
let algorithm = match c.method_id {
0 => "lzss".to_string(),
1 => "lzfse".to_string(),
other => format!("unknown({other})"),
};
CompressionInfo { algorithm, method_id: c.method_id, uncompressed_size: c.uncompressed_len }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4pCompression {
pub method_id: u64, pub uncompressed_len: Option<u64>, }
#[derive(Debug)]
pub struct Im4p {
pub r#type: String,
pub version: String,
pub data: Vec<u8>,
pub kbag_der: Option<Vec<u8>>,
pub kbag_summary: Option<Vec<KbagEntry>>,
pub compression: Option<Im4pCompression>,
pub payload_properties: Option<Vec<TypedIm4mProperty>>,
}
#[derive(Debug)]
pub struct Im4m {
pub raw: Vec<u8>,
}
#[allow(dead_code)]
#[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 },
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub struct Im4mProperty {
pub key: String, pub value: Im4mValue,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum Im4mPropertyValue {
Integer { value: u64 },
Boolean { value: bool },
String { value: String },
OctetString { value: String }, Digest { value: String }, Unknown { der_type: String, #[serde(skip_serializing_if = "Option::is_none")] hex_value: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] hex_values: Option<Vec<String>> },
}
#[derive(Debug, Clone, Serialize)]
pub struct TypedIm4mProperty {
pub key: String,
pub name: String,
pub description: String,
pub value: Im4mPropertyValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub anomaly: Option<String>,
}
#[allow(dead_code)]
#[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 version: String,
pub data_len: usize,
pub kbag: Option<Vec<KbagEntryInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression: Option<CompressionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload_properties: Option<Vec<TypedIm4mProperty>>,
}
#[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>,
}
struct PropertyMetadata {
name: &'static str,
description: &'static str,
expected_type: ExpectedDerType,
}
#[derive(Debug)]
enum ExpectedDerType {
Boolean,
Integer,
OctetString,
Digest,
Ia5String,
}
static KNOWN_PROPERTIES: Lazy<HashMap<&'static str, PropertyMetadata>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("CEPO", PropertyMetadata { name: "ChipEpoch", description: "Chip Epoch", expected_type: ExpectedDerType::Integer });
m.insert("BORD", PropertyMetadata { name: "BoardId", description: "Board Identifier", expected_type: ExpectedDerType::Integer });
m.insert("CHIP", PropertyMetadata { name: "ChipId", description: "Chip Identifier", expected_type: ExpectedDerType::Integer });
m.insert("SDOM", PropertyMetadata { name: "SecurityDomain", description: "Security Domain", expected_type: ExpectedDerType::Integer });
m.insert("ECID", PropertyMetadata { name: "ExclusiveChipId", description: "Unique Chip Identifier", expected_type: ExpectedDerType::Integer });
m.insert("CPRO", PropertyMetadata { name: "CertificateProductionStatus", description: "Certificate Production Status", expected_type: ExpectedDerType::Boolean });
m.insert("CSEC", PropertyMetadata { name: "CertificateSecurityMode", description: "Certificate Security Mode", expected_type: ExpectedDerType::Boolean });
m.insert("EPRO", PropertyMetadata { name: "EffectiveProductionStatus", description: "Effective Production Status", expected_type: ExpectedDerType::Boolean });
m.insert("ESEC", PropertyMetadata { name: "EffectiveSecurityMode", description: "Effective Security Mode", expected_type: ExpectedDerType::Boolean });
m.insert("IUOU", PropertyMetadata { name: "InternalUseOnlyUnit", description: "Internal Use Only Unit", expected_type: ExpectedDerType::Boolean });
m.insert("AMNM", PropertyMetadata { name: "AllowMixNMatch", description: "Allow Mix-n-Match", expected_type: ExpectedDerType::Boolean });
m.insert("UDID", PropertyMetadata { name: "UniqueDeviceIdentifier", description: "Unique Device Identifier (digest)", expected_type: ExpectedDerType::Digest });
m.insert("DGST", PropertyMetadata { name: "Digest", description: "Payload Digest", expected_type: ExpectedDerType::Digest });
m.insert("BNCN", PropertyMetadata { name: "BootNonce", description: "Boot Nonce", expected_type: ExpectedDerType::OctetString });
m.insert("love", PropertyMetadata { name: "LongOsVersion", description: "Long OS Version", expected_type: ExpectedDerType::Ia5String });
m.insert("augs", PropertyMetadata { name: "AugmentedManifest", description: "Augmented Manifest", expected_type: ExpectedDerType::Integer });
m.insert("clas", PropertyMetadata { name: "Class", description: "Manifest Class", expected_type: ExpectedDerType::Integer });
m.insert("fchp", PropertyMetadata { name: "FusingChip", description: "Fusing Chip", expected_type: ExpectedDerType::Integer });
m.insert("pave", PropertyMetadata { name: "PlatformVersion", description: "Platform Version", expected_type: ExpectedDerType::Integer });
m.insert("srvn", PropertyMetadata { name: "SecurityRevision", description: "Security Revision", expected_type: ExpectedDerType::Integer });
m.insert("styp", PropertyMetadata { name: "SystemType", description: "System Type", expected_type: ExpectedDerType::Integer });
m.insert("type", PropertyMetadata { name: "Type", description: "Image Type", expected_type: ExpectedDerType::Ia5String });
m.insert("upcl", PropertyMetadata { name: "UpgradeClaim", description: "Upgrade Claim", expected_type: ExpectedDerType::Integer });
m.insert("vnum", PropertyMetadata { name: "VersionNumber", description: "Version Number", expected_type: ExpectedDerType::Integer });
m.insert("gdmg", PropertyMetadata { name: "GlobalDigest", description: "Global Digest", expected_type: ExpectedDerType::Digest });
m.insert("ginc", PropertyMetadata { name: "GlobalIncrement", description: "Global Increment", expected_type: ExpectedDerType::Integer });
m.insert("ginf", PropertyMetadata { name: "GlobalInfo", description: "Global Info", expected_type: ExpectedDerType::Integer });
m.insert("gtcd", PropertyMetadata { name: "GlobalTrustedCode", description: "Global Trusted Code", expected_type: ExpectedDerType::Integer });
m.insert("gtgv", PropertyMetadata { name: "GlobalTrustGlobalVersion", description: "Global Trust Global Version", expected_type: ExpectedDerType::Integer });
m
});
fn parse_im4p_compression(obj: &DerObject) -> Result<Im4pCompression> {
let seq = obj.as_sequence().map_err(|_| anyhow!("compression not SEQUENCE"))?;
if seq.is_empty() { bail!("compression SEQUENCE empty"); }
let id = seq[0].as_u64().map_err(|_| anyhow!("compression id not INTEGER"))?;
let uncl = seq.get(1).and_then(|o| o.as_u64().ok());
Ok(Im4pCompression { method_id: id, uncompressed_len: uncl })
}
fn extract_property_set(obj: &DerObject, expected_label: &str) -> Result<Vec<TypedIm4mProperty>> {
let seq = obj
.as_sequence()
.map_err(|_| anyhow!("{expected_label}: not SEQUENCE"))?;
let label = seq
.get(0)
.and_then(ia5str)
.ok_or_else(|| anyhow!("{expected_label}: label missing"))?;
if label != expected_label {
bail!("{expected_label}: label is '{label}'");
}
let prop_set = seq
.get(1)
.ok_or_else(|| anyhow!("{expected_label}: missing property SET"))?
.as_set()
.map_err(|_| anyhow!("{expected_label}: properties not in a SET"))?;
let mut out = Vec::new();
for prop_obj in prop_set {
collect_typed_props_from_obj(prop_obj, &mut out)?;
}
Ok(out)
}
pub fn extract_im4r_properties(raw: &[u8]) -> Result<Vec<TypedIm4mProperty>> {
let (_, obj) = parse_der(raw).map_err(|e| anyhow!("IM4R DER: {e}"))?;
extract_property_set(&obj, "IM4R")
}
#[allow(dead_code)]
#[deprecated(note = "Use extract_im4r_properties instead")]
pub fn extract_im4r_bncn_nonce(raw: &[u8]) -> Result<Option<Vec<u8>>> {
let props = extract_im4r_properties(raw)?;
for prop in props {
if prop.key == "BNCN" {
if let Im4mPropertyValue::OctetString { value: hex_val } = prop.value {
return Ok(Some(hex::decode(hex_val).unwrap_or_default()));
}
}
}
Ok(None)
}
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 version_str = seq
.get(2)
.and_then(as_bytes)
.map(|b| String::from_utf8_lossy(b).into_owned())
.ok_or_else(|| anyhow!("IM4P version missing"))?;
debug!("IM4P version: {}", version_str);
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 mut kbag_der_vec: Option<Vec<u8>> = None;
let mut compression: Option<Im4pCompression> = None;
let mut payload_properties: Option<Vec<TypedIm4mProperty>> = None;
for item in seq.iter().skip(4) {
let tag = item.header.tag().0;
let universal = item.header.class() == Class::Universal;
if universal && tag == 4 {
match as_bytes(item) {
Some(b) if kbag_der_vec.is_none() => kbag_der_vec = Some(b.to_vec()),
Some(_) => warn!("IM4P: extra OCTET STRING element ignored"),
None => {}
}
} else if universal && tag == 16 {
match item.as_sequence() {
Ok(children) if children.first().and_then(ia5str) == Some("PAYP") => {
match extract_property_set(item, "PAYP") {
Ok(props) => {
debug!("IM4P PAYP: {} payload properties", props.len());
payload_properties = Some(props);
}
Err(e) => warn!("IM4P PAYP parse failed: {}", e),
}
}
Ok(_) => {
match parse_im4p_compression(item) {
Ok(c) => {
debug!(
"IM4P compression: id={}, uncompressed_len={:?}",
c.method_id, c.uncompressed_len
);
compression = Some(c);
}
Err(e) => warn!("IM4P: SEQUENCE element is neither PAYP nor valid compression: {}", e),
}
}
Err(_) => debug!("IM4P: ignoring malformed SEQUENCE element"),
}
} else {
debug!(
"IM4P: ignoring unexpected optional element (class={:?}, tag={})",
item.header.class(),
tag
);
}
}
if kbag_der_vec.is_some() {
debug!("IM4P contains KBAG");
} else {
debug!("IM4P does not contain KBAG");
}
let kbag_summary = match &kbag_der_vec {
Some(kraw) => 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
}
},
None => None,
};
Ok(Im4p {
r#type: ty,
version: version_str,
data,
kbag_der: kbag_der_vec,
kbag_summary,
compression,
payload_properties,
})
}
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 (expected >= 3 fields, got {})", es.len());
}
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() })
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4mImage {
pub fourcc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub properties: Vec<TypedIm4mProperty>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Im4mManifestData {
pub manifest_properties: Vec<TypedIm4mProperty>,
pub images: Vec<Im4mImage>,
}
pub fn extract_im4m_manifest(raw: &[u8]) -> Result<Im4mManifestData> {
let (_, obj) = parse_der(raw).map_err(|e| anyhow!("IM4M DER: {e}"))?;
let seq = obj.as_sequence().map_err(|_| anyhow!("IM4M not SEQUENCE"))?;
let mut manifest_properties = Vec::new();
let mut images = Vec::new();
let body = seq.get(2).ok_or_else(|| anyhow!("IM4M missing manifest body"))?;
let body_set = body.as_set().map_err(|_| anyhow!("IM4M body not a SET"))?;
for wrapper in body_set {
let inner = match wrapper.as_slice() {
Ok(b) => b,
Err(_) => continue,
};
let manb_seq_obj = match parse_der(inner) {
Ok((_, o)) => o,
Err(_) => continue,
};
let manb_seq = match manb_seq_obj.as_sequence() {
Ok(s) => s,
Err(_) => continue,
};
if manb_seq.get(0).and_then(ia5str) != Some("MANB") {
continue;
}
let manb_set = match manb_seq.get(1).and_then(|o| o.as_set().ok()) {
Some(s) => s,
None => continue,
};
for child in manb_set {
let cinner = match child.as_slice() {
Ok(b) => b,
Err(_) => continue,
};
let cseq_obj = match parse_der(cinner) {
Ok((_, o)) => o,
Err(_) => continue,
};
let cseq = match cseq_obj.as_sequence() {
Ok(s) => s,
Err(_) => continue,
};
let label = match cseq.get(0).and_then(ia5str) {
Some(l) => l.to_string(),
None => continue,
};
let mut props = Vec::new();
if let Some(pset) = cseq.get(1).and_then(|o| o.as_set().ok()) {
for p in pset {
collect_typed_props_from_obj(p, &mut props)?;
}
}
if label == "MANP" {
manifest_properties = props;
} else {
let name = crate::fourcc::get_description(&label);
images.push(Im4mImage { fourcc: label, name, properties: props });
}
}
}
images.sort_by(|a, b| a.fourcc.cmp(&b.fourcc));
Ok(Im4mManifestData { manifest_properties, images })
}
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());
match version {
Some(v) if v > 2 => warn!("IM4M version {} is out of spec range 0..=2", v),
Some(v) => debug!("IM4M version: {}", v),
None => warn!("IM4M version field missing or not an integer"),
}
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
);
let images_present = match extract_im4m_manifest(&im4m.raw) {
Ok(m) => m.images.into_iter().map(|i| i.fourcc).collect::<Vec<_>>(),
Err(e) => {
warn!("structural image enumeration failed: {}", e);
Vec::new()
}
};
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 at = |p: usize| -> Result<&[u8]> {
raw.get(p..).ok_or_else(|| anyhow!("IM4M: offset {p} past end ({})", raw.len()))
};
let advance = |p: usize, by: usize| -> Result<usize> {
p.checked_add(by).ok_or_else(|| anyhow!("IM4M: offset overflow"))
};
let mut pos = 0usize;
let (tag_len, _, _, _) = der_read_tag(at(pos)?).map_err(|e| anyhow!("IM4M tag: {e}"))?;
pos = advance(pos, tag_len)?;
let (len_len, _) = der_read_len(at(pos)?).map_err(|e| anyhow!("IM4M len: {e}"))?;
pos = advance(pos, len_len)?;
for idx in 0..5 {
let (tag_len, class, _constructed, tag_no) =
der_read_tag(at(pos)?).map_err(|e| anyhow!("elem[{idx}] tag: {e}"))?;
pos = advance(pos, tag_len)?;
let (len_len, elem_len) =
der_read_len(at(pos)?).map_err(|e| anyhow!("elem[{idx}] len: {e}"))?;
pos = advance(pos, len_len)?;
let elem_end = advance(pos, elem_len)?;
if elem_end > raw.len() {
bail!("elem[{idx}] content ({elem_len} bytes) exceeds buffer");
}
if idx == 4 {
if class != 0 || tag_no != 16 {
return Ok(Vec::new());
}
let chain_end = elem_end;
while pos < chain_end {
let cert_start = pos;
let (cert_tag_len, cert_class, _cert_constructed, cert_tag) =
der_read_tag(at(pos)?).map_err(|e| anyhow!("cert tag: {e}"))?;
pos = advance(pos, cert_tag_len)?;
let (cert_len_len, cert_len) =
der_read_len(at(pos)?).map_err(|e| anyhow!("cert len: {e}"))?;
pos = advance(pos, cert_len_len)?;
let content_end = advance(pos, cert_len)?;
if content_end > chain_end {
bail!("cert content ({cert_len} bytes) exceeds cert chain");
}
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..content_end].to_vec()),
(0, 16) => out.push(raw[cert_start..content_end].to_vec()),
_ => {
let content = &raw[pos..content_end];
if !content.is_empty() && content[0] == 0x30 {
out.push(content.to_vec());
} else {
out.push(encode_der_sequence(content));
}
}
}
pos = content_end;
}
break;
} else {
pos = elem_end;
}
}
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..]);
}
}
#[allow(dead_code)]
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)
}
#[allow(dead_code)]
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 collect_typed_props_from_obj(o: &DerObject, out: &mut Vec<TypedIm4mProperty>) -> 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 (value, anomaly) = decode_typed_value(&seq[1], Some(k))?;
let meta = KNOWN_PROPERTIES.get(k);
let (name, description) = if let Some(m) = meta {
(m.name.to_string(), m.description.to_string())
} else {
("UnknownProperty".to_string(), "An unknown or undocumented property.".to_string())
};
out.push(TypedIm4mProperty {
key: k.to_string(),
name,
description,
value,
anomaly,
});
}
}
}
for ch in seq {
collect_typed_props_from_obj(ch, out)?;
}
return Ok(());
}
if let Ok(set) = o.as_set() {
for ch in set {
collect_typed_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_typed_props_from_obj(&child, out)?;
off += consumed;
}
}
}
Ok(())
}
fn integer_value(o: &DerObject) -> Im4mPropertyValue {
match o.as_u64() {
Ok(i) => Im4mPropertyValue::Integer { value: i },
Err(_) => Im4mPropertyValue::Unknown {
der_type: "INTEGER".to_string(),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
},
}
}
fn decode_typed_value(o: &DerObject, key_hint: Option<&str>) -> Result<(Im4mPropertyValue, Option<String>)> {
let meta = key_hint.and_then(|k| KNOWN_PROPERTIES.get(k));
let mut anomaly = None;
let val = match meta.map(|m| &m.expected_type) {
Some(ExpectedDerType::Boolean) => {
if let Ok(b) = o.as_bool() {
Im4mPropertyValue::Boolean { value: b }
} else {
anomaly = Some(format!("Expected BOOLEAN, got {:?}", o.header.tag()));
Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
Some(ExpectedDerType::Integer) => {
if o.header.tag().0 == 2 {
integer_value(o)
} else {
anomaly = Some(format!("Expected INTEGER, got {:?}", o.header.tag()));
Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
Some(ExpectedDerType::Digest) => {
if let Ok(s) = o.as_slice() {
Im4mPropertyValue::Digest { value: hex::encode(s) }
} else {
anomaly = Some(format!("Expected OCTET STRING for Digest, got {:?}", o.header.tag()));
Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
Some(ExpectedDerType::OctetString) => {
if let Ok(s) = o.as_slice() {
Im4mPropertyValue::OctetString { value: hex::encode(s) }
} else {
anomaly = Some(format!("Expected OCTET STRING, got {:?}", o.header.tag()));
Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
Some(ExpectedDerType::Ia5String) => {
if let Some(s) = ia5str(o) {
Im4mPropertyValue::String { value: s.to_string() }
} else {
anomaly = Some(format!("Expected IA5String, got {:?}", o.header.tag()));
Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
None => {
match o.header.tag().0 {
1 => Im4mPropertyValue::Boolean { value: o.as_bool().unwrap_or(false) },
2 => integer_value(o),
4 => Im4mPropertyValue::OctetString { value: hex::encode(o.as_slice().unwrap_or_default()) },
22 => Im4mPropertyValue::String { value: ia5str(o).unwrap_or("").to_string() },
16 | 17 => {
let (type_name, children) = if o.header.tag().0 == 16 {
("Sequence", o.as_sequence().ok())
} else {
("Set", o.as_set().ok())
};
if let Some(items) = children {
if items.is_empty() {
Im4mPropertyValue::Unknown {
der_type: format!("{}(empty)", type_name),
hex_value: None,
hex_values: None,
}
} else {
let mut parts = Vec::new();
for child in items {
if let Ok(slice) = child.as_slice() {
parts.push(hex::encode(slice));
}
}
Im4mPropertyValue::Unknown {
der_type: format!("{}({} elements)", type_name, items.len()),
hex_value: None,
hex_values: Some(parts),
}
}
} else {
Im4mPropertyValue::Unknown {
der_type: format!("{}(malformed)", type_name),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
}
}
}
_ => Im4mPropertyValue::Unknown {
der_type: format!("{:?}", o.header.tag()),
hex_value: Some(hex::encode(o.as_slice().unwrap_or_default())),
hex_values: None,
},
}
}
};
Ok((val, anomaly))
}
#[allow(dead_code)]
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;
let mut groups = 0usize;
loop {
if idx >= i.len() {
bail!("short high-tag-number");
}
let b = i[idx];
idx += 1;
groups += 1;
if groups > 5 || (tag_no >> 25) != 0 {
bail!("high-tag-number overflows u32");
}
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 n == 0x7F {
bail!("reserved length form (0xFF)");
}
if n > core::mem::size_of::<usize>() {
bail!("length field too large ({n} octets) for this platform");
}
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())
}
pub fn get_property_metadata(code: &str) -> Option<String> {
KNOWN_PROPERTIES.get(code).map(|meta| {
format!("{} ({})", meta.name, meta.description)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn len_short_form() {
assert_eq!(der_read_len(&[0x05]).unwrap(), (1, 5));
assert_eq!(der_read_len(&[0x00]).unwrap(), (1, 0));
assert_eq!(der_read_len(&[0x7F]).unwrap(), (1, 127));
}
#[test]
fn len_long_form() {
assert_eq!(der_read_len(&[0x82, 0x01, 0x00]).unwrap(), (3, 256));
assert_eq!(der_read_len(&[0x81, 0x80]).unwrap(), (2, 128));
}
#[test]
fn len_indefinite_rejected() {
assert!(der_read_len(&[0x80]).is_err());
}
#[test]
fn len_reserved_rejected() {
assert!(der_read_len(&[0xFF, 0, 0, 0, 0, 0, 0, 0, 0]).is_err());
}
#[test]
fn len_oversized_rejected_no_overflow() {
let buf = [0x89u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
assert!(der_read_len(&buf).is_err());
}
#[test]
fn len_truncated_rejected() {
assert!(der_read_len(&[0x82, 0x01]).is_err()); assert!(der_read_len(&[]).is_err());
}
#[test]
fn tag_low_form() {
let (used, class, constructed, tag) = der_read_tag(&[0x30]).unwrap();
assert_eq!((used, class, constructed, tag), (1, 0, true, 16));
let (used, class, constructed, tag) = der_read_tag(&[0x16]).unwrap();
assert_eq!((used, class, constructed, tag), (1, 0, false, 22));
}
#[test]
fn tag_high_form_ok() {
let bytes = [0xE0u8 | 0x1F, 0x84, 0xEA, 0x85, 0x9C, 0x42];
let (_used, class, constructed, tag) = der_read_tag(&bytes).unwrap();
assert_eq!(class, 3);
assert!(constructed);
assert_eq!(tag, 0x4D414E42);
}
#[test]
fn tag_high_form_overflow_rejected_no_panic() {
let bytes = [0x1Fu8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F];
assert!(der_read_tag(&bytes).is_err());
}
#[test]
fn tag_high_form_truncated_rejected() {
assert!(der_read_tag(&[0x1F, 0x84]).is_err()); }
#[test]
fn cert_chain_overrun_is_rejected() {
let inner: Vec<u8> = [
&[0x16, 0x04, b'I', b'M', b'4', b'M'][..], &[0x02, 0x01, 0x00][..], &[0x31, 0x00][..], &[0x04, 0x00][..], &[0x30, 0x02, 0x30, 0x7F][..], ]
.concat();
let mut raw = vec![0x30u8, inner.len() as u8];
raw.extend_from_slice(&inner);
let r = extract_im4m_cert_chain(&raw);
assert!(r.is_err(), "overrunning cert length must be rejected");
}
#[test]
fn cert_chain_truncated_is_rejected() {
assert!(extract_im4m_cert_chain(&[0x30]).is_err());
assert!(extract_im4m_cert_chain(&[]).is_err());
}
#[test]
fn cert_chain_valid_two_certs() {
let cert_a = [0x30u8, 0x03, 0x02, 0x01, 0x07]; let cert_b = [0x30u8, 0x03, 0x02, 0x01, 0x09];
let mut chain = Vec::new();
chain.extend_from_slice(&cert_a);
chain.extend_from_slice(&cert_b);
let mut elem4 = vec![0x30u8, chain.len() as u8];
elem4.extend_from_slice(&chain);
let inner: Vec<u8> = [
&[0x16, 0x04, b'I', b'M', b'4', b'M'][..],
&[0x02, 0x01, 0x00][..],
&[0x31, 0x00][..],
&[0x04, 0x00][..],
&elem4[..],
]
.concat();
let mut raw = vec![0x30u8, inner.len() as u8];
raw.extend_from_slice(&inner);
let certs = extract_im4m_cert_chain(&raw).unwrap();
assert_eq!(certs.len(), 2);
assert_eq!(certs[0], cert_a);
assert_eq!(certs[1], cert_b);
}
}