1use crate::error::LogError;
4use crate::log_entry::LogEntry;
5use crate::merkle_service::{self, MerkleProof};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
11
12const CHAIN_PROOF_LEAF_PREFIX: u8 = 0x02;
13const CHAIN_PROOF_NODE_PREFIX: u8 = 0x03;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ChainProof {
18 pub target_entry_id: String,
19 pub target_index: usize,
20 pub chain_length: usize,
21 pub chain_head_hash: String,
22 pub target_entry: LogEntry,
23 pub target_entry_hash: String,
24 pub chain_root_hash: String,
25 pub target_membership_proof: Vec<ChainProofPathStep>,
26 pub head_membership_proof: Vec<ChainProofPathStep>,
27 pub merkle_proof: Option<MerkleProof>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChainProofPathStep {
33 pub side: String, pub hash: String,
35}
36
37impl ChainProof {
38 pub fn attach_merkle_proof(&mut self, proof: Option<MerkleProof>) {
39 self.merkle_proof = proof;
40 }
41}
42
43pub struct LogChain {
45 entries: Vec<LogEntry>,
46 current_hash: String,
47 entry_index: std::collections::HashMap<String, usize>,
48}
49
50impl LogChain {
51 pub fn new() -> Self {
53 LogChain {
54 entries: Vec::new(),
55 current_hash: GENESIS_HASH.to_string(),
56 entry_index: std::collections::HashMap::new(),
57 }
58 }
59
60 pub async fn append(&mut self, entry: LogEntry) -> Result<LogEntry, LogError> {
62 let entry = entry.commit_with_previous_hash(&self.current_hash)?;
63 let new_hash = entry.compute_hash(&self.current_hash)?;
64
65 let index = self.entries.len();
66 self.entry_index.insert(entry.entry_id().to_string(), index);
67 self.entries.push(entry.clone());
68 self.current_hash = new_hash;
69
70 Ok(entry)
71 }
72
73 pub async fn append_committed(&mut self, entry: LogEntry) -> Result<(), LogError> {
75 if !entry.verify_content_hash() {
76 return Err(LogError::ChainError(
77 "WAL replay rejected entry with invalid content hash".to_string(),
78 ));
79 }
80 if entry.previous_entry_hash() != self.current_hash {
81 return Err(LogError::ChainError(format!(
82 "WAL replay previous hash mismatch: expected {}, got {}",
83 self.current_hash,
84 entry.previous_entry_hash()
85 )));
86 }
87
88 let new_hash = entry.compute_hash(&self.current_hash)?;
89 let index = self.entries.len();
90 self.entry_index.insert(entry.entry_id().to_string(), index);
91 self.entries.push(entry);
92 self.current_hash = new_hash;
93 Ok(())
94 }
95
96 pub fn verify(&self) -> bool {
98 let mut previous_hash = GENESIS_HASH.to_string();
99 for entry in &self.entries {
100 if !entry.verify_content_hash() {
101 return false;
102 }
103 if entry.previous_entry_hash() != previous_hash {
104 return false;
105 }
106
107 let computed = match entry.compute_hash(&previous_hash) {
108 Ok(v) => v,
109 Err(_) => return false,
110 };
111 previous_hash = computed;
112 }
113
114 previous_hash == self.current_hash
115 }
116
117 pub fn get_entry(&self, entry_id: &str) -> Option<&LogEntry> {
118 self.entry_index
119 .get(entry_id)
120 .and_then(|idx| self.entries.get(*idx))
121 }
122
123 pub fn generate_proof(&self, entry_id: &str) -> Option<ChainProof> {
129 let &target_index = self.entry_index.get(entry_id)?;
130 if target_index >= self.entries.len() {
131 return None;
132 }
133
134 let entry_hashes = self.compute_entry_hashes()?;
135 let chain_length = entry_hashes.len();
136 if chain_length == 0 {
137 return None;
138 }
139
140 let target_entry = self.entries.get(target_index)?.clone();
141 let target_entry_hash = entry_hashes.get(target_index)?.clone();
142 let head_index = chain_length - 1;
143 let head_hash = entry_hashes.get(head_index)?;
144 if head_hash != &self.current_hash {
145 return None;
146 }
147
148 let leaves = entry_hashes
149 .iter()
150 .map(|h| Self::hex_decode(h).map(|bytes| Self::hash_chain_leaf(&bytes)))
151 .collect::<Option<Vec<_>>>()?;
152 let chain_root_hash = Self::hex_encode(&Self::build_merkle_root(&leaves));
153 let target_membership_proof = Self::build_merkle_path(&leaves, target_index)?;
154 let head_membership_proof = Self::build_merkle_path(&leaves, head_index)?;
155
156 Some(ChainProof {
157 target_entry_id: entry_id.to_string(),
158 target_index,
159 chain_length,
160 chain_head_hash: self.current_hash.clone(),
161 target_entry,
162 target_entry_hash,
163 chain_root_hash,
164 target_membership_proof,
165 head_membership_proof,
166 merkle_proof: None,
167 })
168 }
169
170 fn compute_entry_hashes(&self) -> Option<Vec<String>> {
171 let mut hashes = Vec::with_capacity(self.entries.len());
172 let mut previous_hash = GENESIS_HASH.to_string();
173 for entry in &self.entries {
174 if !entry.verify_content_hash() {
175 return None;
176 }
177 if entry.previous_entry_hash() != previous_hash {
178 return None;
179 }
180 let entry_hash = entry.compute_hash(&previous_hash).ok()?;
181 previous_hash = entry_hash.clone();
182 hashes.push(entry_hash);
183 }
184
185 if previous_hash != self.current_hash {
186 return None;
187 }
188
189 Some(hashes)
190 }
191
192 fn hash_chain_leaf(data: &[u8]) -> Vec<u8> {
193 let mut hasher = Sha256::new();
194 hasher.update([CHAIN_PROOF_LEAF_PREFIX]);
195 hasher.update(data);
196 hasher.finalize().to_vec()
197 }
198
199 fn hash_chain_node(left: &[u8], right: &[u8]) -> Vec<u8> {
200 let mut hasher = Sha256::new();
201 hasher.update([CHAIN_PROOF_NODE_PREFIX]);
202 hasher.update(left);
203 hasher.update(right);
204 hasher.finalize().to_vec()
205 }
206
207 fn next_level(level: &[Vec<u8>]) -> Vec<Vec<u8>> {
208 let mut next = Vec::with_capacity(level.len().div_ceil(2));
209 for chunk in level.chunks(2) {
210 let left = &chunk[0];
211 let right = if chunk.len() == 2 {
212 &chunk[1]
213 } else {
214 &chunk[0]
215 };
216 next.push(Self::hash_chain_node(left, right));
217 }
218 next
219 }
220
221 fn build_merkle_root(leaves: &[Vec<u8>]) -> Vec<u8> {
222 if leaves.is_empty() {
223 return vec![0u8; 32];
224 }
225 let mut level = leaves.to_vec();
226 while level.len() > 1 {
227 level = Self::next_level(&level);
228 }
229 level[0].clone()
230 }
231
232 fn build_merkle_path(leaves: &[Vec<u8>], mut index: usize) -> Option<Vec<ChainProofPathStep>> {
233 if leaves.is_empty() || index >= leaves.len() {
234 return None;
235 }
236 let mut path = Vec::new();
237 let mut level = leaves.to_vec();
238
239 while level.len() > 1 {
240 let is_right = index % 2 == 1;
241 let sibling_index = if is_right {
242 index - 1
243 } else {
244 (index + 1).min(level.len() - 1)
245 };
246 path.push(ChainProofPathStep {
247 side: if is_right {
248 "left".to_string()
249 } else {
250 "right".to_string()
251 },
252 hash: Self::hex_encode(&level[sibling_index]),
253 });
254 level = Self::next_level(&level);
255 index /= 2;
256 }
257
258 Some(path)
259 }
260
261 fn hex_encode(data: &[u8]) -> String {
262 data.iter().map(|b| format!("{:02x}", b)).collect()
263 }
264
265 fn hex_decode(s: &str) -> Option<Vec<u8>> {
266 if !s.len().is_multiple_of(2) {
267 return None;
268 }
269 (0..s.len())
270 .step_by(2)
271 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
272 .collect()
273 }
274
275 pub fn len(&self) -> usize {
277 self.entries.len()
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.entries.is_empty()
283 }
284
285 pub fn current_hash(&self) -> &str {
287 &self.current_hash
288 }
289}
290
291impl Default for LogChain {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297pub fn verify_chain_proof(proof: &ChainProof) -> bool {
299 if proof.chain_length == 0 || proof.target_index >= proof.chain_length {
300 return false;
301 }
302 if proof.target_entry.entry_id() != proof.target_entry_id {
303 return false;
304 }
305 if proof.chain_head_hash.len() != 64 || proof.target_entry_hash.len() != 64 {
306 return false;
307 }
308 if !proof.target_entry.verify_content_hash() {
309 return false;
310 }
311
312 let target_entry_hash = match proof
313 .target_entry
314 .compute_hash(proof.target_entry.previous_entry_hash())
315 {
316 Ok(v) => v,
317 Err(_) => return false,
318 };
319 if target_entry_hash != proof.target_entry_hash {
320 return false;
321 }
322
323 if !verify_compact_membership(
324 &proof.target_entry_hash,
325 &proof.target_membership_proof,
326 &proof.chain_root_hash,
327 ) {
328 return false;
329 }
330
331 if !verify_compact_membership(
332 &proof.chain_head_hash,
333 &proof.head_membership_proof,
334 &proof.chain_root_hash,
335 ) {
336 return false;
337 }
338
339 if let Some(merkle) = &proof.merkle_proof {
340 if merkle.entry_id != proof.target_entry_id {
341 return false;
342 }
343 if !merkle_service::verify_proof(merkle) {
344 return false;
345 }
346 }
347
348 true
349}
350
351fn verify_compact_membership(
352 entry_hash_hex: &str,
353 path: &[ChainProofPathStep],
354 expected_root_hex: &str,
355) -> bool {
356 let entry_hash = match LogChain::hex_decode(entry_hash_hex) {
357 Some(v) => v,
358 None => return false,
359 };
360 let mut current = LogChain::hash_chain_leaf(&entry_hash);
361
362 for step in path {
363 let sibling = match LogChain::hex_decode(&step.hash) {
364 Some(v) => v,
365 None => return false,
366 };
367 match step.side.as_str() {
368 "left" => current = LogChain::hash_chain_node(&sibling, ¤t),
369 "right" => current = LogChain::hash_chain_node(¤t, &sibling),
370 _ => return false,
371 }
372 }
373
374 LogChain::hex_encode(¤t) == expected_root_hex
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::log_entry::EventType;
381
382 #[test]
383 fn test_genesis_hash() {
384 assert_eq!(GENESIS_HASH.len(), 64);
385 }
386
387 #[tokio::test]
388 async fn test_append_entry() {
389 let mut chain = LogChain::new();
390 let entry = LogEntry::new(
391 EventType::AccountQuery,
392 "AGENT_001".to_string(),
393 "DGFiP".to_string(),
394 )
395 .unwrap();
396
397 let result = chain.append(entry).await;
398 assert!(result.is_ok());
399 assert!(chain.verify());
400 }
401
402 #[tokio::test]
403 async fn test_chain_proof_detects_tampering() {
404 let mut chain = LogChain::new();
405 let e1 = chain
406 .append(
407 LogEntry::new(
408 EventType::AuthSuccess,
409 "AGENT_001".to_string(),
410 "DGFiP".to_string(),
411 )
412 .unwrap(),
413 )
414 .await
415 .unwrap();
416 let _e2 = chain
417 .append(
418 LogEntry::new(
419 EventType::DataAccess,
420 "AGENT_002".to_string(),
421 "DGFiP".to_string(),
422 )
423 .unwrap(),
424 )
425 .await
426 .unwrap();
427
428 let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
429 assert!(verify_chain_proof(&proof));
430
431 proof.target_entry_hash = "0".repeat(64);
432 assert!(!verify_chain_proof(&proof));
433 }
434
435 #[tokio::test]
436 async fn test_chain_proof_is_compact_logarithmic() {
437 let mut chain = LogChain::new();
438 let mut target_id = String::new();
439 let n = 64usize;
440
441 for i in 0..n {
442 let entry = chain
443 .append(
444 LogEntry::new(
445 EventType::DataAccess,
446 format!("AGENT_{i:03}"),
447 "DGFiP".to_string(),
448 )
449 .unwrap(),
450 )
451 .await
452 .unwrap();
453 if i == 17 {
454 target_id = entry.entry_id().to_string();
455 }
456 }
457
458 let proof = chain.generate_proof(&target_id).unwrap();
459 assert!(verify_chain_proof(&proof));
460
461 let max_depth = (usize::BITS - (n - 1).leading_zeros()) as usize;
462 assert!(proof.target_membership_proof.len() <= max_depth);
463 assert!(proof.head_membership_proof.len() <= max_depth);
464 }
465
466 #[tokio::test]
467 async fn test_chain_proof_detects_path_tampering() {
468 let mut chain = LogChain::new();
469 let e1 = chain
470 .append(
471 LogEntry::new(
472 EventType::AuthSuccess,
473 "AGENT_001".to_string(),
474 "DGFiP".to_string(),
475 )
476 .unwrap(),
477 )
478 .await
479 .unwrap();
480 let _e2 = chain
481 .append(
482 LogEntry::new(
483 EventType::DataAccess,
484 "AGENT_002".to_string(),
485 "DGFiP".to_string(),
486 )
487 .unwrap(),
488 )
489 .await
490 .unwrap();
491
492 let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
493 assert!(verify_chain_proof(&proof));
494
495 if let Some(first) = proof.target_membership_proof.first_mut() {
496 first.hash = "f".repeat(64);
497 } else {
498 proof.chain_root_hash = "e".repeat(64);
499 }
500 assert!(!verify_chain_proof(&proof));
501 }
502}