use anyhow::{Context, Result};
use argon2::{Algorithm as Argon2Algorithm, Argon2, Params, Version};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use std::path::Path;
const APP_SALT: &[u8] = b"blazehash-signing-v1";
fn sig_path_for(manifest_path: &Path) -> std::path::PathBuf {
let mut p = manifest_path.to_path_buf();
let new_name = format!(
"{}.sig",
manifest_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("manifest")
);
p.set_file_name(new_name);
p
}
fn pub_path_for(manifest_path: &Path) -> std::path::PathBuf {
let mut p = manifest_path.to_path_buf();
let new_name = format!(
"{}.pub",
manifest_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("manifest")
);
p.set_file_name(new_name);
p
}
fn derive_key(password: &str) -> Result<SigningKey> {
let params =
Params::new(65536, 3, 1, Some(32)).map_err(|e| anyhow::anyhow!("argon2 params: {e}"))?;
let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
let mut seed = [0u8; 32];
argon2
.hash_password_into(password.as_bytes(), APP_SALT, &mut seed)
.map_err(|e| anyhow::anyhow!("argon2 hash: {e}"))?;
Ok(SigningKey::from_bytes(&seed))
}
pub fn read_password() -> Result<String> {
if let Ok(pw) = std::env::var("BLAZEHASH_SIGN_PASSWORD") {
eprintln!(
"[!] Warning: using BLAZEHASH_SIGN_PASSWORD env var — not recommended for production"
);
return Ok(pw);
}
let pw = rpassword::prompt_password("Enter signing password: ")
.context("failed to read password from terminal")?;
Ok(pw)
}
pub fn sign(manifest_path: &Path) -> Result<()> {
let password = read_password()?;
let signing_key = derive_key(&password)?;
let verifying_key = signing_key.verifying_key();
let manifest_bytes = std::fs::read(manifest_path)
.with_context(|| format!("cannot read manifest {}", manifest_path.display()))?;
let signature: Signature = signing_key.sign(&manifest_bytes);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let pubkey_hex = hex::encode(verifying_key.to_bytes());
let sig_content = format!(
"blazehash-sig-v1\npubkey: {pubkey_hex}\nsigned: {timestamp}\nsig: {}\n",
hex::encode(signature.to_bytes()),
);
let sig_path = sig_path_for(manifest_path);
std::fs::write(&sig_path, &sig_content)
.with_context(|| format!("cannot write {}", sig_path.display()))?;
let pub_path = pub_path_for(manifest_path);
std::fs::write(&pub_path, &pubkey_hex)
.with_context(|| format!("cannot write {}", pub_path.display()))?;
eprintln!("[+] Signed: {}", manifest_path.display());
eprintln!("[+] Public key: {pubkey_hex}");
eprintln!("[+] Signature: {}", sig_path.display());
eprintln!("[+] Public key file: {}", pub_path.display());
Ok(())
}
pub fn verify_sig(manifest_path: &Path, expected_pubkey_hex: &str) -> Result<bool> {
let sig_path = sig_path_for(manifest_path);
let sig_content = std::fs::read_to_string(&sig_path)
.with_context(|| format!("cannot read sig file {}", sig_path.display()))?;
let mut sig_hex = None;
let mut embedded_pubkey_hex = None;
for line in sig_content.lines() {
if let Some(v) = line.strip_prefix("sig: ") {
sig_hex = Some(v.to_string());
}
if let Some(v) = line.strip_prefix("pubkey: ") {
embedded_pubkey_hex = Some(v.to_string());
}
}
let pub_hex = if expected_pubkey_hex.is_empty() {
embedded_pubkey_hex.ok_or_else(|| anyhow::anyhow!("no pubkey: line in sig file"))?
} else {
if let Some(ref embedded) = embedded_pubkey_hex {
if embedded.trim() != expected_pubkey_hex.trim() {
eprintln!("[!] Sig file pubkey does not match --expected-pubkey");
return Ok(false);
}
}
expected_pubkey_hex.to_string()
};
let pub_bytes = hex::decode(pub_hex.trim()).context("invalid pubkey hex")?;
let pub_arr: [u8; 32] = pub_bytes
.try_into()
.map_err(|_| anyhow::anyhow!("invalid pubkey length"))?;
let verifying_key = VerifyingKey::from_bytes(&pub_arr)?;
let sig_hex = sig_hex.ok_or_else(|| anyhow::anyhow!("no sig: line in sig file"))?;
let sig_bytes = hex::decode(sig_hex.trim()).context("invalid sig hex")?;
let sig_arr: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| anyhow::anyhow!("invalid sig length"))?;
let signature = Signature::from_bytes(&sig_arr);
let manifest_bytes = std::fs::read(manifest_path)
.with_context(|| format!("cannot read manifest {}", manifest_path.display()))?;
match verifying_key.verify(&manifest_bytes, &signature) {
Ok(()) => {
eprintln!("[+] Signature valid — {}", manifest_path.display());
Ok(true)
}
Err(_) => {
eprintln!("[!] Signature INVALID — {}", manifest_path.display());
Ok(false)
}
}
}
pub fn auto_verify_sidecar(
manifest_path: &Path,
expected_pubkey_hex: Option<&str>,
) -> Result<bool> {
let sig_path = sig_path_for(manifest_path);
if !sig_path.exists() {
return Ok(false);
}
eprintln!("[*] Found signature sidecar: {}", sig_path.display());
let pubkey = expected_pubkey_hex.unwrap_or("");
let valid = verify_sig(manifest_path, pubkey)?;
if !valid {
anyhow::bail!("manifest signature is INVALID — aborting. Use --ignore-sig to override.");
}
eprintln!("[K] Signature verified — {}", manifest_path.display());
Ok(true)
}