Skip to main content

lens_core/
attestation.rs

1//! Anti-fraud attestation and tripwire system
2
3use anyhow::{Result, bail};
4use sha2::{Sha256, Digest};
5use crate::built_info;
6
7/// Verify we're running in real mode, not mock
8pub fn verify_real_mode() -> Result<()> {
9    let mode = std::env::var("LENS_MODE").unwrap_or_else(|_| "real".to_string());
10    
11    if mode != "real" {
12        bail!("TRIPWIRE VIOLATION: LENS_MODE must be 'real', got '{}'", mode);
13    }
14    
15    Ok(())
16}
17
18/// Generate build information for handshake
19pub fn get_build_info() -> crate::proto::BuildInfoResponse {
20    crate::proto::BuildInfoResponse {
21        version: env!("CARGO_PKG_VERSION").to_string(),
22        commit: built_info::GIT_VERSION.unwrap_or("unknown").to_string(),
23        build_timestamp: env!("BUILD_TIMESTAMP", "unknown").to_string(),
24        features: "default,lsp,benchmarks,attestation".to_string(), // Static for now
25    }
26}
27
28/// Perform anti-fraud handshake with nonce/response
29pub fn perform_handshake(nonce: &str) -> Result<String> {
30    let build_sha = built_info::GIT_VERSION.unwrap_or("unknown");
31    let challenge_input = format!("{}{}", nonce, build_sha);
32    
33    let mut hasher = Sha256::new();
34    hasher.update(challenge_input.as_bytes());
35    let response = format!("{:x}", hasher.finalize());
36    
37    Ok(response)
38}
39
40/// Check for banned patterns in input
41pub fn check_banned_patterns(text: &str) -> Vec<String> {
42    let banned = [
43        "generateMock", "simulate", "MOCK_RESULT", "mock_file_", "fake"
44    ];
45    
46    let mut violations = Vec::new();
47    for pattern in &banned {
48        if text.to_lowercase().contains(&pattern.to_lowercase()) {
49            violations.push(format!("Banned pattern detected: {}", pattern));
50        }
51    }
52    
53    violations
54}
55
56/// Attestation manager for system integrity verification
57pub struct AttestationManager {
58    enabled: bool,
59    build_hash: String,
60    runtime_checks: Vec<String>,
61}
62
63impl AttestationManager {
64    /// Create new attestation manager
65    pub fn new(enabled: bool) -> Result<Self> {
66        let build_hash = Self::compute_build_hash()?;
67        
68        Ok(Self {
69            enabled,
70            build_hash,
71            runtime_checks: Vec::new(),
72        })
73    }
74
75    /// Verify system integrity
76    pub fn verify_integrity(&self) -> Result<AttestationReport> {
77        let mut violations = Vec::new();
78        let mut checks_passed = 0;
79        let mut total_checks = 0;
80
81        // Verify real mode
82        total_checks += 1;
83        if let Err(e) = verify_real_mode() {
84            violations.push(e.to_string());
85        } else {
86            checks_passed += 1;
87        }
88
89        // Verify build integrity
90        total_checks += 1;
91        if let Err(e) = self.verify_build_integrity() {
92            violations.push(e.to_string());
93        } else {
94            checks_passed += 1;
95        }
96
97        // Verify environment
98        total_checks += 1;
99        if let Err(e) = self.verify_environment() {
100            violations.push(e.to_string());
101        } else {
102            checks_passed += 1;
103        }
104
105        Ok(AttestationReport {
106            timestamp: chrono::Utc::now(),
107            total_checks,
108            checks_passed,
109            violations,
110            build_hash: self.build_hash.clone(),
111            integrity_score: (checks_passed as f64 / total_checks as f64) * 100.0,
112        })
113    }
114
115    /// Perform handshake with client
116    pub fn handshake(&self, nonce: &str) -> Result<HandshakeResponse> {
117        if !self.enabled {
118            bail!("Attestation is disabled");
119        }
120
121        let response_hash = perform_handshake(nonce)?;
122        let build_info = get_build_info();
123
124        Ok(HandshakeResponse {
125            response_hash,
126            build_info,
127            attestation_enabled: self.enabled,
128        })
129    }
130
131    fn compute_build_hash() -> Result<String> {
132        let build_data = format!(
133            "{}{}{}{}",
134            built_info::GIT_VERSION.unwrap_or("unknown"),
135            env!("BUILD_TIMESTAMP", "unknown"),
136            built_info::RUSTC_VERSION,
137            built_info::CFG_TARGET_ARCH
138        );
139
140        let mut hasher = Sha256::new();
141        hasher.update(build_data.as_bytes());
142        Ok(format!("{:x}", hasher.finalize()))
143    }
144
145    fn verify_build_integrity(&self) -> Result<()> {
146        let current_hash = Self::compute_build_hash()?;
147        if current_hash != self.build_hash {
148            bail!("Build integrity check failed: hash mismatch");
149        }
150        Ok(())
151    }
152
153    fn verify_environment(&self) -> Result<()> {
154        // Check for suspicious environment variables
155        let suspicious_vars = ["LENS_MOCK", "LENS_SIMULATE", "LENS_FAKE"];
156        for var in suspicious_vars {
157            if std::env::var(var).is_ok() {
158                bail!("Suspicious environment variable detected: {}", var);
159            }
160        }
161        Ok(())
162    }
163}
164
165/// Attestation report
166#[derive(Debug, Clone)]
167pub struct AttestationReport {
168    pub timestamp: chrono::DateTime<chrono::Utc>,
169    pub total_checks: usize,
170    pub checks_passed: usize,
171    pub violations: Vec<String>,
172    pub build_hash: String,
173    pub integrity_score: f64,
174}
175
176impl AttestationReport {
177    pub fn is_valid(&self) -> bool {
178        self.violations.is_empty() && self.integrity_score >= 100.0
179    }
180}
181
182/// Handshake response
183#[derive(Debug, Clone)]
184pub struct HandshakeResponse {
185    pub response_hash: String,
186    pub build_info: crate::proto::BuildInfoResponse,
187    pub attestation_enabled: bool,
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::env;
194    use std::sync::Mutex;
195    
196    // Shared mutex to prevent race conditions with environment variables across all attestation tests
197    static ATTESTATION_ENV_MUTEX: Mutex<()> = Mutex::new(());
198
199    #[test]
200    fn test_verify_real_mode_default() {
201        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
202        
203        // Default mode should be "real"
204        env::remove_var("LENS_MODE");
205        assert!(verify_real_mode().is_ok());
206    }
207
208    #[test]
209    fn test_verify_real_mode_explicit() {
210        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
211        
212        env::set_var("LENS_MODE", "real");
213        assert!(verify_real_mode().is_ok());
214        // Clean up
215        env::remove_var("LENS_MODE");
216    }
217
218    #[test]
219    fn test_verify_real_mode_rejects_mock() {
220        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
221        
222        env::set_var("LENS_MODE", "mock");
223        let result = verify_real_mode();
224        assert!(result.is_err());
225        assert!(result.unwrap_err().to_string().contains("TRIPWIRE VIOLATION"));
226        
227        // Clean up
228        env::remove_var("LENS_MODE");
229    }
230
231    #[test]
232    fn test_verify_real_mode_rejects_invalid() {
233        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
234        
235        env::set_var("LENS_MODE", "test");
236        let result = verify_real_mode();
237        assert!(result.is_err());
238        assert!(result.unwrap_err().to_string().contains("TRIPWIRE VIOLATION"));
239        
240        // Clean up
241        env::remove_var("LENS_MODE");
242    }
243
244    #[test]
245    fn test_get_build_info() {
246        let info = get_build_info();
247        assert!(!info.version.is_empty());
248        assert!(!info.commit.is_empty());
249        assert!(!info.build_timestamp.is_empty());
250        assert!(!info.features.is_empty());
251    }
252
253    #[test]
254    fn test_perform_handshake() {
255        let nonce = "test_nonce_123";
256        let response = perform_handshake(nonce);
257        assert!(response.is_ok());
258        
259        let response_hash = response.unwrap();
260        assert_eq!(response_hash.len(), 64); // SHA256 hex string length
261        
262        // Same nonce should produce same response
263        let response2 = perform_handshake(nonce).unwrap();
264        assert_eq!(response_hash, response2);
265    }
266
267    #[test]
268    fn test_perform_handshake_different_nonces() {
269        let response1 = perform_handshake("nonce1").unwrap();
270        let response2 = perform_handshake("nonce2").unwrap();
271        assert_ne!(response1, response2);
272    }
273
274    #[test]
275    fn test_check_banned_patterns_clean() {
276        let clean_text = "This is a normal search query";
277        let violations = check_banned_patterns(clean_text);
278        assert!(violations.is_empty());
279    }
280
281    #[test]
282    fn test_check_banned_patterns_violations() {
283        let violations = check_banned_patterns("generateMock data");
284        assert_eq!(violations.len(), 1);
285        assert!(violations[0].contains("generateMock"));
286        
287        let violations = check_banned_patterns("MOCK_RESULT and fake data");
288        assert_eq!(violations.len(), 2);
289    }
290
291    #[test]
292    fn test_check_banned_patterns_case_insensitive() {
293        let violations = check_banned_patterns("GenerateMOCK and FAKE");
294        assert_eq!(violations.len(), 2);
295    }
296
297    #[test]
298    fn test_attestation_manager_creation() {
299        let manager = AttestationManager::new(true);
300        assert!(manager.is_ok());
301        
302        let manager = manager.unwrap();
303        assert!(manager.enabled);
304        assert!(!manager.build_hash.is_empty());
305        assert_eq!(manager.build_hash.len(), 64); // SHA256 hex
306    }
307
308    #[test]
309    fn test_attestation_manager_disabled() {
310        let manager = AttestationManager::new(false).unwrap();
311        assert!(!manager.enabled);
312    }
313
314    #[test]
315    fn test_verify_integrity_clean_environment() {
316        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
317        
318        // Ensure clean environment
319        let suspicious_vars = ["LENS_MOCK", "LENS_SIMULATE", "LENS_FAKE", "LENS_MODE"];
320        for var in suspicious_vars {
321            env::remove_var(var);
322        }
323        
324        let manager = AttestationManager::new(true).unwrap();
325        let report = manager.verify_integrity();
326        assert!(report.is_ok());
327        
328        let report = report.unwrap();
329        assert_eq!(report.total_checks, 3);
330        assert_eq!(report.checks_passed, 3);
331        assert!(report.violations.is_empty());
332        assert_eq!(report.integrity_score, 100.0);
333        assert!(report.is_valid());
334    }
335
336    #[test]
337    fn test_verify_integrity_with_violations() {
338        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
339        
340        // Set up violation condition
341        env::set_var("LENS_MOCK", "true");
342        
343        let manager = AttestationManager::new(true).unwrap();
344        let report = manager.verify_integrity().unwrap();
345        
346        assert!(report.checks_passed < report.total_checks);
347        assert!(!report.violations.is_empty());
348        assert!(report.integrity_score < 100.0);
349        assert!(!report.is_valid());
350        
351        // Clean up
352        env::remove_var("LENS_MOCK");
353    }
354
355    #[test]
356    fn test_handshake_enabled() {
357        let manager = AttestationManager::new(true).unwrap();
358        let response = manager.handshake("test_nonce");
359        assert!(response.is_ok());
360        
361        let response = response.unwrap();
362        assert!(!response.response_hash.is_empty());
363        assert_eq!(response.response_hash.len(), 64);
364        assert!(response.attestation_enabled);
365        assert!(!response.build_info.version.is_empty());
366    }
367
368    #[test]
369    fn test_handshake_disabled() {
370        let manager = AttestationManager::new(false).unwrap();
371        let response = manager.handshake("test_nonce");
372        assert!(response.is_err());
373        assert!(response.unwrap_err().to_string().contains("Attestation is disabled"));
374    }
375
376    #[test]
377    fn test_compute_build_hash_consistency() {
378        let hash1 = AttestationManager::compute_build_hash().unwrap();
379        let hash2 = AttestationManager::compute_build_hash().unwrap();
380        assert_eq!(hash1, hash2);
381        assert_eq!(hash1.len(), 64); // SHA256 hex string
382    }
383
384    #[test]
385    fn test_verify_build_integrity() {
386        let manager = AttestationManager::new(true).unwrap();
387        assert!(manager.verify_build_integrity().is_ok());
388    }
389
390    #[test]
391    fn test_verify_environment_clean() {
392        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
393        
394        // Clean environment
395        let suspicious_vars = ["LENS_MOCK", "LENS_SIMULATE", "LENS_FAKE"];
396        for var in suspicious_vars {
397            env::remove_var(var);
398        }
399        
400        let manager = AttestationManager::new(true).unwrap();
401        assert!(manager.verify_environment().is_ok());
402    }
403
404    #[test]
405    fn test_verify_environment_suspicious() {
406        let _lock = ATTESTATION_ENV_MUTEX.lock().unwrap();
407        
408        env::set_var("LENS_MOCK", "true");
409        
410        let manager = AttestationManager::new(true).unwrap();
411        let result = manager.verify_environment();
412        assert!(result.is_err());
413        assert!(result.unwrap_err().to_string().contains("LENS_MOCK"));
414        
415        // Clean up
416        env::remove_var("LENS_MOCK");
417    }
418
419    #[test]
420    fn test_attestation_report_validity() {
421        use chrono::Utc;
422        
423        let valid_report = AttestationReport {
424            timestamp: Utc::now(),
425            total_checks: 3,
426            checks_passed: 3,
427            violations: Vec::new(),
428            build_hash: "test_hash".to_string(),
429            integrity_score: 100.0,
430        };
431        assert!(valid_report.is_valid());
432        
433        let invalid_report = AttestationReport {
434            timestamp: Utc::now(),
435            total_checks: 3,
436            checks_passed: 2,
437            violations: vec!["test violation".to_string()],
438            build_hash: "test_hash".to_string(),
439            integrity_score: 66.67,
440        };
441        assert!(!invalid_report.is_valid());
442    }
443
444    #[test] 
445    fn test_handshake_response_structure() {
446        let manager = AttestationManager::new(true).unwrap();
447        let response = manager.handshake("test").unwrap();
448        
449        // Verify all fields are properly set
450        assert!(!response.response_hash.is_empty());
451        assert!(response.attestation_enabled);
452        assert!(!response.build_info.version.is_empty());
453        assert!(!response.build_info.commit.is_empty());
454        assert!(!response.build_info.build_timestamp.is_empty());
455    }
456}