Files
Linux-Hello/tests/integration/phase3_security_test.rs
eliott fd5d8c87d5 fix(02-01): fix clippy warnings to pass lint checks
- config.rs: Added Default derive, removed manual impl
- template.rs: Simplified iterator with flatten()
- camera/linux.rs: Added transmute annotations, fixed doc syntax
- camera/mod.rs: Allow unused imports from linux module
- ipc.rs: Renamed default() to with_default_path(), simplified closures
- secure_memory.rs: Use is_multiple_of() instead of modulo
- phase3_security_test.rs: Use std::f32::consts::PI
2026-02-14 11:14:41 +01:00

430 lines
14 KiB
Rust

//! Phase 3 Security Hardening Integration Tests
//!
//! Tests for TPM storage, secure memory, and anti-spoofing functionality.
use linux_hello_common::FaceTemplate;
use linux_hello_daemon::anti_spoofing::{
AntiSpoofingConfig, AntiSpoofingDetector, AntiSpoofingFrame,
};
use linux_hello_daemon::secure_memory::{memory_protection, SecureBytes, SecureEmbedding};
use linux_hello_daemon::tpm::{EncryptedTemplate, SoftwareTpmFallback, TpmStorage};
use linux_hello_daemon::SecureTemplateStore;
use tempfile::TempDir;
// =============================================================================
// TPM Storage Tests
// =============================================================================
#[test]
fn test_software_tpm_initialization() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
assert!(storage.is_available());
storage.initialize().unwrap();
}
#[test]
fn test_software_tpm_encrypt_decrypt_roundtrip() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
storage.initialize().unwrap();
let plaintext = b"Sensitive face embedding data for security testing";
let encrypted = storage.encrypt("testuser", plaintext).unwrap();
assert!(!encrypted.ciphertext.is_empty());
assert_ne!(encrypted.ciphertext.as_slice(), plaintext);
assert!(!encrypted.tpm_encrypted); // Software fallback
let decrypted = storage.decrypt("testuser", &encrypted).unwrap();
assert_eq!(decrypted.as_slice(), plaintext);
}
#[test]
fn test_software_tpm_user_key_management() {
let temp = TempDir::new().unwrap();
let mut storage = SoftwareTpmFallback::new(temp.path());
storage.initialize().unwrap();
storage.create_user_key("user1").unwrap();
storage.create_user_key("user2").unwrap();
storage.remove_user_key("user1").unwrap();
// user2's key should still exist
// Can still encrypt for both users (key derivation is deterministic)
let encrypted = storage.encrypt("user1", b"test").unwrap();
assert!(!encrypted.ciphertext.is_empty());
}
#[test]
fn test_encrypted_template_structure() {
let template = EncryptedTemplate {
ciphertext: vec![1, 2, 3, 4, 5, 6, 7, 8],
iv: vec![
0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
], // 12 bytes for AES-GCM
salt: vec![0u8; 32], // 32 bytes for PBKDF2 salt
key_handle: 0x81000001,
tpm_encrypted: true,
};
let json = serde_json::to_string(&template).unwrap();
let restored: EncryptedTemplate = serde_json::from_str(&json).unwrap();
assert_eq!(restored.ciphertext, template.ciphertext);
assert_eq!(restored.iv, template.iv);
assert_eq!(restored.salt, template.salt);
assert_eq!(restored.key_handle, template.key_handle);
assert_eq!(restored.tpm_encrypted, template.tpm_encrypted);
}
// =============================================================================
// Secure Memory Tests
// =============================================================================
#[test]
fn test_secure_embedding_operations() {
let data = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let embedding = SecureEmbedding::new(data.clone());
assert_eq!(embedding.len(), 5);
assert!(!embedding.is_empty());
assert_eq!(embedding.as_slice(), data.as_slice());
}
#[test]
fn test_secure_embedding_similarity_metrics() {
// Identical vectors
let emb1 = SecureEmbedding::new(vec![1.0, 0.0, 0.0, 0.0]);
let emb2 = SecureEmbedding::new(vec![1.0, 0.0, 0.0, 0.0]);
let similarity = emb1.cosine_similarity(&emb2);
assert!((similarity - 1.0).abs() < 0.001);
let distance = emb1.euclidean_distance(&emb2);
assert!(distance.abs() < 0.001);
// Orthogonal vectors
let emb3 = SecureEmbedding::new(vec![0.0, 1.0, 0.0, 0.0]);
let similarity2 = emb1.cosine_similarity(&emb3);
assert!(similarity2.abs() < 0.001);
}
#[test]
fn test_secure_embedding_serialization() {
let original = SecureEmbedding::new(vec![1.5, -2.5, std::f32::consts::PI, 0.0, f32::MAX]);
let bytes = original.to_bytes();
let restored = SecureEmbedding::from_bytes(&bytes).unwrap();
assert_eq!(original.as_slice(), restored.as_slice());
}
#[test]
fn test_secure_bytes_constant_time_comparison() {
let secret1 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x78]);
let secret2 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x78]);
let secret3 = SecureBytes::new(vec![0x12, 0x34, 0x56, 0x79]);
assert!(secret1.constant_time_eq(&secret2));
assert!(!secret1.constant_time_eq(&secret3));
}
#[test]
fn test_secure_memory_zeroization() {
let mut data = vec![0xAA_u8; 64];
memory_protection::secure_zero(&mut data);
assert!(data.iter().all(|&b| b == 0));
}
#[test]
fn test_secure_embedding_debug_hides_data() {
let embedding = SecureEmbedding::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let debug_str = format!("{:?}", embedding);
assert!(debug_str.contains("REDACTED"));
assert!(!debug_str.contains("1.0"));
assert!(!debug_str.contains("2.0"));
}
// =============================================================================
// Secure Template Store Tests
// =============================================================================
fn create_test_template(user: &str, label: &str) -> FaceTemplate {
FaceTemplate {
user: user.to_string(),
label: label.to_string(),
embedding: vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
enrolled_at: 1700000000,
frame_count: 10,
}
}
#[test]
fn test_secure_template_store_unencrypted() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
assert!(!store.is_encryption_enabled());
let template = create_test_template("alice", "default");
store.store(&template).unwrap();
let loaded = store.load("alice", "default").unwrap();
assert_eq!(loaded.user, "alice");
assert_eq!(loaded.embedding, template.embedding);
}
#[test]
fn test_secure_template_store_enrollment_check() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
assert!(!store.is_enrolled("bob"));
store
.store(&create_test_template("bob", "primary"))
.unwrap();
assert!(store.is_enrolled("bob"));
store.remove("bob", "primary").unwrap();
assert!(!store.is_enrolled("bob"));
}
#[test]
fn test_secure_template_store_load_all() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
store
.store(&create_test_template("charlie", "default"))
.unwrap();
store
.store(&create_test_template("charlie", "backup"))
.unwrap();
store
.store(&create_test_template("charlie", "outdoor"))
.unwrap();
let templates = store.load_all("charlie").unwrap();
assert_eq!(templates.len(), 3);
}
#[test]
fn test_secure_template_store_load_secure_embedding() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
let template = create_test_template("dave", "default");
store.store(&template).unwrap();
let secure = store.load_secure("dave", "default").unwrap();
assert_eq!(secure.len(), template.embedding.len());
assert_eq!(secure.as_slice(), template.embedding.as_slice());
}
// =============================================================================
// Anti-Spoofing Tests
// =============================================================================
fn create_test_frame(brightness: u8, is_ir: bool, width: u32, height: u32) -> AntiSpoofingFrame {
let size = (width * height) as usize;
let mut pixels = vec![brightness; size];
// Add realistic variation
for (i, pixel) in pixels.iter_mut().enumerate() {
let x = (i % width as usize) as u32;
let y = (i / width as usize) as u32;
// Add gradient and noise
let gradient = ((x + y) % 20) as i16 - 10;
let noise = ((i * 17 + 31) % 15) as i16 - 7;
*pixel = (brightness as i16 + gradient + noise).clamp(0, 255) as u8;
}
AntiSpoofingFrame {
pixels,
width,
height,
is_ir,
face_bbox: Some((width / 4, height / 4, width / 2, height / 2)),
timestamp_ms: 0,
}
}
#[test]
fn test_anti_spoofing_basic_check() {
let config = AntiSpoofingConfig::default();
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
assert!(result.score >= 0.0 && result.score <= 1.0);
assert!(result.checks.ir_check.is_some());
assert!(result.checks.depth_check.is_some());
assert!(result.checks.texture_check.is_some());
}
#[test]
fn test_anti_spoofing_ir_verification() {
let config = AntiSpoofingConfig::default();
let mut detector = AntiSpoofingDetector::new(config);
// Normal IR frame (should pass)
let normal_frame = create_test_frame(100, true, 200, 200);
let result1 = detector.check_frame(&normal_frame).unwrap();
let ir_score1 = result1.checks.ir_check.unwrap();
detector.reset();
// Very dark frame (suspicious)
let dark_frame = create_test_frame(10, true, 200, 200);
let result2 = detector.check_frame(&dark_frame).unwrap();
let ir_score2 = result2.checks.ir_check.unwrap();
// Normal frame should score higher than very dark frame
assert!(ir_score1 > ir_score2);
}
#[test]
fn test_anti_spoofing_temporal_analysis() {
let mut config = AntiSpoofingConfig::default();
config.enable_movement_check = true;
config.temporal_frames = 5;
let mut detector = AntiSpoofingDetector::new(config);
// Simulate multiple frames with natural movement
for i in 0..6 {
let mut frame = create_test_frame(100, true, 200, 200);
frame.timestamp_ms = i * 100;
// Add slight position variation
let offset = (i % 3) as u32;
frame.face_bbox = Some((50 + offset, 50, 100, 100));
let result = detector.check_frame(&frame).unwrap();
// After enough frames, movement check should be available
if i >= 3 {
assert!(result.checks.movement_check.is_some());
}
}
}
#[test]
fn test_anti_spoofing_reset() {
let mut config = AntiSpoofingConfig::default();
config.enable_movement_check = true;
let mut detector = AntiSpoofingDetector::new(config);
// Process some frames
for _ in 0..5 {
let frame = create_test_frame(100, true, 200, 200);
let _ = detector.check_frame(&frame);
}
detector.reset();
// After reset, first frame should not have movement analysis
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
assert!(result.checks.movement_check.is_none());
}
#[test]
fn test_anti_spoofing_rejection_reasons() {
let mut config = AntiSpoofingConfig::default();
config.threshold = 0.95; // Very high threshold
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
if !result.is_live {
assert!(result.rejection_reason.is_some());
let reason = result.rejection_reason.unwrap();
assert!(!reason.is_empty());
}
}
#[test]
fn test_anti_spoofing_config_customization() {
let config = AntiSpoofingConfig {
threshold: 0.8,
enable_ir_check: true,
enable_depth_check: true,
enable_texture_check: false, // Disabled
enable_blink_check: false,
enable_movement_check: false,
temporal_frames: 10,
};
let mut detector = AntiSpoofingDetector::new(config);
let frame = create_test_frame(100, true, 200, 200);
let result = detector.check_frame(&frame).unwrap();
// Texture check should not be performed
assert!(result.checks.texture_check.is_none());
// IR and depth checks should be performed
assert!(result.checks.ir_check.is_some());
assert!(result.checks.depth_check.is_some());
}
// =============================================================================
// Integration Tests
// =============================================================================
#[test]
fn test_secure_workflow_enroll_and_verify() {
let temp = TempDir::new().unwrap();
let mut store = SecureTemplateStore::new(temp.path());
store.initialize(false).unwrap();
// Simulate enrollment embedding
let enroll_embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
let template = FaceTemplate {
user: "secure_user".to_string(),
label: "default".to_string(),
embedding: enroll_embedding.clone(),
enrolled_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
frame_count: 5,
};
// Store securely
store.store(&template).unwrap();
assert!(store.is_enrolled("secure_user"));
// Load as secure embedding for matching
let stored = store.load_secure("secure_user", "default").unwrap();
// Simulate authentication embedding (slightly different)
let auth_embedding = SecureEmbedding::new(vec![0.11, 0.19, 0.31, 0.39, 0.51, 0.59, 0.71, 0.79]);
// Compare securely
let similarity = stored.cosine_similarity(&auth_embedding);
assert!(similarity > 0.9); // Should be very similar
}
#[test]
fn test_memory_protection_basics() {
// Test that we can lock and unlock memory (even if mlock fails due to permissions)
let data = vec![0xABu8; 1024];
let result = memory_protection::lock_memory(&data);
assert!(result.is_ok()); // Should not error, even if lock fails
let _ = memory_protection::unlock_memory(&data);
}