use crate::{digest::DigestAlgorithm, error::AdesError};
pub const FREETSA_URL: &str = "https://freetsa.org/tsr";
#[cfg(feature = "tsp")]
pub struct TspClient {
url: String,
}
#[cfg(feature = "tsp")]
impl TspClient {
#[must_use]
pub fn new(url: &str) -> Self {
Self {
url: url.to_owned(),
}
}
pub fn timestamp(&self, hash: &[u8], algo: DigestAlgorithm) -> Result<Vec<u8>, AdesError> {
let req_der = build_ts_req(hash, algo)?;
let resp_der = self.post(&req_der)?;
extract_token(&resp_der)
}
fn post(&self, req_der: &[u8]) -> Result<Vec<u8>, AdesError> {
let resp = ureq::post(&self.url)
.set("Content-Type", "application/timestamp-query")
.send_bytes(req_der)
.map_err(|e| AdesError::Tsp(e.to_string()))?;
let mut body = Vec::new();
resp.into_reader()
.read_to_end(&mut body)
.map_err(|e| AdesError::Tsp(e.to_string()))?;
Ok(body)
}
}
fn build_ts_req(hash: &[u8], algo: DigestAlgorithm) -> Result<Vec<u8>, AdesError> {
let oid = algo.oid();
let alg_oid = der_tlv(0x06, oid.as_bytes()); let alg_null = [0x05u8, 0x00]; let alg_seq = der_tlv(0x30, &[alg_oid.as_slice(), &alg_null].concat());
let hash_os = der_tlv(0x04, hash); let msg_imprint = der_tlv(0x30, &[alg_seq.as_slice(), hash_os.as_slice()].concat());
let version = [0x02u8, 0x01, 0x01];
let cert_req = [0x01u8, 0x01, 0xff];
let body = [version.as_slice(), &msg_imprint, &cert_req].concat();
Ok(der_tlv(0x30, &body))
}
fn extract_token(resp_der: &[u8]) -> Result<Vec<u8>, AdesError> {
let outer = der_unwrap_seq(resp_der)
.ok_or_else(|| AdesError::Tsp("invalid TimeStampResp: not a SEQUENCE".to_owned()))?;
let (status_seq, rest) =
der_next_tlv(outer).ok_or_else(|| AdesError::Tsp("missing PKIStatusInfo".to_owned()))?;
let status_inner = der_unwrap_seq(status_seq)
.ok_or_else(|| AdesError::Tsp("PKIStatusInfo not a SEQUENCE".to_owned()))?;
let (status_int, _) = der_next_tlv(status_inner)
.ok_or_else(|| AdesError::Tsp("missing status INTEGER".to_owned()))?;
let status_value = status_int
.last()
.copied()
.ok_or_else(|| AdesError::Tsp("empty status INTEGER".to_owned()))?;
if status_value > 1 {
return Err(AdesError::Tsp(format!(
"TSA rejected request with status {status_value}"
)));
}
if rest.is_empty() {
return Err(AdesError::Tsp(
"TimeStampResp missing timeStampToken".to_owned(),
));
}
let (token_bytes, _) =
der_next_tlv(rest).ok_or_else(|| AdesError::Tsp("invalid timeStampToken".to_owned()))?;
Ok(token_bytes.to_vec())
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let len = value.len();
let mut out = vec![tag];
if len < 128 {
out.push(len as u8);
} else if len < 256 {
out.extend_from_slice(&[0x81, len as u8]);
} else if len < 65536 {
out.extend_from_slice(&[0x82, (len >> 8) as u8, (len & 0xff) as u8]);
} else {
out.extend_from_slice(&[
0x83,
(len >> 16) as u8,
(len >> 8) as u8,
(len & 0xff) as u8,
]);
}
out.extend_from_slice(value);
out
}
fn der_unwrap_seq(data: &[u8]) -> Option<&[u8]> {
if data.first()? != &0x30 {
return None;
}
let (len, contents) = der_decode_len(&data[1..])?;
if contents.len() < len {
return None;
}
Some(&contents[..len])
}
fn der_next_tlv(data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.is_empty() {
return None;
}
let (len, contents) = der_decode_len(&data[1..])?;
let header_size = contents.as_ptr() as usize - data.as_ptr() as usize;
let end = header_size + len;
if end > data.len() {
return None;
}
Some((&data[..end], &data[end..]))
}
fn der_decode_len(data: &[u8]) -> Option<(usize, &[u8])> {
let first = *data.first()?;
if first < 0x80 {
return Some((first as usize, &data[1..]));
}
let num_bytes = (first & 0x7f) as usize;
if num_bytes == 0 || num_bytes > 4 || data.len() <= num_bytes {
return None;
}
let mut len = 0usize;
for &b in &data[1..=num_bytes] {
len = (len << 8) | b as usize;
}
Some((len, &data[1 + num_bytes..]))
}