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().expect("just assigned above"));
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().expect("just assigned above"))
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(
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 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 pub fn verify_receipt(&self, receipt_path: &PathBuf) -> Result<VerifyOutput> {
220 let public_key_path = self.keys_dir.join("public.pem");
222 let verifying_key = self.read_verifying_key(&public_key_path)?;
223
224 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 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 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 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 pub fn receipts_dir(&self) -> &PathBuf {
279 &self.receipts_dir
280 }
281
282 pub fn keys_dir(&self) -> &PathBuf {
284 &self.keys_dir
285 }
286
287 pub fn generate_composition_receipt(
299 &mut self, capability_id: &str, atomic_packs: &[String], _project_root: &PathBuf,
300 ) -> Result<PathBuf> {
301 self.load_or_generate_keys()?;
303
304 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
306 let operation_id = format!("capability-{}-{}", capability_id, timestamp);
307
308 let input_data = format!("{}@composition", capability_id);
310 let input_hash = hash_data(input_data.as_bytes());
311
312 let output_hashes: Vec<String> = atomic_packs
314 .iter()
315 .map(|pack| hash_data(pack.as_bytes()))
316 .collect();
317
318 let receipt = Receipt::new(
320 operation_id.clone(),
321 vec![input_hash],
322 output_hashes,
323 None, )
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 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 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 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 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}