use {
crate::signing::CodeSigningCertificate,
anyhow::{anyhow, Context, Result},
log::warn,
std::{
io::{BufRead, BufReader},
path::{Path, PathBuf},
},
};
#[cfg(target_family = "windows")]
use tugger_windows::find_windows_sdk_current_arch_bin_path;
#[derive(Clone, Debug)]
pub enum TimestampServer {
Simple(String),
Rfc3161(String, String),
}
#[cfg(target_family = "windows")]
pub fn find_signtool() -> Result<PathBuf> {
let bin_path = find_windows_sdk_current_arch_bin_path(None).context("finding Windows SDK")?;
let p = bin_path.join("signtool.exe");
if p.exists() {
Ok(p)
} else {
Err(anyhow!(
"unable to locate signtool.exe in Windows SDK at {}",
bin_path.display()
))
}
}
#[cfg(target_family = "unix")]
pub fn find_signtool() -> Result<PathBuf> {
Err(anyhow!("finding signtool.exe only supported on Windows"))
}
#[derive(Clone, Debug)]
pub struct SigntoolSign {
certificate: CodeSigningCertificate,
verbose: bool,
debug: bool,
description: Option<String>,
file_digest_algorithm: String,
timestamp_server: Option<TimestampServer>,
extra_args: Vec<String>,
sign_files: Vec<PathBuf>,
}
impl SigntoolSign {
pub fn new(certificate: CodeSigningCertificate) -> Self {
Self {
certificate,
verbose: false,
debug: false,
description: None,
file_digest_algorithm: "SHA256".to_string(),
timestamp_server: None,
extra_args: vec![],
sign_files: vec![],
}
}
#[must_use]
pub fn clone_settings(&self) -> Self {
Self {
certificate: self.certificate.clone(),
verbose: self.verbose,
debug: self.debug,
description: self.description.clone(),
file_digest_algorithm: self.file_digest_algorithm.clone(),
timestamp_server: self.timestamp_server.clone(),
extra_args: self.extra_args.clone(),
sign_files: vec![],
}
}
pub fn verbose(&mut self) -> &mut Self {
self.verbose = true;
self
}
pub fn debug(&mut self) -> &mut Self {
self.debug = true;
self
}
pub fn description(&mut self, description: impl ToString) -> &mut Self {
self.description = Some(description.to_string());
self
}
pub fn file_digest_algorithm(&mut self, algorithm: impl ToString) -> &mut Self {
self.file_digest_algorithm = algorithm.to_string();
self
}
pub fn timestamp_server(&mut self, server: TimestampServer) -> &mut Self {
self.timestamp_server = Some(server);
self
}
pub fn extra_args(&mut self, extra_args: impl Iterator<Item = impl ToString>) -> &mut Self {
self.extra_args = extra_args.map(|x| x.to_string()).collect::<_>();
self
}
pub fn sign_file(&mut self, path: impl AsRef<Path>) -> &mut Self {
self.sign_files.push(path.as_ref().to_path_buf());
self
}
pub fn run(&self) -> Result<()> {
let signtool = find_signtool().context("locating signtool.exe")?;
let mut args = vec!["sign".to_string()];
if self.verbose {
args.push("/v".to_string());
}
if self.debug {
args.push("/debug".to_string());
}
match &self.certificate {
CodeSigningCertificate::Auto => {
args.push("/a".to_string());
}
CodeSigningCertificate::File(file) => {
args.push("/f".to_string());
args.push(file.path().display().to_string());
if let Some(password) = file.password() {
args.push("/p".to_string());
args.push(password.to_string());
}
}
CodeSigningCertificate::SubjectName(store, sn) => {
args.push("/s".to_string());
args.push(store.as_ref().to_string());
args.push("/n".to_string());
args.push(sn.to_string());
}
CodeSigningCertificate::Sha1Thumbprint(store, sha1) => {
args.push("/s".to_string());
args.push(store.as_ref().to_string());
args.push("/sha1".to_string());
args.push(sha1.to_string());
}
}
if let Some(description) = &self.description {
args.push("/d".to_string());
args.push(description.to_string());
}
args.push("/fd".to_string());
args.push(self.file_digest_algorithm.clone());
if let Some(server) = &self.timestamp_server {
match server {
TimestampServer::Simple(url) => {
args.push("/t".to_string());
args.push(url.to_string());
}
TimestampServer::Rfc3161(url, algorithm) => {
args.push("/tr".to_string());
args.push(url.to_string());
args.push("/td".to_string());
args.push(algorithm.to_string());
}
}
}
args.extend(self.extra_args.iter().cloned());
args.extend(self.sign_files.iter().map(|p| p.display().to_string()));
let command = duct::cmd(signtool, args)
.stderr_to_stdout()
.reader()
.context("running signtool")?;
{
let reader = BufReader::new(&command);
for line in reader.lines() {
warn!("{}", line?);
}
}
let output = command
.try_wait()?
.ok_or_else(|| anyhow!("unable to wait on command"))?;
if output.status.success() {
Ok(())
} else {
Err(anyhow!("error running signtool"))
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
create_self_signed_code_signing_certificate,
signing::{certificate_to_pfx, FileBasedCodeSigningCertificate},
},
tugger_common::testutil::*,
};
#[test]
fn test_find_signtool() -> Result<()> {
let res = find_signtool();
if cfg!(target_family = "windows") {
res?;
} else {
assert!(res.is_err());
}
Ok(())
}
#[test]
fn test_sign_executable() -> Result<()> {
if cfg!(target_family = "unix") {
eprintln!("skipping test because only works on Windows");
return Ok(());
}
let temp_path = DEFAULT_TEMP_DIR.path().join("test_sign_executable");
std::fs::create_dir(&temp_path)?;
let cert = create_self_signed_code_signing_certificate("tugger@example.com")?;
let pfx_data = certificate_to_pfx(&cert, "some_password", "cert_name")?;
let key_path = temp_path.join("signing.pfx");
std::fs::write(&key_path, pfx_data)?;
let sign_path = temp_path.join("test.exe");
let current_exe = std::env::current_exe()?;
std::fs::copy(current_exe, &sign_path)?;
let mut c = FileBasedCodeSigningCertificate::new(&key_path);
c.set_password("some_password");
SigntoolSign::new(c.into())
.verbose()
.debug()
.description("tugger test executable")
.file_digest_algorithm("sha256")
.sign_file(&sign_path)
.run()?;
Ok(())
}
}