use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq};
use crate::error::{Error, Result};
use sigstore_types::{ArtifactDigest, SignatureBytes, TimestampToken, USER_AGENT};
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct TimestampClient {
url: String,
client: reqwest::Client,
}
#[derive(Debug, Clone)]
#[must_use]
pub struct TimestampClientBuilder {
url: String,
http_client: Option<reqwest::Client>,
}
impl TimestampClientBuilder {
pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self {
self.http_client = Some(http_client);
self
}
pub fn build(self) -> Result<TimestampClient> {
let client = match self.http_client {
Some(client) => client,
None => reqwest::Client::builder()
.timeout(DEFAULT_TIMEOUT)
.user_agent(USER_AGENT)
.build()
.map_err(|e| Error::Http(format!("failed to build HTTP client: {e}")))?,
};
Ok(TimestampClient {
url: self.url,
client,
})
}
}
impl TimestampClient {
pub fn new(url: impl Into<String>) -> Result<Self> {
Self::builder(url).build()
}
pub fn builder(url: impl Into<String>) -> TimestampClientBuilder {
TimestampClientBuilder {
url: url.into(),
http_client: None,
}
}
async fn timestamp(
&self,
digest: &[u8],
algorithm: AlgorithmIdentifier,
) -> Result<TimestampToken> {
let imprint = Asn1MessageImprint::new(algorithm, digest.to_vec());
let request = TimeStampReq::new(imprint);
let request_der = request
.to_der()
.map_err(|e| Error::Asn1(format!("failed to encode request: {}", e)))?;
let response = self
.client
.post(&self.url)
.header("Content-Type", "application/timestamp-query")
.body(request_der)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::Http(format!(
"TSA returned status {}",
response.status()
)));
}
let response_bytes = response
.bytes()
.await
.map_err(|e| Error::Http(e.to_string()))?;
let (tst_info, _) = crate::verify::parse_timestamp_token(&response_bytes)?;
if let Some(req_nonce) = &request.nonce {
if tst_info.nonce.as_ref() != Some(req_nonce) {
return Err(Error::InvalidResponse(format!(
"TSA response nonce mismatch: expected {:?}, got {:?}",
req_nonce, tst_info.nonce
)));
}
}
Ok(TimestampToken::new(response_bytes.to_vec()))
}
pub async fn timestamp_digest(&self, digest: &ArtifactDigest) -> Result<TimestampToken> {
let algorithm = AlgorithmIdentifier::try_from(digest.algorithm())?;
self.timestamp(digest.as_bytes(), algorithm).await
}
pub async fn timestamp_signature(&self, signature: &SignatureBytes) -> Result<TimestampToken> {
let digest = sigstore_crypto::sha256(signature.as_bytes());
self.timestamp(digest.as_bytes(), AlgorithmIdentifier::sha256())
.await
}
}