Skip to main content

immutable_logging/
publication.rs

1//! Publication - Daily audit publication
2
3use serde::{Deserialize, Serialize};
4use chrono::Utc;
5
6/// Daily audit publication
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct DailyPublication {
9    /// Publication date
10    pub date: String,
11    /// Root hash of all hourly roots
12    pub root_hash: String,
13    /// Total entry count
14    pub entry_count: u64,
15    /// Hourly root hashes
16    pub hourly_roots: Vec<String>,
17    /// Previous day root (for chaining)
18    pub previous_day_root: String,
19    /// Creation timestamp
20    pub created_at: String,
21    /// Signature
22    pub signature: Option<PublicationSignature>,
23    /// TSA timestamp
24    pub tsa_timestamp: Option<TsaTimestamp>,
25}
26
27/// Publication signature
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PublicationSignature {
30    pub algorithm: String,
31    pub key_id: String,
32    pub value: String,
33}
34
35/// TSA timestamp
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TsaTimestamp {
38    pub tsa_url: String,
39    pub timestamp: String,
40    pub token: String,
41}
42
43/// Publication service
44pub struct PublicationService {
45    /// Previous day root
46    previous_day_root: Option<String>,
47}
48
49impl PublicationService {
50    /// Create new publication service
51    pub fn new() -> Self {
52        PublicationService {
53            previous_day_root: None,
54        }
55    }
56    
57    /// Create daily publication
58    pub fn create_daily_publication(
59        &self,
60        hourly_roots: &[String],
61        entry_count: u64,
62    ) -> DailyPublication {
63        let date = Utc::now().format("%Y-%m-%d").to_string();
64        let previous = self.previous_day_root.clone().unwrap_or_else(|| {
65            "0000000000000000000000000000000000000000000000000000000000000000".to_string()
66        });
67        
68        // Compute root hash of all hourly roots
69        let root_hash = Self::compute_merkle_root(hourly_roots);
70        
71        DailyPublication {
72            date,
73            root_hash,
74            entry_count,
75            hourly_roots: hourly_roots.to_vec(),
76            previous_day_root: previous,
77            created_at: Utc::now().to_rfc3339(),
78            signature: None,
79            tsa_timestamp: None,
80        }
81    }
82    
83    /// Compute merkle root from list of hashes
84    fn compute_merkle_root(hashes: &[String]) -> String {
85        if hashes.is_empty() {
86            return "0000000000000000000000000000000000000000000000000000000000000000".to_string();
87        }
88        
89        use sha2::{Sha256, Digest};
90        
91        let mut current: Vec<String> = hashes.to_vec();
92        
93        while current.len() > 1 {
94            let mut next = Vec::new();
95            
96            for chunk in current.chunks(2) {
97                if chunk.len() == 2 {
98                    let mut hasher = Sha256::new();
99                    hasher.update(chunk[0].as_bytes());
100                    hasher.update(chunk[1].as_bytes());
101                    next.push(format!("{:x}", hasher.finalize()));
102                } else {
103                    next.push(chunk[0].clone());
104                }
105            }
106            
107            current = next;
108        }
109        
110        current[0].clone()
111    }
112    
113    /// Sign publication
114    pub fn sign_publication(&mut self, publication: &mut DailyPublication, signature: &[u8]) {
115        publication.signature = Some(PublicationSignature {
116            algorithm: "RSA-PSS-SHA256".to_string(),
117            key_id: "rnbc-audit-sig-2026".to_string(),
118            value: base64_encode(signature),
119        });
120        
121        // Store previous day root for chaining
122        self.previous_day_root = Some(publication.root_hash.clone());
123    }
124    
125    /// Add TSA timestamp (RFC 3161 compliant)
126    /// This provides external proof of existence at a specific time
127    pub async fn add_tsa_timestamp(
128        &mut self,
129        publication: &mut DailyPublication,
130        tsa_url: &str,
131    ) -> Result<(), TsaError> {
132        // Serialize publication hash for TSA request
133        let hash_to_timestamp = &publication.root_hash;
134        
135        // In production, this would be a proper RFC 3161 request
136        // For now, we'll implement a basic timestamp request structure
137        let timestamp_request = TsaRequest {
138            hash: hash_to_timestamp.clone(),
139            algorithm: "SHA256".to_string(),
140            nonce: uuid::Uuid::new_v4().to_string(),
141        };
142        
143        // Make request to TSA (in production, use actual TSA server)
144        let response = self.request_timestamp(&tsa_url, &timestamp_request).await?;
145        
146        publication.tsa_timestamp = Some(TsaTimestamp {
147            tsa_url: tsa_url.to_string(),
148            timestamp: response.timestamp,
149            token: response.token,
150        });
151        
152        tracing::info!(
153            "TSA timestamp added for publication {} at {}",
154            publication.date,
155            publication
156                .tsa_timestamp
157                .as_ref()
158                .map(|t| t.timestamp.as_str())
159                .map_or("unknown", |v| v)
160        );
161        
162        Ok(())
163    }
164    
165    /// Request timestamp from TSA server
166    async fn request_timestamp(&self, tsa_url: &str, request: &TsaRequest) -> Result<TsaResponse, TsaError> {
167        // In production, this would make an actual HTTP request to a TSA
168        // RFC 3161 specifies the request/response format
169        
170        // For demonstration, we create a verifiable response structure
171        // Real implementation would use a TSA like:
172        // - freetsa.org (free, German)
173        // - Sectigo RSA Time Stamping Authority
174        // - DigiCert Timestamp Authority
175        
176        tracing::debug!("Requesting timestamp from TSA: {}", tsa_url);
177        
178        // Simulate TSA response (in production, parse actual TSA response)
179        let response = TsaResponse {
180            timestamp: chrono::Utc::now().to_rfc3339(),
181            token: format!("sha256={}", request.hash),
182            tsa_certificate: "placeholder".to_string(),
183        };
184        
185        Ok(response)
186    }
187}
188
189/// TSA Request structure (RFC 3161 subset)
190#[derive(Debug, Clone, Serialize, Deserialize)]
191struct TsaRequest {
192    hash: String,
193    algorithm: String,
194    nonce: String,
195}
196
197/// TSA Response structure (RFC 3161 subset)
198#[derive(Debug, Clone, Serialize, Deserialize)]
199struct TsaResponse {
200    timestamp: String,
201    token: String,
202    tsa_certificate: String,
203}
204
205/// TSA Error type
206#[derive(Debug, thiserror::Error)]
207pub enum TsaError {
208    #[error("Network error: {0}")]
209    Network(#[from] reqwest::Error),
210    
211    #[error("TSA server error: {0}")]
212    Server(String),
213    
214    #[error("Invalid response from TSA")]
215    InvalidResponse,
216}
217
218/// Base64 encode
219fn base64_encode(data: &[u8]) -> String {
220    use base64::{Engine as _, engine::general_purpose::STANDARD};
221    STANDARD.encode(data)
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    
228    // TODO: Complete test implementation
229}