Skip to main content

ggen_cli_lib/
receipt_manager.rs

1//! Receipt manager for CLI operations
2//!
3//! This module provides utilities for generating cryptographic receipts
4//! after CLI operations like pack installation.
5
6use ed25519_dalek::{SigningKey, VerifyingKey};
7use ggen_core::receipt::{hash_data, Receipt};
8use ggen_core::utils::Result;
9use serde::Serialize;
10use std::fs;
11use std::path::PathBuf;
12use tracing::info;
13
14/// Verification output for receipt operations
15#[derive(Serialize)]
16pub struct VerifyOutput {
17    pub receipt_file: String,
18    pub is_valid: bool,
19    pub message: String,
20    pub operation_id: Option<String>,
21    pub timestamp: Option<String>,
22    pub input_hashes: Option<usize>,
23    pub output_hashes: Option<usize>,
24    pub chain_position: Option<String>,
25}
26
27/// Receipt manager for generating and storing receipts
28pub struct ReceiptManager {
29    /// Path to receipts directory
30    receipts_dir: PathBuf,
31    /// Path to keys directory
32    keys_dir: PathBuf,
33    /// Signing key
34    signing_key: Option<SigningKey>,
35    /// Verifying key
36    verifying_key: Option<VerifyingKey>,
37}
38
39impl ReceiptManager {
40    /// Create a new receipt manager
41    ///
42    /// # Arguments
43    ///
44    /// * `base_dir` - Base directory (usually .ggen/)
45    pub fn new(base_dir: PathBuf) -> Result<Self> {
46        let receipts_dir = base_dir.join("receipts");
47        let keys_dir = base_dir.join("keys");
48
49        // Create directories if they don't exist
50        fs::create_dir_all(&receipts_dir).map_err(|e| {
51            ggen_core::utils::Error::new(&format!("Failed to create receipts directory: {}", e))
52        })?;
53
54        fs::create_dir_all(&keys_dir).map_err(|e| {
55            ggen_core::utils::Error::new(&format!("Failed to create keys directory: {}", e))
56        })?;
57
58        Ok(Self {
59            receipts_dir,
60            keys_dir,
61            signing_key: None,
62            verifying_key: None,
63        })
64    }
65
66    /// Load or generate Ed25519 keypair
67    ///
68    /// Keys are stored in .ggen/keys/ directory:
69    /// - private.pem - Signing key (hex-encoded)
70    /// - public.pem - Verifying key (hex-encoded)
71    pub fn load_or_generate_keys(&mut self) -> Result<&VerifyingKey> {
72        let private_key_path = self.keys_dir.join("private.pem");
73        let public_key_path = self.keys_dir.join("public.pem");
74
75        // Try to load existing keys
76        if private_key_path.exists() && public_key_path.exists() {
77            info!("Loading existing keys from {:?}", self.keys_dir);
78
79            let private_key_hex = fs::read_to_string(&private_key_path).map_err(|e| {
80                ggen_core::utils::Error::new(&format!("Failed to read private key: {}", e))
81            })?;
82
83            let public_key_hex = fs::read_to_string(&public_key_path).map_err(|e| {
84                ggen_core::utils::Error::new(&format!("Failed to read public key: {}", e))
85            })?;
86
87            // Decode hex keys
88            let signing_key_bytes = hex::decode(private_key_hex.trim()).map_err(|e| {
89                ggen_core::utils::Error::new(&format!("Failed to decode private key: {}", e))
90            })?;
91
92            let verifying_key_bytes = hex::decode(public_key_hex.trim()).map_err(|e| {
93                ggen_core::utils::Error::new(&format!("Failed to decode public key: {}", e))
94            })?;
95
96            // Parse keys - convert slices to fixed arrays for ed25519-dalek 2.x
97            let signing_key_array: [u8; 32] = signing_key_bytes[..32]
98                .try_into()
99                .map_err(|_| ggen_core::utils::Error::new("Invalid signing key length"))?;
100            let verifying_key_array: [u8; 32] = verifying_key_bytes[..32]
101                .try_into()
102                .map_err(|_| ggen_core::utils::Error::new("Invalid verifying key length"))?;
103
104            let signing_key = SigningKey::from_bytes(&signing_key_array);
105            let verifying_key = VerifyingKey::from_bytes(&verifying_key_array).map_err(|e| {
106                ggen_core::utils::Error::new(&format!("Invalid verifying key: {}", e))
107            })?;
108
109            self.signing_key = Some(signing_key);
110            self.verifying_key = Some(verifying_key);
111
112            return Ok(self.verifying_key.as_ref().unwrap());
113        }
114
115        // Generate new keys
116        info!("Generating new Ed25519 keypair");
117        let (signing_key, verifying_key) = ggen_core::receipt::generate_keypair();
118
119        // Store keys
120        let private_key_hex = hex::encode(signing_key.to_bytes());
121        let public_key_hex = hex::encode(verifying_key.to_bytes());
122
123        fs::write(&private_key_path, private_key_hex).map_err(|e| {
124            ggen_core::utils::Error::new(&format!("Failed to write private key: {}", e))
125        })?;
126
127        fs::write(&public_key_path, public_key_hex).map_err(|e| {
128            ggen_core::utils::Error::new(&format!("Failed to write public key: {}", e))
129        })?;
130
131        info!(
132            "Generated new keypair: private={:?}, public={:?}",
133            private_key_path, public_key_path
134        );
135
136        self.signing_key = Some(signing_key);
137        self.verifying_key = Some(verifying_key);
138
139        Ok(self.verifying_key.as_ref().unwrap())
140    }
141
142    /// Generate a receipt for a pack installation
143    ///
144    /// # Arguments
145    ///
146    /// * `pack_id` - Pack identifier
147    /// * `pack_version` - Pack version
148    /// * `packages_installed` - List of packages installed
149    /// * `install_path` - Installation path
150    ///
151    /// # Returns
152    ///
153    /// Path to the generated receipt file
154    pub fn generate_pack_install_receipt(
155        &mut self, pack_id: &str, pack_version: &str, packages_installed: &[String],
156        _install_path: &PathBuf,
157    ) -> Result<PathBuf> {
158        // Ensure keys are loaded
159        self.load_or_generate_keys()?;
160
161        // Create operation ID
162        let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
163        let operation_id = format!("pack-install-{}-{}", pack_id, timestamp);
164
165        // Hash input data (pack spec)
166        let input_data = format!("{}@{}", pack_id, pack_version);
167        let input_hash = hash_data(input_data.as_bytes());
168
169        // Hash output data (installed packages)
170        let output_hashes: Vec<String> = packages_installed
171            .iter()
172            .map(|pkg| hash_data(pkg.as_bytes()))
173            .collect();
174
175        // Create and sign receipt
176        let receipt = Receipt::new(
177            operation_id.clone(),
178            vec![input_hash],
179            output_hashes,
180            None, // Genesis receipt (no previous)
181        )
182        .sign(self.signing_key.as_ref().unwrap())
183        .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to sign receipt: {}", e)))?;
184
185        // Write receipt to file
186        let receipt_filename = format!("{}.json", operation_id);
187        let receipt_path = self.receipts_dir.join(receipt_filename);
188
189        let receipt_json = serde_json::to_string_pretty(&receipt).map_err(|e| {
190            ggen_core::utils::Error::new(&format!("Failed to serialize receipt: {}", e))
191        })?;
192
193        fs::write(&receipt_path, receipt_json).map_err(|e| {
194            ggen_core::utils::Error::new(&format!("Failed to write receipt: {}", e))
195        })?;
196
197        info!(
198            "Generated receipt: {} ({} packages installed)",
199            receipt_path.display(),
200            packages_installed.len()
201        );
202
203        Ok(receipt_path)
204    }
205
206    /// Verify a receipt file
207    ///
208    /// # Arguments
209    ///
210    /// * `receipt_path` - Path to receipt file
211    ///
212    /// # Returns
213    ///
214    /// Verification output with status
215    pub fn verify_receipt(&self, receipt_path: &PathBuf) -> Result<VerifyOutput> {
216        // Load verifying key
217        let public_key_path = self.keys_dir.join("public.pem");
218        let verifying_key = self.read_verifying_key(&public_key_path)?;
219
220        // Read receipt file
221        let receipt_content = fs::read_to_string(receipt_path)
222            .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to read receipt: {}", e)))?;
223
224        // Parse receipt
225        let receipt: Receipt = serde_json::from_str(&receipt_content).map_err(|e| {
226            ggen_core::utils::Error::new(&format!("Failed to parse receipt: {}", e))
227        })?;
228
229        // Verify signature
230        let is_valid = receipt.verify(&verifying_key).is_ok();
231
232        Ok(VerifyOutput {
233            receipt_file: receipt_path.display().to_string(),
234            is_valid,
235            message: if is_valid {
236                "Receipt signature verified successfully".to_string()
237            } else {
238                "Signature verification failed".to_string()
239            },
240            operation_id: Some(receipt.operation_id.clone()),
241            timestamp: Some(
242                receipt
243                    .timestamp
244                    .format("%Y-%m-%d %H:%M:%S UTC")
245                    .to_string(),
246            ),
247            input_hashes: Some(receipt.input_hashes.len()),
248            output_hashes: Some(receipt.output_hashes.len()),
249            chain_position: receipt
250                .previous_receipt_hash
251                .as_ref()
252                .map(|_| "chained".to_string()),
253        })
254    }
255
256    /// Read verifying key from file
257    fn read_verifying_key(&self, key_path: &PathBuf) -> Result<VerifyingKey> {
258        let content = fs::read_to_string(key_path).map_err(|e| {
259            ggen_core::utils::Error::new(&format!("Failed to read public key: {}", e))
260        })?;
261
262        let key_bytes = hex::decode(content.trim()).map_err(|e| {
263            ggen_core::utils::Error::new(&format!("Failed to decode public key: {}", e))
264        })?;
265
266        let key_array: [u8; 32] = key_bytes[..32]
267            .try_into()
268            .map_err(|_| ggen_core::utils::Error::new("Invalid key length"))?;
269        VerifyingKey::from_bytes(&key_array)
270            .map_err(|e| ggen_core::utils::Error::new(&format!("Invalid verifying key: {}", e)))
271    }
272
273    /// Get path to receipts directory
274    pub fn receipts_dir(&self) -> &PathBuf {
275        &self.receipts_dir
276    }
277
278    /// Get path to keys directory
279    pub fn keys_dir(&self) -> &PathBuf {
280        &self.keys_dir
281    }
282
283    /// Generate a receipt for capability composition
284    ///
285    /// # Arguments
286    ///
287    /// * `capability_id` - Capability identifier
288    /// * `atomic_packs` - List of atomic packs in the composition
289    /// * `project_root` - Project root path
290    ///
291    /// # Returns
292    ///
293    /// Path to the generated receipt file
294    pub fn generate_composition_receipt(
295        &mut self, capability_id: &str, atomic_packs: &[String], _project_root: &PathBuf,
296    ) -> Result<PathBuf> {
297        // Ensure keys are loaded
298        self.load_or_generate_keys()?;
299
300        // Create operation ID
301        let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
302        let operation_id = format!("capability-{}-{}", capability_id, timestamp);
303
304        // Hash input data (capability spec)
305        let input_data = format!("{}@composition", capability_id);
306        let input_hash = hash_data(input_data.as_bytes());
307
308        // Hash output data (atomic packs)
309        let output_hashes: Vec<String> = atomic_packs
310            .iter()
311            .map(|pack| hash_data(pack.as_bytes()))
312            .collect();
313
314        // Create and sign receipt
315        let receipt = Receipt::new(
316            operation_id.clone(),
317            vec![input_hash],
318            output_hashes,
319            None, // Genesis receipt (no previous)
320        )
321        .sign(self.signing_key.as_ref().unwrap())
322        .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to sign receipt: {}", e)))?;
323
324        // Write receipt to file
325        let receipt_filename = format!("{}.json", operation_id);
326        let receipt_path = self.receipts_dir.join(receipt_filename);
327
328        let receipt_json = serde_json::to_string_pretty(&receipt).map_err(|e| {
329            ggen_core::utils::Error::new(&format!("Failed to serialize receipt: {}", e))
330        })?;
331
332        fs::write(&receipt_path, receipt_json).map_err(|e| {
333            ggen_core::utils::Error::new(&format!("Failed to write receipt: {}", e))
334        })?;
335
336        info!(
337            "Generated composition receipt: {} ({} packs composed)",
338            receipt_path.display(),
339            atomic_packs.len()
340        );
341
342        Ok(receipt_path)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use tempfile::TempDir;
350
351    #[test]
352    fn test_receipt_manager_creation() {
353        let temp_dir = TempDir::new().unwrap();
354        let base_dir = temp_dir.path().join(".ggen");
355
356        let manager = ReceiptManager::new(base_dir).unwrap();
357
358        assert!(manager.receipts_dir().exists());
359        assert!(manager.keys_dir().exists());
360    }
361
362    #[test]
363    fn test_key_generation() {
364        let temp_dir = TempDir::new().unwrap();
365        let base_dir = temp_dir.path().join(".ggen");
366
367        let mut manager = ReceiptManager::new(base_dir).unwrap();
368        let _verifying_key = manager.load_or_generate_keys().unwrap();
369
370        // Verify keys were created
371        assert!(manager.keys_dir().join("private.pem").exists());
372        assert!(manager.keys_dir().join("public.pem").exists());
373    }
374
375    #[test]
376    fn test_pack_install_receipt() {
377        let temp_dir = TempDir::new().unwrap();
378        let base_dir = temp_dir.path().join(".ggen");
379
380        let mut manager = ReceiptManager::new(base_dir).unwrap();
381        let receipt_path = manager
382            .generate_pack_install_receipt(
383                "test-pack",
384                "1.0.0",
385                &["pkg1".to_string(), "pkg2".to_string()],
386                &temp_dir.path().join("install"),
387            )
388            .unwrap();
389
390        assert!(receipt_path.exists());
391        assert!(receipt_path
392            .to_string_lossy()
393            .contains("pack-install-test-pack"));
394    }
395
396    #[test]
397    fn test_receipt_verification() {
398        let temp_dir = TempDir::new().unwrap();
399        let base_dir = temp_dir.path().join(".ggen");
400
401        let mut manager = ReceiptManager::new(base_dir).unwrap();
402
403        // Generate receipt
404        let receipt_path = manager
405            .generate_pack_install_receipt(
406                "test-pack",
407                "1.0.0",
408                &["pkg1".to_string()],
409                &temp_dir.path().join("install"),
410            )
411            .unwrap();
412
413        // Verify receipt
414        let verify_output = manager.verify_receipt(&receipt_path).unwrap();
415
416        assert!(verify_output.is_valid);
417        assert_eq!(
418            verify_output.message,
419            "Receipt signature verified successfully"
420        );
421        assert!(verify_output.operation_id.is_some());
422        assert!(verify_output.timestamp.is_some());
423        assert_eq!(verify_output.input_hashes, Some(1));
424        assert_eq!(verify_output.output_hashes, Some(1));
425    }
426}