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().expect("just assigned above"));
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().expect("just assigned above"))
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(
183            self.signing_key
184                .as_ref()
185                .expect("signing_key must be initialized before generating receipt"),
186        )
187        .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to sign receipt: {}", e)))?;
188
189        // Write receipt to file
190        let receipt_filename = format!("{}.json", operation_id);
191        let receipt_path = self.receipts_dir.join(receipt_filename);
192
193        let receipt_json = serde_json::to_string_pretty(&receipt).map_err(|e| {
194            ggen_core::utils::Error::new(&format!("Failed to serialize receipt: {}", e))
195        })?;
196
197        fs::write(&receipt_path, receipt_json).map_err(|e| {
198            ggen_core::utils::Error::new(&format!("Failed to write receipt: {}", e))
199        })?;
200
201        info!(
202            "Generated receipt: {} ({} packages installed)",
203            receipt_path.display(),
204            packages_installed.len()
205        );
206
207        Ok(receipt_path)
208    }
209
210    /// Verify a receipt file
211    ///
212    /// # Arguments
213    ///
214    /// * `receipt_path` - Path to receipt file
215    ///
216    /// # Returns
217    ///
218    /// Verification output with status
219    pub fn verify_receipt(&self, receipt_path: &PathBuf) -> Result<VerifyOutput> {
220        // Load verifying key
221        let public_key_path = self.keys_dir.join("public.pem");
222        let verifying_key = self.read_verifying_key(&public_key_path)?;
223
224        // Read receipt file
225        let receipt_content = fs::read_to_string(receipt_path)
226            .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to read receipt: {}", e)))?;
227
228        // Parse receipt
229        let receipt: Receipt = serde_json::from_str(&receipt_content).map_err(|e| {
230            ggen_core::utils::Error::new(&format!("Failed to parse receipt: {}", e))
231        })?;
232
233        // Verify signature
234        let is_valid = receipt.verify(&verifying_key).is_ok();
235
236        Ok(VerifyOutput {
237            receipt_file: receipt_path.display().to_string(),
238            is_valid,
239            message: if is_valid {
240                "Receipt signature verified successfully".to_string()
241            } else {
242                "Signature verification failed".to_string()
243            },
244            operation_id: Some(receipt.operation_id.clone()),
245            timestamp: Some(
246                receipt
247                    .timestamp
248                    .format("%Y-%m-%d %H:%M:%S UTC")
249                    .to_string(),
250            ),
251            input_hashes: Some(receipt.input_hashes.len()),
252            output_hashes: Some(receipt.output_hashes.len()),
253            chain_position: receipt
254                .previous_receipt_hash
255                .as_ref()
256                .map(|_| "chained".to_string()),
257        })
258    }
259
260    /// Read verifying key from file
261    fn read_verifying_key(&self, key_path: &PathBuf) -> Result<VerifyingKey> {
262        let content = fs::read_to_string(key_path).map_err(|e| {
263            ggen_core::utils::Error::new(&format!("Failed to read public key: {}", e))
264        })?;
265
266        let key_bytes = hex::decode(content.trim()).map_err(|e| {
267            ggen_core::utils::Error::new(&format!("Failed to decode public key: {}", e))
268        })?;
269
270        let key_array: [u8; 32] = key_bytes[..32]
271            .try_into()
272            .map_err(|_| ggen_core::utils::Error::new("Invalid key length"))?;
273        VerifyingKey::from_bytes(&key_array)
274            .map_err(|e| ggen_core::utils::Error::new(&format!("Invalid verifying key: {}", e)))
275    }
276
277    /// Get path to receipts directory
278    pub fn receipts_dir(&self) -> &PathBuf {
279        &self.receipts_dir
280    }
281
282    /// Get path to keys directory
283    pub fn keys_dir(&self) -> &PathBuf {
284        &self.keys_dir
285    }
286
287    /// Generate a receipt for capability composition
288    ///
289    /// # Arguments
290    ///
291    /// * `capability_id` - Capability identifier
292    /// * `atomic_packs` - List of atomic packs in the composition
293    /// * `project_root` - Project root path
294    ///
295    /// # Returns
296    ///
297    /// Path to the generated receipt file
298    pub fn generate_composition_receipt(
299        &mut self, capability_id: &str, atomic_packs: &[String], _project_root: &PathBuf,
300    ) -> Result<PathBuf> {
301        // Ensure keys are loaded
302        self.load_or_generate_keys()?;
303
304        // Create operation ID
305        let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
306        let operation_id = format!("capability-{}-{}", capability_id, timestamp);
307
308        // Hash input data (capability spec)
309        let input_data = format!("{}@composition", capability_id);
310        let input_hash = hash_data(input_data.as_bytes());
311
312        // Hash output data (atomic packs)
313        let output_hashes: Vec<String> = atomic_packs
314            .iter()
315            .map(|pack| hash_data(pack.as_bytes()))
316            .collect();
317
318        // Create and sign receipt
319        let receipt = Receipt::new(
320            operation_id.clone(),
321            vec![input_hash],
322            output_hashes,
323            None, // Genesis receipt (no previous)
324        )
325        .sign(
326            self.signing_key
327                .as_ref()
328                .expect("signing_key must be initialized before generating receipt"),
329        )
330        .map_err(|e| ggen_core::utils::Error::new(&format!("Failed to sign receipt: {}", e)))?;
331
332        // Write receipt to file
333        let receipt_filename = format!("{}.json", operation_id);
334        let receipt_path = self.receipts_dir.join(receipt_filename);
335
336        let receipt_json = serde_json::to_string_pretty(&receipt).map_err(|e| {
337            ggen_core::utils::Error::new(&format!("Failed to serialize receipt: {}", e))
338        })?;
339
340        fs::write(&receipt_path, receipt_json).map_err(|e| {
341            ggen_core::utils::Error::new(&format!("Failed to write receipt: {}", e))
342        })?;
343
344        info!(
345            "Generated composition receipt: {} ({} packs composed)",
346            receipt_path.display(),
347            atomic_packs.len()
348        );
349
350        Ok(receipt_path)
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use tempfile::TempDir;
358
359    #[test]
360    fn test_receipt_manager_creation() {
361        let temp_dir = TempDir::new().unwrap();
362        let base_dir = temp_dir.path().join(".ggen");
363
364        let manager = ReceiptManager::new(base_dir).unwrap();
365
366        assert!(manager.receipts_dir().exists());
367        assert!(manager.keys_dir().exists());
368    }
369
370    #[test]
371    fn test_key_generation() {
372        let temp_dir = TempDir::new().unwrap();
373        let base_dir = temp_dir.path().join(".ggen");
374
375        let mut manager = ReceiptManager::new(base_dir).unwrap();
376        let _verifying_key = manager.load_or_generate_keys().unwrap();
377
378        // Verify keys were created
379        assert!(manager.keys_dir().join("private.pem").exists());
380        assert!(manager.keys_dir().join("public.pem").exists());
381    }
382
383    #[test]
384    fn test_pack_install_receipt() {
385        let temp_dir = TempDir::new().unwrap();
386        let base_dir = temp_dir.path().join(".ggen");
387
388        let mut manager = ReceiptManager::new(base_dir).unwrap();
389        let receipt_path = manager
390            .generate_pack_install_receipt(
391                "test-pack",
392                "1.0.0",
393                &["pkg1".to_string(), "pkg2".to_string()],
394                &temp_dir.path().join("install"),
395            )
396            .unwrap();
397
398        assert!(receipt_path.exists());
399        assert!(receipt_path
400            .to_string_lossy()
401            .contains("pack-install-test-pack"));
402    }
403
404    #[test]
405    fn test_receipt_verification() {
406        let temp_dir = TempDir::new().unwrap();
407        let base_dir = temp_dir.path().join(".ggen");
408
409        let mut manager = ReceiptManager::new(base_dir).unwrap();
410
411        // Generate receipt
412        let receipt_path = manager
413            .generate_pack_install_receipt(
414                "test-pack",
415                "1.0.0",
416                &["pkg1".to_string()],
417                &temp_dir.path().join("install"),
418            )
419            .unwrap();
420
421        // Verify receipt
422        let verify_output = manager.verify_receipt(&receipt_path).unwrap();
423
424        assert!(verify_output.is_valid);
425        assert_eq!(
426            verify_output.message,
427            "Receipt signature verified successfully"
428        );
429        assert!(verify_output.operation_id.is_some());
430        assert!(verify_output.timestamp.is_some());
431        assert_eq!(verify_output.input_hashes, Some(1));
432        assert_eq!(verify_output.output_hashes, Some(1));
433    }
434}