immutable_logging/
publication.rs1use serde::{Deserialize, Serialize};
4use chrono::Utc;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct DailyPublication {
9 pub date: String,
11 pub root_hash: String,
13 pub entry_count: u64,
15 pub hourly_roots: Vec<String>,
17 pub previous_day_root: String,
19 pub created_at: String,
21 pub signature: Option<PublicationSignature>,
23 pub tsa_timestamp: Option<TsaTimestamp>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PublicationSignature {
30 pub algorithm: String,
31 pub key_id: String,
32 pub value: String,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TsaTimestamp {
38 pub tsa_url: String,
39 pub timestamp: String,
40 pub token: String,
41}
42
43pub struct PublicationService {
45 previous_day_root: Option<String>,
47}
48
49impl PublicationService {
50 pub fn new() -> Self {
52 PublicationService {
53 previous_day_root: None,
54 }
55 }
56
57 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 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 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 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 self.previous_day_root = Some(publication.root_hash.clone());
123 }
124
125 pub async fn add_tsa_timestamp(
128 &mut self,
129 publication: &mut DailyPublication,
130 tsa_url: &str,
131 ) -> Result<(), TsaError> {
132 let hash_to_timestamp = &publication.root_hash;
134
135 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 let response = self.request_timestamp(&tsa_url, ×tamp_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 async fn request_timestamp(&self, tsa_url: &str, request: &TsaRequest) -> Result<TsaResponse, TsaError> {
167 tracing::debug!("Requesting timestamp from TSA: {}", tsa_url);
177
178 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#[derive(Debug, Clone, Serialize, Deserialize)]
191struct TsaRequest {
192 hash: String,
193 algorithm: String,
194 nonce: String,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199struct TsaResponse {
200 timestamp: String,
201 token: String,
202 tsa_certificate: String,
203}
204
205#[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
218fn 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 }