_hope_core/
auditor.rs

1//! # Hope Genome v1.4.0 - Proof Auditor
2//!
3//! **Hardened Security Edition - Pluggable Architecture**
4//!
5//! ## Major Changes in v1.4.0
6//!
7//! ### 1. Persistent Nonce Store Integration
8//! - **Replay Attack Protection**: Survives process restarts
9//! - **Pluggable Backends**: Memory, RocksDB, Redis
10//! - **Production Ready**: No more lost nonces on crash!
11//!
12//! ### 2. KeyStore Trait Integration
13//! - **Flexible Cryptography**: Software or HSM
14//! - **Ed25519 Support**: Fast, secure signatures
15//! - **Future-Proof**: Easy to add new key backends
16//!
17//! ## Example (New API)
18//!
19//! ```rust
20//! use _hope_core::auditor::ProofAuditor;
21//! use _hope_core::crypto::SoftwareKeyStore;
22//! use _hope_core::nonce_store::MemoryNonceStore;
23//!
24//! // Create with pluggable backends
25//! let key_store = SoftwareKeyStore::generate().unwrap();
26//! let nonce_store = MemoryNonceStore::new();
27//!
28//! let auditor = ProofAuditor::new(
29//!     Box::new(key_store),
30//!     Box::new(nonce_store),
31//! );
32//! ```
33//!
34//! ---
35//!
36//! **Date**: 2025-12-30
37//! **Version**: 1.4.0 (Hardened Security Edition)
38//! **Author**: Máté Róbert <stratosoiteam@gmail.com>
39
40use crate::crypto::{CryptoError, KeyStore};
41use crate::nonce_store::{NonceStore, NonceStoreError};
42use crate::proof::IntegrityProof;
43use thiserror::Error;
44
45// ============================================================================
46// ERROR TYPES
47// ============================================================================
48
49#[derive(Debug, Error)]
50pub enum AuditorError {
51    #[error("Invalid signature")]
52    InvalidSignature,
53
54    #[error("Proof expired: issued at {issued}, now {now}, TTL {ttl}s")]
55    ProofExpired { issued: u64, now: u64, ttl: u64 },
56
57    #[error("Nonce already used (replay attack detected): {0}")]
58    NonceReused(String),
59
60    #[error("Nonce store error: {0}")]
61    NonceStoreError(#[from] NonceStoreError),
62
63    #[error("Crypto error: {0}")]
64    CryptoError(String),
65}
66
67impl From<CryptoError> for AuditorError {
68    fn from(err: CryptoError) -> Self {
69        match err {
70            CryptoError::InvalidSignature => AuditorError::InvalidSignature,
71            other => AuditorError::CryptoError(other.to_string()),
72        }
73    }
74}
75
76pub type Result<T> = std::result::Result<T, AuditorError>;
77
78// ============================================================================
79// PROOF AUDITOR (v1.4.0 - Trait-Based Architecture)
80// ============================================================================
81
82/// Proof verification engine with pluggable backends
83///
84/// The auditor performs cryptographic verification of `IntegrityProof` objects
85/// with multi-layer security:
86///
87/// 1. **Signature Verification**: Ed25519 cryptographic signatures (KeyStore)
88/// 2. **TTL Enforcement**: Time-based proof expiry
89/// 3. **Replay Attack Prevention**: Persistent nonce tracking (NonceStore)
90///
91/// ## Architecture (v1.4.0)
92///
93/// ```text
94/// ┌─────────────────┐
95/// │  ProofAuditor   │
96/// └────────┬────────┘
97///          │
98///          ├─── KeyStore ───────> SoftwareKeyStore (Ed25519)
99///          │                      HsmKeyStore (PKCS#11)
100///          │
101///          └─── NonceStore ─────> MemoryNonceStore (testing)
102///                                 RocksDbNonceStore (production)
103///                                 RedisNonceStore (distributed)
104/// ```
105///
106/// ## Example (Production Setup)
107///
108/// ```no_run
109/// use _hope_core::auditor::ProofAuditor;
110/// use _hope_core::crypto::SoftwareKeyStore;
111/// # #[cfg(feature = "rocksdb-nonce-store")]
112/// use _hope_core::nonce_store::RocksDbNonceStore;
113///
114/// # #[cfg(feature = "rocksdb-nonce-store")]
115/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
116/// // Production setup: persistent nonce store
117/// let key_store = SoftwareKeyStore::generate()?;
118/// let nonce_store = RocksDbNonceStore::new("./hope_nonces.db")?;
119///
120/// let mut auditor = ProofAuditor::new(
121///     Box::new(key_store),
122///     Box::new(nonce_store),
123/// );
124///
125/// // Verify proofs - nonces persist across restarts!
126/// // auditor.verify_proof(&proof)?;
127/// # Ok(())
128/// # }
129/// # #[cfg(not(feature = "rocksdb-nonce-store"))]
130/// # fn main() {}
131/// ```
132pub struct ProofAuditor {
133    /// Cryptographic key store (pluggable: software or HSM)
134    key_store: Box<dyn KeyStore>,
135
136    /// Nonce store for replay attack prevention (pluggable: memory, RocksDB, Redis)
137    nonce_store: Box<dyn NonceStore>,
138}
139
140impl ProofAuditor {
141    /// Create a new proof auditor with custom backends
142    ///
143    /// # Arguments
144    /// * `key_store` - Cryptographic key storage (SoftwareKeyStore, HsmKeyStore, etc.)
145    /// * `nonce_store` - Nonce tracking storage (MemoryNonceStore, RocksDbNonceStore, etc.)
146    ///
147    /// # Example
148    /// ```rust
149    /// use _hope_core::auditor::ProofAuditor;
150    /// use _hope_core::crypto::SoftwareKeyStore;
151    /// use _hope_core::nonce_store::MemoryNonceStore;
152    ///
153    /// let key_store = SoftwareKeyStore::generate().unwrap();
154    /// let nonce_store = MemoryNonceStore::new();
155    ///
156    /// let auditor = ProofAuditor::new(
157    ///     Box::new(key_store),
158    ///     Box::new(nonce_store),
159    /// );
160    /// ```
161    pub fn new(key_store: Box<dyn KeyStore>, nonce_store: Box<dyn NonceStore>) -> Self {
162        ProofAuditor {
163            key_store,
164            nonce_store,
165        }
166    }
167
168    /// Verify a cryptographic proof
169    ///
170    /// This performs comprehensive multi-layer verification:
171    ///
172    /// 1. **Signature Verification**: Validates Ed25519 signature
173    /// 2. **TTL Check**: Ensures proof hasn't expired
174    /// 3. **Nonce Check**: Prevents replay attacks (atomic check-and-insert)
175    ///
176    /// # Arguments
177    /// * `proof` - The `IntegrityProof` to verify
178    ///
179    /// # Returns
180    /// - `Ok(())` if proof is valid and not replayed
181    /// - `Err(InvalidSignature)` if signature verification fails
182    /// - `Err(ProofExpired)` if proof TTL has expired
183    /// - `Err(NonceReused)` if nonce was already used (replay attack)
184    ///
185    /// # Security Guarantees
186    ///
187    /// - **Constant-time**: Ed25519 verification prevents timing attacks
188    /// - **Atomic nonce check**: Race-condition free (even in distributed setup)
189    /// - **Persistent protection**: With RocksDB/Redis, survives restarts
190    ///
191    /// # Example
192    /// ```rust
193    /// use _hope_core::auditor::ProofAuditor;
194    /// use _hope_core::crypto::{KeyStore, SoftwareKeyStore}; // KeyStore trait needed for .sign()
195    /// use _hope_core::nonce_store::MemoryNonceStore;
196    /// use _hope_core::proof::{Action, IntegrityProof};
197    ///
198    /// let key_store = SoftwareKeyStore::generate().unwrap();
199    /// let nonce_store = MemoryNonceStore::new();
200    /// let mut auditor = ProofAuditor::new(
201    ///     Box::new(key_store.clone()),
202    ///     Box::new(nonce_store),
203    /// );
204    ///
205    /// // Create and sign a proof
206    /// let action = Action::delete("test.txt");
207    /// let mut proof = IntegrityProof::new(&action, "capsule123".into(), 3600);
208    /// proof.signature = key_store.sign(&proof.signing_data()).unwrap();
209    ///
210    /// // Verify proof
211    /// assert!(auditor.verify_proof(&proof).is_ok());
212    ///
213    /// // Replay attack: blocked!
214    /// assert!(auditor.verify_proof(&proof).is_err());
215    /// ```
216    pub fn verify_proof(&mut self, proof: &IntegrityProof) -> Result<()> {
217        // Step 1: Verify signature (most critical - fail fast)
218        self.verify_signature(proof)?;
219
220        // Step 2: Check TTL (time-to-live)
221        // v1.6.0: Fixed H-1 - Integer underflow protection using saturating_sub()
222        let now = chrono::Utc::now().timestamp() as u64;
223
224        // Reject proofs from the future (clock skew protection)
225        const MAX_CLOCK_SKEW: u64 = 300; // 5 minutes tolerance
226        if proof.timestamp > now + MAX_CLOCK_SKEW {
227            return Err(AuditorError::ProofExpired {
228                issued: proof.timestamp,
229                now,
230                ttl: proof.ttl,
231            });
232        }
233
234        // Safe subtraction prevents underflow attack
235        let elapsed = now.saturating_sub(proof.timestamp);
236        if elapsed > proof.ttl {
237            return Err(AuditorError::ProofExpired {
238                issued: proof.timestamp,
239                now,
240                ttl: proof.ttl,
241            });
242        }
243
244        // Step 3: Check and insert nonce (atomic operation)
245        // This prevents replay attacks even across restarts (with persistent store)
246        self.nonce_store
247            .check_and_insert(proof.nonce, proof.ttl)
248            .map_err(|e| match e {
249                NonceStoreError::NonceReused(hex) => AuditorError::NonceReused(hex),
250                other => AuditorError::NonceStoreError(other),
251            })?;
252
253        Ok(())
254    }
255
256    /// Verify signature only (without nonce/TTL checks)
257    ///
258    /// Use this for read-only verification without state changes.
259    /// Does NOT mark nonce as used.
260    ///
261    /// # Example
262    /// ```rust
263    /// use _hope_core::auditor::ProofAuditor;
264    /// use _hope_core::crypto::{KeyStore, SoftwareKeyStore}; // KeyStore trait needed for .sign()
265    /// use _hope_core::nonce_store::MemoryNonceStore;
266    /// use _hope_core::proof::{Action, IntegrityProof};
267    ///
268    /// let key_store = SoftwareKeyStore::generate().unwrap();
269    /// let nonce_store = MemoryNonceStore::new();
270    /// let auditor = ProofAuditor::new(
271    ///     Box::new(key_store.clone()),
272    ///     Box::new(nonce_store),
273    /// );
274    ///
275    /// let action = Action::delete("test.txt");
276    /// let mut proof = IntegrityProof::new(&action, "capsule123".into(), 3600);
277    /// proof.signature = key_store.sign(&proof.signing_data()).unwrap();
278    ///
279    /// // Verify signature multiple times (no nonce consumption)
280    /// assert!(auditor.verify_signature(&proof).is_ok());
281    /// assert!(auditor.verify_signature(&proof).is_ok());
282    /// ```
283    pub fn verify_signature(&self, proof: &IntegrityProof) -> Result<()> {
284        let message = proof.signing_data();
285        self.key_store.verify(&message, &proof.signature)?;
286        Ok(())
287    }
288
289    /// Check if a nonce has been used
290    ///
291    /// Read-only operation - does not modify state.
292    pub fn is_nonce_used(&self, nonce: &[u8; 32]) -> bool {
293        self.nonce_store.contains(nonce)
294    }
295
296    /// Get count of used nonces
297    ///
298    /// Useful for monitoring and debugging.
299    pub fn used_nonce_count(&self) -> usize {
300        self.nonce_store.count()
301    }
302
303    /// Clear all nonces (DANGEROUS - use only for testing!)
304    ///
305    /// # Security Warning
306    /// This allows replay attacks! Only use for:
307    /// - Unit tests
308    /// - Development reset
309    /// - Maintenance with full system shutdown
310    pub fn clear_nonces(&mut self) -> Result<()> {
311        self.nonce_store.clear()?;
312        Ok(())
313    }
314
315    /// Cleanup expired nonces (optional maintenance)
316    ///
317    /// Most backends handle this automatically (e.g., Redis TTL),
318    /// but RocksDB may benefit from periodic cleanup.
319    ///
320    /// # Returns
321    /// Number of nonces removed
322    pub fn cleanup_expired_nonces(&mut self) -> Result<usize> {
323        Ok(self.nonce_store.cleanup_expired()?)
324    }
325
326    /// Get key store identifier (for logging/debugging)
327    pub fn key_store_info(&self) -> String {
328        self.key_store.identifier()
329    }
330}
331
332// ============================================================================
333// TESTS
334// ============================================================================
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::crypto::SoftwareKeyStore;
340    use crate::nonce_store::MemoryNonceStore;
341    use crate::proof::Action;
342
343    fn create_test_auditor() -> (ProofAuditor, SoftwareKeyStore) {
344        let key_store = SoftwareKeyStore::generate().unwrap();
345        let key_store_clone = key_store.clone();
346        let nonce_store = MemoryNonceStore::new();
347
348        let auditor = ProofAuditor::new(Box::new(key_store), Box::new(nonce_store));
349
350        (auditor, key_store_clone)
351    }
352
353    fn create_test_proof(key_store: &SoftwareKeyStore) -> IntegrityProof {
354        let action = Action::delete("test.txt");
355        let mut proof = IntegrityProof::new(&action, "test_capsule".into(), 3600);
356
357        // Sign the proof
358        let signing_data = proof.signing_data();
359        proof.signature = key_store.sign(&signing_data).unwrap();
360
361        proof
362    }
363
364    #[test]
365    fn test_verify_valid_proof() {
366        let (mut auditor, key_store) = create_test_auditor();
367        let proof = create_test_proof(&key_store);
368
369        // First verification: should succeed
370        assert!(auditor.verify_proof(&proof).is_ok());
371    }
372
373    #[test]
374    fn test_replay_attack_prevention() {
375        let (mut auditor, key_store) = create_test_auditor();
376        let proof = create_test_proof(&key_store);
377
378        // First use: should succeed
379        assert!(auditor.verify_proof(&proof).is_ok());
380        assert_eq!(auditor.used_nonce_count(), 1);
381
382        // Second use: should FAIL (nonce reused)
383        let result = auditor.verify_proof(&proof);
384        assert!(result.is_err());
385        assert!(matches!(result, Err(AuditorError::NonceReused(_))));
386    }
387
388    #[test]
389    fn test_ttl_expiration() {
390        let (mut auditor, key_store) = create_test_auditor();
391
392        // Create proof with very short TTL
393        let action = Action::delete("test.txt");
394        let mut proof = IntegrityProof::new(&action, "test_capsule".into(), 0);
395
396        // Set timestamp to 10 seconds ago
397        proof.timestamp = chrono::Utc::now().timestamp() as u64 - 10;
398
399        // Sign it
400        let signing_data = proof.signing_data();
401        proof.signature = key_store.sign(&signing_data).unwrap();
402
403        // Should fail (expired)
404        let result = auditor.verify_proof(&proof);
405        assert!(result.is_err());
406        assert!(matches!(result, Err(AuditorError::ProofExpired { .. })));
407    }
408
409    #[test]
410    fn test_invalid_signature() {
411        let (mut auditor, key_store) = create_test_auditor();
412        let mut proof = create_test_proof(&key_store);
413
414        // Tamper with signature
415        proof.signature[0] ^= 0xFF;
416
417        // Should fail (invalid signature)
418        let result = auditor.verify_proof(&proof);
419        assert!(result.is_err());
420        assert!(matches!(result, Err(AuditorError::InvalidSignature)));
421    }
422
423    #[test]
424    fn test_verify_signature_readonly() {
425        let (auditor, key_store) = create_test_auditor();
426        let proof = create_test_proof(&key_store);
427
428        // Verify signature multiple times (should not consume nonce)
429        assert!(auditor.verify_signature(&proof).is_ok());
430        assert!(auditor.verify_signature(&proof).is_ok());
431        assert!(auditor.verify_signature(&proof).is_ok());
432
433        assert_eq!(auditor.used_nonce_count(), 0); // No nonce consumed
434    }
435
436    #[test]
437    fn test_nonce_tracking() {
438        let (mut auditor, key_store) = create_test_auditor();
439
440        // Create two different proofs
441        let proof1 = create_test_proof(&key_store);
442        let proof2 = create_test_proof(&key_store);
443
444        assert_eq!(auditor.used_nonce_count(), 0);
445
446        auditor.verify_proof(&proof1).unwrap();
447        assert_eq!(auditor.used_nonce_count(), 1);
448
449        auditor.verify_proof(&proof2).unwrap();
450        assert_eq!(auditor.used_nonce_count(), 2);
451    }
452
453    #[test]
454    fn test_clear_nonces() {
455        let (mut auditor, key_store) = create_test_auditor();
456
457        // Add some nonces
458        for _ in 0..5 {
459            let proof = create_test_proof(&key_store);
460            auditor.verify_proof(&proof).unwrap();
461        }
462
463        assert_eq!(auditor.used_nonce_count(), 5);
464
465        // Clear all nonces
466        auditor.clear_nonces().unwrap();
467        assert_eq!(auditor.used_nonce_count(), 0);
468    }
469
470    #[test]
471    fn test_different_actions_different_proofs() {
472        let (mut auditor, key_store) = create_test_auditor();
473
474        let action1 = Action::delete("file1.txt");
475        let mut proof1 = IntegrityProof::new(&action1, "capsule1".into(), 3600);
476        proof1.signature = key_store.sign(&proof1.signing_data()).unwrap();
477
478        let action2 = Action::delete("file2.txt");
479        let mut proof2 = IntegrityProof::new(&action2, "capsule2".into(), 3600);
480        proof2.signature = key_store.sign(&proof2.signing_data()).unwrap();
481
482        // Both should verify successfully (different nonces)
483        assert!(auditor.verify_proof(&proof1).is_ok());
484        assert!(auditor.verify_proof(&proof2).is_ok());
485    }
486
487    #[test]
488    fn test_is_nonce_used() {
489        let (mut auditor, key_store) = create_test_auditor();
490        let proof = create_test_proof(&key_store);
491
492        assert!(!auditor.is_nonce_used(&proof.nonce));
493
494        auditor.verify_proof(&proof).unwrap();
495
496        assert!(auditor.is_nonce_used(&proof.nonce));
497    }
498
499    #[test]
500    fn test_key_store_info() {
501        let (auditor, _) = create_test_auditor();
502        let info = auditor.key_store_info();
503
504        assert!(info.contains("SoftwareKeyStore"));
505        assert!(info.contains("Ed25519"));
506    }
507}