use std::path::Path;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use crate::error::LoadError;
pub fn sig_path_for(path: &Path) -> std::path::PathBuf {
path.with_extension(format!(
"{}.sig",
path.extension().and_then(|e| e.to_str()).unwrap_or("")
))
}
pub fn verify_signature(dylib_path: &Path, trusted_keys: &[VerifyingKey]) -> Result<(), LoadError> {
let path_str = dylib_path.display().to_string();
let sig_path = sig_path_for(dylib_path);
let sig_bytes = std::fs::read(&sig_path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
LoadError::SignatureRequired {
path: path_str.clone(),
}
} else {
LoadError::Io(e)
}
})?;
let signature = Signature::from_slice(&sig_bytes).map_err(|_| LoadError::SignatureInvalid {
path: path_str.clone(),
})?;
let dylib_bytes = std::fs::read(dylib_path)?;
for key in trusted_keys {
if key.verify(&dylib_bytes, &signature).is_ok() {
return Ok(());
}
}
Err(LoadError::SignatureInvalid { path: path_str })
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use std::io::Write;
use tempfile::NamedTempFile;
fn create_test_file(content: &[u8]) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(content).unwrap();
f
}
fn sign_file(path: &Path, signing_key: &SigningKey) {
let content = std::fs::read(path).unwrap();
let signature = signing_key.sign(&content);
let sig_path = path.with_extension(format!(
"{}.sig",
path.extension().and_then(|e| e.to_str()).unwrap_or("")
));
std::fs::write(sig_path, signature.to_bytes()).unwrap();
}
#[test]
fn valid_signature_succeeds() {
let signing_key = SigningKey::from_bytes(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let file = create_test_file(b"test plugin content");
sign_file(file.path(), &signing_key);
let result = verify_signature(file.path(), &[verifying_key]);
assert!(result.is_ok());
}
#[test]
fn tampered_file_fails() {
let signing_key = SigningKey::from_bytes(&[2u8; 32]);
let verifying_key = signing_key.verifying_key();
let file = create_test_file(b"original content");
sign_file(file.path(), &signing_key);
std::fs::write(file.path(), b"tampered content").unwrap();
let result = verify_signature(file.path(), &[verifying_key]);
assert!(matches!(result, Err(LoadError::SignatureInvalid { .. })));
}
#[test]
fn wrong_key_fails() {
let signing_key = SigningKey::from_bytes(&[3u8; 32]);
let wrong_key = SigningKey::from_bytes(&[4u8; 32]).verifying_key();
let file = create_test_file(b"test content");
sign_file(file.path(), &signing_key);
let result = verify_signature(file.path(), &[wrong_key]);
assert!(matches!(result, Err(LoadError::SignatureInvalid { .. })));
}
#[test]
fn missing_sig_file_returns_required() {
let key = SigningKey::from_bytes(&[5u8; 32]).verifying_key();
let file = create_test_file(b"no sig for this");
let result = verify_signature(file.path(), &[key]);
assert!(matches!(result, Err(LoadError::SignatureRequired { .. })));
}
}