use ed25519_dalek::{SecretKey, SigningKey, VerifyingKey};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum PackReceiptError {
Runtime(String),
}
impl std::fmt::Display for PackReceiptError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackReceiptError::Runtime(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for PackReceiptError {}
pub type Result<T> = std::result::Result<T, PackReceiptError>;
pub struct PackInstallClosure<'a> {
pub pack_id: &'a str,
pub pack_version: &'a str,
pub pack_digest: &'a str,
pub packages_installed: &'a [String],
pub artifact_paths: &'a [PathBuf],
}
pub fn emit_install_receipt(root: &Path, closure: &PackInstallClosure<'_>) -> Result<PathBuf> {
use crate::receipt::{hash_data, Receipt};
if closure.pack_digest.trim().is_empty() {
return Err(PackReceiptError::Runtime(format!(
"Refusing to emit receipt for '{}': empty pack digest (no durable install to witness)",
closure.pack_id
)));
}
let receipts_dir = root.join(".ggen").join("receipts");
let keys_dir = root.join(".ggen").join("keys");
fs::create_dir_all(&receipts_dir).map_err(|e| {
PackReceiptError::Runtime(format!("Failed to create receipts directory: {}", e))
})?;
fs::create_dir_all(&keys_dir).map_err(|e| {
PackReceiptError::Runtime(format!("Failed to create keys directory: {}", e))
})?;
let private_key_path = keys_dir.join("private.pem");
let public_key_path = keys_dir.join("public.pem");
let (signing_key, _verifying_key) = if private_key_path.exists() {
load_keypair(&private_key_path)?
} else {
let (signing_key, verifying_key) = crate::receipt::generate_keypair();
save_keypair(
&signing_key,
&verifying_key,
&private_key_path,
&public_key_path,
)?;
(signing_key, verifying_key)
};
let timestamp = chrono::Utc::now();
let operation_id = format!(
"pack-install-{}-{}",
closure.pack_id,
timestamp.format("%Y%m%d-%H%M%S")
);
let mut input_hashes: Vec<String> = Vec::new();
input_hashes.push(format!(
"actuator:ggen-pack-install@{}",
env!("CARGO_PKG_VERSION")
));
input_hashes.push(format!(
"pack:{}@{}:{}",
closure.pack_id, closure.pack_version, closure.pack_digest
));
for package in closure.packages_installed {
input_hashes.push(format!("package:{}", package));
}
let mut output_hashes: Vec<String> = Vec::new();
for path in closure.artifact_paths {
let display = path.display();
match read_artifact_bytes(path) {
Some(bytes) => output_hashes.push(format!("{}:{}", display, hash_data(&bytes))),
None => output_hashes.push(format!("{}:MISSING", display)),
}
}
if output_hashes.is_empty() {
output_hashes.push(format!("pack-digest:sha256-{}", closure.pack_digest));
}
let receipt = Receipt::new(operation_id, input_hashes, output_hashes, None)
.sign(&signing_key)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to sign receipt: {}", e)))?;
let receipt_filename = format!(
"pack-{}-{}.json",
closure.pack_id,
timestamp.format("%Y%m%d-%H%M%S")
);
let receipt_path = receipts_dir.join(&receipt_filename);
let receipt_json = serde_json::to_string_pretty(&receipt)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to serialize receipt: {}", e)))?;
fs::write(&receipt_path, receipt_json)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to write receipt: {}", e)))?;
Ok(receipt_path)
}
fn read_artifact_bytes(path: &Path) -> Option<Vec<u8>> {
let meta = fs::metadata(path).ok()?;
if meta.is_dir() {
let mut entries: Vec<String> = fs::read_dir(path)
.ok()?
.filter_map(|e| e.ok())
.map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
let len = e.metadata().map(|m| m.len()).unwrap_or(0);
format!("{}:{}", name, len)
})
.collect();
entries.sort();
Some(entries.join("\n").into_bytes())
} else {
fs::read(path).ok()
}
}
fn load_keypair(private_key_path: &Path) -> Result<(SigningKey, VerifyingKey)> {
use ed25519_dalek::SECRET_KEY_LENGTH;
let private_key_raw = fs::read(private_key_path)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to read private key: {}", e)))?;
let private_key_hex = std::str::from_utf8(&private_key_raw)
.map_err(|_| PackReceiptError::Runtime("Private key file is not valid UTF-8".to_string()))?
.trim();
let private_key_bytes = hex::decode(private_key_hex).map_err(|e| {
PackReceiptError::Runtime(format!("Failed to hex-decode private key: {}", e))
})?;
if private_key_bytes.len() != SECRET_KEY_LENGTH {
return Err(PackReceiptError::Runtime(
"Invalid private key length".to_string(),
));
}
let secret_key = SecretKey::try_from(private_key_bytes.as_slice())
.map_err(|_| PackReceiptError::Runtime("Invalid private key".to_string()))?;
let signing_key: SigningKey = secret_key.into();
let verifying_key = signing_key.verifying_key();
Ok((signing_key, verifying_key))
}
fn save_keypair(
signing_key: &SigningKey, verifying_key: &VerifyingKey, private_key_path: &Path,
public_key_path: &Path,
) -> Result<()> {
let private_key_hex = hex::encode(signing_key.to_bytes());
fs::write(private_key_path, private_key_hex)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to write private key: {}", e)))?;
let public_key_hex = hex::encode(verifying_key.to_bytes());
fs::write(public_key_path, public_key_hex)
.map_err(|e| PackReceiptError::Runtime(format!("Failed to write public key: {}", e)))?;
Ok(())
}
pub fn verify_install_receipt(
root: &Path, receipt_path: &Path,
) -> (bool, Option<String>, Option<String>) {
use crate::receipt::Receipt;
let receipt_bytes = match fs::read(receipt_path) {
Ok(b) => b,
Err(e) => return (false, None, Some(format!("cannot read receipt: {}", e))),
};
let receipt: Receipt = match serde_json::from_slice(&receipt_bytes) {
Ok(r) => r,
Err(e) => return (false, None, Some(format!("malformed receipt: {}", e))),
};
let operation_id = Some(receipt.operation_id.clone());
if receipt.signature.trim().is_empty() {
return (false, operation_id, Some("empty signature".to_string()));
}
let public_key_path = root.join(".ggen").join("keys").join("public.pem");
let verifying_key = match load_verifying_key(&public_key_path) {
Ok(k) => k,
Err(e) => return (false, operation_id, Some(e)),
};
match receipt.verify(&verifying_key) {
Ok(()) => (true, operation_id, None),
Err(e) => (
false,
operation_id,
Some(format!("signature invalid: {}", e)),
),
}
}
fn load_verifying_key(public_key_path: &Path) -> std::result::Result<VerifyingKey, String> {
let raw = fs::read(public_key_path).map_err(|e| {
format!(
"cannot read public key {}: {}",
public_key_path.display(),
e
)
})?;
let hex_str = std::str::from_utf8(&raw)
.map_err(|_| "public key file is not valid UTF-8".to_string())?
.trim();
let bytes = hex::decode(hex_str).map_err(|e| format!("cannot hex-decode public key: {}", e))?;
let arr: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| "public key is not 32 bytes".to_string())?;
VerifyingKey::from_bytes(&arr).map_err(|e| format!("invalid public key: {}", e))
}