1use 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#[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
27pub struct ReceiptManager {
29 receipts_dir: PathBuf,
31 keys_dir: PathBuf,
33 signing_key: Option<SigningKey>,
35 verifying_key: Option<VerifyingKey>,
37}
38
39impl ReceiptManager {
40 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 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 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 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 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 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 info!("Generating new Ed25519 keypair");
117 let (signing_key, verifying_key) = ggen_core::receipt::generate_keypair();
118
119 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 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 self.load_or_generate_keys()?;
160
161 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
163 let operation_id = format!("pack-install-{}-{}", pack_id, timestamp);
164
165 let input_data = format!("{}@{}", pack_id, pack_version);
167 let input_hash = hash_data(input_data.as_bytes());
168
169 let output_hashes: Vec<String> = packages_installed
171 .iter()
172 .map(|pkg| hash_data(pkg.as_bytes()))
173 .collect();
174
175 let receipt = Receipt::new(
177 operation_id.clone(),
178 vec![input_hash],
179 output_hashes,
180 None, )
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 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 pub fn verify_receipt(&self, receipt_path: &PathBuf) -> Result<VerifyOutput> {
216 let public_key_path = self.keys_dir.join("public.pem");
218 let verifying_key = self.read_verifying_key(&public_key_path)?;
219
220 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 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 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 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 pub fn receipts_dir(&self) -> &PathBuf {
275 &self.receipts_dir
276 }
277
278 pub fn keys_dir(&self) -> &PathBuf {
280 &self.keys_dir
281 }
282
283 pub fn generate_composition_receipt(
295 &mut self, capability_id: &str, atomic_packs: &[String], _project_root: &PathBuf,
296 ) -> Result<PathBuf> {
297 self.load_or_generate_keys()?;
299
300 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
302 let operation_id = format!("capability-{}-{}", capability_id, timestamp);
303
304 let input_data = format!("{}@composition", capability_id);
306 let input_hash = hash_data(input_data.as_bytes());
307
308 let output_hashes: Vec<String> = atomic_packs
310 .iter()
311 .map(|pack| hash_data(pack.as_bytes()))
312 .collect();
313
314 let receipt = Receipt::new(
316 operation_id.clone(),
317 vec![input_hash],
318 output_hashes,
319 None, )
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 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 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 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 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}