use crate::{LoaderResult, PluginLoaderConfig, PluginLoaderError};
use ring::signature;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum SignatureAlgorithm {
#[serde(rename = "RSA_PKCS1_2048_SHA256")]
RsaPkcs1_2048Sha256,
#[serde(rename = "RSA_PKCS1_3072_SHA256")]
RsaPkcs1_3072Sha256,
#[serde(rename = "RSA_PKCS1_4096_SHA256")]
RsaPkcs1_4096SHA256,
#[serde(rename = "ED25519")]
Ed25519,
}
impl SignatureAlgorithm {
fn as_ring_algorithm(&self) -> &'static dyn signature::VerificationAlgorithm {
match self {
SignatureAlgorithm::RsaPkcs1_2048Sha256 => &signature::RSA_PKCS1_2048_8192_SHA256,
SignatureAlgorithm::RsaPkcs1_3072Sha256 => &signature::RSA_PKCS1_2048_8192_SHA256,
SignatureAlgorithm::RsaPkcs1_4096SHA256 => &signature::RSA_PKCS1_2048_8192_SHA256,
SignatureAlgorithm::Ed25519 => &signature::ED25519,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSignature {
pub algorithm: SignatureAlgorithm,
pub key_id: String,
pub signature: String,
pub signed_content_hash: String,
}
impl PluginSignature {
pub fn new(
algorithm: SignatureAlgorithm,
key_id: String,
signature: Vec<u8>,
content_hash: Vec<u8>,
) -> Self {
Self {
algorithm,
key_id,
signature: hex::encode(signature),
signed_content_hash: hex::encode(content_hash),
}
}
pub fn signature_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
hex::decode(&self.signature)
}
pub fn content_hash_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
hex::decode(&self.signed_content_hash)
}
}
pub struct SignatureVerifier<'a> {
config: &'a PluginLoaderConfig,
}
impl<'a> SignatureVerifier<'a> {
pub fn new(config: &'a PluginLoaderConfig) -> Self {
Self { config }
}
pub fn verify_plugin_signature(&self, plugin_dir: &Path) -> LoaderResult<()> {
let sig_file = plugin_dir.join("plugin.sig");
if !sig_file.exists() {
if self.config.allow_unsigned {
tracing::warn!("No signature file found, but unsigned plugins are allowed");
return Ok(());
}
return Err(PluginLoaderError::security("No signature file found (plugin.sig)"));
}
let sig_contents = fs::read_to_string(&sig_file).map_err(|e| {
PluginLoaderError::security(format!("Failed to read signature file: {}", e))
})?;
let signature: PluginSignature = serde_json::from_str(&sig_contents).map_err(|e| {
PluginLoaderError::security(format!("Failed to parse signature file: {}", e))
})?;
tracing::debug!(
"Verifying signature with algorithm {:?} and key_id {}",
signature.algorithm,
signature.key_id
);
if !self.config.trusted_keys.contains(&signature.key_id) {
return Err(PluginLoaderError::security(format!(
"Signature key '{}' is not in trusted keys list",
signature.key_id
)));
}
let public_key_bytes = self.config.key_data.get(&signature.key_id).ok_or_else(|| {
PluginLoaderError::security(format!(
"Public key data not found for key_id '{}'",
signature.key_id
))
})?;
let manifest_file = plugin_dir.join("plugin.toml");
if !manifest_file.exists() {
return Err(PluginLoaderError::security("Plugin manifest (plugin.toml) not found"));
}
let manifest_content = fs::read(&manifest_file).map_err(|e| {
PluginLoaderError::security(format!("Failed to read plugin manifest: {}", e))
})?;
let computed_hash = ring::digest::digest(&ring::digest::SHA256, &manifest_content);
let computed_hash_bytes = computed_hash.as_ref();
let signed_hash_bytes = signature.content_hash_bytes().map_err(|e| {
PluginLoaderError::security(format!("Failed to decode signed content hash: {}", e))
})?;
if computed_hash_bytes != signed_hash_bytes.as_slice() {
return Err(PluginLoaderError::security(
"Plugin manifest hash does not match signed hash. The plugin may have been modified.",
));
}
let signature_bytes = signature.signature_bytes().map_err(|e| {
PluginLoaderError::security(format!("Failed to decode signature: {}", e))
})?;
let public_key = signature::UnparsedPublicKey::new(
signature.algorithm.as_ring_algorithm(),
public_key_bytes,
);
public_key.verify(&manifest_content, &signature_bytes).map_err(|_| {
PluginLoaderError::security("Signature verification failed. Invalid signature.")
})?;
tracing::info!("Plugin signature verified successfully with key '{}'", signature.key_id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signature_algorithm_serialization() {
let alg = SignatureAlgorithm::Ed25519;
let json = serde_json::to_string(&alg).unwrap();
assert_eq!(json, "\"ED25519\"");
let alg = SignatureAlgorithm::RsaPkcs1_2048Sha256;
let json = serde_json::to_string(&alg).unwrap();
assert_eq!(json, "\"RSA_PKCS1_2048_SHA256\"");
}
#[test]
fn test_plugin_signature_encoding() {
let sig = PluginSignature::new(
SignatureAlgorithm::Ed25519,
"test-key".to_string(),
vec![0x01, 0x02, 0x03],
vec![0xaa, 0xbb, 0xcc],
);
assert_eq!(sig.signature, "010203");
assert_eq!(sig.signed_content_hash, "aabbcc");
assert_eq!(sig.signature_bytes().unwrap(), vec![0x01, 0x02, 0x03]);
assert_eq!(sig.content_hash_bytes().unwrap(), vec![0xaa, 0xbb, 0xcc]);
}
#[test]
fn test_signature_verification_with_unsigned_allowed() {
let config = PluginLoaderConfig {
allow_unsigned: true,
..Default::default()
};
let verifier = SignatureVerifier::new(&config);
let temp_dir = tempfile::tempdir().unwrap();
let result = verifier.verify_plugin_signature(temp_dir.path());
assert!(result.is_ok());
}
#[test]
fn test_signature_verification_missing_file() {
let config = PluginLoaderConfig {
allow_unsigned: false,
..Default::default()
};
let verifier = SignatureVerifier::new(&config);
let temp_dir = tempfile::tempdir().unwrap();
let result = verifier.verify_plugin_signature(temp_dir.path());
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PluginLoaderError::SecurityViolation { .. }));
}
}