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 {
);
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)?;
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")]
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,18 +3,45 @@
// 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,
};
#[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().sign_command.is_some() || self.windows().certificate_thumbprint.is_some()
}
pub(crate) fn sign_params(&self) -> SignParams {
SignParams {
product_name: self.product_name().into(),
digest_algorithm: self
.windows()
.digest_algorithm
.as_ref()
.map(|algorithm| algorithm.to_string())
.unwrap_or_else(|| "sha256".to_string()),
certificate_thumbprint: self
.windows()
.certificate_thumbprint
.clone()
.unwrap_or_default(),
timestamp_url: self
.windows()
.timestamp_url
.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,
@ -22,35 +49,34 @@ pub struct SignParams {
pub certificate_thumbprint: String,
pub timestamp_url: Option<String>,
pub tsp: bool,
pub sign_command: Option<String>,
}
// sign code forked from https://github.com/forbjok/rust-codesign
fn locate_signtool() -> crate::Result<PathBuf> {
#[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";
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)
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_value(KITS_ROOT_REGVALUE_NAME)
.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
.enum_keys()
/* Report and ignore errors, pass on values. */
.filter_map(|res| match res {
Ok(v) => Some(v),
Err(_) => None,
})
.keys()
.map_err(|_| crate::Error::FailedToEnumerateRegKeys)?
.collect();
// Sort installed kits
@ -85,13 +111,17 @@ fn locate_signtool() -> crate::Result<PathBuf> {
}
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> {
// Construct SignTool command
let signtool = locate_signtool()?;
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
let mut cmd = Command::new(signtool);
cmd.arg("verify");
@ -101,9 +131,31 @@ pub fn verify(path: &Path) -> crate::Result<bool> {
Ok(cmd.status()?.success())
}
pub fn sign_command(path: &str, params: &SignParams) -> crate::Result<(Command, PathBuf)> {
// Construct SignTool command
let signtool = locate_signtool()?;
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");
@ -120,17 +172,46 @@ pub fn sign_command(path: &str, params: &SignParams) -> crate::Result<(Command,
}
}
cmd.arg(path);
cmd.arg(path.as_ref());
Ok((cmd, signtool))
Ok(cmd)
}
pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
let path_str = path.as_ref().to_str().unwrap();
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),
log::info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint);
// should not be reachable
#[cfg(not(windows))]
None => Ok(Command::new("")),
}
}
let (mut cmd, signtool) = sign_command(path_str, params)?;
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
@ -142,31 +223,15 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
Ok(())
}
impl Settings {
pub(crate) fn can_sign(&self) -> bool {
self.windows().certificate_thumbprint.is_some()
}
pub(crate) fn sign_params(&self) -> SignParams {
SignParams {
product_name: self.product_name().into(),
digest_algorithm: self
.windows()
.digest_algorithm
.as_ref()
.map(|algorithm| algorithm.to_string())
.unwrap_or_else(|| "sha256".to_string()),
certificate_thumbprint: self
.windows()
.certificate_thumbprint
.clone()
.unwrap_or_default(),
timestamp_url: self
.windows()
.timestamp_url
.as_ref()
.map(|url| url.to_string()),
tsp: self.windows().tsp,
}
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(()),
}
}

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