1use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq};
4use crate::error::{Error, Result};
5use sigstore_types::SignatureBytes;
6use sigstore_types::TimestampToken;
7use std::time::Duration;
8
9const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
10
11pub struct TimestampClient {
13 url: String,
15 client: reqwest::Client,
17}
18
19impl TimestampClient {
20 pub fn new(url: impl Into<String>) -> Self {
22 Self::new_with_timeout(url, DEFAULT_TIMEOUT)
23 }
24
25 pub fn new_with_timeout(url: impl Into<String>, timeout: Duration) -> Self {
28 Self {
29 url: url.into(),
30 client: reqwest::Client::builder()
31 .timeout(timeout)
32 .build()
33 .expect("HTTP client configuration is valid"),
34 }
35 }
36
37 pub fn sigstore() -> Self {
39 Self::new("https://timestamp.sigstore.dev/api/v1/timestamp")
40 }
41
42 pub fn freetsa() -> Self {
44 Self::new("https://freetsa.org/tsr")
45 }
46
47 async fn timestamp(
56 &self,
57 digest: &[u8],
58 algorithm: AlgorithmIdentifier,
59 ) -> Result<TimestampToken> {
60 let imprint = Asn1MessageImprint::new(algorithm, digest.to_vec());
62 let request = TimeStampReq::new(imprint);
63
64 let request_der = request
66 .to_der()
67 .map_err(|e| Error::Asn1(format!("failed to encode request: {}", e)))?;
68
69 let response = self
71 .client
72 .post(&self.url)
73 .header("Content-Type", "application/timestamp-query")
74 .body(request_der)
75 .send()
76 .await
77 .map_err(|e| Error::Http(e.to_string()))?;
78
79 if !response.status().is_success() {
80 return Err(Error::Http(format!(
81 "TSA returned status {}",
82 response.status()
83 )));
84 }
85
86 let response_bytes = response
88 .bytes()
89 .await
90 .map_err(|e| Error::Http(e.to_string()))?;
91
92 let (tst_info, _) = crate::verify::parse_timestamp_token(&response_bytes)?;
94
95 if let Some(req_nonce) = &request.nonce {
97 if tst_info.nonce.as_ref() != Some(req_nonce) {
98 return Err(Error::InvalidResponse(format!(
99 "TSA response nonce mismatch: expected {:?}, got {:?}",
100 req_nonce, tst_info.nonce
101 )));
102 }
103 }
104
105 Ok(TimestampToken::new(response_bytes.to_vec()))
107 }
108
109 pub async fn timestamp_signature(&self, signature: &SignatureBytes) -> Result<TimestampToken> {
113 let digest = sigstore_crypto::sha256(signature.as_bytes());
114 self.timestamp(digest.as_bytes(), AlgorithmIdentifier::sha256())
115 .await
116 }
117}