use std::io::Write;
use std::process::{Command, Stdio};
use crate::error::{Result, ToriiError};
#[derive(Debug, Clone, PartialEq)]
pub enum VerifyStatus {
Good { signer: String },
UnknownKey { key_id: Option<String> },
Bad,
Other(String),
}
pub fn sign_blob(content: &[u8], key: &str, program: Option<&str>) -> Result<String> {
let bin = resolve_program(program);
let mut child = Command::new(&bin)
.args([
"--detach-sign",
"--armor",
"--local-user", key,
"--no-tty",
"--batch",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ToriiError::Subprocess { tool: "gpg".into(), message: format!(
"gpg binary not found (tried `{}`). Install gpg or \
`torii config set git.gpg_program /path/to/gpg2`.",
bin
) }
} else {
ToriiError::Subprocess { tool: "gpg".into(), message: format!("failed to spawn gpg: {}", e) }
}
})?;
{
let stdin = child.stdin.as_mut()
.ok_or_else(|| ToriiError::Subprocess { tool: "gpg".into(), message: "gpg stdin unavailable".into() })?;
stdin.write_all(content)
.map_err(|e| ToriiError::Subprocess { tool: "gpg".into(), message: format!("writing to gpg stdin: {}", e) })?;
}
let output = child.wait_with_output()
.map_err(|e| ToriiError::Subprocess { tool: "gpg".into(), message: format!("waiting for gpg: {}", e) })?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ToriiError::Subprocess { tool: "gpg".into(), message: format!(
"gpg signing failed (exit {}). gpg stderr:\n{}",
output.status.code().unwrap_or(-1),
stderr.trim()
) });
}
String::from_utf8(output.stdout)
.map_err(|e| ToriiError::Subprocess { tool: "gpg".into(), message: format!(
"gpg output was not valid UTF-8: {}", e
) })
}
pub fn resolve_program(program: Option<&str>) -> String {
program
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("gpg")
.to_string()
}
pub fn verify(armor: &str, payload: &[u8], program: Option<&str>) -> Result<VerifyStatus> {
use std::fs::write;
let bin = resolve_program(program);
let sig_path = std::env::temp_dir().join(format!(
"torii-verify-{}.asc",
armor.len()
));
write(&sig_path, armor)
.map_err(|e| ToriiError::Fs(format!("write sig tempfile: {}", e)))?;
let mut child = Command::new(&bin)
.args([
"--status-fd", "1",
"--no-tty", "--batch",
"--verify", sig_path.to_str().unwrap_or(""),
"-",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ToriiError::Subprocess { tool: "gpg".into(), message: format!(
"gpg binary not found (tried `{}`). Set git.gpg_program in config.",
bin
) }
} else {
ToriiError::Subprocess { tool: "gpg".into(), message: format!("failed to spawn gpg: {}", e) }
}
})?;
{
let stdin = child.stdin.as_mut()
.ok_or_else(|| ToriiError::Subprocess { tool: "gpg".into(), message: "gpg stdin unavailable".into() })?;
stdin.write_all(payload)
.map_err(|e| ToriiError::Fs(format!("writing payload: {}", e)))?;
}
let out = child.wait_with_output()
.map_err(|e| ToriiError::Subprocess { tool: "gpg".into(), message: format!("waiting for gpg: {}", e) })?;
let _ = std::fs::remove_file(&sig_path);
let status_lines = String::from_utf8_lossy(&out.stdout).to_string();
let stderr_lines = String::from_utf8_lossy(&out.stderr).to_string();
let mut signer: Option<String> = None;
let mut bad = false;
let mut no_key: Option<String> = None;
for line in status_lines.lines() {
let l = line.trim_start_matches("[GNUPG:] ").trim();
if let Some(rest) = l.strip_prefix("GOODSIG ") {
let mut parts = rest.splitn(2, ' ');
let _keyid = parts.next();
signer = parts.next().map(|s| s.to_string());
} else if l.starts_with("BADSIG ") {
bad = true;
} else if let Some(rest) = l.strip_prefix("NO_PUBKEY ") {
no_key = Some(rest.trim().to_string());
} else if l.starts_with("ERRSIG ") && no_key.is_none() {
let parts: Vec<&str> = l.split_whitespace().collect();
if let Some(k) = parts.get(1) {
no_key = Some(k.to_string());
}
}
}
if bad {
return Ok(VerifyStatus::Bad);
}
if let Some(s) = signer {
return Ok(VerifyStatus::Good { signer: s });
}
if let Some(k) = no_key {
return Ok(VerifyStatus::UnknownKey { key_id: Some(k) });
}
Ok(VerifyStatus::Other(stderr_lines.lines().last().unwrap_or("unknown").to_string()))
}