mockforge_plugin_loader/
signature_gen.rs

1//! Plugin signature generation utilities
2//!
3//! This module provides utilities for generating plugin signatures during development.
4//! It's primarily used for testing and plugin development workflows.
5
6use crate::signature::{PluginSignature, SignatureAlgorithm};
7use crate::{LoaderResult, PluginLoaderError};
8use ring::rand::SystemRandom;
9use ring::signature::KeyPair;
10use std::fs;
11use std::path::Path;
12
13/// Sign a plugin manifest with Ed25519
14///
15/// This function:
16/// 1. Reads the plugin manifest (plugin.toml)
17/// 2. Computes SHA-256 hash of the manifest
18/// 3. Signs the hash with the provided private key
19/// 4. Writes the signature to plugin.sig
20pub fn sign_plugin_ed25519(
21    plugin_dir: &Path,
22    key_id: &str,
23    private_key_bytes: &[u8],
24) -> LoaderResult<PluginSignature> {
25    // Read manifest
26    let manifest_file = plugin_dir.join("plugin.toml");
27    if !manifest_file.exists() {
28        return Err(PluginLoaderError::security("Plugin manifest (plugin.toml) not found"));
29    }
30
31    let manifest_content = fs::read(&manifest_file).map_err(|e| {
32        PluginLoaderError::security(format!("Failed to read plugin manifest: {}", e))
33    })?;
34
35    // Compute hash
36    let hash = ring::digest::digest(&ring::digest::SHA256, &manifest_content);
37    let hash_bytes = hash.as_ref();
38
39    // Sign the manifest content (not just the hash)
40    let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(private_key_bytes).map_err(|e| {
41        PluginLoaderError::security(format!("Failed to parse Ed25519 private key: {:?}", e))
42    })?;
43
44    let signature_bytes = key_pair.sign(&manifest_content);
45
46    // Create signature object
47    let signature = PluginSignature::new(
48        SignatureAlgorithm::Ed25519,
49        key_id.to_string(),
50        signature_bytes.as_ref().to_vec(),
51        hash_bytes.to_vec(),
52    );
53
54    // Write signature file
55    let sig_file = plugin_dir.join("plugin.sig");
56    let sig_json = serde_json::to_string_pretty(&signature).map_err(|e| {
57        PluginLoaderError::security(format!("Failed to serialize signature: {}", e))
58    })?;
59
60    fs::write(&sig_file, sig_json).map_err(|e| {
61        PluginLoaderError::security(format!("Failed to write signature file: {}", e))
62    })?;
63
64    Ok(signature)
65}
66
67/// Generate a new Ed25519 key pair for plugin signing
68///
69/// Returns (private_key_pkcs8, public_key_bytes)
70pub fn generate_ed25519_keypair() -> LoaderResult<(Vec<u8>, Vec<u8>)> {
71    let rng = SystemRandom::new();
72    let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).map_err(|e| {
73        PluginLoaderError::security(format!("Failed to generate Ed25519 key pair: {:?}", e))
74    })?;
75
76    let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).map_err(|e| {
77        PluginLoaderError::security(format!("Failed to parse generated key pair: {:?}", e))
78    })?;
79
80    let public_key = key_pair.public_key().as_ref().to_vec();
81    let private_key = pkcs8.as_ref().to_vec();
82
83    Ok((private_key, public_key))
84}
85
86/// Save a key pair to files
87pub fn save_keypair(
88    private_key: &[u8],
89    public_key: &[u8],
90    output_dir: &Path,
91    key_name: &str,
92) -> LoaderResult<()> {
93    let private_key_file = output_dir.join(format!("{}.private.key", key_name));
94    let public_key_file = output_dir.join(format!("{}.public.key", key_name));
95
96    fs::write(&private_key_file, hex::encode(private_key))
97        .map_err(|e| PluginLoaderError::fs(format!("Failed to write private key: {}", e)))?;
98
99    fs::write(&public_key_file, hex::encode(public_key))
100        .map_err(|e| PluginLoaderError::fs(format!("Failed to write public key: {}", e)))?;
101
102    Ok(())
103}
104
105/// Load a key from a hex-encoded file
106pub fn load_key_from_file(path: &Path) -> LoaderResult<Vec<u8>> {
107    let hex_content = fs::read_to_string(path)
108        .map_err(|e| PluginLoaderError::fs(format!("Failed to read key file: {}", e)))?;
109
110    hex::decode(hex_content.trim())
111        .map_err(|e| PluginLoaderError::fs(format!("Failed to decode hex key: {}", e)))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use tempfile::TempDir;
118
119    #[test]
120    fn test_generate_keypair() {
121        let result = generate_ed25519_keypair();
122        assert!(result.is_ok());
123
124        let (private_key, public_key) = result.unwrap();
125        assert!(!private_key.is_empty());
126        assert!(!public_key.is_empty());
127    }
128
129    #[test]
130    fn test_save_and_load_keypair() {
131        let temp_dir = TempDir::new().unwrap();
132        let (private_key, public_key) = generate_ed25519_keypair().unwrap();
133
134        // Save keys
135        save_keypair(&private_key, &public_key, temp_dir.path(), "test-key").unwrap();
136
137        // Load keys
138        let loaded_private =
139            load_key_from_file(&temp_dir.path().join("test-key.private.key")).unwrap();
140        let loaded_public =
141            load_key_from_file(&temp_dir.path().join("test-key.public.key")).unwrap();
142
143        assert_eq!(private_key, loaded_private);
144        assert_eq!(public_key, loaded_public);
145    }
146
147    #[test]
148    fn test_sign_plugin() {
149        let temp_dir = TempDir::new().unwrap();
150        let (private_key, _public_key) = generate_ed25519_keypair().unwrap();
151
152        // Create a dummy plugin manifest
153        let manifest_content = r#"
154[info]
155id = "test-plugin"
156name = "Test Plugin"
157version = "1.0.0"
158description = "A test plugin"
159author = "Test Author"
160"#;
161        fs::write(temp_dir.path().join("plugin.toml"), manifest_content).unwrap();
162
163        // Sign the plugin
164        let result = sign_plugin_ed25519(temp_dir.path(), "test-key", &private_key);
165        assert!(result.is_ok());
166
167        // Check that signature file was created
168        assert!(temp_dir.path().join("plugin.sig").exists());
169
170        // Read and verify signature format
171        let sig_content = fs::read_to_string(temp_dir.path().join("plugin.sig")).unwrap();
172        let signature: PluginSignature = serde_json::from_str(&sig_content).unwrap();
173        assert_eq!(signature.algorithm, SignatureAlgorithm::Ed25519);
174        assert_eq!(signature.key_id, "test-key");
175    }
176}