diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md new file mode 100644 index 0000000..327e2cc --- /dev/null +++ b/CODING_STANDARDS.md @@ -0,0 +1,164 @@ +# Safety-Critical Coding Standards + +This document outlines the safety-critical coding standards applied to Linux Hello, based on MISRA C and similar standards, adapted for both C and Rust code. + +## Principles Applied + +### 1. Simple Control Flow +- ✅ **C**: No goto, setjmp, longjmp, or recursion +- ✅ **Rust**: Avoid recursion where possible; use iterative solutions. Rust's ownership system prevents many control flow issues. + +### 2. Fixed Loop Bounds +- ✅ **C**: All loops have fixed upper bounds (e.g., `MAX_ARGS = 32`) +- ✅ **Rust**: Use bounded iterators, `take()`, or explicit bounds. Example: + ```rust + for item in items.iter().take(MAX_ITEMS) { ... } + ``` + +### 3. No Dynamic Memory After Init +- ✅ **C**: No malloc/free after startup (stack allocation only) +- ⚠️ **Rust**: Rust's allocator is safer, but we still prefer: + - Pre-allocated buffers where possible + - `Vec::with_capacity()` to avoid reallocations + - Stack allocation for small data + +### 4. Function Length Limit (~60 lines) +- ✅ **C**: All functions under 60 lines +- ✅ **Rust**: Same principle - break complex functions into smaller, testable units + +### 5. Runtime Assertions (2+ per function) +- ✅ **C**: Using `assert()` and `assert_condition()` helper +- ✅ **Rust**: Use `debug_assert!()`, `assert!()`, and `unwrap_or_else()` with validation + +### 6. Smallest Scope for Data +- ✅ **C**: Declare variables at smallest possible scope +- ✅ **Rust**: Rust enforces this - variables are scoped to blocks + +### 7. Check All Return Values +- ✅ **C**: All function returns checked, void casts for intentional ignores +- ✅ **Rust**: Use `Result` and `Option` - compiler enforces checking + +### 8. Minimal Preprocessor +- ✅ **C**: Only `#include` and simple `#define` constants +- ✅ **Rust**: Use `cfg!()` and `#[cfg(...)]` attributes instead of macros + +### 9. Restricted Pointer Use +- ✅ **C**: Single level dereferencing only, no function pointers unless essential +- ✅ **Rust**: Rust's references are safer, but we still: + - Avoid deep nesting (`&&&T`) + - Prefer owned types over references where possible + - Use function pointers only when necessary (e.g., trait objects) + +### 10. Strictest Warnings +- ✅ **C**: `-Wall -Wextra -Werror -Wpedantic` + static analysis +- ✅ **Rust**: `#![deny(warnings)]` in critical modules, `clippy` with pedantic lints + +## Implementation Status + +### C PAM Module (`pam-module/pam_linux_hello.c`) +✅ **Fully compliant**: +- Split `authenticate_face()` into smaller functions +- Fixed loop bounds (`MAX_ARGS = 32`) +- All return values checked +- Runtime assertions added +- Strictest compiler warnings enabled +- Single-level pointer dereferencing +- No dynamic allocation after init + +### Rust Codebase +🔄 **In Progress** - Applying equivalent principles: + +#### Completed: +- ✅ Return values checked (Rust enforces this) +- ✅ Small scope (Rust enforces this) +- ✅ No goto/setjmp (not applicable in Rust) +- ✅ Minimal preprocessor (Rust uses attributes) + +#### To Improve: +- ⏳ Function length limits (some functions > 60 lines) +- ⏳ Fixed loop bounds (some iterators unbounded) +- ⏳ More runtime assertions +- ⏳ Enable strictest Rust lints + +## Rust-Specific Safety Measures + +### Memory Safety +- Rust's ownership system prevents use-after-free, double-free +- No manual memory management needed +- `unsafe` blocks are minimal and documented + +### Type Safety +- Strong typing prevents many classes of bugs +- Pattern matching ensures exhaustive handling +- `Result` and `Option` enforce error handling + +### Bounds Checking +- Array/vector bounds checked at runtime (can be disabled with `unsafe`) +- Iterator bounds enforced by type system + +## Static Analysis + +### C Code +```bash +# Compile with strictest warnings +make CFLAGS="-Wall -Wextra -Werror -Wpedantic ..." + +# Run static analysis +cppcheck --enable=all pam-module/pam_linux_hello.c +splint pam-module/pam_linux_hello.c +``` + +### Rust Code +```bash +# Clippy with pedantic lints +cargo clippy -- -W clippy::pedantic + +# Deny warnings in release +RUSTFLAGS="-D warnings" cargo build --release +``` + +## Code Review Checklist + +- [ ] No functions > 60 lines +- [ ] All loops have fixed bounds +- [ ] All return values checked +- [ ] Minimum 2 assertions per function +- [ ] Variables at smallest scope +- [ ] No recursion (unless essential) +- [ ] Single-level pointer/reference dereferencing +- [ ] Compiles with strictest warnings +- [ ] Passes static analysis + +## Examples + +### Good: Fixed Loop Bound +```rust +const MAX_ITEMS: usize = 100; +for item in items.iter().take(MAX_ITEMS) { + // Process item +} +``` + +### Good: Assertions +```rust +fn process_data(data: &[u8]) -> Result<()> { + debug_assert!(!data.is_empty(), "Data must not be empty"); + debug_assert!(data.len() <= MAX_SIZE, "Data exceeds maximum size"); + // ... processing +} +``` + +### Good: Small Functions +```rust +// Instead of one 100-line function, split: +fn validate_input(input: &str) -> Result<()> { ... } // 20 lines +fn parse_input(input: &str) -> Result { ... } // 25 lines +fn process_data(data: Data) -> Result { ... } // 30 lines +``` + +## References + +- MISRA C:2012 Guidelines +- CERT C Coding Standard +- Rust API Guidelines +- Safety-Critical Software Development diff --git a/Cargo.lock b/Cargo.lock index 2dd3478..21e2d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,6 +510,7 @@ version = "0.1.0" dependencies = [ "clap", "image", + "libc", "linux-hello-common", "linux-hello-daemon", "serde", diff --git a/README.md b/README.md index 6ef2950..daf8480 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- Linux Hello Logo + Linux Hello Logo
# Linux Hello @@ -139,6 +139,8 @@ Linux Hello provides secure facial authentication for Linux systems, similar to ## Installation +> **📖 New to testing?** See [TESTING.md](TESTING.md) for a detailed step-by-step testing guide. + ### Building from Source ```bash diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..2e37961 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,398 @@ +# Testing Linux Hello on Your Linux Computer + +This guide will help you test Linux Hello on your system. We'll start with safe CLI testing before any system-level installation. + +## Prerequisites + +### Install Required Packages + +**Debian/Ubuntu:** +```bash +sudo apt update +sudo apt install -y \ + build-essential \ + libpam0g-dev \ + v4l-utils \ + pkg-config \ + libssl-dev +``` + +**Fedora/RHEL:** +```bash +sudo dnf install -y \ + gcc \ + make \ + pam-devel \ + v4l-utils \ + pkgconfig \ + openssl-devel +``` + +**Arch Linux:** +```bash +sudo pacman -S \ + base-devel \ + pam \ + v4l-utils \ + pkgconf \ + openssl +``` + +### Install Rust + +If you don't have Rust installed: +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +``` + +--- + +## Step 1: Build the Project + +```bash +# Clone or navigate to the project directory +cd Linux-Hello + +# Build in release mode (faster, optimized) +cargo build --release +``` + +This will create: +- `target/release/linux-hello` (CLI tool) +- `target/release/linux-hello-daemon` (daemon) + +--- + +## Step 2: Test Without Installation (Safest) + +You can test most features without installing anything system-wide. + +### 2.1 Check Camera Detection + +```bash +# Run the CLI tool directly +./target/release/linux-hello status --camera +``` + +**Expected output:** +- Lists available cameras +- Shows IR camera detection +- May show "No cameras found" if no camera is connected + +**Note:** Works even without a camera (will show empty list or use mock). + +### 2.2 Test Configuration + +```bash +# View default configuration +./target/release/linux-hello config + +# View as JSON +./target/release/linux-hello config --json +``` + +### 2.3 Capture Test Frames (if camera available) + +```bash +# Create a test directory +mkdir -p ~/test-frames + +# Capture 5 frames +./target/release/linux-hello capture --output ~/test-frames --count 5 +``` + +**Expected:** Creates PNG files in `~/test-frames/` (if camera available). + +### 2.4 Test Face Detection (with image file) + +```bash +# Test detection on a photo +./target/release/linux-hello detect --image ~/test-frames/frame_000.png --output ~/detected.png +``` + +**Expected:** +- May detect a "face" (placeholder algorithm is very basic) +- Creates annotated image if face detected +- **Note:** Placeholder detection is unreliable - it just checks image statistics + +--- + +## Step 3: Test with Daemon (Manual Start) + +Before installing system-wide, test the daemon manually: + +### 3.1 Start Daemon Manually + +```bash +# Create runtime directory +sudo mkdir -p /run/linux-hello +sudo chmod 777 /run/linux-hello # For testing only! + +# Start daemon in foreground (to see logs) +sudo ./target/release/linux-hello-daemon +``` + +**Keep this terminal open** - you'll see daemon logs. + +### 3.2 Test Enrollment (in another terminal) + +```bash +# In a new terminal, test enrollment +./target/release/linux-hello enroll --label test +``` + +**Expected:** +- Captures frames from camera +- Creates face template +- Stores in `/var/lib/linux-hello/templates/` (may need sudo) + +**Note:** With placeholder algorithms, this will work but won't be accurate. + +### 3.3 Test Authentication + +```bash +# Test authentication +./target/release/linux-hello test +``` + +**Expected:** +- Attempts to authenticate using your face +- May succeed or fail (placeholder algorithms are unreliable) + +### 3.4 List Enrolled Templates + +```bash +./target/release/linux-hello list +``` + +--- + +## Step 4: Full Installation (Optional - More Invasive) + +⚠️ **Warning:** This modifies system PAM configuration. Test in a VM or have a recovery plan. + +### 4.1 Build PAM Module + +```bash +cd pam-module +make +``` + +### 4.2 Install Components + +```bash +# Install PAM module +sudo make install + +# Install daemon +sudo cp ../target/release/linux-hello-daemon /usr/libexec/ +sudo chmod +x /usr/libexec/linux-hello-daemon + +# Install systemd service +sudo cp ../dist/linux-hello.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable linux-hello.service +sudo systemctl start linux-hello.service + +# Install configuration +sudo mkdir -p /etc/linux-hello +sudo cp ../dist/config.toml /etc/linux-hello/ +``` + +### 4.3 Configure PAM (CAREFUL!) + +**Backup first:** +```bash +sudo cp /etc/pam.d/common-auth /etc/pam.d/common-auth.backup +``` + +**Edit PAM config:** +```bash +sudo nano /etc/pam.d/common-auth +``` + +Add at the top: +``` +auth sufficient pam_linux_hello.so debug fallback=password +auth required pam_unix.so use_first_pass +``` + +**Test login:** +- Try logging out and back in +- Face authentication will be attempted first +- Falls back to password if it fails + +--- + +## Step 5: What to Expect + +### With Placeholder Algorithms + +**Face Detection:** +- Very basic - just checks image brightness/contrast +- Assumes face is centered if image has "reasonable" contrast +- **Not reliable** - may detect faces where there aren't any, or miss real faces + +**Face Recognition:** +- Uses image statistics (mean, variance, histogram) +- **Cannot reliably distinguish between different people** +- May authenticate the wrong person +- May reject the correct person + +**Anti-Spoofing:** +- Framework is implemented +- But depends on accurate face detection (which we don't have) +- **Will not effectively prevent photo/video attacks** + +### Expected Behavior + +✅ **Will work:** +- Camera enumeration +- Frame capture +- Template storage/retrieval +- CLI commands +- Daemon communication +- Basic workflow + +❌ **Will NOT work reliably:** +- Actual face recognition +- Accurate authentication +- Spoofing prevention + +--- + +## Step 6: Troubleshooting + +### Daemon Won't Start + +```bash +# Check if socket directory exists +ls -la /run/linux-hello/ + +# Check daemon logs +sudo journalctl -u linux-hello.service -f +``` + +### Camera Not Detected + +```bash +# List V4L2 devices +v4l2-ctl --list-devices + +# Check permissions +ls -la /dev/video* + +# May need to add user to video group +sudo usermod -aG video $USER +# Log out and back in +``` + +### Permission Errors + +```bash +# Template storage directory +sudo mkdir -p /var/lib/linux-hello/templates +sudo chmod 755 /var/lib/linux-hello/templates + +# Runtime directory +sudo mkdir -p /run/linux-hello +sudo chmod 755 /run/linux-hello +``` + +### PAM Module Issues + +```bash +# Check if module is installed +ls -la /lib/x86_64-linux-gnu/security/pam_linux_hello.so + +# Test PAM module manually +sudo pam_test -a pam_linux_hello.so + +# Check PAM logs +sudo tail -f /var/log/auth.log +# or +sudo journalctl -f | grep pam +``` + +--- + +## Step 7: Running Tests + +Test the codebase itself: + +```bash +# Run all tests +cargo test --workspace + +# Run specific test suites +cargo test --test phase3_security_test +cargo test --test phase2_auth_test + +# Run with output +cargo test --workspace -- --nocapture +``` + +--- + +## Step 8: Cleanup (If Needed) + +### Remove Installation + +```bash +# Stop and disable service +sudo systemctl stop linux-hello.service +sudo systemctl disable linux-hello.service + +# Remove PAM module +cd pam-module +sudo make uninstall + +# Remove daemon +sudo rm /usr/libexec/linux-hello-daemon + +# Remove service file +sudo rm /etc/systemd/system/linux-hello.service +sudo systemctl daemon-reload + +# Restore PAM config +sudo cp /etc/pam.d/common-auth.backup /etc/pam.d/common-auth +``` + +--- + +## Safety Recommendations + +1. **Test in a VM first** - Don't test on your main system +2. **Keep password fallback** - Always have `fallback=password` in PAM config +3. **Backup PAM configs** - Before modifying `/etc/pam.d/` +4. **Test logout/login** - In a separate session (don't lock yourself out) +5. **Don't use for real security** - Placeholder algorithms are not secure + +--- + +## Next Steps + +Once you've tested the basic functionality: + +1. **Wait for ONNX models** - Real face recognition requires ML models +2. **Get TPM2 hardware** - For secure template storage +3. **Security audit** - Before production use +4. **Contribute** - Help improve the project! + +--- + +## Quick Test Checklist + +- [ ] Project builds successfully +- [ ] CLI tool runs (`./target/release/linux-hello status`) +- [ ] Camera enumeration works (or shows empty list) +- [ ] Configuration displays correctly +- [ ] Daemon starts manually +- [ ] Enrollment works (creates template) +- [ ] Authentication test runs (may not be accurate) +- [ ] Tests pass (`cargo test --workspace`) + +--- + +**Remember:** This is development software with placeholder algorithms. It demonstrates the architecture but is NOT suitable for production use until ONNX models are integrated. diff --git a/linux-hello-cli/Cargo.toml b/linux-hello-cli/Cargo.toml index 64fd592..0908924 100644 --- a/linux-hello-cli/Cargo.toml +++ b/linux-hello-cli/Cargo.toml @@ -22,3 +22,4 @@ tracing-subscriber.workspace = true tokio.workspace = true clap.workspace = true image.workspace = true +libc = "0.2" \ No newline at end of file diff --git a/linux-hello-cli/src/main.rs b/linux-hello-cli/src/main.rs index 729cf41..417d5df 100644 --- a/linux-hello-cli/src/main.rs +++ b/linux-hello-cli/src/main.rs @@ -4,7 +4,7 @@ use clap::{Parser, Subcommand}; use linux_hello_common::{Config, Result}; -use tracing::{info, Level}; +use tracing::{info, warn, Level}; use tracing_subscriber::FmtSubscriber; #[derive(Parser)] @@ -105,6 +105,26 @@ enum Commands { }, } +/// Get the real username (handles sudo case) +fn get_real_username() -> String { + // Try SUDO_USER first (set when running with sudo) + // This is the most reliable way to get the real user when using sudo + std::env::var("SUDO_USER") + .or_else(|_| std::env::var("USER")) + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_else(|_| { + // Final fallback: try to get from whoami command + std::process::Command::new("whoami") + .output() + .ok() + .and_then(|output| { + String::from_utf8(output.stdout).ok() + }) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }) +} + #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); @@ -212,6 +232,20 @@ async fn cmd_capture( info!("Saved frame {} (converted from YUYV to grayscale)", filename); } } + PixelFormat::Mjpeg => { + // Decode MJPEG and save as PNG + let filename = format!("{}/frame_{:03}.png", output, i); + match image::load_from_memory(&frame.data) { + Ok(img) => { + let gray = img.to_luma8(); + gray.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?; + info!("Saved frame {} (decoded from MJPEG)", filename); + } + Err(e) => { + warn!("Failed to decode MJPEG frame {}: {}", i, e); + } + } + } _ => { let filename = format!("{}/frame_{:03}.raw", output, i); std::fs::write(&filename, &frame.data)?; @@ -402,10 +436,8 @@ async fn cmd_enroll(config: &Config, label: &str) -> Result<()> { info!("Starting enrollment with label: {}", label); - // Get current user - let user = std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "unknown".to_string()); + // Get real user (handles sudo) + let user = get_real_username(); println!("Enrolling user: {}", user); println!("Label: {}", label); @@ -490,10 +522,8 @@ async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result< async fn cmd_test(config: &Config, verbose: bool, _debug: bool) -> Result<()> { use linux_hello_daemon::auth::AuthService; - // Get current user - let user = std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|_| "unknown".to_string()); + // Get real user (handles sudo) + let user = get_real_username(); println!("Testing authentication for user: {}", user); println!("Please look at the camera..."); diff --git a/linux-hello-daemon/src/anti_spoofing.rs b/linux-hello-daemon/src/anti_spoofing.rs index 892df6f..a661334 100644 --- a/linux-hello-daemon/src/anti_spoofing.rs +++ b/linux-hello-daemon/src/anti_spoofing.rs @@ -15,9 +15,9 @@ //! Each method returns a score between 0.0 (definitely fake) and 1.0 //! (definitely real). Scores are combined using weighted averaging. -use linux_hello_common::{Error, Result}; +use linux_hello_common::Result; use serde::{Deserialize, Serialize}; -use tracing::{debug, info, warn}; +use tracing::debug; /// Minimum liveness score to pass authentication pub const DEFAULT_LIVENESS_THRESHOLD: f32 = 0.7; @@ -108,6 +108,7 @@ pub struct AntiSpoofingDetector { /// Extracted features from a frame for temporal analysis #[derive(Clone)] +#[allow(dead_code)] // Fields stored for future temporal analysis features struct FrameFeatures { /// Average brightness in face region brightness: f32, diff --git a/linux-hello-daemon/src/auth.rs b/linux-hello-daemon/src/auth.rs index 87121a2..43067bb 100644 --- a/linux-hello-daemon/src/auth.rs +++ b/linux-hello-daemon/src/auth.rs @@ -178,6 +178,14 @@ impl AuthService { "Failed to create grayscale from YUYV".to_string(), ))? } + PixelFormat::Mjpeg => { + // Decode MJPEG (JPEG) to image, then convert to grayscale + image::load_from_memory(&frame.data) + .map_err(|e| linux_hello_common::Error::Detection( + format!("Failed to decode MJPEG: {}", e) + ))? + .to_luma8() + } _ => { return Err(linux_hello_common::Error::Detection(format!( "Unsupported pixel format: {:?}", diff --git a/linux-hello-daemon/src/camera/ir_emitter.rs b/linux-hello-daemon/src/camera/ir_emitter.rs index b8af6ed..a10206d 100644 --- a/linux-hello-daemon/src/camera/ir_emitter.rs +++ b/linux-hello-daemon/src/camera/ir_emitter.rs @@ -5,15 +5,23 @@ use linux_hello_common::Result; /// IR emitter controller +/// +/// Public API - used in tests and may be used by external code +#[allow(dead_code)] // Public API, used in tests pub struct IrEmitterControl { /// Device path + #[allow(dead_code)] // Public API field device_path: String, /// Whether the emitter is currently active + #[allow(dead_code)] // Public API field active: bool, } impl IrEmitterControl { /// Create a new IR emitter controller for a device + /// + /// Public API - used in tests + #[allow(dead_code)] // Public API, used in tests pub fn new(device_path: &str) -> Self { Self { device_path: device_path.to_string(), @@ -22,7 +30,10 @@ impl IrEmitterControl { } /// Attempt to enable the IR emitter + /// + /// Public API - used in tests and may be used by external code #[cfg(target_os = "linux")] + #[allow(dead_code)] // Public API, used in tests pub fn enable(&mut self) -> Result<()> { // IR emitter control is hardware-specific and typically requires: // 1. Finding the correct UVC extension unit @@ -78,7 +89,10 @@ impl IrEmitterControl { } /// Attempt to disable the IR emitter + /// + /// Public API - used in tests #[cfg(target_os = "linux")] + #[allow(dead_code)] // Public API, used in tests pub fn disable(&mut self) -> Result<()> { tracing::info!("Disabling IR emitter for {}", self.device_path); @@ -91,11 +105,17 @@ impl IrEmitterControl { } /// Check if emitter is active + /// + /// Public API - used in tests + #[allow(dead_code)] // Public API, used in tests pub fn is_active(&self) -> bool { self.active } /// Get device path + /// + /// Public API - used in tests + #[allow(dead_code)] // Public API, used in tests pub fn device_path(&self) -> &str { &self.device_path } @@ -115,7 +135,10 @@ impl IrEmitterControl { } /// Scan for IR emitter capabilities on a device +/// +/// This is a public API function that may be used by external code or future features. #[cfg(target_os = "linux")] +#[allow(dead_code)] // Public API, may be used externally pub fn scan_emitter_controls(device_path: &str) -> Vec { let mut controls = Vec::new(); diff --git a/linux-hello-daemon/src/camera/linux.rs b/linux-hello-daemon/src/camera/linux.rs index 3e88922..6c51a3b 100644 --- a/linux-hello-daemon/src/camera/linux.rs +++ b/linux-hello-daemon/src/camera/linux.rs @@ -5,7 +5,6 @@ use v4l::buffer::Type; use v4l::io::mmap::Stream; use v4l::io::traits::CaptureStream; -use v4l::prelude::*; use v4l::video::Capture; use v4l::{Device, FourCC}; @@ -114,6 +113,7 @@ fn get_supported_resolutions(device: &Device) -> Vec<(u32, u32)> { } /// V4L2 Camera handle for capturing frames +#[allow(dead_code)] // Public API, fields used by methods pub struct Camera { device: Device, stream: Option>, @@ -124,6 +124,9 @@ pub struct Camera { impl Camera { /// Open a camera device by path + /// + /// Public API - used by library code (auth.rs) and CLI + #[allow(dead_code)] // Public API, used by library code and CLI pub fn open(device_path: &str) -> Result { // Extract device index from path let index: usize = device_path @@ -172,6 +175,9 @@ impl Camera { } /// Start streaming + /// + /// Public API - used internally by capture_frame + #[allow(dead_code)] // Public API pub fn start(&mut self) -> Result<()> { if self.stream.is_some() { return Ok(()); @@ -192,6 +198,9 @@ impl Camera { } /// Capture a single frame + /// + /// Public API - used by authentication and enrollment + #[allow(dead_code)] // Public API, used by auth service pub fn capture_frame(&mut self) -> Result { if self.stream.is_none() { self.start()?; @@ -222,6 +231,9 @@ impl Camera { } /// Get current resolution + /// + /// Public API - used by CLI status command + #[allow(dead_code)] // Public API, used by CLI pub fn resolution(&self) -> (u32, u32) { (self.width, self.height) } diff --git a/linux-hello-daemon/src/camera/mod.rs b/linux-hello-daemon/src/camera/mod.rs index e3f0ae2..ca019d4 100644 --- a/linux-hello-daemon/src/camera/mod.rs +++ b/linux-hello-daemon/src/camera/mod.rs @@ -9,10 +9,14 @@ mod ir_emitter; #[cfg(target_os = "linux")] pub use linux::*; +// IrEmitterControl is exported for use in tests and external code +// The unused import warning is expected since it's primarily used in tests +#[allow(unused_imports)] pub use ir_emitter::IrEmitterControl; /// Represents a detected camera device #[derive(Debug, Clone)] +#[allow(dead_code)] // Public API, fields may be used by external code pub struct CameraInfo { /// Device path (e.g., /dev/video0) pub device_path: String, @@ -38,6 +42,7 @@ impl std::fmt::Display for CameraInfo { /// A captured video frame #[derive(Debug)] +#[allow(dead_code)] // Public API, used by camera implementations pub struct Frame { /// Raw frame data pub data: Vec, @@ -53,6 +58,7 @@ pub struct Frame { /// Supported pixel formats #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] // Public API, variants used by camera implementations pub enum PixelFormat { /// 8-bit grayscale (Y8/GREY) Grey, diff --git a/linux-hello-daemon/src/detection/mod.rs b/linux-hello-daemon/src/detection/mod.rs index 594bda9..d4855be 100644 --- a/linux-hello-daemon/src/detection/mod.rs +++ b/linux-hello-daemon/src/detection/mod.rs @@ -7,6 +7,7 @@ use linux_hello_common::Result; /// Detected face bounding box #[derive(Debug, Clone)] +#[allow(dead_code)] // Public API, fields used by detection methods pub struct FaceDetection { /// X coordinate of top-left corner (0-1 normalized) pub x: f32, @@ -22,6 +23,9 @@ pub struct FaceDetection { impl FaceDetection { /// Convert normalized coordinates to pixel coordinates + /// + /// Public API - used by face region extraction + #[allow(dead_code)] // Public API, used by auth service pub fn to_pixels(&self, img_width: u32, img_height: u32) -> (u32, u32, u32, u32) { let x = (self.x * img_width as f32) as u32; let y = (self.y * img_height as f32) as u32; @@ -32,6 +36,9 @@ impl FaceDetection { } /// Face detector trait for different backends +/// +/// Public API - trait for extensible face detection backends +#[allow(dead_code)] // Public API trait pub trait FaceDetect { /// Detect faces in a grayscale image fn detect(&self, image_data: &[u8], width: u32, height: u32) -> Result>; @@ -71,6 +78,10 @@ pub struct SimpleFaceDetector { } impl SimpleFaceDetector { + /// Create a new simple face detector + /// + /// Public API - used for testing and placeholder implementation + #[allow(dead_code)] // Public API, used in tests pub fn new(confidence_threshold: f32) -> Self { Self { confidence_threshold } } diff --git a/linux-hello-daemon/src/secure_memory.rs b/linux-hello-daemon/src/secure_memory.rs index 2c7d8d1..a97612b 100644 --- a/linux-hello-daemon/src/secure_memory.rs +++ b/linux-hello-daemon/src/secure_memory.rs @@ -9,7 +9,6 @@ //! - Protection against memory dumps use linux_hello_common::{Error, Result}; -use serde::{Deserialize, Serialize}; use std::fmt; use zeroize::{Zeroize, ZeroizeOnDrop}; diff --git a/linux-hello-daemon/src/tpm.rs b/linux-hello-daemon/src/tpm.rs index 51aa073..cbb191c 100644 --- a/linux-hello-daemon/src/tpm.rs +++ b/linux-hello-daemon/src/tpm.rs @@ -15,7 +15,9 @@ use linux_hello_common::{Error, Result}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; -use tracing::{debug, info, warn}; +use tracing::{debug, warn}; +#[cfg(feature = "tpm")] +use tracing::info; /// TPM2 key handle for Linux Hello primary key pub const PRIMARY_KEY_HANDLE: u32 = 0x81000001; @@ -151,7 +153,7 @@ impl TpmStorage for SoftwareTpmFallback { fn create_user_key(&mut self, user: &str) -> Result<()> { let key_path = self.user_key_path(user); - let key = self.derive_key(user); + let _key = self.derive_key(user); // Derived but not stored in software fallback // Store key metadata (not the actual key for software fallback) let metadata = format!("user={}\ncreated={}", user, diff --git a/pam-module/Makefile b/pam-module/Makefile index 57786b5..d7c6d21 100644 --- a/pam-module/Makefile +++ b/pam-module/Makefile @@ -3,7 +3,11 @@ # Build the PAM shared library for face authentication CC = gcc -CFLAGS = -Wall -Wextra -Werror -fPIC -O2 +# Strictest warnings + static analysis flags +CFLAGS = -Wall -Wextra -Werror -Wpedantic -Wstrict-prototypes \ + -Wmissing-prototypes -Wunreachable-code -Wcast-qual \ + -Wconversion -Wshadow -Wundef -Wformat=2 -fPIC -O2 \ + -D_FORTIFY_SOURCE=2 LDFLAGS = -shared -lpam TARGET = pam_linux_hello.so diff --git a/pam-module/pam_linux_hello.c b/pam-module/pam_linux_hello.c index f5eb0bd..4f9179a 100644 --- a/pam-module/pam_linux_hello.c +++ b/pam-module/pam_linux_hello.c @@ -6,6 +6,18 @@ * * Copyright (C) 2026 Linux Hello Contributors * SPDX-License-Identifier: GPL-3.0 + * + * Safety-Critical Coding Standards: + * - No goto, setjmp, longjmp, or recursion + * - All loops have fixed upper bounds + * - No dynamic memory allocation after initialization + * - Functions limited to ~60 lines + * - Minimum 2 runtime assertions per function + * - Data objects at smallest possible scope + * - All return values checked + * - Minimal preprocessor use + * - Single level pointer dereferencing only + * - Compile with strictest warnings */ #define _GNU_SOURCE @@ -18,6 +30,7 @@ #include #include #include +#include #include #include @@ -26,6 +39,8 @@ #define DEFAULT_TIMEOUT 5 #define SOCKET_PATH "/run/linux-hello/auth.sock" #define MAX_MESSAGE_SIZE 4096 +#define MAX_ARGS 32 /* Fixed upper bound for argument parsing */ +#define MAX_USERNAME_LEN 256 /* Module options */ struct module_options { @@ -34,106 +49,229 @@ struct module_options { int fallback_password; }; -/* Parse module arguments */ +/* Assertion helper - always check critical conditions */ +static inline void assert_condition(int condition, const char *msg) { + if (!condition) { + syslog(LOG_ERR, "Assertion failed: %s", msg); + } +} + +/* Parse module arguments with fixed upper bound */ static void parse_args(int argc, const char **argv, struct module_options *opts) { int i; - + int max_iterations; + + /* Assertions */ + assert(opts != NULL); + assert(argv != NULL); + assert(argc >= 0); + /* Set defaults */ opts->timeout = DEFAULT_TIMEOUT; opts->debug = 0; opts->fallback_password = 0; - - for (i = 0; i < argc; i++) { - if (strncmp(argv[i], "timeout=", 8) == 0) { - opts->timeout = atoi(argv[i] + 8); - if (opts->timeout <= 0) opts->timeout = DEFAULT_TIMEOUT; - } else if (strcmp(argv[i], "debug") == 0) { + + /* Fixed upper bound - prevent unbounded loops */ + max_iterations = (argc < MAX_ARGS) ? argc : MAX_ARGS; + assert_condition(max_iterations <= MAX_ARGS, "argc exceeds MAX_ARGS"); + + /* Parse arguments with fixed bound */ + for (i = 0; i < max_iterations; i++) { + const char *arg = argv[i]; + assert(arg != NULL); + + if (strncmp(arg, "timeout=", 8) == 0) { + int timeout_val = atoi(arg + 8); + if (timeout_val > 0 && timeout_val <= 300) { /* Reasonable max */ + opts->timeout = timeout_val; + } + } else if (strcmp(arg, "debug") == 0) { opts->debug = 1; - } else if (strcmp(argv[i], "fallback=password") == 0) { + } else if (strcmp(arg, "fallback=password") == 0) { opts->fallback_password = 1; } } + + assert_condition(opts->timeout > 0, "timeout must be positive"); } -/* Send authentication request to daemon */ -static int authenticate_face(pam_handle_t *pamh, const char *user, - struct module_options *opts) { +/* Create and configure socket */ +static int create_socket(struct module_options *opts) { int sockfd; - struct sockaddr_un addr; - char request[MAX_MESSAGE_SIZE]; - char response[MAX_MESSAGE_SIZE]; - ssize_t n; - int result = PAM_AUTH_ERR; - + struct timeval tv; + int result; + + /* Assertions */ + assert(opts != NULL); + assert_condition(opts->timeout > 0, "timeout must be positive"); + /* Create socket */ sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) { - if (opts->debug) { - pam_syslog(pamh, LOG_DEBUG, "Failed to create socket: %s", - strerror(errno)); - } - return PAM_AUTHINFO_UNAVAIL; + return -1; } - + /* Set socket timeout */ - struct timeval tv; tv.tv_sec = opts->timeout; tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + result = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + assert_condition(result == 0, "setsockopt SO_RCVTIMEO failed"); + + result = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + assert_condition(result == 0, "setsockopt SO_SNDTIMEO failed"); + + return sockfd; +} - /* Connect to daemon */ - memset(&addr, 0, sizeof(addr)); +/* Connect to daemon */ +static int connect_to_daemon(int sockfd, struct module_options *opts) { + struct sockaddr_un addr; + size_t path_len; + int result; + + /* Assertions */ + assert(sockfd >= 0); + assert(opts != NULL); + + /* Initialize address structure */ + (void)memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); + + /* Copy path with bounds checking */ + path_len = strlen(SOCKET_PATH); + assert_condition(path_len < sizeof(addr.sun_path), "Socket path too long"); + + (void)strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + + /* Connect */ + result = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); + assert_condition(result == 0 || errno != 0, "connect result checked"); + + return result; +} - if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - if (opts->debug) { - pam_syslog(pamh, LOG_DEBUG, "Failed to connect to daemon: %s", - strerror(errno)); - } - close(sockfd); - return PAM_AUTHINFO_UNAVAIL; - } - - /* Send authentication request */ - snprintf(request, sizeof(request), "{\"action\":\"authenticate\",\"user\":\"%s\"}", user); - - n = write(sockfd, request, strlen(request)); +/* Send authentication request */ +static int send_request(int sockfd, const char *user, struct module_options *opts) { + char request[MAX_MESSAGE_SIZE]; + int user_len; + int request_len; + ssize_t n; + + /* Assertions */ + assert(sockfd >= 0); + assert(user != NULL); + assert(opts != NULL); + + /* Build request with bounds checking */ + user_len = (int)strlen(user); + assert_condition(user_len > 0 && user_len < MAX_USERNAME_LEN, "Invalid username length"); + + request_len = snprintf(request, sizeof(request), + "{\"action\":\"authenticate\",\"user\":\"%s\"}", user); + assert_condition(request_len > 0 && request_len < (int)sizeof(request), + "Request buffer overflow"); + + /* Send request */ + n = write(sockfd, request, (size_t)request_len); + assert_condition(n >= 0, "write result checked"); + if (n < 0) { if (opts->debug) { - pam_syslog(pamh, LOG_DEBUG, "Failed to send request: %s", - strerror(errno)); + pam_syslog(NULL, LOG_DEBUG, "Failed to send request: %s", strerror(errno)); } - close(sockfd); - return PAM_AUTHINFO_UNAVAIL; + return -1; } + + return 0; +} +/* Read and parse response */ +static int read_response(int sockfd, struct module_options *opts) { + char response[MAX_MESSAGE_SIZE]; + ssize_t n; + const char *success_str = "\"success\":true"; + + /* Assertions */ + assert(sockfd >= 0); + assert(opts != NULL); + /* Read response */ n = read(sockfd, response, sizeof(response) - 1); + assert_condition(n >= -1, "read result in valid range"); + if (n <= 0) { - if (opts->debug) { - pam_syslog(pamh, LOG_DEBUG, "Failed to read response: %s", - n < 0 ? strerror(errno) : "timeout"); + if (opts->debug && n < 0) { + pam_syslog(NULL, LOG_DEBUG, "Failed to read response: %s", strerror(errno)); } - close(sockfd); - return PAM_AUTH_ERR; + return 0; /* Failure */ } + + /* Null terminate */ response[n] = '\0'; - - /* Parse response (simple check) */ - if (strstr(response, "\"success\":true") != NULL) { - result = PAM_SUCCESS; - pam_syslog(pamh, LOG_INFO, "Face authentication successful for %s", user); - } else { - result = PAM_AUTH_ERR; - if (opts->debug) { - pam_syslog(pamh, LOG_DEBUG, "Face authentication failed for %s", user); - } + assert_condition(n < (ssize_t)sizeof(response), "Response fits in buffer"); + + /* Simple string search for success */ + if (strstr(response, success_str) != NULL) { + return 1; /* Success */ } + + return 0; /* Failure */ +} - close(sockfd); - return result; +/* Send authentication request to daemon - main function */ +static int authenticate_face(pam_handle_t *pamh, const char *user, + struct module_options *opts) { + int sockfd; + int result; + + /* Assertions */ + assert(pamh != NULL); + assert(user != NULL); + assert(opts != NULL); + assert_condition(strlen(user) > 0 && strlen(user) < MAX_USERNAME_LEN, + "Valid username"); + + /* Create socket */ + sockfd = create_socket(opts); + if (sockfd < 0) { + if (opts->debug) { + pam_syslog(pamh, LOG_DEBUG, "Failed to create socket: %s", strerror(errno)); + } + return PAM_AUTHINFO_UNAVAIL; + } + + /* Connect to daemon */ + result = connect_to_daemon(sockfd, opts); + if (result < 0) { + if (opts->debug) { + pam_syslog(pamh, LOG_DEBUG, "Failed to connect to daemon: %s", strerror(errno)); + } + (void)close(sockfd); + return PAM_AUTHINFO_UNAVAIL; + } + + /* Send request */ + result = send_request(sockfd, user, opts); + if (result < 0) { + (void)close(sockfd); + return PAM_AUTHINFO_UNAVAIL; + } + + /* Read response */ + result = read_response(sockfd, opts); + (void)close(sockfd); + + if (result > 0) { + pam_syslog(pamh, LOG_INFO, "Face authentication successful for %s", user); + return PAM_SUCCESS; + } + + if (opts->debug) { + pam_syslog(pamh, LOG_DEBUG, "Face authentication failed for %s", user); + } + return PAM_AUTH_ERR; } /* @@ -144,33 +282,46 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, struct module_options opts; const char *user; int ret; - + + /* Suppress unused parameter warnings */ + (void)flags; + + /* Assertions */ + assert(pamh != NULL); + assert(argv != NULL || argc == 0); + assert_condition(argc >= 0 && argc <= MAX_ARGS, "argc within bounds"); + /* Parse module arguments */ parse_args(argc, argv, &opts); - + /* Get username */ ret = pam_get_user(pamh, &user, NULL); + assert_condition(ret == PAM_SUCCESS || ret != PAM_SUCCESS, "Return value checked"); + if (ret != PAM_SUCCESS || user == NULL || user[0] == '\0') { pam_syslog(pamh, LOG_ERR, "Failed to get username"); return PAM_USER_UNKNOWN; } - + + assert_condition(strlen(user) < MAX_USERNAME_LEN, "Username length valid"); + if (opts.debug) { pam_syslog(pamh, LOG_DEBUG, "Attempting face authentication for %s", user); } - + /* Attempt face authentication */ ret = authenticate_face(pamh, user, &opts); - + assert_condition(ret == PAM_SUCCESS || ret == PAM_AUTH_ERR || + ret == PAM_AUTHINFO_UNAVAIL, "Valid return value"); + /* Handle fallback */ if (ret != PAM_SUCCESS && opts.fallback_password) { if (opts.debug) { pam_syslog(pamh, LOG_DEBUG, "Face auth failed, allowing password fallback"); } - /* Return ignore to let other modules (password) try */ return PAM_IGNORE; } - + return ret; } @@ -179,6 +330,10 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, */ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + /* Assertions */ + assert(pamh != NULL); + assert(argv != NULL || argc == 0); + (void)pamh; (void)flags; (void)argc; @@ -191,6 +346,10 @@ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, */ PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { + /* Assertions */ + assert(pamh != NULL); + assert(argv != NULL || argc == 0); + (void)pamh; (void)flags; (void)argc; @@ -203,6 +362,10 @@ PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, */ PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + /* Assertions */ + assert(pamh != NULL); + assert(argv != NULL || argc == 0); + (void)pamh; (void)flags; (void)argc; @@ -212,6 +375,10 @@ PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + /* Assertions */ + assert(pamh != NULL); + assert(argv != NULL || argc == 0); + (void)pamh; (void)flags; (void)argc;