feat(bundler): support custom sign command on Windows (#9865)

* feat(bundler): support custom sign command on Windows

closes #7188
closes #9578

* fix double quotes

* fix build

* fix build

* clippy

* Update sign.rs

* clippy && replace `winreg` with `windows-registry`

* remove log [skip ci]

* Apply suggestions from code review

* tweak arg so path with spaces work on macOS

* create nsis toolset paths

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2024-05-24 16:25:13 +03:00 committed by GitHub
parent fc1543c65e
commit d6d3efbd12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 310 additions and 181 deletions

View File

@ -0,0 +1,5 @@
---
"tauri-bundler": "patch:feat"
---
On Windows, add option to specify a custom signing command to be used. This opens an endless possibilities, for example use `osslsigncode` on non-Windows or use hardware tokens and HSM or even using Azure Trusted Signing.

View File

@ -0,0 +1,5 @@
---
"tauri-utils": "patch:feat"
---
Add `sign_command` in `WindowsConfig`

View File

@ -112,6 +112,7 @@
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -1619,6 +1620,7 @@
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -1977,6 +1979,13 @@
"type": "null"
}
]
},
"signCommand": {
"description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nExample: ```text sign-cli --arg1 --arg2 %1 ```\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false

View File

@ -859,6 +859,20 @@ pub struct WindowsConfig {
pub wix: Option<WixConfig>,
/// Configuration for the installer generated with NSIS.
pub nsis: Option<NsisConfig>,
/// Specify a custom command to sign the binaries.
/// This command needs to have a `%1` in it which is just a placeholder for the binary path,
/// which we will detect and replace before calling the command.
///
/// Example:
/// ```text
/// sign-cli --arg1 --arg2 %1
/// ```
///
/// By Default we use `signtool.exe` which can be found only on Windows so
/// if you are on another platform and want to cross-compile and sign you will
/// need to use another tool like `osslsigncode`.
#[serde(alias = "sign-command")]
pub sign_command: Option<String>,
}
impl Default for WindowsConfig {
@ -873,6 +887,7 @@ impl Default for WindowsConfig {
allow_downgrades: true,
wix: None,
nsis: None,
sign_command: None,
}
}
}

View File

@ -44,7 +44,7 @@ dunce = "1"
[target."cfg(target_os = \"windows\")".dependencies]
uuid = { version = "1", features = [ "v4", "v5" ] }
bitness = "0.4"
winreg = "0.52"
windows-registry = "0.1.1"
glob = "0.3"
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]

View File

@ -63,8 +63,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
}
#[cfg(target_os = "windows")]
{
if settings.can_sign() {
// Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
for bin in settings.binaries() {
let bin_path = settings.binary_path(bin);
@ -75,16 +74,24 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
for bin in settings.external_binaries() {
let path = bin?;
let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").map_or(false, |v| v == "true");
if skip {
continue;
}
if !skip && windows::sign::verify(&path)? {
#[cfg(windows)]
if windows::sign::verify(&path)? {
log::info!(
"sidecar at \"{}\" already signed. Skipping...",
path.display()
)
} else {
windows::sign::try_sign(&path, &settings)?;
);
continue;
}
windows::sign::try_sign(&path, &settings)?;
}
} else {
#[cfg(not(target_os = "windows"))]
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
}
for package_type in &package_types {

View File

@ -447,6 +447,20 @@ pub struct WindowsSettings {
///
/// /// The default value of this flag is `true`.
pub allow_downgrades: bool,
/// Specify a custom command to sign the binaries.
/// This command needs to have a `%1` in it which is just a placeholder for the binary path,
/// which we will detect and replace before calling the command.
///
/// Example:
/// ```text
/// sign-cli --arg1 --arg2 %1
/// ```
///
/// By Default we use `signtool.exe` which can be found only on Windows so
/// if you are on another platform and want to cross-compile and sign you will
/// need to use another tool like `osslsigncode`.
pub sign_command: Option<String>,
}
impl Default for WindowsSettings {
@ -462,6 +476,7 @@ impl Default for WindowsSettings {
webview_install_mode: Default::default(),
webview_fixed_runtime_path: None,
allow_downgrades: true,
sign_command: None,
}
}
}

View File

@ -6,7 +6,6 @@
#[cfg(target_os = "windows")]
pub mod msi;
pub mod nsis;
#[cfg(target_os = "windows")]
pub mod sign;
mod util;

View File

@ -798,7 +798,11 @@ pub fn build_wix_app_installer(
&msi_output_path,
)?;
rename(&msi_output_path, &msi_path)?;
try_sign(&msi_path, settings)?;
if settings.can_sign() {
try_sign(&msi_path, settings)?;
}
output_paths.push(msi_path);
}

View File

@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(target_os = "windows")]
use crate::bundle::windows::sign::{sign_command, try_sign};
use crate::{
bundle::{
common::CommandExt,
@ -67,6 +67,7 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
let nsis_toolset_path = tauri_tools_path.join("NSIS");
if !nsis_toolset_path.exists() {
create_dir_all(&nsis_toolset_path)?;
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
} else if NSIS_REQUIRED_FILES
.iter()
@ -114,12 +115,10 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
NSIS_TAURI_UTILS_SHA1,
HashAlgorithm::Sha1,
)?;
write(
nsis_plugins
.join("x86-unicode")
.join("nsis_tauri_utils.dll"),
data,
)?;
let target_folder = nsis_plugins.join("x86-unicode");
create_dir_all(&target_folder)?;
write(target_folder.join("nsis_tauri_utils.dll"), data)?;
Ok(())
}
@ -163,9 +162,6 @@ fn build_nsis_app_installer(
log::info!("Target: {}", arch);
#[cfg(not(target_os = "windows"))]
log::info!("Code signing is currently only supported on Windows hosts, skipping...");
let output_path = settings.project_out_directory().join("nsis").join(arch);
if output_path.exists() {
remove_dir_all(&output_path)?;
@ -197,16 +193,9 @@ fn build_nsis_app_installer(
);
data.insert("copyright", to_json(settings.copyright_string()));
// Code signing is currently only supported on Windows hosts
#[cfg(target_os = "windows")]
if settings.can_sign() {
data.insert(
"uninstaller_sign_cmd",
to_json(format!(
"{:?}",
sign_command("%1", &settings.sign_params())?.0
)),
);
let sign_cmd = format!("{:?}", sign_command("%1", &settings.sign_params())?);
data.insert("uninstaller_sign_cmd", to_json(sign_cmd));
}
let version = settings.version_string();
@ -517,9 +506,12 @@ fn build_nsis_app_installer(
rename(nsis_output_path, &nsis_installer_path)?;
// Code signing is currently only supported on Windows hosts
#[cfg(target_os = "windows")]
try_sign(&nsis_installer_path, settings)?;
if settings.can_sign() {
try_sign(&nsis_installer_path, settings)?;
} else {
#[cfg(not(target_os = "windows"))]
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
}
Ok(vec![nsis_installer_path])
}

View File

@ -3,149 +3,21 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
bundle::{common::CommandExt, windows::util},
Settings,
};
use std::{
path::{Path, PathBuf},
process::Command,
};
use winreg::{
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY},
RegKey,
};
pub struct SignParams {
pub product_name: String,
pub digest_algorithm: String,
pub certificate_thumbprint: String,
pub timestamp_url: Option<String>,
pub tsp: bool,
}
// sign code forked from https://github.com/forbjok/rust-codesign
fn locate_signtool() -> crate::Result<PathBuf> {
const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10";
let installed_roots_key_path = Path::new(INSTALLED_ROOTS_REGKEY_PATH);
// Open 32-bit HKLM "Installed Roots" key
let installed_roots_key = RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey_with_flags(installed_roots_key_path, KEY_READ | KEY_WOW64_32KEY)
.map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
// Get the Windows SDK root path
let kits_root_10_path: String = installed_roots_key
.get_value(KITS_ROOT_REGVALUE_NAME)
.map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
// Construct Windows SDK bin path
let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
let mut installed_kits: Vec<String> = installed_roots_key
.enum_keys()
/* Report and ignore errors, pass on values. */
.filter_map(|res| match res {
Ok(v) => Some(v),
Err(_) => None,
})
.collect();
// Sort installed kits
installed_kits.sort();
/* Iterate through installed kit version keys in reverse (from newest to oldest),
adding their bin paths to the list.
Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */
let mut kit_bin_paths: Vec<PathBuf> = installed_kits
.iter()
.rev()
.map(|kit| kits_root_10_bin_path.join(kit))
.collect();
/* Add kits root bin path.
For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */
kit_bin_paths.push(kits_root_10_bin_path);
// Choose which version of SignTool to use based on OS bitness
let arch_dir = util::os_bitness().ok_or(crate::Error::UnsupportedBitness)?;
/* Iterate through all bin paths, checking for existence of a SignTool executable. */
for kit_bin_path in &kit_bin_paths {
/* Construct SignTool path. */
let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe");
/* Check if SignTool exists at this location. */
if signtool_path.exists() {
// SignTool found. Return it.
return Ok(signtool_path);
}
}
Err(crate::Error::SignToolNotFound)
}
/// Check if binary is already signed.
/// Used to skip sidecar binaries that are already signed.
pub fn verify(path: &Path) -> crate::Result<bool> {
// Construct SignTool command
let signtool = locate_signtool()?;
let mut cmd = Command::new(signtool);
cmd.arg("verify");
cmd.arg("/pa");
cmd.arg(path);
Ok(cmd.status()?.success())
}
pub fn sign_command(path: &str, params: &SignParams) -> crate::Result<(Command, PathBuf)> {
// Construct SignTool command
let signtool = locate_signtool()?;
let mut cmd = Command::new(&signtool);
cmd.arg("sign");
cmd.args(["/fd", &params.digest_algorithm]);
cmd.args(["/sha1", &params.certificate_thumbprint]);
cmd.args(["/d", &params.product_name]);
if let Some(ref timestamp_url) = params.timestamp_url {
if params.tsp {
cmd.args(["/tr", timestamp_url]);
cmd.args(["/td", &params.digest_algorithm]);
} else {
cmd.args(["/t", timestamp_url]);
}
}
cmd.arg(path);
Ok((cmd, signtool))
}
pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
let path_str = path.as_ref().to_str().unwrap();
log::info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint);
let (mut cmd, signtool) = sign_command(path_str, params)?;
log::debug!("Running signtool {:?}", signtool);
// Execute SignTool command
let output = cmd.output_ok()?;
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
log::info!("{:?}", stdout);
Ok(())
}
#[cfg(windows)]
use crate::bundle::windows::util;
use crate::{bundle::common::CommandExt, Settings};
use anyhow::Context;
#[cfg(windows)]
use std::path::PathBuf;
#[cfg(windows)]
use std::sync::OnceLock;
use std::{path::Path, process::Command};
impl Settings {
pub(crate) fn can_sign(&self) -> bool {
self.windows().certificate_thumbprint.is_some()
self.windows().sign_command.is_some() || self.windows().certificate_thumbprint.is_some()
}
pub(crate) fn sign_params(&self) -> SignParams {
SignParams {
product_name: self.product_name().into(),
@ -166,10 +38,203 @@ impl Settings {
.as_ref()
.map(|url| url.to_string()),
tsp: self.windows().tsp,
sign_command: self.windows().sign_command.clone(),
}
}
}
pub struct SignParams {
pub product_name: String,
pub digest_algorithm: String,
pub certificate_thumbprint: String,
pub timestamp_url: Option<String>,
pub tsp: bool,
pub sign_command: Option<String>,
}
#[cfg(windows)]
fn signtool() -> Option<PathBuf> {
// sign code forked from https://github.com/forbjok/rust-codesign
static SIGN_TOOL: OnceLock<crate::Result<PathBuf>> = OnceLock::new();
SIGN_TOOL
.get_or_init(|| {
const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10";
// Open 32-bit HKLM "Installed Roots" key
let installed_roots_key = windows_registry::LOCAL_MACHINE
.open(INSTALLED_ROOTS_REGKEY_PATH)
.map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
// Get the Windows SDK root path
let kits_root_10_path: String = installed_roots_key
.get_string(KITS_ROOT_REGVALUE_NAME)
.map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
// Construct Windows SDK bin path
let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
let mut installed_kits: Vec<String> = installed_roots_key
.keys()
.map_err(|_| crate::Error::FailedToEnumerateRegKeys)?
.collect();
// Sort installed kits
installed_kits.sort();
/* Iterate through installed kit version keys in reverse (from newest to oldest),
adding their bin paths to the list.
Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */
let mut kit_bin_paths: Vec<PathBuf> = installed_kits
.iter()
.rev()
.map(|kit| kits_root_10_bin_path.join(kit))
.collect();
/* Add kits root bin path.
For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */
kit_bin_paths.push(kits_root_10_bin_path);
// Choose which version of SignTool to use based on OS bitness
let arch_dir = util::os_bitness().ok_or(crate::Error::UnsupportedBitness)?;
/* Iterate through all bin paths, checking for existence of a SignTool executable. */
for kit_bin_path in &kit_bin_paths {
/* Construct SignTool path. */
let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe");
/* Check if SignTool exists at this location. */
if signtool_path.exists() {
// SignTool found. Return it.
return Ok(signtool_path);
}
}
Err(crate::Error::SignToolNotFound)
})
.as_ref()
.ok()
.cloned()
}
/// Check if binary is already signed.
/// Used to skip sidecar binaries that are already signed.
#[cfg(windows)]
pub fn verify(path: &Path) -> crate::Result<bool> {
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let mut cmd = Command::new(signtool);
cmd.arg("verify");
cmd.arg("/pa");
cmd.arg(path);
Ok(cmd.status()?.success())
}
pub fn sign_command_custom<P: AsRef<Path>>(path: P, command: &str) -> crate::Result<Command> {
let path = path.as_ref();
let mut args = command.trim().split(' ');
let bin = args
.next()
.context("custom signing command doesn't contain a bin?")?;
let mut cmd = Command::new(bin);
for arg in args {
if arg == "%1" {
cmd.arg(path);
} else {
cmd.arg(arg);
}
}
Ok(cmd)
}
#[cfg(windows)]
pub fn sign_command_default<P: AsRef<Path>>(
path: P,
params: &SignParams,
) -> crate::Result<Command> {
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let mut cmd = Command::new(&signtool);
cmd.arg("sign");
cmd.args(["/fd", &params.digest_algorithm]);
cmd.args(["/sha1", &params.certificate_thumbprint]);
cmd.args(["/d", &params.product_name]);
if let Some(ref timestamp_url) = params.timestamp_url {
if params.tsp {
cmd.args(["/tr", timestamp_url]);
cmd.args(["/td", &params.digest_algorithm]);
} else {
cmd.args(["/t", timestamp_url]);
}
}
cmd.arg(path.as_ref());
Ok(cmd)
}
pub fn sign_command<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<Command> {
match &params.sign_command {
Some(custom_command) => sign_command_custom(path, custom_command),
#[cfg(windows)]
None => sign_command_default(path, params),
// should not be reachable
#[cfg(not(windows))]
None => Ok(Command::new("")),
}
}
pub fn sign_custom<P: AsRef<Path>>(path: P, custom_command: &str) -> crate::Result<()> {
let path = path.as_ref();
log::info!(action = "Signing";"{} with a custom signing command", tauri_utils::display_path(path));
let mut cmd = sign_command_custom(path, custom_command)?;
let output = cmd.output_ok()?;
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
log::info!("{:?}", stdout);
Ok(())
}
#[cfg(windows)]
pub fn sign_default<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let path = path.as_ref();
log::info!(action = "Signing"; "{} with identity \"{}\"", tauri_utils::display_path(path), params.certificate_thumbprint);
let mut cmd = sign_command_default(path, params)?;
log::debug!("Running signtool {:?}", signtool);
// Execute SignTool command
let output = cmd.output_ok()?;
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
log::info!("{:?}", stdout);
Ok(())
}
pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
match &params.sign_command {
Some(custom_command) => sign_custom(path, custom_command),
#[cfg(windows)]
None => sign_default(path, params),
// should not be reachable, as user should either use Windows
// or specify a custom sign_command but we succeed anyways
#[cfg(not(windows))]
None => Ok(()),
}
}
pub fn try_sign(file_path: &std::path::PathBuf, settings: &Settings) -> crate::Result<()> {
if settings.can_sign() {
log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path));

View File

@ -94,6 +94,9 @@ pub enum Error {
/// Failed to get registry value.
#[error("failed to get {0} value on registry")]
GetRegistryValue(String),
/// Failed to enumerate registry keys.
#[error("failed to enumerate registry keys")]
FailedToEnumerateRegKeys,
/// Unsupported OS bitness.
#[error("unsupported OS bitness")]
UnsupportedBitness,

22
tooling/cli/Cargo.lock generated
View File

@ -4874,8 +4874,8 @@ dependencies = [
"ureq",
"uuid",
"walkdir",
"windows-registry",
"windows-sys 0.52.0",
"winreg 0.52.0",
"zip",
]
@ -5952,6 +5952,16 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "windows-registry"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f721bc2e55efb506a1a395a545cb76c2481fb023d33b51f0050e7888716281cf"
dependencies = [
"windows-result",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-result"
version = "0.1.1"
@ -6147,16 +6157,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winsafe"
version = "0.0.19"

View File

@ -112,6 +112,7 @@
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -1619,6 +1620,7 @@
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -1977,6 +1979,13 @@
"type": "null"
}
]
},
"signCommand": {
"description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nExample: ```text sign-cli --arg1 --arg2 %1 ```\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false

View File

@ -1420,6 +1420,7 @@ fn tauri_config_to_bundle_settings(
webview_install_mode: config.windows.webview_install_mode,
webview_fixed_runtime_path: config.windows.webview_fixed_runtime_path,
allow_downgrades: config.windows.allow_downgrades,
sign_command: config.windows.sign_command,
},
license: config.license.or_else(|| {
settings