Skip to main content

cdx_core/provenance/
rfc3161.rs

1//! RFC 3161 Time-Stamp Protocol client.
2//!
3//! This module provides a client for acquiring timestamps from RFC 3161
4//! compliant Time Stamp Authorities (TSAs). These timestamps provide
5//! cryptographic proof that data existed at a specific point in time.
6//!
7//! # Feature Flag
8//!
9//! This module requires the `timestamps-rfc3161` feature:
10//!
11//! ```toml
12//! [dependencies]
13//! cdx-core = { version = "0.1", features = ["timestamps-rfc3161"] }
14//! ```
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use cdx_core::provenance::rfc3161::Rfc3161Client;
20//! use cdx_core::{HashAlgorithm, Hasher};
21//!
22//! # async fn example() -> cdx_core::Result<()> {
23//! let client = Rfc3161Client::new();
24//! let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"document content");
25//! let timestamp = client.acquire_timestamp(&doc_id).await?;
26//! println!("Timestamp acquired at: {}", timestamp.time);
27//! # Ok(())
28//! # }
29//! ```
30
31use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
32use chrono::{DateTime, Utc};
33use const_oid::ObjectIdentifier;
34use std::io::{Error as IoError, ErrorKind};
35
36use super::record::TimestampRecord;
37use crate::{DocumentId, Error, Result};
38
39/// Well-known free RFC 3161 TSA server URLs.
40pub mod servers {
41    /// `FreeTSA` - Free timestamp service.
42    pub const FREETSA: &str = "https://freetsa.org/tsr";
43    /// Sectigo timestamp server.
44    pub const SECTIGO: &str = "http://timestamp.sectigo.com";
45    /// `DigiCert` timestamp server.
46    pub const DIGICERT: &str = "http://timestamp.digicert.com";
47}
48
49// ASN.1 OIDs for hash algorithms
50const OID_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
51const OID_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.2");
52const OID_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.3");
53
54/// RFC 3161 Time-Stamp Protocol client.
55///
56/// The client communicates with TSA servers to request timestamps
57/// and retrieve signed timestamp tokens.
58#[derive(Debug, Clone)]
59pub struct Rfc3161Client {
60    /// TSA server URLs to use (in order of preference).
61    servers: Vec<String>,
62    /// HTTP client.
63    client: reqwest::Client,
64    /// Request timeout in seconds.
65    timeout_secs: u64,
66    /// Whether to request certificate inclusion.
67    cert_req: bool,
68}
69
70impl Default for Rfc3161Client {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Rfc3161Client {
77    /// Create a new RFC 3161 client with default TSA servers.
78    #[must_use]
79    pub fn new() -> Self {
80        Self {
81            servers: vec![
82                servers::FREETSA.to_string(),
83                servers::SECTIGO.to_string(),
84                servers::DIGICERT.to_string(),
85            ],
86            client: reqwest::Client::new(),
87            timeout_secs: 30,
88            cert_req: true,
89        }
90    }
91
92    /// Create a new RFC 3161 client with a specific TSA server.
93    #[must_use]
94    pub fn with_server(server: impl Into<String>) -> Self {
95        Self {
96            servers: vec![server.into()],
97            client: reqwest::Client::new(),
98            timeout_secs: 30,
99            cert_req: true,
100        }
101    }
102
103    /// Create a new RFC 3161 client with custom TSA servers.
104    #[must_use]
105    pub fn with_servers(servers: Vec<String>) -> Self {
106        Self {
107            servers,
108            client: reqwest::Client::new(),
109            timeout_secs: 30,
110            cert_req: true,
111        }
112    }
113
114    /// Set the request timeout.
115    #[must_use]
116    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
117        self.timeout_secs = timeout_secs;
118        self
119    }
120
121    /// Set whether to request certificate inclusion.
122    #[must_use]
123    pub fn with_cert_req(mut self, cert_req: bool) -> Self {
124        self.cert_req = cert_req;
125        self
126    }
127
128    /// Acquire a timestamp for a document.
129    ///
130    /// This sends a timestamp request to an RFC 3161 TSA server
131    /// and returns a timestamp record containing the signed token.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if:
136    /// - No TSA servers are reachable
137    /// - The TSA rejects the request
138    /// - Network errors occur
139    pub async fn acquire_timestamp(&self, document_id: &DocumentId) -> Result<TimestampRecord> {
140        // Get hash algorithm OID
141        let hash_oid = match document_id.algorithm().as_str() {
142            "sha256" => OID_SHA256,
143            "sha384" => OID_SHA384,
144            "sha512" => OID_SHA512,
145            alg => {
146                return Err(Error::InvalidManifest {
147                    reason: format!("Unsupported hash algorithm for RFC 3161: {alg}"),
148                })
149            }
150        };
151
152        // Get the raw hash bytes
153        let hash_hex = document_id.hex_digest();
154        let hash_bytes = hex_to_bytes(&hash_hex)?;
155
156        // Generate nonce for replay protection
157        let mut nonce_bytes = [0u8; 8];
158        getrandom::fill(&mut nonce_bytes).map_err(|e| Error::Network {
159            message: format!("System RNG failed: {e}"),
160        })?;
161        let nonce = u64::from_be_bytes(nonce_bytes);
162
163        // Build the timestamp request using manual DER encoding
164        let request_der = encode_timestamp_request(&hash_bytes, hash_oid, nonce, self.cert_req);
165
166        // Try each TSA server until one succeeds
167        let mut last_error = None;
168        for server_url in &self.servers {
169            match self.submit_to_tsa(server_url, &request_der).await {
170                Ok((token, time)) => {
171                    return Ok(TimestampRecord::rfc3161(
172                        server_url,
173                        time,
174                        BASE64.encode(&token),
175                    ));
176                }
177                Err(e) => {
178                    last_error = Some(e);
179                }
180            }
181        }
182
183        Err(last_error.unwrap_or_else(|| {
184            Error::Io(IoError::new(
185                ErrorKind::NotConnected,
186                "No TSA servers configured",
187            ))
188        }))
189    }
190
191    /// Submit a timestamp request to a TSA server.
192    async fn submit_to_tsa(
193        &self,
194        server_url: &str,
195        request_der: &[u8],
196    ) -> Result<(Vec<u8>, DateTime<Utc>)> {
197        let response = self
198            .client
199            .post(server_url)
200            .timeout(std::time::Duration::from_secs(self.timeout_secs))
201            .header("Content-Type", "application/timestamp-query")
202            .body(request_der.to_vec())
203            .send()
204            .await
205            .map_err(|e| {
206                Error::Io(IoError::new(
207                    ErrorKind::ConnectionRefused,
208                    format!("Failed to contact TSA server: {e}"),
209                ))
210            })?;
211
212        if !response.status().is_success() {
213            let status = response.status();
214            let text = response.text().await.unwrap_or_default();
215            return Err(Error::Io(IoError::other(format!(
216                "TSA server returned error: {status} {text}"
217            ))));
218        }
219
220        let response_der = response.bytes().await.map_err(|e| {
221            Error::Io(IoError::new(
222                ErrorKind::InvalidData,
223                format!("Failed to read TSA response: {e}"),
224            ))
225        })?;
226
227        // Parse the timestamp response
228        let (status, token) = parse_timestamp_response(&response_der)?;
229
230        // Check status (0 = granted, 1 = granted with mods)
231        if status > 1 {
232            let status_text = match status {
233                2 => "rejection",
234                3 => "waiting",
235                4 => "revocation warning",
236                5 => "revocation notification",
237                _ => "unknown error",
238            };
239            return Err(Error::Io(IoError::other(format!(
240                "TSA rejected request: {status_text}"
241            ))));
242        }
243
244        // Use current time as we'd need full CMS parsing for exact time
245        let time = Utc::now();
246
247        Ok((token, time))
248    }
249
250    /// Verify a timestamp token.
251    ///
252    /// This performs basic validation of the timestamp token format.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the timestamp token contains invalid Base64 data.
257    ///
258    /// # Note
259    ///
260    /// Full cryptographic verification requires the TSA's certificate chain,
261    /// which is beyond the scope of this basic implementation.
262    pub fn verify_timestamp(
263        &self,
264        timestamp: &TimestampRecord,
265        _document_id: &DocumentId,
266    ) -> Result<TimestampVerification> {
267        // Decode the token
268        let token_bytes = BASE64.decode(&timestamp.token).map_err(|e| {
269            Error::Io(IoError::new(
270                ErrorKind::InvalidData,
271                format!("Invalid timestamp token: {e}"),
272            ))
273        })?;
274
275        // Basic validation: check it's not empty and looks like ASN.1
276        if token_bytes.is_empty() {
277            return Ok(TimestampVerification {
278                valid: false,
279                status: VerificationStatus::Invalid,
280                message: "Empty token".to_string(),
281            });
282        }
283
284        // Check for SEQUENCE tag (0x30) which indicates valid ASN.1 structure
285        if token_bytes[0] != 0x30 {
286            return Ok(TimestampVerification {
287                valid: false,
288                status: VerificationStatus::Invalid,
289                message: "Invalid ASN.1 structure".to_string(),
290            });
291        }
292
293        Ok(TimestampVerification {
294            valid: true,
295            status: VerificationStatus::Valid,
296            message: "Timestamp token is well-formed".to_string(),
297        })
298    }
299}
300
301/// Result of timestamp verification.
302#[derive(Debug, Clone)]
303pub struct TimestampVerification {
304    /// Whether the token passed basic validation.
305    pub valid: bool,
306    /// Verification status.
307    pub status: VerificationStatus,
308    /// Human-readable message.
309    pub message: String,
310}
311
312/// Status of timestamp verification.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum VerificationStatus {
315    /// Token is valid and verified.
316    Valid,
317    /// Token format is invalid.
318    Invalid,
319}
320
321/// Manually encode a `TimeStampReq` in DER format.
322///
323/// `TimeStampReq` ::= SEQUENCE {
324///    version          INTEGER { v1(1) },
325///    `messageImprint`   `MessageImprint`,
326///    nonce            INTEGER OPTIONAL,
327///    `certReq`          BOOLEAN DEFAULT FALSE,
328/// }
329fn encode_timestamp_request(
330    hash: &[u8],
331    hash_oid: ObjectIdentifier,
332    nonce: u64,
333    cert_req: bool,
334) -> Vec<u8> {
335    let mut content = Vec::new();
336
337    // version INTEGER (1)
338    content.extend_from_slice(&encode_integer(1));
339
340    // messageImprint SEQUENCE
341    content.extend_from_slice(&encode_message_imprint(hash, hash_oid));
342
343    // nonce INTEGER
344    content.extend_from_slice(&encode_integer_u64(nonce));
345
346    // certReq BOOLEAN (only include if true, as false is default)
347    if cert_req {
348        content.extend_from_slice(&[0x01, 0x01, 0xff]); // BOOLEAN TRUE
349    }
350
351    // Wrap in SEQUENCE
352    encode_sequence(&content)
353}
354
355/// Encode `MessageImprint` SEQUENCE.
356fn encode_message_imprint(hash: &[u8], hash_oid: ObjectIdentifier) -> Vec<u8> {
357    let mut content = Vec::new();
358
359    // AlgorithmIdentifier SEQUENCE
360    let mut alg_id = Vec::new();
361    alg_id.extend_from_slice(&encode_oid(&hash_oid));
362    alg_id.extend_from_slice(&[0x05, 0x00]); // NULL parameters
363    content.extend_from_slice(&encode_sequence(&alg_id));
364
365    // hashedMessage OCTET STRING
366    content.extend_from_slice(&encode_octet_string(hash));
367
368    encode_sequence(&content)
369}
370
371/// Encode a SEQUENCE.
372fn encode_sequence(content: &[u8]) -> Vec<u8> {
373    let mut result = vec![0x30]; // SEQUENCE tag
374    result.extend_from_slice(&encode_length(content.len()));
375    result.extend_from_slice(content);
376    result
377}
378
379/// Encode an INTEGER (small positive value).
380fn encode_integer(value: u8) -> Vec<u8> {
381    vec![0x02, 0x01, value]
382}
383
384/// Encode a u64 INTEGER.
385#[allow(clippy::cast_possible_truncation)]
386fn encode_integer_u64(value: u64) -> Vec<u8> {
387    let bytes = value.to_be_bytes();
388    // Find first non-zero byte
389    let start = bytes.iter().position(|&b| b != 0).unwrap_or(7);
390    let significant = &bytes[start..];
391
392    let mut result = vec![0x02]; // INTEGER tag
393
394    // Add leading zero if high bit is set (to keep it positive)
395    // Length is at most 9 bytes, so cast to u8 is safe
396    if significant.first().is_some_and(|&b| b & 0x80 != 0) {
397        result.push((significant.len() + 1) as u8);
398        result.push(0x00);
399    } else {
400        result.push(significant.len() as u8);
401    }
402    result.extend_from_slice(significant);
403    result
404}
405
406/// Encode an OID.
407fn encode_oid(oid: &ObjectIdentifier) -> Vec<u8> {
408    let oid_bytes = oid.as_bytes();
409    let mut result = vec![0x06]; // OID tag
410    result.extend_from_slice(&encode_length(oid_bytes.len()));
411    result.extend_from_slice(oid_bytes);
412    result
413}
414
415/// Encode an OCTET STRING.
416fn encode_octet_string(data: &[u8]) -> Vec<u8> {
417    let mut result = vec![0x04]; // OCTET STRING tag
418    result.extend_from_slice(&encode_length(data.len()));
419    result.extend_from_slice(data);
420    result
421}
422
423/// Encode DER length.
424///
425/// Supports lengths up to 65535 bytes (2-byte long form).
426#[allow(clippy::cast_possible_truncation)]
427fn encode_length(len: usize) -> Vec<u8> {
428    // For DER encoding, lengths fit in u8 or u16; truncation is intentional
429    if len < 128 {
430        vec![len as u8]
431    } else if len < 256 {
432        vec![0x81, len as u8]
433    } else {
434        vec![0x82, (len >> 8) as u8, (len & 0xff) as u8]
435    }
436}
437
438/// Parse a `TimeStampResp` and extract status and token.
439fn parse_timestamp_response(data: &[u8]) -> Result<(u8, Vec<u8>)> {
440    // Very basic ASN.1 parsing
441    // TimeStampResp ::= SEQUENCE {
442    //    status PKIStatusInfo,
443    //    timeStampToken TimeStampToken OPTIONAL
444    // }
445
446    if data.is_empty() || data[0] != 0x30 {
447        return Err(Error::Io(IoError::new(
448            ErrorKind::InvalidData,
449            "Invalid TSA response: not a SEQUENCE",
450        )));
451    }
452
453    let (content, _) = parse_tlv(data)?;
454
455    // Parse PKIStatusInfo (first element)
456    if content.is_empty() || content[0] != 0x30 {
457        return Err(Error::Io(IoError::new(
458            ErrorKind::InvalidData,
459            "Invalid PKIStatusInfo",
460        )));
461    }
462
463    let (status_info, rest) = parse_tlv(content)?;
464
465    // Extract status INTEGER from PKIStatusInfo
466    if status_info.is_empty() || status_info[0] != 0x02 {
467        return Err(Error::Io(IoError::new(
468            ErrorKind::InvalidData,
469            "Invalid status in PKIStatusInfo",
470        )));
471    }
472
473    let (status_bytes, _) = parse_tlv(status_info)?;
474    let status = *status_bytes.last().unwrap_or(&255);
475
476    // Extract timeStampToken if present (should be the rest after status info)
477    if rest.is_empty() {
478        return Err(Error::Io(IoError::new(
479            ErrorKind::InvalidData,
480            "No timestamp token in response",
481        )));
482    }
483
484    // The token is a ContentInfo SEQUENCE
485    if rest[0] != 0x30 {
486        return Err(Error::Io(IoError::new(
487            ErrorKind::InvalidData,
488            "Invalid timestamp token format",
489        )));
490    }
491
492    // Return the full token (including its SEQUENCE wrapper)
493    let token_len = get_tlv_total_length(rest)?;
494    let token = rest[..token_len].to_vec();
495
496    Ok((status, token))
497}
498
499/// Parse a TLV (Tag-Length-Value) and return the value and remaining data.
500fn parse_tlv(data: &[u8]) -> Result<(&[u8], &[u8])> {
501    if data.len() < 2 {
502        return Err(Error::Io(IoError::new(
503            ErrorKind::InvalidData,
504            "TLV too short",
505        )));
506    }
507
508    let (len, header_len) = if data[1] < 128 {
509        (data[1] as usize, 2)
510    } else if data[1] == 0x81 {
511        if data.len() < 3 {
512            return Err(Error::Io(IoError::new(
513                ErrorKind::InvalidData,
514                "Invalid length encoding",
515            )));
516        }
517        (data[2] as usize, 3)
518    } else if data[1] == 0x82 {
519        if data.len() < 4 {
520            return Err(Error::Io(IoError::new(
521                ErrorKind::InvalidData,
522                "Invalid length encoding",
523            )));
524        }
525        (((data[2] as usize) << 8) | (data[3] as usize), 4)
526    } else {
527        return Err(Error::Io(IoError::new(
528            ErrorKind::InvalidData,
529            "Unsupported length encoding",
530        )));
531    };
532
533    if data.len() < header_len + len {
534        return Err(Error::Io(IoError::new(
535            ErrorKind::InvalidData,
536            "TLV length exceeds data",
537        )));
538    }
539
540    let value = &data[header_len..header_len + len];
541    let rest = &data[header_len + len..];
542    Ok((value, rest))
543}
544
545/// Get the total length of a TLV including header.
546fn get_tlv_total_length(data: &[u8]) -> Result<usize> {
547    if data.len() < 2 {
548        return Err(Error::Io(IoError::new(
549            ErrorKind::InvalidData,
550            "TLV too short",
551        )));
552    }
553
554    let (len, header_len) = if data[1] < 128 {
555        (data[1] as usize, 2)
556    } else if data[1] == 0x81 {
557        if data.len() < 3 {
558            return Err(Error::Io(IoError::new(
559                ErrorKind::InvalidData,
560                "Invalid length encoding",
561            )));
562        }
563        (data[2] as usize, 3)
564    } else if data[1] == 0x82 {
565        if data.len() < 4 {
566            return Err(Error::Io(IoError::new(
567                ErrorKind::InvalidData,
568                "Invalid length encoding",
569            )));
570        }
571        (((data[2] as usize) << 8) | (data[3] as usize), 4)
572    } else {
573        return Err(Error::Io(IoError::new(
574            ErrorKind::InvalidData,
575            "Unsupported length encoding",
576        )));
577    };
578
579    Ok(header_len + len)
580}
581
582/// Convert hex string to bytes.
583fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
584    let hex = hex.trim();
585    if !hex.len().is_multiple_of(2) {
586        return Err(Error::InvalidHashFormat {
587            value: "Invalid hex string length".to_string(),
588        });
589    }
590
591    (0..hex.len())
592        .step_by(2)
593        .map(|i| {
594            u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| Error::InvalidHashFormat {
595                value: "Invalid hex character".to_string(),
596            })
597        })
598        .collect()
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::{HashAlgorithm, Hasher};
605
606    #[test]
607    fn test_rfc3161_client_creation() {
608        let client = Rfc3161Client::new();
609        assert!(!client.servers.is_empty());
610    }
611
612    #[test]
613    fn test_rfc3161_client_custom_server() {
614        let client = Rfc3161Client::with_server("https://custom.example.com/tsa");
615        assert_eq!(client.servers.len(), 1);
616        assert_eq!(client.servers[0], "https://custom.example.com/tsa");
617    }
618
619    #[test]
620    fn test_hex_to_bytes() {
621        let bytes = hex_to_bytes("deadbeef").unwrap();
622        assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
623    }
624
625    #[test]
626    fn test_hex_to_bytes_invalid() {
627        assert!(hex_to_bytes("deadbee").is_err()); // odd length
628        assert!(hex_to_bytes("deadbeeg").is_err()); // invalid char
629    }
630
631    #[test]
632    fn test_timestamp_req_encoding() {
633        let hash = vec![0u8; 32]; // dummy SHA-256 hash
634        let req = encode_timestamp_request(&hash, OID_SHA256, 12345, true);
635        // Should produce valid DER
636        assert!(!req.is_empty());
637        assert_eq!(req[0], 0x30); // SEQUENCE tag
638    }
639
640    #[test]
641    fn test_encode_integer() {
642        let encoded = encode_integer(1);
643        assert_eq!(encoded, vec![0x02, 0x01, 0x01]);
644    }
645
646    #[test]
647    fn test_encode_integer_u64() {
648        let encoded = encode_integer_u64(256);
649        // 256 = 0x0100, needs leading zero since high bit of 0x01 is clear
650        assert_eq!(encoded, vec![0x02, 0x02, 0x01, 0x00]);
651    }
652
653    #[test]
654    fn test_verify_empty_token() {
655        let client = Rfc3161Client::new();
656        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
657        let timestamp = TimestampRecord::rfc3161("https://example.com", Utc::now(), "");
658
659        let result = client.verify_timestamp(&timestamp, &doc_id).unwrap();
660        assert!(!result.valid);
661        assert_eq!(result.status, VerificationStatus::Invalid);
662    }
663
664    #[test]
665    fn test_verify_invalid_base64() {
666        let client = Rfc3161Client::new();
667        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
668        let timestamp =
669            TimestampRecord::rfc3161("https://example.com", Utc::now(), "!!!invalid!!!");
670
671        let result = client.verify_timestamp(&timestamp, &doc_id);
672        assert!(result.is_err());
673    }
674
675    #[test]
676    fn test_verify_valid_structure() {
677        let client = Rfc3161Client::new();
678        let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"test");
679        // A minimal valid ASN.1 SEQUENCE
680        let token = BASE64.encode([0x30, 0x00]);
681        let timestamp = TimestampRecord::rfc3161("https://example.com", Utc::now(), token);
682
683        let result = client.verify_timestamp(&timestamp, &doc_id).unwrap();
684        assert!(result.valid);
685        assert_eq!(result.status, VerificationStatus::Valid);
686    }
687}