use std::path::Path; use std::process::Command; /// Verification status for an AppImage. #[derive(Debug, Clone)] pub enum VerificationStatus { /// Has a valid embedded GPG signature. SignedValid { signer: String }, /// Has an embedded signature that could not be verified. SignedInvalid { reason: String }, /// No embedded signature found. Unsigned, /// SHA256 checksum matches the expected value. ChecksumMatch, /// SHA256 checksum does not match. ChecksumMismatch { expected: String, actual: String }, /// Not checked yet. NotChecked, } impl VerificationStatus { pub fn label(&self) -> String { match self { Self::SignedValid { signer } => format!("Signed: {}", signer), Self::SignedInvalid { reason } => format!("Invalid signature: {}", reason), Self::Unsigned => "Unsigned".to_string(), Self::ChecksumMatch => "Checksum OK".to_string(), Self::ChecksumMismatch { expected, actual } => { format!("Mismatch: expected {}... got {}...", &expected[..expected.len().min(12)], &actual[..actual.len().min(12)]) } Self::NotChecked => "Not checked".to_string(), } } pub fn badge_class(&self) -> &'static str { match self { Self::SignedValid { .. } | Self::ChecksumMatch => "success", Self::SignedInvalid { .. } | Self::ChecksumMismatch { .. } => "error", Self::Unsigned | Self::NotChecked => "neutral", } } pub fn as_str(&self) -> &'static str { match self { Self::SignedValid { .. } => "signed_valid", Self::SignedInvalid { .. } => "signed_invalid", Self::Unsigned => "unsigned", Self::ChecksumMatch => "checksum_match", Self::ChecksumMismatch { .. } => "checksum_mismatch", Self::NotChecked => "not_checked", } } } /// Check for an embedded GPG signature in the AppImage. /// Uses the `--appimage-signature` flag (Type 2 only). pub fn check_embedded_signature(path: &Path) -> VerificationStatus { // Try to validate with gpg if --appimage-signature outputs something let output = Command::new(path) .arg("--appimage-signature") .env("APPIMAGE_EXTRACT_AND_RUN", "1") .output(); match output { Ok(out) if out.status.success() => { let stdout = String::from_utf8_lossy(&out.stdout); let trimmed = stdout.trim(); if trimmed.is_empty() { return VerificationStatus::Unsigned; } // Try to verify with gpg let gpg_result = Command::new("gpg") .arg("--verify") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output(); match gpg_result { Ok(gpg_out) => { let stderr = String::from_utf8_lossy(&gpg_out.stderr); if gpg_out.status.success() { // Extract signer info from gpg output let signer = stderr .lines() .find(|l| l.contains("Good signature")) .map(|l| l.trim().to_string()) .unwrap_or_else(|| "Valid signature".to_string()); VerificationStatus::SignedValid { signer } } else { VerificationStatus::SignedInvalid { reason: stderr.lines().next().unwrap_or("Unknown error").to_string(), } } } Err(_) => { // gpg not available but signature data exists VerificationStatus::SignedInvalid { reason: "gpg not available for verification".to_string(), } } } } _ => VerificationStatus::Unsigned, } } /// Compute the SHA256 hash of a file. pub fn compute_sha256(path: &Path) -> Result { use sha2::{Sha256, Digest}; use std::io::Read; let mut file = std::fs::File::open(path) .map_err(|e| format!("Failed to open file: {}", e))?; let mut hasher = Sha256::new(); let mut buf = vec![0u8; 64 * 1024]; loop { let n = file.read(&mut buf) .map_err(|e| format!("Read error: {}", e))?; if n == 0 { break; } hasher.update(&buf[..n]); } let hash = hasher.finalize(); Ok(format!("{:x}", hash)) } /// Verify a file against an expected SHA256 hash. pub fn verify_sha256(path: &Path, expected: &str) -> VerificationStatus { match compute_sha256(path) { Ok(hash) if hash == expected.to_lowercase() => VerificationStatus::ChecksumMatch, Ok(hash) => VerificationStatus::ChecksumMismatch { expected: expected.to_lowercase(), actual: hash, }, Err(_) => VerificationStatus::NotChecked, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_verification_status_labels() { assert_eq!(VerificationStatus::Unsigned.label(), "Unsigned"); assert_eq!(VerificationStatus::NotChecked.label(), "Not checked"); assert_eq!(VerificationStatus::ChecksumMatch.label(), "Checksum OK"); assert!(VerificationStatus::SignedValid { signer: "test".into() } .label().contains("test")); assert!(VerificationStatus::ChecksumMismatch { expected: "abc123456789".into(), actual: "def987654321".into(), }.label().contains("Mismatch")); } #[test] fn test_verification_status_badge_classes() { assert_eq!(VerificationStatus::ChecksumMatch.badge_class(), "success"); assert_eq!(VerificationStatus::Unsigned.badge_class(), "neutral"); } }