use std::time::Duration;
use reqwest::Client;
use crate::crypto::algorithm::DigestAlgorithm;
use crate::error::TspError;
use super::token;
const TSP_REQUEST_CONTENT_TYPE: &str = "application/timestamp-query";
const TSP_RESPONSE_CONTENT_TYPE: &str = "application/timestamp-reply";
#[derive(Debug, Clone)]
pub struct TsaClient {
url: String,
http_client: Client,
digest_algorithm: DigestAlgorithm,
policy_oid: Option<const_oid::ObjectIdentifier>,
timeout: Duration,
cert_req: bool,
}
impl TsaClient {
pub fn new(url: &str) -> Self {
Self {
url: url.to_string(),
http_client: Client::new(),
digest_algorithm: DigestAlgorithm::Sha256,
policy_oid: None,
timeout: Duration::from_secs(30),
cert_req: true,
}
}
pub fn digest_algorithm(mut self, alg: DigestAlgorithm) -> Self {
self.digest_algorithm = alg;
self
}
pub fn policy_oid(mut self, oid: const_oid::ObjectIdentifier) -> Self {
self.policy_oid = Some(oid);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn cert_req(mut self, cert_req: bool) -> Self {
self.cert_req = cert_req;
self
}
pub fn http_client(mut self, client: Client) -> Self {
self.http_client = client;
self
}
pub fn url(&self) -> &str {
&self.url
}
pub async fn timestamp(&self, data_hash: &[u8]) -> Result<Vec<u8>, TspError> {
let nonce = token::generate_nonce();
let req_der = token::build_timestamp_request(
self.digest_algorithm,
data_hash,
self.policy_oid.as_ref(),
Some(nonce),
self.cert_req,
)?;
log::debug!(
"Sending TimeStampReq to {} ({} bytes, nonce={})",
self.url,
req_der.len(),
nonce,
);
let response = self
.http_client
.post(&self.url)
.header("Content-Type", TSP_REQUEST_CONTENT_TYPE)
.timeout(self.timeout)
.body(req_der)
.send()
.await
.map_err(|e| TspError::HttpError(format!("TSA request to {} failed: {e}", self.url)))?;
let status = response.status();
if !status.is_success() {
return Err(TspError::HttpError(format!(
"TSA {} returned HTTP {status}",
self.url,
)));
}
if let Some(ct) = response.headers().get("content-type") {
let ct_str = ct.to_str().unwrap_or("");
if !ct_str.starts_with(TSP_RESPONSE_CONTENT_TYPE)
&& !ct_str.starts_with("application/timestamp-response")
{
log::warn!(
"TSA {} returned unexpected Content-Type: {ct_str}",
self.url,
);
}
}
let resp_bytes = response
.bytes()
.await
.map_err(|e| TspError::HttpError(format!("failed to read TSA response body: {e}")))?;
log::debug!(
"Received TimeStampResp from {} ({} bytes)",
self.url,
resp_bytes.len(),
);
let resp = token::parse_timestamp_response(&resp_bytes)?;
let token_der = token::validate_timestamp_response(
&resp,
data_hash,
Some(nonce),
self.digest_algorithm,
)?;
log::debug!(
"Timestamp token obtained from {} ({} bytes)",
self.url,
token_der.len(),
);
Ok(token_der)
}
#[cfg(feature = "blocking")]
pub fn timestamp_blocking(&self, data_hash: &[u8]) -> Result<Vec<u8>, TspError> {
tokio::runtime::Handle::current().block_on(self.timestamp(data_hash))
}
}
#[derive(Debug, Clone)]
pub struct TsaClientPool {
clients: Vec<TsaClient>,
}
impl TsaClientPool {
pub fn new(clients: Vec<TsaClient>) -> Self {
assert!(!clients.is_empty(), "TsaClientPool requires at least one client");
Self { clients }
}
pub fn from_url(url: &str) -> Self {
Self::new(vec![TsaClient::new(url)])
}
pub fn from_urls(urls: &[&str]) -> Self {
let clients = urls.iter().map(|u| TsaClient::new(u)).collect();
Self::new(clients)
}
pub async fn timestamp(&self, data_hash: &[u8]) -> Result<Vec<u8>, TspError> {
let mut last_error = None;
for (i, client) in self.clients.iter().enumerate() {
match client.timestamp(data_hash).await {
Ok(token) => return Ok(token),
Err(e) => {
log::warn!(
"TSA {} ({}) failed: {e}; trying next ({}/{} remaining)",
client.url(),
i + 1,
self.clients.len() - i - 1,
self.clients.len(),
);
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| {
TspError::HttpError("no TSA clients configured".into())
}))
}
#[cfg(feature = "blocking")]
pub fn timestamp_blocking(&self, data_hash: &[u8]) -> Result<Vec<u8>, TspError> {
tokio::runtime::Handle::current().block_on(self.timestamp(data_hash))
}
pub fn len(&self) -> usize {
self.clients.len()
}
pub fn is_empty(&self) -> bool {
self.clients.is_empty()
}
}