430 lines
14 KiB
Rust
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, 3.14159, 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);
|
|
}
|