pub fn parse_tlv(data: &[u8]) -> Result<(u8, Vec<u8>), String> {
let (tag, body, _rest) = parse_tlv_with_rest(data)?;
Ok((tag, body.to_vec()))
}
pub fn parse_tlv_with_rest(data: &[u8]) -> Result<(u8, &[u8], &[u8]), String> {
if data.is_empty() {
return Err("empty input".into());
}
let tag = data[0];
let (len, header_len) = parse_der_length(&data[1..])?;
let total_header = 1 + header_len;
if total_header + len > data.len() {
return Err(format!(
"TLV length exceeds data: header={total_header}, len={len}, available={}",
data.len()
));
}
let value = &data[total_header..total_header + len];
let rest = &data[total_header + len..];
Ok((tag, value, rest))
}
pub fn parse_der_length(data: &[u8]) -> Result<(usize, usize), String> {
if data.is_empty() {
return Err("empty length".into());
}
let first = data[0];
if first < 0x80 {
Ok((first as usize, 1))
} else if first == 0x80 {
Err("indefinite length not supported".into())
} else {
let num_bytes = (first & 0x7F) as usize;
if num_bytes > 4 {
return Err(format!("length too large: {num_bytes} bytes"));
}
if 1 + num_bytes > data.len() {
return Err("insufficient data for length".into());
}
let mut len: usize = 0;
for i in 0..num_bytes {
len = (len << 8) | (data[1 + i] as usize);
}
Ok((len, 1 + num_bytes))
}
}
pub fn encode_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 5 + value.len());
out.push(tag);
encode_der_length(&mut out, value.len());
out.extend_from_slice(value);
out
}
pub fn encode_sequence_from_parts(parts: &[&[u8]]) -> Vec<u8> {
let total_len: usize = parts.iter().map(|p| p.len()).sum();
let mut body = Vec::with_capacity(total_len);
for part in parts {
body.extend_from_slice(part);
}
encode_sequence_raw(&body)
}
pub fn encode_sequence_raw(body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 5 + body.len());
out.push(0x30); encode_der_length(&mut out, body.len());
out.extend_from_slice(body);
out
}
pub fn encode_der_length(out: &mut Vec<u8>, len: usize) {
if len < 0x80 {
out.push(len as u8);
} else if len <= 0xFF {
out.push(0x81);
out.push(len as u8);
} else if len <= 0xFFFF {
out.push(0x82);
out.push((len >> 8) as u8);
out.push(len as u8);
} else if len <= 0xFF_FFFF {
out.push(0x83);
out.push((len >> 16) as u8);
out.push((len >> 8) as u8);
out.push(len as u8);
} else {
out.push(0x84);
out.push((len >> 24) as u8);
out.push((len >> 16) as u8);
out.push((len >> 8) as u8);
out.push(len as u8);
}
}
pub fn encode_integer_u64(val: u64) -> Vec<u8> {
let be_bytes = val.to_be_bytes();
let start = be_bytes.iter().position(|&b| b != 0).unwrap_or(7);
let significant = &be_bytes[start..];
let needs_padding = significant.is_empty() || (significant[0] & 0x80) != 0;
let mut value_bytes = Vec::with_capacity(significant.len() + 1);
if needs_padding {
value_bytes.push(0x00);
}
value_bytes.extend_from_slice(significant);
encode_tlv(0x02, &value_bytes)
}
pub fn decode_integer_u64(bytes: &[u8]) -> u64 {
let mut val: u64 = 0;
for &b in bytes {
val = (val << 8) | (b as u64);
}
val
}
pub fn encode_boolean(val: bool) -> Vec<u8> {
encode_tlv(0x01, &[if val { 0xFF } else { 0x00 }])
}
pub fn parse_integer_body(body: &[u8]) -> Vec<u8> {
if body.len() > 1 && body[0] == 0x00 {
body[1..].to_vec()
} else {
body.to_vec()
}
}
pub fn find_tagged_value<'a>(body: &'a [u8], target_tag: u8) -> Option<&'a [u8]> {
let mut pos = body;
while !pos.is_empty() {
match parse_tlv_with_rest(pos) {
Ok((tag, value, rest)) => {
if tag == target_tag {
return Some(value);
}
pos = rest;
}
Err(_) => break,
}
}
None
}
pub fn for_each_tlv<F>(body: &[u8], mut f: F)
where
F: FnMut(u8, &[u8]),
{
let mut pos = body;
while !pos.is_empty() {
match parse_tlv_with_rest(pos) {
Ok((tag, value, rest)) => {
f(tag, value);
pos = rest;
}
Err(_) => break,
}
}
}
pub fn integer_bodies_equal(a: &[u8], b: &[u8]) -> bool {
parse_integer_body(a) == parse_integer_body(b)
}
pub fn parse_generalized_time(body: &[u8]) -> Result<chrono::DateTime<chrono::Utc>, String> {
let s =
std::str::from_utf8(body).map_err(|e| format!("GeneralizedTime: invalid UTF-8: {e}"))?;
let s = s.strip_suffix('Z').unwrap_or(s);
let s = if let Some(dot_pos) = s.find('.') {
&s[..dot_pos]
} else {
s
};
if s.len() < 14 {
return Err(format!("GeneralizedTime too short: {s}"));
}
let year: i32 = s[0..4].parse().map_err(|e| format!("year: {e}"))?;
let month: u32 = s[4..6].parse().map_err(|e| format!("month: {e}"))?;
let day: u32 = s[6..8].parse().map_err(|e| format!("day: {e}"))?;
let hour: u32 = s[8..10].parse().map_err(|e| format!("hour: {e}"))?;
let min: u32 = s[10..12].parse().map_err(|e| format!("minute: {e}"))?;
let sec: u32 = s[12..14].parse().map_err(|e| format!("second: {e}"))?;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| format!("invalid date: {year}-{month}-{day}"))?;
let time = NaiveTime::from_hms_opt(hour, min, sec)
.ok_or_else(|| format!("invalid time: {hour}:{min}:{sec}"))?;
let dt = NaiveDateTime::new(date, time);
Ok(chrono::Utc.from_utc_datetime(&dt))
}
pub fn parse_utc_time(body: &[u8]) -> Result<chrono::DateTime<chrono::Utc>, String> {
let s = std::str::from_utf8(body).map_err(|e| format!("UTCTime: invalid UTF-8: {e}"))?;
let s = s.strip_suffix('Z').unwrap_or(s);
if s.len() < 12 {
return Err(format!("UTCTime too short: {s}"));
}
let yy: i32 = s[0..2].parse().map_err(|e| format!("year: {e}"))?;
let year = if yy >= 50 { 1900 + yy } else { 2000 + yy };
let month: u32 = s[2..4].parse().map_err(|e| format!("month: {e}"))?;
let day: u32 = s[4..6].parse().map_err(|e| format!("day: {e}"))?;
let hour: u32 = s[6..8].parse().map_err(|e| format!("hour: {e}"))?;
let min: u32 = s[8..10].parse().map_err(|e| format!("minute: {e}"))?;
let sec: u32 = s[10..12].parse().map_err(|e| format!("second: {e}"))?;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| format!("invalid date: {year}-{month}-{day}"))?;
let time = NaiveTime::from_hms_opt(hour, min, sec)
.ok_or_else(|| format!("invalid time: {hour}:{min}:{sec}"))?;
let dt = NaiveDateTime::new(date, time);
Ok(chrono::Utc.from_utc_datetime(&dt))
}
pub fn parse_x509_time(tag: u8, body: &[u8]) -> Result<chrono::DateTime<chrono::Utc>, String> {
match tag {
0x17 => parse_utc_time(body),
0x18 => parse_generalized_time(body),
_ => Err(format!(
"expected UTCTime (0x17) or GeneralizedTime (0x18), got 0x{tag:02x}"
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tlv_roundtrip() {
let value = b"hello";
let encoded = encode_tlv(0x04, value); let (tag, body) = parse_tlv(&encoded).unwrap();
assert_eq!(tag, 0x04);
assert_eq!(body, value);
}
#[test]
fn test_sequence_encoding() {
let int1 = encode_integer_u64(42);
let int2 = encode_integer_u64(100);
let seq = encode_sequence_from_parts(&[&int1, &int2]);
let (tag, _body) = parse_tlv(&seq).unwrap();
assert_eq!(tag, 0x30);
}
#[test]
fn test_der_length_roundtrip() {
for len in [0, 1, 127, 128, 255, 256, 65535, 65536] {
let mut buf = Vec::new();
encode_der_length(&mut buf, len);
let (parsed_len, consumed) = parse_der_length(&buf).unwrap();
assert_eq!(parsed_len, len, "length roundtrip failed for {len}");
assert_eq!(consumed, buf.len());
}
}
#[test]
fn test_encode_decode_integer() {
assert_eq!(encode_integer_u64(0), vec![0x02, 0x01, 0x00]);
assert_eq!(encode_integer_u64(1), vec![0x02, 0x01, 0x01]);
assert_eq!(encode_integer_u64(128), vec![0x02, 0x02, 0x00, 0x80]);
assert_eq!(decode_integer_u64(&[0x01]), 1);
assert_eq!(decode_integer_u64(&[0x00, 0x80]), 128);
}
#[test]
fn test_parse_integer_body_strips_padding() {
assert_eq!(parse_integer_body(&[0x00, 0x80]), vec![0x80]);
assert_eq!(parse_integer_body(&[0x42]), vec![0x42]);
assert_eq!(parse_integer_body(&[0x00, 0x01, 0x02]), vec![0x01, 0x02]);
}
#[test]
fn test_integer_bodies_equal() {
assert!(integer_bodies_equal(&[0x00, 0x80], &[0x80]));
assert!(integer_bodies_equal(&[0x42], &[0x42]));
assert!(!integer_bodies_equal(&[0x42], &[0x43]));
}
#[test]
fn test_find_tagged_value() {
let int = encode_integer_u64(42);
let oct = encode_tlv(0x04, b"hi");
let boolean = encode_boolean(true);
let mut body = Vec::new();
body.extend_from_slice(&int);
body.extend_from_slice(&oct);
body.extend_from_slice(&boolean);
let found = find_tagged_value(&body, 0x04);
assert_eq!(found, Some(b"hi".as_slice()));
let found = find_tagged_value(&body, 0x01);
assert_eq!(found, Some(&[0xFF][..]));
assert!(find_tagged_value(&body, 0x06).is_none());
}
#[test]
fn test_parse_generalized_time() {
let dt = parse_generalized_time(b"20260303120000Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-03-03T12:00:00+00:00");
let dt = parse_generalized_time(b"20260303120000.5Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-03-03T12:00:00+00:00");
}
#[test]
fn test_parse_utc_time() {
let dt = parse_utc_time(b"260303120000Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-03-03T12:00:00+00:00");
let dt = parse_utc_time(b"990101000000Z").unwrap();
assert_eq!(dt.to_rfc3339(), "1999-01-01T00:00:00+00:00");
}
#[test]
fn test_parse_x509_time() {
let dt = parse_x509_time(0x17, b"260303120000Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-03-03T12:00:00+00:00");
let dt = parse_x509_time(0x18, b"20260303120000Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-03-03T12:00:00+00:00");
assert!(parse_x509_time(0x04, b"whatever").is_err());
}
#[test]
fn test_for_each_tlv() {
let int = encode_integer_u64(1);
let oct = encode_tlv(0x04, b"test");
let mut body = Vec::new();
body.extend_from_slice(&int);
body.extend_from_slice(&oct);
let mut tags = Vec::new();
for_each_tlv(&body, |tag, _value| {
tags.push(tag);
});
assert_eq!(tags, vec![0x02, 0x04]);
}
#[test]
fn test_empty_input_errors() {
assert!(parse_tlv(&[]).is_err());
assert!(parse_tlv_with_rest(&[]).is_err());
assert!(parse_der_length(&[]).is_err());
}
}