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 =
49 fs::read_to_string(key_path).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).map_err(|e| format!("Invalid verifying key: {}", e))
57}
58
59fn load_receipt(path: &PathBuf) -> std::result::Result<Receipt, String> {
60 let content = fs::read_to_string(path).map_err(|e| format!("Failed to read receipt: {}", e))?;
61 serde_json::from_str(&content).map_err(|e| format!("Failed to parse receipt JSON: {}", e))
62}
63
64fn resolve_key_path(public_key: Option<String>) -> Option<PathBuf> {
65 public_key.map(PathBuf::from).or_else(|| {
66 let d = PathBuf::from(".ggen/keys/public.pem");
67 d.exists().then_some(d)
68 })
69}
70
71fn do_verify(
72 receipt_path: String, public_key: Option<String>,
73) -> std::result::Result<VerifyOutput, NounVerbError> {
74 let receipt_file = PathBuf::from(&receipt_path);
75
76 if !receipt_file.exists() {
77 return Ok(VerifyOutput {
78 receipt_file: receipt_path,
79 is_valid: false,
80 message: format!("Receipt file not found: {}", receipt_file.display()),
81 operation_id: None,
82 timestamp: None,
83 input_hashes: None,
84 output_hashes: None,
85 chain_position: None,
86 });
87 }
88
89 let key_path = match resolve_key_path(public_key) {
90 Some(p) => p,
91 None => {
92 return Ok(VerifyOutput {
93 receipt_file: receipt_path,
94 is_valid: false,
95 message: "Public key required: pass --public-key <path> or ensure .ggen/keys/public.pem exists".to_string(),
96 operation_id: None,
97 timestamp: None,
98 input_hashes: None,
99 output_hashes: None,
100 chain_position: None,
101 });
102 }
103 };
104
105 let receipt = load_receipt(&receipt_file).map_err(|e| NounVerbError::execution_error(e))?;
106 let verifying_key =
107 load_verifying_key(&key_path).map_err(|e| NounVerbError::execution_error(e))?;
108 let is_valid = receipt.verify(&verifying_key).is_ok();
109
110 Ok(VerifyOutput {
111 receipt_file: receipt_path,
112 is_valid,
113 message: if is_valid {
114 "Receipt signature verified successfully".to_string()
115 } else {
116 "Signature verification failed".to_string()
117 },
118 operation_id: Some(receipt.operation_id.clone()),
119 timestamp: Some(receipt.timestamp.to_rfc3339()),
120 input_hashes: Some(receipt.input_hashes.len()),
121 output_hashes: Some(receipt.output_hashes.len()),
122 chain_position: receipt
123 .previous_receipt_hash
124 .as_ref()
125 .map(|_| "chained".to_string()),
126 })
127}
128
129fn do_info(receipt_path: String) -> std::result::Result<InfoOutput, NounVerbError> {
130 let receipt_file = PathBuf::from(&receipt_path);
131 if !receipt_file.exists() {
132 return Err(NounVerbError::execution_error(format!(
133 "Receipt file not found: {}",
134 receipt_file.display()
135 )));
136 }
137 let receipt = load_receipt(&receipt_file).map_err(|e| NounVerbError::execution_error(e))?;
138 Ok(InfoOutput {
139 receipt_file: receipt_path,
140 operation_id: receipt.operation_id,
141 timestamp: receipt.timestamp.to_rfc3339(),
142 input_hashes: receipt.input_hashes.len(),
143 output_hashes: receipt.output_hashes.len(),
144 has_previous: receipt.previous_receipt_hash.is_some(),
145 signature_present: !receipt.signature.is_empty(),
146 })
147}
148
149#[verb]
158pub fn verify(receipt_path: String, public_key: Option<String>) -> Result<VerifyOutput> {
159 do_verify(receipt_path, public_key)
160}
161
162#[verb]
164pub fn info(receipt_path: String) -> Result<InfoOutput> {
165 do_info(receipt_path)
166}