mockforge_plugin_loader/
signature.rs1use crate::{LoaderResult, PluginLoaderConfig, PluginLoaderError};
7use ring::signature;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14pub enum SignatureAlgorithm {
15 #[serde(rename = "RSA_PKCS1_2048_SHA256")]
17 RsaPkcs1_2048Sha256,
18 #[serde(rename = "RSA_PKCS1_3072_SHA256")]
20 RsaPkcs1_3072Sha256,
21 #[serde(rename = "RSA_PKCS1_4096_SHA256")]
23 RsaPkcs1_4096SHA256,
24 #[serde(rename = "ED25519")]
26 Ed25519,
27}
28
29impl SignatureAlgorithm {
30 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#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PluginSignature {
44 pub algorithm: SignatureAlgorithm,
46 pub key_id: String,
48 pub signature: String,
50 pub signed_content_hash: String,
52}
53
54impl PluginSignature {
55 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 pub fn signature_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
72 hex::decode(&self.signature)
73 }
74
75 pub fn content_hash_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
77 hex::decode(&self.signed_content_hash)
78 }
79}
80
81pub struct SignatureVerifier<'a> {
83 config: &'a PluginLoaderConfig,
84}
85
86impl<'a> SignatureVerifier<'a> {
87 pub fn new(config: &'a PluginLoaderConfig) -> Self {
89 Self { config }
90 }
91
92 pub fn verify_plugin_signature(&self, plugin_dir: &Path) -> LoaderResult<()> {
99 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 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 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 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 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 let computed_hash = ring::digest::digest(&ring::digest::SHA256, &manifest_content);
152 let computed_hash_bytes = computed_hash.as_ref();
153
154 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 let signature_bytes = signature.signature_bytes().map_err(|e| {
167 PluginLoaderError::security(format!("Failed to decode signature: {}", e))
168 })?;
169
170 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 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 let result = verifier.verify_plugin_signature(temp_dir.path());
242 assert!(result.is_err());
243 assert!(matches!(result.unwrap_err(), PluginLoaderError::SecurityViolation { .. }));
244 }
245}