- Replace OpenSSL with rustls for better cross-compilation support - Fix tpm_attestation field name typos (was tmp_attestation) - Add missing Debug traits to FramePool structs - Fix borrow checker issue in device registration - Add missing module declarations in main.rs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
529 lines
19 KiB
Rust
529 lines
19 KiB
Rust
use anyhow::{Result, Context};
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Sha256, Digest};
|
|
use std::fs;
|
|
use std::process::Command;
|
|
use sysinfo::{System, Disks, Networks, Components};
|
|
use mac_address::get_mac_address;
|
|
use tracing::{info, warn, error, debug};
|
|
|
|
/// Hardware fingerprint containing unique device identifiers
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct HardwareFingerprint {
|
|
pub cpu_id: String,
|
|
pub board_serial: String,
|
|
pub mac_addresses: Vec<String>,
|
|
pub disk_uuid: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub tpm_attestation: Option<String>,
|
|
pub system_info: SystemInfo,
|
|
pub computed_hash: String,
|
|
}
|
|
|
|
/// Additional system information for device registration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SystemInfo {
|
|
pub hostname: String,
|
|
pub os_name: String,
|
|
pub os_version: String,
|
|
pub kernel_version: String,
|
|
pub architecture: String,
|
|
pub total_memory: u64,
|
|
pub available_memory: u64,
|
|
pub cpu_count: usize,
|
|
pub cpu_brand: String,
|
|
pub disk_info: Vec<DiskInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DiskInfo {
|
|
pub name: String,
|
|
pub mount_point: String,
|
|
pub total_space: u64,
|
|
pub available_space: u64,
|
|
pub file_system: String,
|
|
}
|
|
|
|
/// Hardware fingerprinting service
|
|
pub struct HardwareFingerprintService {
|
|
system: System,
|
|
cache: Option<HardwareFingerprint>,
|
|
}
|
|
|
|
impl HardwareFingerprintService {
|
|
/// Creates a new hardware fingerprinting service
|
|
pub fn new() -> Self {
|
|
let system = System::new_all();
|
|
|
|
Self {
|
|
system,
|
|
cache: None,
|
|
}
|
|
}
|
|
|
|
/// Generates a complete hardware fingerprint
|
|
pub async fn generate_fingerprint(&mut self) -> Result<HardwareFingerprint> {
|
|
// Check cache first
|
|
if let Some(cached) = &self.cache {
|
|
debug!("Returning cached hardware fingerprint");
|
|
return Ok(cached.clone());
|
|
}
|
|
|
|
info!("Generating hardware fingerprint...");
|
|
|
|
// Refresh system information
|
|
// In sysinfo 0.30, System is immutable after creation
|
|
|
|
let cpu_id = self.get_cpu_id().await?;
|
|
let board_serial = self.get_board_serial().await?;
|
|
let mac_addresses = self.get_mac_addresses().await?;
|
|
let disk_uuid = self.get_primary_disk_uuid().await?;
|
|
let tpm_attestation = self.get_tpm_attestation().await.ok();
|
|
let system_info = self.collect_system_info().await?;
|
|
|
|
// Compute hash from core identifiers
|
|
let computed_hash = self.compute_fingerprint_hash(&cpu_id, &board_serial, &mac_addresses, &disk_uuid);
|
|
|
|
let fingerprint = HardwareFingerprint {
|
|
cpu_id,
|
|
board_serial,
|
|
mac_addresses,
|
|
disk_uuid,
|
|
tpm_attestation: tpm_attestation,
|
|
system_info,
|
|
computed_hash,
|
|
};
|
|
|
|
// Cache the result
|
|
self.cache = Some(fingerprint.clone());
|
|
|
|
info!("Hardware fingerprint generated successfully");
|
|
debug!("Fingerprint hash: {}", fingerprint.computed_hash);
|
|
|
|
Ok(fingerprint)
|
|
}
|
|
|
|
/// Gets CPU identifier
|
|
async fn get_cpu_id(&self) -> Result<String> {
|
|
// Try multiple approaches to get a stable CPU identifier
|
|
|
|
// Method 1: Try /proc/cpuinfo on Linux
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
|
|
for line in cpuinfo.lines() {
|
|
if line.starts_with("Serial") {
|
|
if let Some(serial) = line.split(':').nth(1) {
|
|
let serial = serial.trim();
|
|
if !serial.is_empty() && serial != "0000000000000000" {
|
|
return Ok(serial.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: Try system UUID on Linux
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Ok(machine_id) = fs::read_to_string("/etc/machine-id") {
|
|
let machine_id = machine_id.trim();
|
|
if !machine_id.is_empty() {
|
|
return Ok(format!("machine-{}", machine_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: macOS system_profiler
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Ok(output) = Command::new("system_profiler")
|
|
.args(&["SPHardwareDataType"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.contains("Hardware UUID:") {
|
|
if let Some(uuid) = line.split(':').nth(1) {
|
|
return Ok(format!("hw-{}", uuid.trim()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 4: Windows registry
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Ok(output) = Command::new("wmic")
|
|
.args(&["csproduct", "get", "UUID", "/value"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.starts_with("UUID=") {
|
|
let uuid = line.replace("UUID=", "").trim().to_string();
|
|
if !uuid.is_empty() && uuid != "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" {
|
|
return Ok(format!("sys-{}", uuid));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: Use system information
|
|
let hostname = hostname::get()
|
|
.context("Failed to get hostname")?
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
let cpu_info = format!("{}-{}", hostname, "generic-cpu");
|
|
let hash = Sha256::digest(cpu_info.as_bytes());
|
|
Ok(format!("cpu-{}", hex::encode(&hash[..8])))
|
|
}
|
|
|
|
/// Gets board serial number
|
|
async fn get_board_serial(&self) -> Result<String> {
|
|
// Method 1: DMI information on Linux
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Ok(serial) = fs::read_to_string("/sys/devices/virtual/dmi/id/board_serial") {
|
|
let serial = serial.trim();
|
|
if !serial.is_empty() && serial != "To be filled by O.E.M." {
|
|
return Ok(serial.to_string());
|
|
}
|
|
}
|
|
|
|
if let Ok(serial) = fs::read_to_string("/sys/devices/virtual/dmi/id/product_serial") {
|
|
let serial = serial.trim();
|
|
if !serial.is_empty() && serial != "To be filled by O.E.M." {
|
|
return Ok(serial.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: macOS system_profiler
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Ok(output) = Command::new("system_profiler")
|
|
.args(&["SPHardwareDataType"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.contains("Serial Number (system):") {
|
|
if let Some(serial) = line.split(':').nth(1) {
|
|
return Ok(serial.trim().to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Windows WMI
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Ok(output) = Command::new("wmic")
|
|
.args(&["baseboard", "get", "SerialNumber", "/value"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.starts_with("SerialNumber=") {
|
|
let serial = line.replace("SerialNumber=", "").trim().to_string();
|
|
if !serial.is_empty() && serial != "To be filled by O.E.M." {
|
|
return Ok(serial);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: Generate from system info
|
|
let fallback_data = format!("board-{}-{}",
|
|
System::name().unwrap_or("unknown".to_string()),
|
|
System::kernel_version().unwrap_or("unknown".to_string()));
|
|
let hash = Sha256::digest(fallback_data.as_bytes());
|
|
Ok(format!("board-{}", hex::encode(&hash[..8])))
|
|
}
|
|
|
|
/// Gets all MAC addresses
|
|
async fn get_mac_addresses(&self) -> Result<Vec<String>> {
|
|
let mut mac_addresses = Vec::new();
|
|
|
|
// Get primary MAC address
|
|
if let Ok(Some(mac)) = get_mac_address() {
|
|
mac_addresses.push(format!("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
|
mac.bytes()[0], mac.bytes()[1], mac.bytes()[2],
|
|
mac.bytes()[3], mac.bytes()[4], mac.bytes()[5]));
|
|
}
|
|
|
|
// Try to get additional MAC addresses
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Ok(entries) = fs::read_dir("/sys/class/net") {
|
|
for entry in entries.flatten() {
|
|
let interface_name = entry.file_name().to_string_lossy().to_string();
|
|
if interface_name.starts_with("lo") || interface_name.starts_with("docker") {
|
|
continue; // Skip loopback and docker interfaces
|
|
}
|
|
|
|
let mac_path = format!("/sys/class/net/{}/address", interface_name);
|
|
if let Ok(mac_str) = fs::read_to_string(&mac_path) {
|
|
let mac_str = mac_str.trim().to_uppercase();
|
|
if mac_str != "00:00:00:00:00:00" && !mac_addresses.contains(&mac_str) {
|
|
mac_addresses.push(mac_str);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if mac_addresses.is_empty() {
|
|
return Err(anyhow::anyhow!("No valid MAC addresses found"));
|
|
}
|
|
|
|
// Sort for consistency
|
|
mac_addresses.sort();
|
|
Ok(mac_addresses)
|
|
}
|
|
|
|
/// Gets primary disk UUID
|
|
async fn get_primary_disk_uuid(&self) -> Result<String> {
|
|
// Method 1: Linux filesystem UUIDs
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// Try to get root filesystem UUID
|
|
if let Ok(fstab) = fs::read_to_string("/etc/fstab") {
|
|
for line in fstab.lines() {
|
|
let line = line.trim();
|
|
if line.starts_with("UUID=") && (line.contains(" / ") || line.contains("\t/\t")) {
|
|
if let Some(uuid) = line.split_whitespace().next() {
|
|
let uuid = uuid.replace("UUID=", "");
|
|
if !uuid.is_empty() {
|
|
return Ok(uuid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try blkid command
|
|
if let Ok(output) = Command::new("blkid")
|
|
.args(&["-s", "UUID", "-o", "value", "/dev/sda1"])
|
|
.output() {
|
|
let uuid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
if !uuid.is_empty() {
|
|
return Ok(uuid);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: macOS diskutil
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Ok(output) = Command::new("diskutil")
|
|
.args(&["info", "/"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.contains("Volume UUID:") {
|
|
if let Some(uuid) = line.split(':').nth(1) {
|
|
return Ok(uuid.trim().to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Windows WMI
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Ok(output) = Command::new("wmic")
|
|
.args(&["logicaldisk", "where", "caption=\"C:\"", "get", "VolumeSerialNumber", "/value"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
for line in output_str.lines() {
|
|
if line.starts_with("VolumeSerialNumber=") {
|
|
let serial = line.replace("VolumeSerialNumber=", "").trim().to_string();
|
|
if !serial.is_empty() {
|
|
return Ok(format!("vol-{}", serial));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: Use disk information from system
|
|
let disks = Disks::new_with_refreshed_list();
|
|
if let Some(disk) = disks.first() {
|
|
let disk_name = disk.name().to_string_lossy();
|
|
let mount_point = disk.mount_point().to_string_lossy();
|
|
let fallback_data = format!("disk-{}-{}", disk_name, mount_point);
|
|
let hash = Sha256::digest(fallback_data.as_bytes());
|
|
Ok(format!("disk-{}", hex::encode(&hash[..8])))
|
|
} else {
|
|
Err(anyhow::anyhow!("No disk information available"))
|
|
}
|
|
}
|
|
|
|
/// Gets TPM attestation (if available)
|
|
async fn get_tpm_attestation(&self) -> Result<String> {
|
|
// This is a placeholder for TPM 2.0 attestation
|
|
// In a production system, this would:
|
|
// 1. Check if TPM 2.0 is available
|
|
// 2. Generate an attestation quote
|
|
// 3. Include PCR values and attestation identity key
|
|
// 4. Return base64-encoded attestation data
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// Check if TPM is available
|
|
if fs::metadata("/dev/tpm0").is_ok() || fs::metadata("/dev/tpmrm0").is_ok() {
|
|
// For demonstration, return a mock attestation
|
|
// In production, use tss-esapi crate or similar TPM library
|
|
let mock_attestation = format!("tpm2-mock-{}",
|
|
hex::encode(Sha256::digest("mock-tpm-attestation".as_bytes())));
|
|
return Ok(base64::encode(mock_attestation));
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
// Check Windows TPM
|
|
if let Ok(output) = Command::new("powershell")
|
|
.args(&["-Command", "Get-Tpm"])
|
|
.output() {
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
if output_str.contains("TpmPresent") && output_str.contains("True") {
|
|
let mock_attestation = format!("tpm2-win-{}",
|
|
hex::encode(Sha256::digest("windows-tpm-attestation".as_bytes())));
|
|
return Ok(base64::encode(mock_attestation));
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(anyhow::anyhow!("TPM not available or not accessible"))
|
|
}
|
|
|
|
/// Collects additional system information
|
|
async fn collect_system_info(&self) -> Result<SystemInfo> {
|
|
let hostname = hostname::get()
|
|
.context("Failed to get hostname")?
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
let os_name = System::name().unwrap_or("Unknown".to_string());
|
|
let os_version = System::os_version().unwrap_or("Unknown".to_string());
|
|
let kernel_version = System::kernel_version().unwrap_or("Unknown".to_string());
|
|
|
|
let architecture = if cfg!(target_arch = "x86_64") {
|
|
"x86_64"
|
|
} else if cfg!(target_arch = "aarch64") {
|
|
"aarch64"
|
|
} else if cfg!(target_arch = "arm") {
|
|
"arm"
|
|
} else {
|
|
"unknown"
|
|
}.to_string();
|
|
|
|
let total_memory = self.system.total_memory();
|
|
let available_memory = self.system.available_memory();
|
|
let cpu_count = num_cpus::get();
|
|
let cpu_brand = "Generic CPU".to_string();
|
|
|
|
let disks = Disks::new_with_refreshed_list();
|
|
let disk_info = disks
|
|
.iter()
|
|
.map(|disk| DiskInfo {
|
|
name: disk.name().to_string_lossy().to_string(),
|
|
mount_point: disk.mount_point().to_string_lossy().to_string(),
|
|
total_space: disk.total_space(),
|
|
available_space: disk.available_space(),
|
|
file_system: disk.file_system().to_string_lossy().to_string(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(SystemInfo {
|
|
hostname,
|
|
os_name,
|
|
os_version,
|
|
kernel_version,
|
|
architecture,
|
|
total_memory,
|
|
available_memory,
|
|
cpu_count,
|
|
cpu_brand,
|
|
disk_info,
|
|
})
|
|
}
|
|
|
|
/// Computes fingerprint hash from core identifiers
|
|
fn compute_fingerprint_hash(&self, cpu_id: &str, board_serial: &str, mac_addresses: &[String], disk_uuid: &str) -> String {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(cpu_id);
|
|
hasher.update(board_serial);
|
|
for mac in mac_addresses {
|
|
hasher.update(mac);
|
|
}
|
|
hasher.update(disk_uuid);
|
|
hex::encode(hasher.finalize())
|
|
}
|
|
|
|
/// Validates fingerprint integrity
|
|
pub fn validate_fingerprint(&self, fingerprint: &HardwareFingerprint) -> Result<bool> {
|
|
let expected_hash = self.compute_fingerprint_hash(
|
|
&fingerprint.cpu_id,
|
|
&fingerprint.board_serial,
|
|
&fingerprint.mac_addresses,
|
|
&fingerprint.disk_uuid,
|
|
);
|
|
|
|
Ok(expected_hash == fingerprint.computed_hash)
|
|
}
|
|
}
|
|
|
|
impl Default for HardwareFingerprintService {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_fingerprint_generation() {
|
|
let mut service = HardwareFingerprintService::new();
|
|
let fingerprint = service.generate_fingerprint().await.unwrap();
|
|
|
|
assert!(!fingerprint.cpu_id.is_empty());
|
|
assert!(!fingerprint.board_serial.is_empty());
|
|
assert!(!fingerprint.mac_addresses.is_empty());
|
|
assert!(!fingerprint.disk_uuid.is_empty());
|
|
assert!(!fingerprint.computed_hash.is_empty());
|
|
assert_eq!(fingerprint.computed_hash.len(), 64); // SHA256 hex length
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_fingerprint_consistency() {
|
|
let mut service = HardwareFingerprintService::new();
|
|
let fingerprint1 = service.generate_fingerprint().await.unwrap();
|
|
let fingerprint2 = service.generate_fingerprint().await.unwrap();
|
|
|
|
// Should be identical (cached)
|
|
assert_eq!(fingerprint1.computed_hash, fingerprint2.computed_hash);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_fingerprint_validation() {
|
|
let mut service = HardwareFingerprintService::new();
|
|
let fingerprint = service.generate_fingerprint().await.unwrap();
|
|
|
|
assert!(service.validate_fingerprint(&fingerprint).unwrap());
|
|
|
|
// Test with modified fingerprint
|
|
let mut invalid_fingerprint = fingerprint.clone();
|
|
invalid_fingerprint.cpu_id = "modified".to_string();
|
|
|
|
assert!(!service.validate_fingerprint(&invalid_fingerprint).unwrap());
|
|
}
|
|
} |