use anyhow::{Context, Result, anyhow};
use base64::Engine;
use chrono::{DateTime, Utc};
use std::fs;
use std::io::Cursor;
use std::path::Path;
use tracing::{debug, info, warn};
pub use zipsign_api::ZipsignError;
pub fn get_embedded_public_key() -> &'static str {
"1uLjooBMO+HlpKeiD16WOtT3COWeC8J/o2ERmDiEMc4="
}
#[derive(Debug, Clone)]
pub struct KeyMetadata {
pub key_id: String,
pub valid_from: DateTime<Utc>,
pub valid_until: Option<DateTime<Utc>>,
pub public_key: String,
}
pub fn get_active_key_metadata() -> KeyMetadata {
KeyMetadata {
key_id: "terraphim-release-key-2025-01".to_string(),
valid_from: "2025-01-12T00:00:00Z".parse().unwrap(),
valid_until: None, public_key: get_embedded_public_key().to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationResult {
Valid,
Invalid { reason: String },
MissingSignature,
Error(String),
}
pub fn verify_archive_signature(
archive_path: &Path,
public_key: Option<&str>,
) -> Result<VerificationResult> {
info!("Starting signature verification for {:?}", archive_path);
if !archive_path.exists() {
return Err(anyhow!("Archive file not found: {:?}", archive_path));
}
let key_str = match public_key {
Some(k) => k,
None => get_embedded_public_key(),
};
if key_str.starts_with("TODO:") {
return Err(anyhow!(
"Placeholder public key detected. Signature verification cannot be bypassed. \
Configure a real Ed25519 public key in get_embedded_public_key()."
));
}
let archive_bytes = fs::read(archive_path).context("Failed to read archive file")?;
let key_bytes = base64::engine::general_purpose::STANDARD
.decode(key_str)
.context("Failed to decode public key base64")?;
if key_bytes.len() != 32 {
return Ok(VerificationResult::Invalid {
reason: format!(
"Invalid public key length: {} bytes (expected 32)",
key_bytes.len()
),
});
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&key_bytes);
let verifying_key = zipsign_api::verify::collect_keys(std::iter::once(Ok(key_array)))
.context("Failed to parse public key")?;
let context: Option<Vec<u8>> = archive_path
.file_name()
.map(|n| n.to_string_lossy().as_bytes().to_vec());
let mut cursor = Cursor::new(archive_bytes);
let context_ref: Option<&[u8]> = context.as_deref();
match zipsign_api::verify::verify_tar(&mut cursor, &verifying_key, context_ref) {
Ok(_index) => {
info!("Signature verification passed for {:?}", archive_path);
Ok(VerificationResult::Valid)
}
Err(e) => {
warn!("Signature verification failed: {}", e);
Ok(VerificationResult::Invalid {
reason: format!("Signature verification failed: {}", e),
})
}
}
}
pub fn verify_with_self_update(
_release_name: &str,
_version: &str,
archive_path: &Path,
public_key: Option<&str>,
) -> Result<VerificationResult> {
info!(
"Verifying signature for {} v{} at {:?}",
_release_name, _version, archive_path
);
if !archive_path.exists() {
return Err(anyhow!("Archive file not found: {:?}", archive_path));
}
verify_archive_signature(archive_path, public_key)
}
pub fn verify_signature_detailed(
archive_path: &Path,
public_key: Option<&str>,
) -> Result<VerificationResult> {
info!("Starting detailed signature verification");
if !archive_path.exists() {
return Ok(VerificationResult::Error(format!(
"Archive file not found: {:?}",
archive_path
)));
}
debug!("Verifying archive {:?}", archive_path);
verify_archive_signature(archive_path, public_key)
}
pub fn is_verification_available() -> bool {
true
}
pub fn get_signature_filename(binary_name: &str) -> String {
format!("{}.sig", binary_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_key_rejects_unsigned_file() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let result = verify_archive_signature(temp_file.path(), None).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_nonexistent_file_returns_error() {
let result = verify_archive_signature(Path::new("/nonexistent/file.tar.gz"), None);
assert!(result.is_err());
}
#[test]
fn test_invalid_base64_key_returns_error() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let result = verify_archive_signature(temp_file.path(), Some("not-valid-base64!!!"));
assert!(result.is_err());
}
#[test]
fn test_wrong_length_key_returns_invalid() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let result = verify_archive_signature(temp_file.path(), Some("VGVzdGluZw==")).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_is_verification_available() {
let available = is_verification_available();
assert!(available);
}
#[test]
fn test_get_signature_filename() {
assert_eq!(get_signature_filename("terraphim"), "terraphim.sig");
assert_eq!(get_signature_filename("test"), "test.sig");
assert_eq!(get_signature_filename("my-binary"), "my-binary.sig");
}
#[test]
fn test_verification_result_equality() {
let valid1 = VerificationResult::Valid;
let valid2 = VerificationResult::Valid;
assert_eq!(valid1, valid2);
let invalid1 = VerificationResult::Invalid {
reason: "test".to_string(),
};
let invalid2 = VerificationResult::Invalid {
reason: "test".to_string(),
};
assert_eq!(invalid1, invalid2);
let missing1 = VerificationResult::MissingSignature;
let missing2 = VerificationResult::MissingSignature;
assert_eq!(missing1, missing2);
assert_ne!(valid1, missing1);
assert_ne!(invalid1, missing1);
}
#[test]
fn test_verification_result_display() {
let valid = VerificationResult::Valid;
let missing = VerificationResult::MissingSignature;
let invalid = VerificationResult::Invalid {
reason: "test error".to_string(),
};
let error = VerificationResult::Error("test error".to_string());
assert_eq!(format!("{:?}", valid), "Valid");
assert_eq!(format!("{:?}", missing), "MissingSignature");
assert_eq!(
format!("{:?}", invalid),
"Invalid { reason: \"test error\" }"
);
assert_eq!(format!("{:?}", error), "Error(\"test error\")");
}
#[test]
fn test_verify_signature_detailed_with_real_key() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let result = verify_signature_detailed(temp_file.path(), None).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_verify_signature_detailed_nonexistent() {
let result =
verify_signature_detailed(Path::new("/nonexistent/file.tar.gz"), None).unwrap();
assert!(matches!(result, VerificationResult::Error(_)));
}
#[test]
fn test_verify_with_self_update() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let test_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
let result =
verify_with_self_update("terraphim", "1.0.0", temp_file.path(), Some(test_key))
.unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_verify_with_self_update_missing_binary() {
let result = verify_with_self_update(
"terraphim",
"1.0.0",
Path::new("/nonexistent/binary"),
Some("test-key"),
);
assert!(result.is_err());
}
}