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]) -> Result<u64, String> {
if bytes.is_empty() {
return Err("empty INTEGER body".into());
}
let (significant, _overflow) = if bytes.len() > 1 && bytes[0] == 0x00 {
(&bytes[1..], false)
} else if bytes[0] & 0x80 != 0 {
return Err(format!(
"negative INTEGER body not representable as u64: 0x{:02x}...",
bytes[0]
));
} else {
(bytes, false)
};
if significant.len() > 8 {
return Err(format!(
"INTEGER body too large for u64: {} significant bytes",
significant.len()
));
}
let mut val: u64 = 0;
for &b in significant {
val = (val << 8) | (b as u64);
}
Ok(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 parse_basic_constraints(ext_value: &[u8]) -> Result<(bool, Option<u64>), String> {
let (tag, seq_body) = parse_tlv(ext_value)?;
if tag != 0x30 {
return Err(format!(
"basicConstraints: expected SEQUENCE (0x30), got 0x{tag:02x}"
));
}
let mut is_ca = false;
let mut path_len: Option<u64> = None;
let mut pos = &seq_body[..];
if !pos.is_empty() {
let (t, value, rest) =
parse_tlv_with_rest(pos).map_err(|e| format!("basicConstraints cA: {e}"))?;
if t == 0x01 {
if value.len() != 1 {
return Err(format!(
"basicConstraints cA: BOOLEAN must be 1 byte, got {}",
value.len()
));
}
is_ca = value[0] != 0x00;
pos = rest;
}
}
if !pos.is_empty() {
let (t, value, rest) = parse_tlv_with_rest(pos)
.map_err(|e| format!("basicConstraints pathLenConstraint: {e}"))?;
if t != 0x02 {
return Err(format!(
"basicConstraints: unexpected element 0x{t:02x} (expected pathLenConstraint INTEGER)"
));
}
if !is_ca {
return Err("basicConstraints: pathLenConstraint present without cA:TRUE".to_string());
}
path_len = Some(decode_integer_u64(value).map_err(|e| format!("pathLenConstraint: {e}"))?);
if !rest.is_empty() {
return Err("basicConstraints: trailing data after pathLenConstraint".to_string());
}
}
Ok((is_ca, path_len))
}
pub fn find_tagged_value(body: &[u8], target_tag: u8) -> Option<&[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}"))?;
if !s.is_ascii() {
return Err("GeneralizedTime: non-ASCII characters".to_string());
}
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}"))?;
if !s.is_ascii() {
return Err("UTCTime: non-ASCII characters".to_string());
}
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]).unwrap(), 1);
assert_eq!(decode_integer_u64(&[0x00, 0x80]).unwrap(), 128);
assert!(decode_integer_u64(&[0x80]).is_err());
assert!(
decode_integer_u64(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]).is_err()
);
}
#[test]
fn test_parse_basic_constraints_ok() {
let bc = encode_sequence_from_parts(&[&encode_boolean(true), &encode_integer_u64(2)]);
assert_eq!(parse_basic_constraints(&bc).unwrap(), (true, Some(2)));
assert_eq!(
parse_basic_constraints(&[0x30, 0x00]).unwrap(),
(false, None)
);
let bc = encode_sequence_from_parts(&[&encode_boolean(true)]);
assert_eq!(parse_basic_constraints(&bc).unwrap(), (true, None));
}
#[test]
fn test_parse_basic_constraints_rejects_malformed_child() {
let malformed = [0x30, 0x02, 0x01, 0x01];
assert!(parse_basic_constraints(&malformed).is_err());
let mut body = encode_boolean(true);
body.extend_from_slice(&[0x02, 0x04, 0x00]); let seq = encode_sequence_raw(&body);
assert!(parse_basic_constraints(&seq).is_err());
}
#[test]
fn test_parse_basic_constraints_rejects_pathlen_without_ca() {
let bc = encode_sequence_from_parts(&[&encode_integer_u64(0)]); assert!(parse_basic_constraints(&bc).is_err());
let bc = encode_sequence_from_parts(&[&encode_boolean(false), &encode_integer_u64(1)]);
assert!(parse_basic_constraints(&bc).is_err());
}
#[test]
fn test_parse_basic_constraints_rejects_unexpected_child() {
let bc = encode_sequence_from_parts(&[
&encode_boolean(true),
&encode_tlv(0x05, &[]), ]);
assert!(parse_basic_constraints(&bc).is_err());
let bc = encode_sequence_from_parts(&[
&encode_boolean(true),
&encode_integer_u64(1),
&encode_tlv(0x05, &[]),
]);
assert!(parse_basic_constraints(&bc).is_err());
}
#[test]
fn test_parse_basic_constraints_rejects_malformed_ca_boolean() {
let bc = encode_sequence_from_parts(&[&encode_tlv(0x01, &[])]); assert!(parse_basic_constraints(&bc).is_err());
let bc = encode_sequence_from_parts(&[&encode_tlv(0x01, &[0xFF, 0xFF])]); assert!(parse_basic_constraints(&bc).is_err());
}
#[test]
fn test_parse_basic_constraints_large_pathlen_not_truncated() {
let big = encode_sequence_from_parts(&[
&encode_boolean(true),
&encode_integer_u64(0x1_0000_0001),
]);
assert_eq!(
parse_basic_constraints(&big).unwrap(),
(true, Some(0x1_0000_0001))
);
}
#[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());
}
}