use anyhow::{Context, Result};
use clap::Args;
use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};
use std::{fs, path::PathBuf};
use crate::cli::CmdCtx;
#[derive(Args, Debug)]
pub struct SignArgs {
pub archive: PathBuf,
#[arg(long, value_name = "PATH")]
pub key: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub out: Option<PathBuf>,
}
pub async fn run(args: SignArgs, _cli: &CmdCtx) -> Result<()> {
let key_path = match args.key {
Some(p) => p,
None => {
let home = dirs::home_dir().context("could not determine home directory")?;
home.join(".dkp").join("private.key")
}
};
if !key_path.exists() {
anyhow::bail!(
"private key not found at {} — run `dkp keygen` first",
key_path.display()
);
}
let key_hex =
fs::read_to_string(&key_path).with_context(|| format!("reading {}", key_path.display()))?;
let key_hex = key_hex.trim();
let key_bytes = hex_to_bytes(key_hex)
.with_context(|| format!("decoding private key at {}", key_path.display()))?;
if key_bytes.len() != 32 {
anyhow::bail!(
"invalid private key: expected 32 bytes, got {}",
key_bytes.len()
);
}
let key_array: [u8; 32] = key_bytes.try_into().unwrap();
let signing_key = SigningKey::from_bytes(&key_array);
let archive_path: PathBuf = if args.archive.is_dir() {
let build_dir = args.archive.join("build");
let entry = fs::read_dir(&build_dir)
.with_context(|| format!("no build/ directory found in {}", args.archive.display()))?
.flatten()
.find(|e| {
let name = e.file_name();
let n = name.to_string_lossy();
n.ends_with(".zip")
|| n.ends_with(".tar.gz")
|| n.ends_with(".tar.xz")
|| n.ends_with(".dkp")
})
.with_context(|| {
format!(
"no archive found in {} — run `dkp build` first",
build_dir.display()
)
})?;
entry.path()
} else {
args.archive.clone()
};
let archive_dir = archive_path
.parent()
.context("archive path has no parent directory")?;
let checksums_path = archive_dir.join("checksums.json");
if !checksums_path.exists() {
anyhow::bail!(
"checksums.json not found at {} — run `dkp build` first",
checksums_path.display()
);
}
let checksums_bytes = fs::read(&checksums_path)
.with_context(|| format!("reading {}", checksums_path.display()))?;
let digest = Sha256::digest(&checksums_bytes);
let signature = signing_key.sign(&digest);
let sig_path = args.out.unwrap_or_else(|| archive_dir.join("bundle.sig"));
fs::write(&sig_path, signature.to_bytes())?;
println!("Wrote {}", sig_path.display());
println!(
" Signed: {}",
archive_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
);
println!(" Key: {}", key_path.display());
println!(
" Pub key: {}",
key_path.with_file_name("public.key").display()
);
Ok(())
}
fn hex_to_bytes(s: &str) -> Result<Vec<u8>> {
if !s.len().is_multiple_of(2) {
anyhow::bail!("odd-length hex string");
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16)
.map_err(|e| anyhow::anyhow!("invalid hex byte at position {}: {}", i, e))
})
.collect()
}