1use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq, TimeStampResp};
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 tsr = TimeStampResp::from_der_bytes(&response_bytes)
82 .map_err(|e| Error::Asn1(format!("failed to decode response: {}", e)))?;
83
84 if !tsr.is_success() {
85 return Err(Error::InvalidResponse(format!(
86 "TSA returned status {:?}",
87 tsr.status.status_enum()
88 )));
89 }
90
91 Ok(TimestampToken::new(response_bytes.to_vec()))
93 }
94
95 pub async fn timestamp_signature(&self, signature: &SignatureBytes) -> Result<TimestampToken> {
99 let digest = sigstore_crypto::sha256(signature.as_bytes());
100 self.timestamp(digest.as_bytes(), AlgorithmIdentifier::sha256())
101 .await
102 }
103}