use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq};
use crate::error::{Error, Result};
use sigstore_types::SignatureBytes;
use sigstore_types::TimestampToken;
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub struct TimestampClient {
url: String,
client: reqwest::Client,
}
impl TimestampClient {
pub fn new(url: impl Into<String>) -> Self {
Self::new_with_timeout(url, DEFAULT_TIMEOUT)
}
pub fn new_with_timeout(url: impl Into<String>, timeout: Duration) -> Self {
Self {
url: url.into(),
client: reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("HTTP client configuration is valid"),
}
}
pub fn sigstore() -> Self {
Self::new("https://timestamp.sigstore.dev/api/v1/timestamp")
}
pub fn freetsa() -> Self {
Self::new("https://freetsa.org/tsr")
}
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_signature(&self, signature: &SignatureBytes) -> Result<TimestampToken> {
let digest = sigstore_crypto::sha256(signature.as_bytes());
self.timestamp(digest.as_bytes(), AlgorithmIdentifier::sha256())
.await
}
}