use clap::Parser;
use directories::BaseDirs;
use duct::cmd;
use serde::{Deserialize, Serialize};
use std::{ffi::OsString, fs, iter, path::Path, vec};
use trauma::{download::Download, downloader::DownloaderBuilder};
use zip_extensions::zip_extract;
#[derive(Serialize, Deserialize, Debug)]
pub struct Metadata {
#[serde(rename = "Endpoint")]
pub endpoint: String,
#[serde(rename = "CodeSigningAccountName")]
pub code_signing_account_name: String,
#[serde(rename = "CertificateProfileName")]
pub certificate_profile: String,
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(required = true, value_name = "FILE(S)", num_args = 1..=99)]
file: Vec<String>,
#[arg(long, env = "AZURE_CLIENT_SECRET")]
azure_client_secret: String,
#[arg(long, env = "AZURE_CLIENT_ID")]
azure_client_id: String,
#[arg(long, env = "AZURE_TENANT_ID")]
azure_tenant_id: String,
#[arg(
long,
env = "AZURE_CLI_PATH",
default_value = r"C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd"
)]
azure_cli_path: String,
#[arg(
long,
env = "SIGNTOOL_PATH",
default_value = r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe"
)]
sign_tool_path: String,
#[arg(long, short = 'e', verbatim_doc_comment)]
endpoint: String,
#[arg(
long,
env = "AZURE_TRUSTED_SIGNING_ACCOUNT_NAME",
short = 'a'
)]
account: String,
#[arg(
long,
env = "AZURE_CERTIFICATE_PROFILE_NAME",
short = 'c'
)]
certificate: String,
#[arg(long, default_value = "SHA256")]
fd: String,
#[arg(long, default_value = "http://timestamp.acs.microsoft.com")]
tr: String,
#[arg(long, default_value = "SHA256")]
td: String,
#[arg(long, short = 'd')]
description: Option<String>,
#[arg(long, short = 'i', default_value = "false")]
ignore_unsupported: bool,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
match run(args).await {
Ok(_) => (),
Err(err) => {
eprintln!("The application signing was not successful.\n\r{}", err);
std::process::exit(1);
}
}
}
async fn run(args: Args) -> Result<(), String> {
println!("DEPRECATED - Please migrate to artifact-signing-cli (https://crates.io/crates/artifact-signing-cli)");
if fs::metadata(&args.azure_cli_path).is_err() {
Err(format!(
"azure cli {} does not exists, please specify PATH with env AZURE_CLI_PATH",
&args.azure_cli_path
))?;
}
if fs::metadata(&args.sign_tool_path).is_err() {
Err(format!(
"signtool {} does not exists, please specify PATH with env SIGNTOOL_PATH",
&args.sign_tool_path
))?;
}
let base = BaseDirs::new().expect("could not find home directory");
let home = base.home_dir();
let config_dir = home.join(".trusted-signing-cli");
if !config_dir.exists() {
fs::create_dir_all(&config_dir).map_err(|err| {
format!(
"config dir '{:?}' could not be created: {:?}",
&config_dir, err
)
})?;
}
let lib_path = config_dir
.join("lib")
.join("bin")
.join("x64")
.join("Azure.CodeSigning.Dlib.dll");
if !lib_path.exists() {
let pkg_version = "1.0.128";
let link = format!(
"https://www.nuget.org/api/v2/package/Microsoft.ArtifactSigning.Client/{}",
pkg_version
);
let downloads = vec![Download::try_from(link.as_str()).map_err(|err| {
format!("could not download signing client from {}: {:?}", link, err)
})?];
let downloader = DownloaderBuilder::new()
.directory(config_dir.clone())
.build();
downloader.download(&downloads).await;
let archive = config_dir.join(pkg_version);
let target_dir = config_dir.join("lib");
zip_extract(&archive, &target_dir)
.map_err(|err| format!("signing client can't be unzipped: {:?}", err))?;
}
let metadata_path = config_dir.join("metadata.json");
let data = Metadata {
certificate_profile: args.certificate,
code_signing_account_name: args.account,
endpoint: args.endpoint,
};
fs::write(
config_dir.join("metadata.json"),
serde_json::to_string(&data)
.map_err(|err| format!("metadata.json could not be parsed: {:?}", err))?,
)
.map_err(|err| format!("metadata.json could not be written: {:?}", err))?;
cmd!(
&args.azure_cli_path,
"login",
"--service-principal",
"-t",
args.azure_tenant_id,
"-u",
args.azure_client_id,
"-p",
args.azure_client_secret
)
.run()
.map_err(|err| {
format!(
"login via azure cli '{}' failed: {:?}",
&args.azure_cli_path, err
)
})?;
let mut cmd_args: Vec<OsString> = vec![
"sign".into(),
"/v".into(),
"/fd".into(),
args.fd.into(),
"/tr".into(),
args.tr.into(),
"/td".into(),
args.td.into(),
"/dlib".into(),
lib_path.into(),
"/dmdf".into(),
metadata_path.into(),
];
if let Some(description) = args.description {
cmd_args.push("/d".into());
cmd_args.push(description.into());
}
for file in args.file {
if args.ignore_unsupported {
if !is_supported(&file) {
continue;
}
}
cmd(
&args.sign_tool_path,
cmd_args.iter().chain(iter::once(&file.clone().into())),
)
.run()
.map_err(|err| {
format!(
"signtool '{}' could not sign the file '{:?}', error: {:?}",
&args.sign_tool_path, &file, &err
)
})?;
}
Ok(())
}
fn is_supported(file: &str) -> bool {
let supported_extensions = vec![
"appx",
"msix",
"appxbundle",
"msixbundle", "cab", "cat", "dll", "exe", "js",
"vbs",
"wsf", "msi",
"msp",
"mst", "ocx", "ps1", "stl", "sys", ];
let extension = Path::new(file).extension().unwrap_or_default();
supported_extensions.contains(&extension.to_str().unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build() {
cmd!("cargo", "build",).run().unwrap();
cmd!("cargo", "build", "--release").run().unwrap();
cmd!(
"target/debug/trusted-signing-cli.exe",
"target/release/trusted-signing-cli.exe",
"-e",
"https://wus2.codesigning.azure.net",
"-a",
"mnr",
"-c",
"Profile3",
)
.run()
.unwrap();
}
}