mockforge_plugin_loader/
signature.rs

1//! Plugin signature verification
2//!
3//! This module provides cryptographic signature verification for plugins.
4//! Supports RSA and Ed25519 signatures using the ring cryptography library.
5
6use crate::{LoaderResult, PluginLoaderConfig, PluginLoaderError};
7use ring::signature;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::Path;
11
12/// Signature algorithm types
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14pub enum SignatureAlgorithm {
15    /// RSA PKCS#1 v1.5 with SHA-256 (2048-bit key)
16    #[serde(rename = "RSA_PKCS1_2048_SHA256")]
17    RsaPkcs1_2048Sha256,
18    /// RSA PKCS#1 v1.5 with SHA-256 (3072-bit key)
19    #[serde(rename = "RSA_PKCS1_3072_SHA256")]
20    RsaPkcs1_3072Sha256,
21    /// RSA PKCS#1 v1.5 with SHA-256 (4096-bit key)
22    #[serde(rename = "RSA_PKCS1_4096_SHA256")]
23    RsaPkcs1_4096SHA256,
24    /// Ed25519 signature scheme
25    #[serde(rename = "ED25519")]
26    Ed25519,
27}
28
29impl SignatureAlgorithm {
30    /// Convert to ring's verification algorithm
31    fn as_ring_algorithm(&self) -> &'static dyn signature::VerificationAlgorithm {
32        match self {
33            SignatureAlgorithm::RsaPkcs1_2048Sha256 => &signature::RSA_PKCS1_2048_8192_SHA256,
34            SignatureAlgorithm::RsaPkcs1_3072Sha256 => &signature::RSA_PKCS1_2048_8192_SHA256,
35            SignatureAlgorithm::RsaPkcs1_4096SHA256 => &signature::RSA_PKCS1_2048_8192_SHA256,
36            SignatureAlgorithm::Ed25519 => &signature::ED25519,
37        }
38    }
39}
40
41/// Plugin signature metadata
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PluginSignature {
44    /// Signature algorithm used
45    pub algorithm: SignatureAlgorithm,
46    /// Key ID that was used to sign
47    pub key_id: String,
48    /// Hex-encoded signature bytes
49    pub signature: String,
50    /// Hex-encoded hash of signed content (for verification)
51    pub signed_content_hash: String,
52}
53
54impl PluginSignature {
55    /// Create a new signature
56    pub fn new(
57        algorithm: SignatureAlgorithm,
58        key_id: String,
59        signature: Vec<u8>,
60        content_hash: Vec<u8>,
61    ) -> Self {
62        Self {
63            algorithm,
64            key_id,
65            signature: hex::encode(signature),
66            signed_content_hash: hex::encode(content_hash),
67        }
68    }
69
70    /// Get signature bytes
71    pub fn signature_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
72        hex::decode(&self.signature)
73    }
74
75    /// Get content hash bytes
76    pub fn content_hash_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
77        hex::decode(&self.signed_content_hash)
78    }
79}
80
81/// Plugin signature verifier
82pub struct SignatureVerifier<'a> {
83    config: &'a PluginLoaderConfig,
84}
85
86impl<'a> SignatureVerifier<'a> {
87    /// Create a new signature verifier
88    pub fn new(config: &'a PluginLoaderConfig) -> Self {
89        Self { config }
90    }
91
92    /// Verify a plugin's signature
93    ///
94    /// This function:
95    /// 1. Reads the signature file (plugin.sig)
96    /// 2. Computes the hash of the plugin manifest
97    /// 3. Verifies the signature using the trusted public key
98    pub fn verify_plugin_signature(&self, plugin_dir: &Path) -> LoaderResult<()> {
99        // Look for signature file
100        let sig_file = plugin_dir.join("plugin.sig");
101        if !sig_file.exists() {
102            if self.config.allow_unsigned {
103                tracing::warn!("No signature file found, but unsigned plugins are allowed");
104                return Ok(());
105            }
106            return Err(PluginLoaderError::security("No signature file found (plugin.sig)"));
107        }
108
109        // Read and parse signature file
110        let sig_contents = fs::read_to_string(&sig_file).map_err(|e| {
111            PluginLoaderError::security(format!("Failed to read signature file: {}", e))
112        })?;
113
114        let signature: PluginSignature = serde_json::from_str(&sig_contents).map_err(|e| {
115            PluginLoaderError::security(format!("Failed to parse signature file: {}", e))
116        })?;
117
118        tracing::debug!(
119            "Verifying signature with algorithm {:?} and key_id {}",
120            signature.algorithm,
121            signature.key_id
122        );
123
124        // Check if key is trusted
125        if !self.config.trusted_keys.contains(&signature.key_id) {
126            return Err(PluginLoaderError::security(format!(
127                "Signature key '{}' is not in trusted keys list",
128                signature.key_id
129            )));
130        }
131
132        // Get public key data
133        let public_key_bytes = self.config.key_data.get(&signature.key_id).ok_or_else(|| {
134            PluginLoaderError::security(format!(
135                "Public key data not found for key_id '{}'",
136                signature.key_id
137            ))
138        })?;
139
140        // Compute hash of plugin manifest
141        let manifest_file = plugin_dir.join("plugin.toml");
142        if !manifest_file.exists() {
143            return Err(PluginLoaderError::security("Plugin manifest (plugin.toml) not found"));
144        }
145
146        let manifest_content = fs::read(&manifest_file).map_err(|e| {
147            PluginLoaderError::security(format!("Failed to read plugin manifest: {}", e))
148        })?;
149
150        // Compute SHA-256 hash
151        let computed_hash = ring::digest::digest(&ring::digest::SHA256, &manifest_content);
152        let computed_hash_bytes = computed_hash.as_ref();
153
154        // Verify the hash matches what was signed
155        let signed_hash_bytes = signature.content_hash_bytes().map_err(|e| {
156            PluginLoaderError::security(format!("Failed to decode signed content hash: {}", e))
157        })?;
158
159        if computed_hash_bytes != signed_hash_bytes.as_slice() {
160            return Err(PluginLoaderError::security(
161                "Plugin manifest hash does not match signed hash. The plugin may have been modified.",
162            ));
163        }
164
165        // Get signature bytes
166        let signature_bytes = signature.signature_bytes().map_err(|e| {
167            PluginLoaderError::security(format!("Failed to decode signature: {}", e))
168        })?;
169
170        // Verify signature
171        let public_key = signature::UnparsedPublicKey::new(
172            signature.algorithm.as_ring_algorithm(),
173            public_key_bytes,
174        );
175
176        public_key.verify(&manifest_content, &signature_bytes).map_err(|_| {
177            PluginLoaderError::security("Signature verification failed. Invalid signature.")
178        })?;
179
180        tracing::info!("Plugin signature verified successfully with key '{}'", signature.key_id);
181        Ok(())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_signature_algorithm_serialization() {
191        let alg = SignatureAlgorithm::Ed25519;
192        let json = serde_json::to_string(&alg).unwrap();
193        assert_eq!(json, "\"ED25519\"");
194
195        let alg = SignatureAlgorithm::RsaPkcs1_2048Sha256;
196        let json = serde_json::to_string(&alg).unwrap();
197        assert_eq!(json, "\"RSA_PKCS1_2048_SHA256\"");
198    }
199
200    #[test]
201    fn test_plugin_signature_encoding() {
202        let sig = PluginSignature::new(
203            SignatureAlgorithm::Ed25519,
204            "test-key".to_string(),
205            vec![0x01, 0x02, 0x03],
206            vec![0xaa, 0xbb, 0xcc],
207        );
208
209        assert_eq!(sig.signature, "010203");
210        assert_eq!(sig.signed_content_hash, "aabbcc");
211        assert_eq!(sig.signature_bytes().unwrap(), vec![0x01, 0x02, 0x03]);
212        assert_eq!(sig.content_hash_bytes().unwrap(), vec![0xaa, 0xbb, 0xcc]);
213    }
214
215    #[test]
216    fn test_signature_verification_with_unsigned_allowed() {
217        let config = PluginLoaderConfig {
218            allow_unsigned: true,
219            ..Default::default()
220        };
221
222        let verifier = SignatureVerifier::new(&config);
223        let temp_dir = tempfile::tempdir().unwrap();
224
225        // No signature file, but unsigned is allowed
226        let result = verifier.verify_plugin_signature(temp_dir.path());
227        assert!(result.is_ok());
228    }
229
230    #[test]
231    fn test_signature_verification_missing_file() {
232        let config = PluginLoaderConfig {
233            allow_unsigned: false,
234            ..Default::default()
235        };
236
237        let verifier = SignatureVerifier::new(&config);
238        let temp_dir = tempfile::tempdir().unwrap();
239
240        // No signature file and unsigned not allowed
241        let result = verifier.verify_plugin_signature(temp_dir.path());
242        assert!(result.is_err());
243        assert!(matches!(result.unwrap_err(), PluginLoaderError::SecurityViolation { .. }));
244    }
245}