Fixed warnings, lots of stubs in the code, will be implemented later.

This commit is contained in:
2026-01-03 08:15:51 +01:00
parent a3fefa4179
commit 2f6b16d946
16 changed files with 917 additions and 88 deletions

164
CODING_STANDARDS.md Normal file
View File

@@ -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<T, E>` and `Option<T>` - 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<Data> { ... } // 25 lines
fn process_data(data: Data) -> Result<Output> { ... } // 30 lines
```
## References
- MISRA C:2012 Guidelines
- CERT C Coding Standard
- Rust API Guidelines
- Safety-Critical Software Development

1
Cargo.lock generated
View File

@@ -510,6 +510,7 @@ version = "0.1.0"
dependencies = [
"clap",
"image",
"libc",
"linux-hello-common",
"linux-hello-daemon",
"serde",

View File

@@ -1,5 +1,5 @@
<div align="center">
<img src="https://your-image-host.com/logo.png" alt="Linux Hello Logo" width="200">
<img src="public/logo.png" alt="Linux Hello Logo" width="200">
</div>
# 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

398
TESTING.md Normal file
View File

@@ -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.

View File

@@ -22,3 +22,4 @@ tracing-subscriber.workspace = true
tokio.workspace = true
clap.workspace = true
image.workspace = true
libc = "0.2"

View File

@@ -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...");

View File

@@ -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,

View File

@@ -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: {:?}",

View File

@@ -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<String> {
let mut controls = Vec::new();

View File

@@ -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<Stream<'static>>,
@@ -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<Self> {
// 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<Frame> {
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)
}

View File

@@ -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<u8>,
@@ -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,

View File

@@ -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<Vec<FaceDetection>>;
@@ -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 }
}

View File

@@ -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};

View File

@@ -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,

View File

@@ -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

View File

@@ -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 <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <assert.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
@@ -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;