pub const ASN1_SEQUENCE_TAG: u8 = 0x30;
pub const MIN_CMS_SIZE: usize = 100;
pub(super) const MAX_CMS_HEX_CHARS: usize = 32 * 1024 * 1024;
const EOC_BYTES: [u8; 2] = [0x00, 0x00];
const MAX_BER_DEPTH: usize = 64;
pub(crate) fn extract_der_from_padded_hex(hex_str: &str) -> Result<Vec<u8>, String> {
if hex_str.len() < 4 {
return Err("Hex string too short for ASN.1 TLV header".to_owned());
}
if !hex_str.is_ascii() {
return Err("Hex string contains non-hex characters".to_owned());
}
if hex_str.len() > MAX_CMS_HEX_CHARS {
return Err(format!(
"ASN.1 input is {} bytes, exceeds maximum ({} bytes)",
hex_str.len() / 2,
MAX_CMS_HEX_CHARS / 2
));
}
let tag = parse_hex_u8(&hex_str[0..2])?;
if tag != ASN1_SEQUENCE_TAG {
return Err(format!("Expected ASN.1 SEQUENCE (0x30), got 0x{tag:02x}"));
}
let length_byte = parse_hex_u8(&hex_str[2..4])?;
if length_byte == 0x80 {
return extract_ber_indefinite(hex_str);
}
let mut header_bytes = 2usize; let content_len = if length_byte < 0x80 {
usize::from(length_byte)
} else {
let num_len_bytes = usize::from(length_byte & 0x7f);
if num_len_bytes > 4 {
return Err(format!(
"ASN.1 length field too large: {num_len_bytes} bytes"
));
}
header_bytes += num_len_bytes;
let needed_hex = 4 + num_len_bytes * 2;
if hex_str.len() < needed_hex {
return Err("Hex string too short for ASN.1 length field".to_owned());
}
parse_hex_uint(&hex_str[4..needed_hex])?
};
let total_der_bytes = header_bytes.saturating_add(content_len);
let total_hex_chars = total_der_bytes.saturating_mul(2);
if total_hex_chars > MAX_CMS_HEX_CHARS {
return Err(format!(
"ASN.1 claims {total_der_bytes} bytes, exceeds maximum ({} bytes)",
MAX_CMS_HEX_CHARS / 2
));
}
if total_hex_chars > hex_str.len() {
return Err(format!(
"ASN.1 length ({total_der_bytes} bytes) exceeds available hex data ({} bytes)",
hex_str.len() / 2
));
}
decode_hex(&hex_str[..total_hex_chars])
}
fn extract_ber_indefinite(hex_str: &str) -> Result<Vec<u8>, String> {
let raw = decode_hex(hex_str)?;
let end = raw.len();
let mut pos = 2usize;
while pos < end {
if raw[pos..].starts_with(&EOC_BYTES) {
return Ok(raw[..pos + 2].to_vec());
}
pos = skip_tlv(&raw, pos, end, 0)?;
}
Err("BER indefinite-length SEQUENCE: EOC marker not found".to_owned())
}
fn skip_tlv(data: &[u8], pos: usize, end: usize, depth: usize) -> Result<usize, String> {
if depth > MAX_BER_DEPTH {
return Err(format!(
"BER parse: nesting too deep (>{MAX_BER_DEPTH} levels)"
));
}
if pos >= end {
return Err(format!("BER parse: unexpected end at offset {pos}"));
}
let tag_byte = data[pos];
let mut cursor = pos + 1;
if tag_byte & 0x1f == 0x1f {
while cursor < end && data[cursor] & 0x80 != 0 {
cursor += 1;
}
cursor += 1; }
if cursor >= end {
return Err("BER parse: tag extends beyond data".to_owned());
}
let length_byte = data[cursor];
cursor += 1;
if length_byte == 0x80 {
while cursor < end {
if data[cursor..].starts_with(&EOC_BYTES) {
return Ok(cursor + 2);
}
cursor = skip_tlv(data, cursor, end, depth + 1)?;
}
return Err("BER parse: nested indefinite-length without EOC".to_owned());
}
let content_len = if length_byte < 0x80 {
usize::from(length_byte)
} else {
let num_len_bytes = usize::from(length_byte & 0x7f);
if num_len_bytes > core::mem::size_of::<usize>() {
return Err(format!(
"BER parse: length field too large: {num_len_bytes} bytes"
));
}
if cursor + num_len_bytes > end {
return Err("BER parse: length field extends beyond data".to_owned());
}
let mut len = 0usize;
for &byte in &data[cursor..cursor + num_len_bytes] {
len = (len << 8) | usize::from(byte);
}
cursor += num_len_bytes;
len
};
Ok(cursor.saturating_add(content_len))
}
fn parse_hex_u8(pair: &str) -> Result<u8, String> {
u8::from_str_radix(pair, 16).map_err(|_| format!("Invalid hex byte: {pair:?}"))
}
fn parse_hex_uint(digits: &str) -> Result<usize, String> {
usize::from_str_radix(digits, 16).map_err(|_| format!("Invalid hex length field: {digits:?}"))
}
fn decode_hex(hex_str: &str) -> Result<Vec<u8>, String> {
hex::decode(hex_str).map_err(|e| format!("invalid hex: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn padded(der: &[u8], width: usize) -> String {
let mut s = hex::encode(der);
s.push_str(&"0".repeat(width - s.len()));
s
}
#[test]
fn extracts_short_form_der() {
let der = [0x30, 0x03, 0x01, 0x02, 0x03];
let hex = padded(&der, 200);
assert_eq!(extract_der_from_padded_hex(&hex).unwrap(), der);
}
#[test]
fn extracts_long_form_der() {
let mut der = vec![0x30, 0x81, 0x80];
der.extend(std::iter::repeat_n(0xAB, 128));
let hex = padded(&der, 1000);
assert_eq!(extract_der_from_padded_hex(&hex).unwrap(), der);
}
#[test]
fn extracts_ber_indefinite() {
let der = [0x30, 0x80, 0x04, 0x02, b'h', b'i', 0x00, 0x00];
let hex = padded(&der, 200);
assert_eq!(extract_der_from_padded_hex(&hex).unwrap(), der);
}
#[test]
fn ber_walks_nested_indefinite_children() {
let der = [
0x30, 0x80, 0x30, 0x80, 0x04, 0x01, b'x', 0x00, 0x00, 0x00, 0x00, ];
let hex = padded(&der, 200);
assert_eq!(extract_der_from_padded_hex(&hex).unwrap(), der);
}
#[test]
fn rejects_non_sequence_tag() {
let err = extract_der_from_padded_hex("02030102030000").unwrap_err();
assert!(err.contains("Expected ASN.1 SEQUENCE"), "{err}");
}
#[test]
fn rejects_too_short() {
let err = extract_der_from_padded_hex("30").unwrap_err();
assert!(err.contains("too short"), "{err}");
}
#[test]
fn rejects_length_exceeding_available_data() {
let err = extract_der_from_padded_hex("3081c8").unwrap_err();
assert!(err.contains("exceeds available hex data"), "{err}");
}
#[test]
fn rejects_oversized_length_field() {
let err = extract_der_from_padded_hex("30850000000000").unwrap_err();
assert!(err.contains("length field too large"), "{err}");
}
#[test]
fn rejects_ber_without_eoc() {
let der = [0x30, 0x80, 0x04, 0x03, b'a', b'b', b'c'];
let hex = hex::encode(der);
let err = extract_der_from_padded_hex(&hex).unwrap_err();
assert!(err.contains("EOC marker not found"), "{err}");
}
#[test]
fn refuses_oversized_indefinite_blob() {
let mut oversized = String::from("3080");
oversized.push_str(&"AB".repeat(MAX_CMS_HEX_CHARS / 2));
let err = extract_der_from_padded_hex(&oversized).unwrap_err();
assert!(err.contains("exceeds maximum"), "{err}");
}
#[test]
fn rejects_non_hex_input() {
let err = extract_der_from_padded_hex("30zz0102").unwrap_err();
assert!(
err.contains("Invalid hex byte") || err.contains("invalid hex"),
"{err}"
);
}
}