Skip to main content

sigstore_tsa/
client.rs

1//! TSA client for RFC 3161 Time-Stamp Protocol
2
3use crate::asn1::{AlgorithmIdentifier, Asn1MessageImprint, TimeStampReq, TimeStampResp};
4use crate::error::{Error, Result};
5use sigstore_types::SignatureBytes;
6use sigstore_types::TimestampToken;
7
8/// A client for interacting with a Time-Stamp Authority
9pub struct TimestampClient {
10    /// Base URL of the TSA
11    url: String,
12    /// HTTP client
13    client: reqwest::Client,
14}
15
16impl TimestampClient {
17    /// Create a new TSA client
18    pub fn new(url: impl Into<String>) -> Self {
19        Self {
20            url: url.into(),
21            client: reqwest::Client::new(),
22        }
23    }
24
25    /// Create a client for the Sigstore TSA
26    pub fn sigstore() -> Self {
27        Self::new("https://timestamp.sigstore.dev/api/v1/timestamp")
28    }
29
30    /// Create a client for the FreeTSA service
31    pub fn freetsa() -> Self {
32        Self::new("https://freetsa.org/tsr")
33    }
34
35    /// Request a timestamp for the given digest
36    ///
37    /// # Arguments
38    /// * `digest` - The hash digest to timestamp
39    /// * `algorithm` - The hash algorithm used
40    ///
41    /// # Returns
42    /// The timestamp token (DER-encoded RFC 3161 response)
43    async fn timestamp(
44        &self,
45        digest: &[u8],
46        algorithm: AlgorithmIdentifier,
47    ) -> Result<TimestampToken> {
48        // Build the timestamp request
49        let imprint = Asn1MessageImprint::new(algorithm, digest.to_vec());
50        let request = TimeStampReq::new(imprint);
51
52        // Encode to DER
53        let request_der = request
54            .to_der()
55            .map_err(|e| Error::Asn1(format!("failed to encode request: {}", e)))?;
56
57        // Send HTTP request
58        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        // Get response bytes
75        let response_bytes = response
76            .bytes()
77            .await
78            .map_err(|e| Error::Http(e.to_string()))?;
79
80        // Parse the response
81        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        // Return the timestamp token
92        Ok(TimestampToken::new(response_bytes.to_vec()))
93    }
94
95    /// Request a timestamp for a signature
96    ///
97    /// This is the most common use case - timestamps the SHA-256 hash of the signature bytes.
98    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}