ggen_cli_lib/cmds/
receipt.rs1use clap_noun_verb::{NounVerbError, Result};
8use clap_noun_verb_macros::verb;
9use ed25519_dalek::VerifyingKey;
10use ggen_core::receipt::Receipt;
11use serde::Serialize;
12use std::{fs, path::PathBuf};
13
14#[derive(Serialize)]
20pub struct VerifyOutput {
21 pub receipt_file: String,
22 pub is_valid: bool,
23 pub message: String,
24 pub operation_id: Option<String>,
25 pub timestamp: Option<String>,
26 pub input_hashes: Option<usize>,
27 pub output_hashes: Option<usize>,
28 pub chain_position: Option<String>,
29}
30
31#[derive(Serialize)]
33pub struct InfoOutput {
34 pub receipt_file: String,
35 pub operation_id: String,
36 pub timestamp: String,
37 pub input_hashes: usize,
38 pub output_hashes: usize,
39 pub has_previous: bool,
40 pub signature_present: bool,
41}
42
43fn load_verifying_key(key_path: &PathBuf) -> std::result::Result<VerifyingKey, String> {
48 let key_content = fs::read_to_string(key_path)
49 .map_err(|e| format!("Failed to read public key: {}", e))?;
50 let key_bytes = hex::decode(key_content.trim())
51 .map_err(|e| format!("Failed to decode public key hex: {}", e))?;
52 let key_array: [u8; 32] = key_bytes
53 .as_slice()
54 .try_into()
55 .map_err(|_| "Public key must be exactly 32 bytes".to_string())?;
56 VerifyingKey::from_bytes(&key_array)
57 .map_err(|e| format!("Invalid verifying key: {}", e))
58}
59
60fn load_receipt(path: &PathBuf) -> std::result::Result<Receipt, String> {
61 let content = fs::read_to_string(path)
62 .map_err(|e| format!("Failed to read receipt: {}", e))?;
63 serde_json::from_str(&content)
64 .map_err(|e| format!("Failed to parse receipt JSON: {}", e))
65}
66
67fn resolve_key_path(public_key: Option<String>) -> Option<PathBuf> {
68 public_key.map(PathBuf::from).or_else(|| {
69 let d = PathBuf::from(".ggen/keys/public.pem");
70 d.exists().then_some(d)
71 })
72}
73
74fn do_verify(receipt_path: String, public_key: Option<String>) -> std::result::Result<VerifyOutput, NounVerbError> {
75 let receipt_file = PathBuf::from(&receipt_path);
76
77 if !receipt_file.exists() {
78 return Ok(VerifyOutput {
79 receipt_file: receipt_path,
80 is_valid: false,
81 message: format!("Receipt file not found: {}", receipt_file.display()),
82 operation_id: None,
83 timestamp: None,
84 input_hashes: None,
85 output_hashes: None,
86 chain_position: None,
87 });
88 }
89
90 let key_path = match resolve_key_path(public_key) {
91 Some(p) => p,
92 None => {
93 return Ok(VerifyOutput {
94 receipt_file: receipt_path,
95 is_valid: false,
96 message: "Public key required: pass --public-key <path> or ensure .ggen/keys/public.pem exists".to_string(),
97 operation_id: None,
98 timestamp: None,
99 input_hashes: None,
100 output_hashes: None,
101 chain_position: None,
102 });
103 }
104 };
105
106 let receipt = load_receipt(&receipt_file)
107 .map_err(|e| NounVerbError::execution_error(e))?;
108 let verifying_key = load_verifying_key(&key_path)
109 .map_err(|e| NounVerbError::execution_error(e))?;
110 let is_valid = receipt.verify(&verifying_key).is_ok();
111
112 Ok(VerifyOutput {
113 receipt_file: receipt_path,
114 is_valid,
115 message: if is_valid {
116 "Receipt signature verified successfully".to_string()
117 } else {
118 "Signature verification failed".to_string()
119 },
120 operation_id: Some(receipt.operation_id.clone()),
121 timestamp: Some(receipt.timestamp.to_rfc3339()),
122 input_hashes: Some(receipt.input_hashes.len()),
123 output_hashes: Some(receipt.output_hashes.len()),
124 chain_position: receipt
125 .previous_receipt_hash
126 .as_ref()
127 .map(|_| "chained".to_string()),
128 })
129}
130
131fn do_info(receipt_path: String) -> std::result::Result<InfoOutput, NounVerbError> {
132 let receipt_file = PathBuf::from(&receipt_path);
133 if !receipt_file.exists() {
134 return Err(NounVerbError::execution_error(format!(
135 "Receipt file not found: {}",
136 receipt_file.display()
137 )));
138 }
139 let receipt = load_receipt(&receipt_file)
140 .map_err(|e| NounVerbError::execution_error(e))?;
141 Ok(InfoOutput {
142 receipt_file: receipt_path,
143 operation_id: receipt.operation_id,
144 timestamp: receipt.timestamp.to_rfc3339(),
145 input_hashes: receipt.input_hashes.len(),
146 output_hashes: receipt.output_hashes.len(),
147 has_previous: receipt.previous_receipt_hash.is_some(),
148 signature_present: !receipt.signature.is_empty(),
149 })
150}
151
152#[verb]
161pub fn verify(receipt_path: String, public_key: Option<String>) -> Result<VerifyOutput> {
162 do_verify(receipt_path, public_key)
163}
164
165#[verb]
167pub fn info(receipt_path: String) -> Result<InfoOutput> {
168 do_info(receipt_path)
169}