Skip to main content

sigstore_tsa/
client.rs

1//! TSA client for RFC 3161 Time-Stamp Protocol
2
3use 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
11/// A client for interacting with a Time-Stamp Authority
12pub struct TimestampClient {
13    /// Base URL of the TSA
14    url: String,
15    /// HTTP client
16    client: reqwest::Client,
17}
18
19impl TimestampClient {
20    /// Create a new TSA client with a 30-second request timeout.
21    pub fn new(url: impl Into<String>) -> Self {
22        Self::new_with_timeout(url, DEFAULT_TIMEOUT)
23    }
24
25    /// Create a TSA client with a custom total HTTP request timeout,
26    /// including reading the response body.
27    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    /// Create a client for the Sigstore TSA
38    pub fn sigstore() -> Self {
39        Self::new("https://timestamp.sigstore.dev/api/v1/timestamp")
40    }
41
42    /// Create a client for the FreeTSA service
43    pub fn freetsa() -> Self {
44        Self::new("https://freetsa.org/tsr")
45    }
46
47    /// Request a timestamp for the given digest
48    ///
49    /// # Arguments
50    /// * `digest` - The hash digest to timestamp
51    /// * `algorithm` - The hash algorithm used
52    ///
53    /// # Returns
54    /// The timestamp token (DER-encoded RFC 3161 response)
55    async fn timestamp(
56        &self,
57        digest: &[u8],
58        algorithm: AlgorithmIdentifier,
59    ) -> Result<TimestampToken> {
60        // Build the timestamp request
61        let imprint = Asn1MessageImprint::new(algorithm, digest.to_vec());
62        let request = TimeStampReq::new(imprint);
63
64        // Encode to DER
65        let request_der = request
66            .to_der()
67            .map_err(|e| Error::Asn1(format!("failed to encode request: {}", e)))?;
68
69        // Send HTTP request
70        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        // Get response bytes
87        let response_bytes = response
88            .bytes()
89            .await
90            .map_err(|e| Error::Http(e.to_string()))?;
91
92        // Parse the response and extract TstInfo
93        let (tst_info, _) = crate::verify::parse_timestamp_token(&response_bytes)?;
94
95        // Verify the nonce matches the request nonce
96        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        // Return the timestamp token
106        Ok(TimestampToken::new(response_bytes.to_vec()))
107    }
108
109    /// Request a timestamp for a signature
110    ///
111    /// This is the most common use case - timestamps the SHA-256 hash of the signature bytes.
112    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}