649 lines
21 KiB
Rust
649 lines
21 KiB
Rust
//! Performance Benchmarks for Linux Hello
|
|
//!
|
|
//! This module contains benchmarks for critical authentication pipeline components:
|
|
//! - Face detection
|
|
//! - Embedding extraction
|
|
//! - Template matching (cosine similarity)
|
|
//! - Anti-spoofing checks
|
|
//! - Encryption/decryption (AES-GCM)
|
|
//! - Secure memory operations
|
|
//!
|
|
//! Run with: cargo bench -p linux-hello-daemon
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};
|
|
use image::GrayImage;
|
|
|
|
use linux_hello_daemon::detection::{detect_face_simple, SimpleFaceDetector, FaceDetect};
|
|
use linux_hello_daemon::embedding::{
|
|
cosine_similarity, euclidean_distance, PlaceholderEmbeddingExtractor, EmbeddingExtractor,
|
|
};
|
|
use linux_hello_daemon::matching::match_template;
|
|
use linux_hello_daemon::anti_spoofing::{
|
|
AntiSpoofingConfig, AntiSpoofingDetector, AntiSpoofingFrame,
|
|
};
|
|
use linux_hello_daemon::secure_memory::{SecureEmbedding, SecureBytes, memory_protection};
|
|
use linux_hello_common::FaceTemplate;
|
|
|
|
// ============================================================================
|
|
// Test Data Generation Helpers
|
|
// ============================================================================
|
|
|
|
/// Generate a synthetic grayscale image with realistic noise
|
|
fn generate_test_image(width: u32, height: u32) -> Vec<u8> {
|
|
let mut image = Vec::with_capacity((width * height) as usize);
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
// Generate a pattern that simulates face-like brightness distribution
|
|
let center_x = width as f32 / 2.0;
|
|
let center_y = height as f32 / 2.0;
|
|
let dx = (x as f32 - center_x) / center_x;
|
|
let dy = (y as f32 - center_y) / center_y;
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
|
|
// Face-like brightness: brighter in center, darker at edges
|
|
let base = (180.0 - dist * 80.0).max(50.0);
|
|
// Add some noise
|
|
let noise = ((x * 17 + y * 31) % 20) as f32 - 10.0;
|
|
let pixel = (base + noise).clamp(0.0, 255.0) as u8;
|
|
image.push(pixel);
|
|
}
|
|
}
|
|
image
|
|
}
|
|
|
|
/// Generate a normalized embedding vector
|
|
fn generate_test_embedding(dimension: usize) -> Vec<f32> {
|
|
let mut embedding: Vec<f32> = (0..dimension)
|
|
.map(|i| ((i as f32 * 0.1).sin() + 0.5) / dimension as f32)
|
|
.collect();
|
|
|
|
// Normalize
|
|
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
|
if norm > 0.0 {
|
|
for val in &mut embedding {
|
|
*val /= norm;
|
|
}
|
|
}
|
|
embedding
|
|
}
|
|
|
|
/// Generate test face templates
|
|
fn generate_test_templates(count: usize, dimension: usize) -> Vec<FaceTemplate> {
|
|
(0..count)
|
|
.map(|i| {
|
|
let mut embedding = generate_test_embedding(dimension);
|
|
// Add slight variation to each template
|
|
for (j, val) in embedding.iter_mut().enumerate() {
|
|
*val += (i as f32 * 0.01 + j as f32 * 0.001).sin() * 0.1;
|
|
}
|
|
// Re-normalize
|
|
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
|
for val in &mut embedding {
|
|
*val /= norm;
|
|
}
|
|
|
|
FaceTemplate {
|
|
user: format!("user_{}", i),
|
|
label: "default".to_string(),
|
|
embedding,
|
|
enrolled_at: 1234567890 + i as u64,
|
|
frame_count: 5,
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
// ============================================================================
|
|
// Face Detection Benchmarks
|
|
// ============================================================================
|
|
|
|
fn bench_face_detection(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("face_detection");
|
|
|
|
// Test at common camera resolutions
|
|
let resolutions = [
|
|
(320, 240, "QVGA"),
|
|
(640, 480, "VGA"),
|
|
(1280, 720, "720p"),
|
|
(1920, 1080, "1080p"),
|
|
];
|
|
|
|
for (width, height, name) in resolutions {
|
|
let image = generate_test_image(width, height);
|
|
let _pixels = (width * height) as u64;
|
|
|
|
group.throughput(Throughput::Elements(1)); // 1 frame per iteration
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("simple_detection", name),
|
|
&(image.clone(), width, height),
|
|
|b, (img, w, h)| {
|
|
b.iter(|| {
|
|
detect_face_simple(black_box(img), black_box(*w), black_box(*h))
|
|
});
|
|
},
|
|
);
|
|
|
|
// SimpleFaceDetector (trait implementation)
|
|
let detector = SimpleFaceDetector::new(0.3);
|
|
group.bench_with_input(
|
|
BenchmarkId::new("detector_trait", name),
|
|
&(image.clone(), width, height),
|
|
|b, (img, w, h)| {
|
|
b.iter(|| {
|
|
detector.detect(black_box(img), black_box(*w), black_box(*h))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Embedding Extraction Benchmarks
|
|
// ============================================================================
|
|
|
|
fn bench_embedding_extraction(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("embedding_extraction");
|
|
|
|
let face_sizes = [
|
|
(64, 64, "64x64"),
|
|
(112, 112, "112x112"),
|
|
(160, 160, "160x160"),
|
|
(224, 224, "224x224"),
|
|
];
|
|
|
|
let extractor = PlaceholderEmbeddingExtractor::new(128);
|
|
|
|
for (width, height, name) in face_sizes {
|
|
let image_data = generate_test_image(width, height);
|
|
let face_image = GrayImage::from_raw(width, height, image_data)
|
|
.expect("Failed to create test image");
|
|
|
|
group.throughput(Throughput::Elements(1)); // 1 embedding per iteration
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("placeholder_extractor", name),
|
|
&face_image,
|
|
|b, img| {
|
|
b.iter(|| {
|
|
extractor.extract(black_box(img))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Also benchmark different embedding dimensions
|
|
let dimensions = [64, 128, 256, 512];
|
|
let face_image = GrayImage::from_raw(112, 112, generate_test_image(112, 112))
|
|
.expect("Failed to create test image");
|
|
|
|
for dim in dimensions {
|
|
let extractor = PlaceholderEmbeddingExtractor::new(dim);
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("dimension", dim),
|
|
&face_image,
|
|
|b, img| {
|
|
b.iter(|| {
|
|
extractor.extract(black_box(img))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Template Matching Benchmarks (Cosine Similarity)
|
|
// ============================================================================
|
|
|
|
fn bench_template_matching(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("template_matching");
|
|
|
|
// Benchmark cosine similarity at different dimensions
|
|
let dimensions = [64, 128, 256, 512, 1024];
|
|
|
|
for dim in dimensions {
|
|
let emb_a = generate_test_embedding(dim);
|
|
let emb_b = generate_test_embedding(dim);
|
|
|
|
group.throughput(Throughput::Elements(1)); // 1 comparison per iteration
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("cosine_similarity", dim),
|
|
&(emb_a.clone(), emb_b.clone()),
|
|
|b, (a, bb)| {
|
|
b.iter(|| {
|
|
cosine_similarity(black_box(a), black_box(bb))
|
|
});
|
|
},
|
|
);
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("euclidean_distance", dim),
|
|
&(emb_a.clone(), emb_b.clone()),
|
|
|b, (a, bb)| {
|
|
b.iter(|| {
|
|
euclidean_distance(black_box(a), black_box(bb))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Benchmark matching against template databases of different sizes
|
|
let template_counts = [1, 5, 10, 50, 100];
|
|
let query = generate_test_embedding(128);
|
|
|
|
for count in template_counts {
|
|
let templates = generate_test_templates(count, 128);
|
|
|
|
group.throughput(Throughput::Elements(count as u64)); // N comparisons
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("match_against_n_templates", count),
|
|
&(query.clone(), templates),
|
|
|b, (q, tmpl)| {
|
|
b.iter(|| {
|
|
match_template(black_box(q), black_box(tmpl), 0.4)
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Anti-Spoofing Benchmarks
|
|
// ============================================================================
|
|
|
|
fn bench_anti_spoofing(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("anti_spoofing");
|
|
|
|
let resolutions = [
|
|
(320, 240, "QVGA"),
|
|
(640, 480, "VGA"),
|
|
];
|
|
|
|
for (width, height, name) in resolutions {
|
|
let pixels = generate_test_image(width, height);
|
|
let frame = AntiSpoofingFrame {
|
|
pixels: pixels.clone(),
|
|
width,
|
|
height,
|
|
is_ir: true,
|
|
face_bbox: Some((width / 4, height / 4, width / 2, height / 2)),
|
|
timestamp_ms: 0,
|
|
};
|
|
|
|
// Single frame check
|
|
group.throughput(Throughput::Elements(1));
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("single_frame_check", name),
|
|
&frame,
|
|
|b, f| {
|
|
let config = AntiSpoofingConfig::default();
|
|
let mut detector = AntiSpoofingDetector::new(config);
|
|
b.iter(|| {
|
|
detector.reset();
|
|
detector.check_frame(black_box(f))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Full pipeline with temporal analysis
|
|
let width = 640;
|
|
let height = 480;
|
|
let frames: Vec<_> = (0..10)
|
|
.map(|i| {
|
|
let pixels = generate_test_image(width, height);
|
|
AntiSpoofingFrame {
|
|
pixels,
|
|
width,
|
|
height,
|
|
is_ir: true,
|
|
face_bbox: Some((width / 4 + i, height / 4, width / 2, height / 2)),
|
|
timestamp_ms: i as u64 * 100,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
group.throughput(Throughput::Elements(10)); // 10 frames
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("full_pipeline", "10_frames"),
|
|
&frames,
|
|
|b, frames| {
|
|
let mut config = AntiSpoofingConfig::default();
|
|
config.enable_movement_check = true;
|
|
config.enable_blink_check = true;
|
|
|
|
b.iter(|| {
|
|
let mut detector = AntiSpoofingDetector::new(config.clone());
|
|
let mut last_result = None;
|
|
for frame in frames {
|
|
last_result = Some(detector.check_frame(black_box(frame)));
|
|
}
|
|
last_result
|
|
});
|
|
},
|
|
);
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Encryption/Decryption Benchmarks (AES-GCM)
|
|
// ============================================================================
|
|
|
|
fn bench_encryption(c: &mut Criterion) {
|
|
use linux_hello_daemon::tpm::SoftwareTpmFallback;
|
|
use linux_hello_daemon::tpm::TpmStorage;
|
|
|
|
let mut group = c.benchmark_group("encryption");
|
|
|
|
// Initialize software TPM fallback in temp directory
|
|
let temp_dir = std::env::temp_dir().join("linux-hello-bench-keys");
|
|
let _ = std::fs::create_dir_all(&temp_dir);
|
|
|
|
let mut storage = SoftwareTpmFallback::new(&temp_dir);
|
|
if storage.initialize().is_err() {
|
|
// Skip encryption benchmarks if we can't initialize
|
|
eprintln!("Warning: Could not initialize encryption storage for benchmarks");
|
|
group.finish();
|
|
return;
|
|
}
|
|
|
|
// Test with different data sizes (embedding sizes)
|
|
let data_sizes = [
|
|
(128 * 4, "128_floats"), // 128-dim embedding
|
|
(256 * 4, "256_floats"), // 256-dim embedding
|
|
(512 * 4, "512_floats"), // 512-dim embedding
|
|
(1024 * 4, "1024_floats"), // 1024-dim embedding
|
|
];
|
|
|
|
for (size, name) in data_sizes {
|
|
let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
|
|
|
|
group.throughput(Throughput::Bytes(size as u64));
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("encrypt", name),
|
|
&plaintext,
|
|
|b, data| {
|
|
b.iter(|| {
|
|
storage.encrypt("bench_user", black_box(data))
|
|
});
|
|
},
|
|
);
|
|
|
|
// Encrypt once for decrypt benchmark
|
|
let encrypted = storage.encrypt("bench_user", &plaintext)
|
|
.expect("Encryption failed");
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("decrypt", name),
|
|
&encrypted,
|
|
|b, enc| {
|
|
b.iter(|| {
|
|
storage.decrypt("bench_user", black_box(enc))
|
|
});
|
|
},
|
|
);
|
|
|
|
// Round-trip benchmark
|
|
group.bench_with_input(
|
|
BenchmarkId::new("round_trip", name),
|
|
&plaintext,
|
|
|b, data| {
|
|
b.iter(|| {
|
|
let enc = storage.encrypt("bench_user", black_box(data)).unwrap();
|
|
storage.decrypt("bench_user", black_box(&enc))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Cleanup
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Secure Memory Operation Benchmarks
|
|
// ============================================================================
|
|
|
|
fn bench_secure_memory(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("secure_memory");
|
|
|
|
// SecureEmbedding creation and operations
|
|
let dimensions = [128, 256, 512];
|
|
|
|
for dim in dimensions {
|
|
let data = generate_test_embedding(dim);
|
|
|
|
group.throughput(Throughput::Elements(dim as u64));
|
|
|
|
// SecureEmbedding creation (includes memory locking attempt)
|
|
group.bench_with_input(
|
|
BenchmarkId::new("secure_embedding_create", dim),
|
|
&data,
|
|
|b, d| {
|
|
b.iter(|| {
|
|
SecureEmbedding::new(black_box(d.clone()))
|
|
});
|
|
},
|
|
);
|
|
|
|
// Secure cosine similarity
|
|
let secure_a = SecureEmbedding::new(data.clone());
|
|
let secure_b = SecureEmbedding::new(generate_test_embedding(dim));
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("secure_cosine_similarity", dim),
|
|
&(secure_a.clone(), secure_b.clone()),
|
|
|b, (a, bb)| {
|
|
b.iter(|| {
|
|
a.cosine_similarity(black_box(bb))
|
|
});
|
|
},
|
|
);
|
|
|
|
// Serialization/deserialization
|
|
group.bench_with_input(
|
|
BenchmarkId::new("secure_to_bytes", dim),
|
|
&secure_a,
|
|
|b, emb| {
|
|
b.iter(|| {
|
|
emb.to_bytes()
|
|
});
|
|
},
|
|
);
|
|
|
|
let bytes = secure_a.to_bytes();
|
|
group.bench_with_input(
|
|
BenchmarkId::new("secure_from_bytes", dim),
|
|
&bytes,
|
|
|b, data| {
|
|
b.iter(|| {
|
|
SecureEmbedding::from_bytes(black_box(data))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// SecureBytes constant-time comparison
|
|
let byte_sizes = [64, 128, 256, 512, 1024];
|
|
|
|
for size in byte_sizes {
|
|
let bytes_a = SecureBytes::new((0..size).map(|i| (i % 256) as u8).collect());
|
|
let bytes_b = SecureBytes::new((0..size).map(|i| (i % 256) as u8).collect());
|
|
let bytes_diff = SecureBytes::new((0..size).map(|i| ((i + 1) % 256) as u8).collect());
|
|
|
|
group.throughput(Throughput::Bytes(size as u64));
|
|
|
|
// Equal bytes comparison
|
|
group.bench_with_input(
|
|
BenchmarkId::new("constant_time_eq_match", size),
|
|
&(bytes_a.clone(), bytes_b.clone()),
|
|
|b, (a, bb)| {
|
|
b.iter(|| {
|
|
a.constant_time_eq(black_box(bb))
|
|
});
|
|
},
|
|
);
|
|
|
|
// Different bytes comparison (should take same time)
|
|
group.bench_with_input(
|
|
BenchmarkId::new("constant_time_eq_differ", size),
|
|
&(bytes_a.clone(), bytes_diff.clone()),
|
|
|b, (a, d)| {
|
|
b.iter(|| {
|
|
a.constant_time_eq(black_box(d))
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Memory zeroization
|
|
for size in byte_sizes {
|
|
group.throughput(Throughput::Bytes(size as u64));
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("secure_zero", size),
|
|
&size,
|
|
|b, &sz| {
|
|
let mut buffer: Vec<u8> = (0..sz).map(|i| (i % 256) as u8).collect();
|
|
b.iter(|| {
|
|
memory_protection::secure_zero(black_box(&mut buffer));
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Full Pipeline Benchmark (End-to-End)
|
|
// ============================================================================
|
|
|
|
fn bench_full_pipeline(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("full_pipeline");
|
|
group.sample_size(50); // Fewer samples for slower benchmarks
|
|
|
|
// Simulate complete authentication flow
|
|
let width = 640;
|
|
let height = 480;
|
|
let image_data = generate_test_image(width, height);
|
|
let templates = generate_test_templates(5, 128);
|
|
|
|
group.throughput(Throughput::Elements(1)); // 1 authentication attempt
|
|
|
|
group.bench_function("auth_pipeline_no_crypto", |b| {
|
|
let detector = SimpleFaceDetector::new(0.3);
|
|
let extractor = PlaceholderEmbeddingExtractor::new(128);
|
|
|
|
b.iter(|| {
|
|
// Step 1: Face detection
|
|
let detections = detector.detect(
|
|
black_box(&image_data),
|
|
black_box(width),
|
|
black_box(height)
|
|
).unwrap();
|
|
|
|
if let Some(detection) = detections.first() {
|
|
// Step 2: Extract face region (simulated)
|
|
let (_x, _y, w, h) = detection.to_pixels(width, height);
|
|
let face_image = GrayImage::from_raw(w, h, vec![128u8; (w * h) as usize])
|
|
.unwrap_or_else(|| GrayImage::new(w, h));
|
|
|
|
// Step 3: Extract embedding
|
|
let embedding = extractor.extract(&face_image).unwrap();
|
|
|
|
// Step 4: Template matching
|
|
let result = match_template(&embedding, &templates, 0.4);
|
|
|
|
black_box(Some(result))
|
|
} else {
|
|
black_box(None)
|
|
}
|
|
});
|
|
});
|
|
|
|
// With anti-spoofing
|
|
group.bench_function("auth_pipeline_with_antispoofing", |b| {
|
|
let detector = SimpleFaceDetector::new(0.3);
|
|
let extractor = PlaceholderEmbeddingExtractor::new(128);
|
|
let config = AntiSpoofingConfig::default();
|
|
|
|
b.iter(|| {
|
|
let mut spoof_detector = AntiSpoofingDetector::new(config.clone());
|
|
|
|
// Step 1: Face detection
|
|
let detections = detector.detect(
|
|
black_box(&image_data),
|
|
black_box(width),
|
|
black_box(height)
|
|
).unwrap();
|
|
|
|
if let Some(detection) = detections.first() {
|
|
// Step 2: Anti-spoofing check
|
|
let frame = AntiSpoofingFrame {
|
|
pixels: image_data.clone(),
|
|
width,
|
|
height,
|
|
is_ir: true,
|
|
face_bbox: Some(detection.to_pixels(width, height)),
|
|
timestamp_ms: 0,
|
|
};
|
|
let liveness = spoof_detector.check_frame(&frame).unwrap();
|
|
|
|
if liveness.is_live {
|
|
// Step 3: Extract face region
|
|
let (_, _, w, h) = detection.to_pixels(width, height);
|
|
let face_image = GrayImage::from_raw(w, h, vec![128u8; (w * h) as usize])
|
|
.unwrap_or_else(|| GrayImage::new(w, h));
|
|
|
|
// Step 4: Extract embedding
|
|
let embedding = extractor.extract(&face_image).unwrap();
|
|
|
|
// Step 5: Template matching
|
|
let result = match_template(&embedding, &templates, 0.4);
|
|
|
|
black_box(Some(result))
|
|
} else {
|
|
black_box(None)
|
|
}
|
|
} else {
|
|
black_box(None)
|
|
}
|
|
});
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Criterion Configuration and Main
|
|
// ============================================================================
|
|
|
|
criterion_group!(
|
|
benches,
|
|
bench_face_detection,
|
|
bench_embedding_extraction,
|
|
bench_template_matching,
|
|
bench_anti_spoofing,
|
|
bench_encryption,
|
|
bench_secure_memory,
|
|
bench_full_pipeline,
|
|
);
|
|
|
|
criterion_main!(benches);
|