use crate::model::Binary;
use serde::Serialize;
use sha1::{Digest as _, Sha1};
use std::collections::BTreeSet;
use std::fmt::Write as _;
#[derive(Debug, Clone, Default, Serialize)]
pub struct SigningSummary {
pub signed: bool,
pub entries: usize,
pub subjects: Vec<String>,
pub thumbprints: Vec<String>,
}
fn der_len(b: &[u8], at: usize) -> Option<(u64, u64)> {
let t = *b.get(at)?;
if t & 0x80 == 0 {
return Some((2, t as u64));
}
let n = (t & 0x7f) as usize;
if n == 0 || n > 4 {
return None; }
let mut v: u64 = 0;
for i in 0..n {
v = (v << 8) | u64::from(*b.get(at + 1 + i)?);
}
Some(((2 + n) as u64, v))
}
fn der_content(b: &[u8], len_at: usize) -> Option<(usize, usize)> {
let t = *b.get(len_at)?;
let (hdr, len) = if t & 0x80 == 0 {
(1usize, usize::from(t))
} else {
let n = (t & 0x7f) as usize;
if n == 0 || n > 4 {
return None;
}
let mut v = 0usize;
for i in 0..n {
v = (v << 8) | usize::from(*b.get(len_at + 1 + i)?);
}
(1 + n, v)
};
let start = len_at + hdr;
let end = start.checked_add(len)?;
(end <= b.len()).then_some((start, end))
}
fn common_names(b: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0usize;
while i + 4 <= b.len() {
if b[i..i + 3] == [0x55, 0x04, 0x03] {
let tag_at = i + 3;
let len_at = i + 4;
let ok_type = matches!(b.get(tag_at), Some(0x0c | 0x13 | 0x16 | 0x1e));
if let Some((start, end)) = der_content(b, len_at).filter(|_| ok_type) {
let txt = String::from_utf8_lossy(&b[start..end]).into_owned();
if !txt.trim().is_empty() {
out.push(txt);
}
}
i += 3;
} else {
i += 1;
}
}
out
}
fn certificates(b: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut i = 0usize;
while i + 2 <= b.len() {
if b[i] == 0x30 {
if let Some((hl, cl)) = der_len(b, i) {
let start = i + hl as usize;
let end = start
.checked_add(cl as usize)
.unwrap_or(b.len())
.min(b.len());
if end > start && b[start] == 0x30 && end - start >= 24 {
out.push(&b[i..end]);
i = end;
continue;
}
}
}
i += 1;
}
out
}
fn sha1_hex(b: &[u8]) -> String {
let mut h = Sha1::new();
h.update(b);
let d = h.finalize();
d.iter().fold(String::new(), |mut s, x| {
let _ = write!(s, "{x:02X}");
s
})
}
pub fn summarize(bin: &Binary, bytes: &[u8]) -> SigningSummary {
let Some((off, size)) = bin.sig_region else {
return SigningSummary::default();
};
let start = off as usize;
let Some(sig) = bytes.get(start..start + size as usize) else {
return SigningSummary {
signed: true,
..Default::default()
};
};
let mut entries = 0usize;
let mut blobs: Vec<&[u8]> = Vec::new();
let mut pos = 0usize;
while pos + 8 <= sig.len() {
let w = u16::from_le_bytes([sig[pos], sig[pos + 1]]) as usize;
let rev = u16::from_le_bytes([sig[pos + 2], sig[pos + 3]]);
let ctype = u16::from_le_bytes([sig[pos + 4], sig[pos + 5]]);
entries += 1;
if rev >= 0x0200 && ctype == 0x0002 && w >= 8 {
let body = &sig[(pos + 8).min(sig.len())..(pos + w).min(sig.len())];
blobs.push(body);
}
if w < 8 {
break;
}
pos += w;
if pos > sig.len() {
break;
}
}
let all: Vec<u8> = blobs.iter().flat_map(|b| b.iter().copied()).collect();
let mut subjects = common_names(&all);
subjects.dedup();
let mut thumbprints = Vec::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
for cert in certificates(&all) {
let tp = sha1_hex(cert);
if seen.insert(tp.clone()) {
thumbprints.push(tp);
}
}
SigningSummary {
signed: true,
entries,
subjects,
thumbprints,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_der() -> Vec<u8> {
let payload = b"Knife Test Signer";
let mut tbs = vec![0x02, 0x01, 0x02]; let mut attr: Vec<u8> = vec![0x06, 0x03, 0x55, 0x04, 0x03]; attr.push(0x13); attr.push(payload.len() as u8);
attr.extend_from_slice(payload);
let mut attr_seq = vec![0x30, attr.len() as u8];
attr_seq.extend_from_slice(&attr);
tbs.extend_from_slice(&attr_seq);
let mut tbs_seq = vec![0x30, tbs.len() as u8];
tbs_seq.extend_from_slice(&tbs);
let mut cert = vec![0x30, tbs_seq.len() as u8];
cert.extend_from_slice(&tbs_seq);
cert
}
#[test]
fn der_scanner_finds_cn_and_thumbprint() {
let der = sample_der();
assert_eq!(common_names(&der), vec!["Knife Test Signer".to_string()]);
let certs = certificates(&der);
assert_eq!(certs.len(), 1);
assert_eq!(sha1_hex(certs[0]), sha1_hex(&der));
let mut seen = BTreeSet::new();
let tp = sha1_hex(certs[0]);
seen.insert(tp.clone());
assert_eq!(seen.len(), 1);
}
#[test]
fn empty_and_short_der_are_harmless() {
assert_eq!(common_names(&[]), Vec::<String>::new());
assert_eq!(certificates(b"\x30\x02\x00\x00").len(), 0);
assert_eq!(sha1_hex(b"").len(), 40);
}
}