1use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq};
4use crate::error::{Error, Result};
5use sigstore_types::SignatureBytes;
6use sigstore_types::TimestampToken;
7
8pub struct TimestampClient {
10 url: String,
12 client: reqwest::Client,
14}
15
16impl TimestampClient {
17 pub fn new(url: impl Into<String>) -> Self {
19 Self {
20 url: url.into(),
21 client: reqwest::Client::new(),
22 }
23 }
24
25 pub fn sigstore() -> Self {
27 Self::new("https://timestamp.sigstore.dev/api/v1/timestamp")
28 }
29
30 pub fn freetsa() -> Self {
32 Self::new("https://freetsa.org/tsr")
33 }
34
35 async fn timestamp(
44 &self,
45 digest: &[u8],
46 algorithm: AlgorithmIdentifier,
47 ) -> Result<TimestampToken> {
48 let imprint = Asn1MessageImprint::new(algorithm, digest.to_vec());
50 let request = TimeStampReq::new(imprint);
51
52 let request_der = request
54 .to_der()
55 .map_err(|e| Error::Asn1(format!("failed to encode request: {}", e)))?;
56
57 let response = self
59 .client
60 .post(&self.url)
61 .header("Content-Type", "application/timestamp-query")
62 .body(request_der)
63 .send()
64 .await
65 .map_err(|e| Error::Http(e.to_string()))?;
66
67 if !response.status().is_success() {
68 return Err(Error::Http(format!(
69 "TSA returned status {}",
70 response.status()
71 )));
72 }
73
74 let response_bytes = response
76 .bytes()
77 .await
78 .map_err(|e| Error::Http(e.to_string()))?;
79
80 let (tst_info, _) = crate::verify::parse_timestamp_token(&response_bytes)?;
82
83 if let Some(req_nonce) = &request.nonce {
85 if tst_info.nonce.as_ref() != Some(req_nonce) {
86 return Err(Error::InvalidResponse(format!(
87 "TSA response nonce mismatch: expected {:?}, got {:?}",
88 req_nonce, tst_info.nonce
89 )));
90 }
91 }
92
93 Ok(TimestampToken::new(response_bytes.to_vec()))
95 }
96
97 pub async fn timestamp_signature(&self, signature: &SignatureBytes) -> Result<TimestampToken> {
101 let digest = sigstore_crypto::sha256(signature.as_bytes());
102 self.timestamp(digest.as_bytes(), AlgorithmIdentifier::sha256())
103 .await
104 }
105}