grabbit 2c90276e3e fix: resolve cross-compilation issues for ARM64 Linux
- 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>
2025-08-14 00:27:46 +08:00

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());
}
}