1use ed25519_dalek::{SecretKey, SigningKey, VerifyingKey};
16use std::fs;
17use std::path::{Path, PathBuf};
18
19#[derive(Debug)]
21pub enum PackReceiptError {
22 Runtime(String),
23}
24
25impl std::fmt::Display for PackReceiptError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 PackReceiptError::Runtime(msg) => write!(f, "{}", msg),
29 }
30 }
31}
32
33impl std::error::Error for PackReceiptError {}
34
35pub type Result<T> = std::result::Result<T, PackReceiptError>;
37
38pub struct PackInstallClosure<'a> {
45 pub pack_id: &'a str,
47 pub pack_version: &'a str,
49 pub pack_digest: &'a str,
52 pub packages_installed: &'a [String],
54 pub artifact_paths: &'a [PathBuf],
58}
59
60pub fn emit_install_receipt(root: &Path, closure: &PackInstallClosure<'_>) -> Result<PathBuf> {
77 use crate::receipt::{hash_data, Receipt};
78
79 if closure.pack_digest.trim().is_empty() {
82 return Err(PackReceiptError::Runtime(format!(
83 "Refusing to emit receipt for '{}': empty pack digest (no durable install to witness)",
84 closure.pack_id
85 )));
86 }
87
88 let receipts_dir = root.join(".ggen").join("receipts");
89 let keys_dir = root.join(".ggen").join("keys");
90
91 fs::create_dir_all(&receipts_dir).map_err(|e| {
92 PackReceiptError::Runtime(format!("Failed to create receipts directory: {}", e))
93 })?;
94 fs::create_dir_all(&keys_dir).map_err(|e| {
95 PackReceiptError::Runtime(format!("Failed to create keys directory: {}", e))
96 })?;
97
98 let private_key_path = keys_dir.join("private.pem");
100 let public_key_path = keys_dir.join("public.pem");
101
102 let (signing_key, _verifying_key) = if private_key_path.exists() {
103 load_keypair(&private_key_path)?
104 } else {
105 let (signing_key, verifying_key) = crate::receipt::generate_keypair();
106 save_keypair(
107 &signing_key,
108 &verifying_key,
109 &private_key_path,
110 &public_key_path,
111 )?;
112 (signing_key, verifying_key)
113 };
114
115 let timestamp = chrono::Utc::now();
116 let operation_id = format!(
117 "pack-install-{}-{}",
118 closure.pack_id,
119 timestamp.format("%Y%m%d-%H%M%S")
120 );
121
122 let mut input_hashes: Vec<String> = Vec::new();
124 input_hashes.push(format!(
125 "actuator:ggen-pack-install@{}",
126 env!("CARGO_PKG_VERSION")
127 ));
128 input_hashes.push(format!(
129 "pack:{}@{}:{}",
130 closure.pack_id, closure.pack_version, closure.pack_digest
131 ));
132 for package in closure.packages_installed {
133 input_hashes.push(format!("package:{}", package));
134 }
135
136 let mut output_hashes: Vec<String> = Vec::new();
140 for path in closure.artifact_paths {
141 let display = path.display();
142 match read_artifact_bytes(path) {
143 Some(bytes) => output_hashes.push(format!("{}:{}", display, hash_data(&bytes))),
144 None => output_hashes.push(format!("{}:MISSING", display)),
145 }
146 }
147 if output_hashes.is_empty() {
150 output_hashes.push(format!("pack-digest:sha256-{}", closure.pack_digest));
151 }
152
153 let receipt = Receipt::new(operation_id, input_hashes, output_hashes, None)
154 .sign(&signing_key)
155 .map_err(|e| PackReceiptError::Runtime(format!("Failed to sign receipt: {}", e)))?;
156
157 let receipt_filename = format!(
158 "pack-{}-{}.json",
159 closure.pack_id,
160 timestamp.format("%Y%m%d-%H%M%S")
161 );
162 let receipt_path = receipts_dir.join(&receipt_filename);
163
164 let receipt_json = serde_json::to_string_pretty(&receipt)
165 .map_err(|e| PackReceiptError::Runtime(format!("Failed to serialize receipt: {}", e)))?;
166
167 fs::write(&receipt_path, receipt_json)
168 .map_err(|e| PackReceiptError::Runtime(format!("Failed to write receipt: {}", e)))?;
169
170 Ok(receipt_path)
171}
172
173fn read_artifact_bytes(path: &Path) -> Option<Vec<u8>> {
179 let meta = fs::metadata(path).ok()?;
180 if meta.is_dir() {
181 let mut entries: Vec<String> = fs::read_dir(path)
182 .ok()?
183 .filter_map(|e| e.ok())
184 .map(|e| {
185 let name = e.file_name().to_string_lossy().into_owned();
186 let len = e.metadata().map(|m| m.len()).unwrap_or(0);
187 format!("{}:{}", name, len)
188 })
189 .collect();
190 entries.sort();
191 Some(entries.join("\n").into_bytes())
192 } else {
193 fs::read(path).ok()
194 }
195}
196
197fn load_keypair(private_key_path: &Path) -> Result<(SigningKey, VerifyingKey)> {
199 use ed25519_dalek::SECRET_KEY_LENGTH;
200
201 let private_key_raw = fs::read(private_key_path)
202 .map_err(|e| PackReceiptError::Runtime(format!("Failed to read private key: {}", e)))?;
203 let private_key_hex = std::str::from_utf8(&private_key_raw)
204 .map_err(|_| PackReceiptError::Runtime("Private key file is not valid UTF-8".to_string()))?
205 .trim();
206 let private_key_bytes = hex::decode(private_key_hex).map_err(|e| {
207 PackReceiptError::Runtime(format!("Failed to hex-decode private key: {}", e))
208 })?;
209
210 if private_key_bytes.len() != SECRET_KEY_LENGTH {
211 return Err(PackReceiptError::Runtime(
212 "Invalid private key length".to_string(),
213 ));
214 }
215
216 let secret_key = SecretKey::try_from(private_key_bytes.as_slice())
217 .map_err(|_| PackReceiptError::Runtime("Invalid private key".to_string()))?;
218 let signing_key: SigningKey = secret_key.into();
219 let verifying_key = signing_key.verifying_key();
220
221 Ok((signing_key, verifying_key))
222}
223
224fn save_keypair(
226 signing_key: &SigningKey, verifying_key: &VerifyingKey, private_key_path: &Path,
227 public_key_path: &Path,
228) -> Result<()> {
229 let private_key_hex = hex::encode(signing_key.to_bytes());
230 fs::write(private_key_path, private_key_hex)
231 .map_err(|e| PackReceiptError::Runtime(format!("Failed to write private key: {}", e)))?;
232
233 let public_key_hex = hex::encode(verifying_key.to_bytes());
234 fs::write(public_key_path, public_key_hex)
235 .map_err(|e| PackReceiptError::Runtime(format!("Failed to write public key: {}", e)))?;
236
237 Ok(())
238}
239
240pub fn verify_install_receipt(
247 root: &Path, receipt_path: &Path,
248) -> (bool, Option<String>, Option<String>) {
249 use crate::receipt::Receipt;
250
251 let receipt_bytes = match fs::read(receipt_path) {
252 Ok(b) => b,
253 Err(e) => return (false, None, Some(format!("cannot read receipt: {}", e))),
254 };
255
256 let receipt: Receipt = match serde_json::from_slice(&receipt_bytes) {
257 Ok(r) => r,
258 Err(e) => return (false, None, Some(format!("malformed receipt: {}", e))),
259 };
260 let operation_id = Some(receipt.operation_id.clone());
261
262 if receipt.signature.trim().is_empty() {
263 return (false, operation_id, Some("empty signature".to_string()));
264 }
265
266 let public_key_path = root.join(".ggen").join("keys").join("public.pem");
267 let verifying_key = match load_verifying_key(&public_key_path) {
268 Ok(k) => k,
269 Err(e) => return (false, operation_id, Some(e)),
270 };
271
272 match receipt.verify(&verifying_key) {
273 Ok(()) => (true, operation_id, None),
274 Err(e) => (
275 false,
276 operation_id,
277 Some(format!("signature invalid: {}", e)),
278 ),
279 }
280}
281
282fn load_verifying_key(public_key_path: &Path) -> std::result::Result<VerifyingKey, String> {
284 let raw = fs::read(public_key_path).map_err(|e| {
285 format!(
286 "cannot read public key {}: {}",
287 public_key_path.display(),
288 e
289 )
290 })?;
291 let hex_str = std::str::from_utf8(&raw)
292 .map_err(|_| "public key file is not valid UTF-8".to_string())?
293 .trim();
294 let bytes = hex::decode(hex_str).map_err(|e| format!("cannot hex-decode public key: {}", e))?;
295 let arr: [u8; 32] = bytes
296 .as_slice()
297 .try_into()
298 .map_err(|_| "public key is not 32 bytes".to_string())?;
299 VerifyingKey::from_bytes(&arr).map_err(|e| format!("invalid public key: {}", e))
300}