diff --git a/.changes/custom-sign-command.md b/.changes/custom-sign-command.md new file mode 100644 index 000000000000..606ac05b9c17 --- /dev/null +++ b/.changes/custom-sign-command.md @@ -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. diff --git a/.changes/utils-sign-command.md b/.changes/utils-sign-command.md new file mode 100644 index 000000000000..67b0e39d7170 --- /dev/null +++ b/.changes/utils-sign-command.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": "patch:feat" +--- + +Add `sign_command` in `WindowsConfig` diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 8ad752c5e2c7..d774c4c4d263 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -155,6 +155,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -299,6 +300,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -1185,6 +1187,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -1560,6 +1563,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 diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 86eb3ca622b0..3bc46207eb00 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -674,6 +674,20 @@ pub struct WindowsConfig { pub wix: Option, /// Configuration for the installer generated with NSIS. pub nsis: Option, + /// 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, } impl Default for WindowsConfig { @@ -688,6 +702,7 @@ impl Default for WindowsConfig { allow_downgrades: true, wix: None, nsis: None, + sign_command: None, } } } diff --git a/tooling/bundler/Cargo.toml b/tooling/bundler/Cargo.toml index ae94c2a5c1d9..447a8ef58853 100644 --- a/tooling/bundler/Cargo.toml +++ b/tooling/bundler/Cargo.toml @@ -43,7 +43,7 @@ dunce = "1" [target."cfg(target_os = \"windows\")".dependencies] uuid = { version = "1", features = [ "v4", "v5" ] } -winreg = "0.51" +windows-registry = "0.1.1" glob = "0.3" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] diff --git a/tooling/bundler/src/bundle.rs b/tooling/bundler/src/bundle.rs index 9616e3201e9a..b81657e7bb08 100644 --- a/tooling/bundler/src/bundle.rs +++ b/tooling/bundler/src/bundle.rs @@ -63,8 +63,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result> { 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,22 @@ pub fn bundle_project(settings: Settings) -> crate::Result> { 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 && windows::sign::verify(&path)? { + if skip { + continue; + } + #[cfg(windows)] + if windows::sign::verify(&path)? { 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 { diff --git a/tooling/bundler/src/bundle/settings.rs b/tooling/bundler/src/bundle/settings.rs index 4331172481a7..3aef502b6a10 100644 --- a/tooling/bundler/src/bundle/settings.rs +++ b/tooling/bundler/src/bundle/settings.rs @@ -385,6 +385,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, } impl Default for WindowsSettings { @@ -400,6 +414,7 @@ impl Default for WindowsSettings { webview_install_mode: Default::default(), webview_fixed_runtime_path: None, allow_downgrades: true, + sign_command: None, } } } diff --git a/tooling/bundler/src/bundle/windows/mod.rs b/tooling/bundler/src/bundle/windows/mod.rs index d2475f8a9995..849c1aea6769 100644 --- a/tooling/bundler/src/bundle/windows/mod.rs +++ b/tooling/bundler/src/bundle/windows/mod.rs @@ -6,7 +6,6 @@ #[cfg(target_os = "windows")] pub mod msi; pub mod nsis; -#[cfg(target_os = "windows")] pub mod sign; mod util; diff --git a/tooling/bundler/src/bundle/windows/msi/wix.rs b/tooling/bundler/src/bundle/windows/msi/wix.rs index 87084c39ee34..b05a6fc63f37 100644 --- a/tooling/bundler/src/bundle/windows/msi/wix.rs +++ b/tooling/bundler/src/bundle/windows/msi/wix.rs @@ -791,7 +791,9 @@ 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); } diff --git a/tooling/bundler/src/bundle/windows/nsis.rs b/tooling/bundler/src/bundle/windows/nsis.rs index 350b7cb65da3..67e666180ff0 100644 --- a/tooling/bundler/src/bundle/windows/nsis.rs +++ b/tooling/bundler/src/bundle/windows/nsis.rs @@ -2,7 +2,6 @@ // 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::{ @@ -68,6 +67,7 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result 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(()) } @@ -164,9 +162,6 @@ fn build_nsis_app_installer( info!("Target: {}", arch); - #[cfg(not(target_os = "windows"))] - 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)?; @@ -194,16 +189,9 @@ fn build_nsis_app_installer( data.insert("short_description", to_json(settings.short_description())); 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(); @@ -498,10 +486,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]) } diff --git a/tooling/bundler/src/bundle/windows/sign.rs b/tooling/bundler/src/bundle/windows/sign.rs index 4f9d47c3f2d1..6e6033ca9320 100644 --- a/tooling/bundler/src/bundle/windows/sign.rs +++ b/tooling/bundler/src/bundle/windows/sign.rs @@ -3,96 +3,126 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{ - bundle::{common::CommandExt, windows::util}, - Settings, -}; -use log::{debug, info}; -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, pub digest_algorithm: String, pub certificate_thumbprint: String, pub timestamp_url: Option, pub tsp: bool, + pub sign_command: Option, } // sign code forked from https://github.com/forbjok/rust-codesign -fn locate_signtool() -> crate::Result { - 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 = installed_roots_key - .enum_keys() - /* Report and ignore errors, pass on values. */ - .filter_map(|res| match res { - Ok(v) => Some(v), - Err(_) => None, +#[cfg(windows)] +fn signtool() -> Option { + // sign code forked from https://github.com/forbjok/rust-codesign + static SIGN_TOOL: OnceLock> = 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 = 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 = 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) }) - .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 = 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 { // Construct SignTool command - let signtool = locate_signtool()?; + let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; let mut cmd = Command::new(signtool); cmd.arg("verify"); @@ -102,11 +132,33 @@ pub fn verify(path: &Path) -> crate::Result { 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>(path: P, command: &str) -> crate::Result { + 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(&signtool); + 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>( + path: P, + params: &SignParams, +) -> crate::Result { + let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; + + let mut cmd = Command::new(signtool); cmd.arg("sign"); cmd.args(["/fd", ¶ms.digest_algorithm]); cmd.args(["/sha1", ¶ms.certificate_thumbprint]); @@ -121,59 +173,70 @@ 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>(path: P, params: &SignParams) -> crate::Result<()> { - let path_str = path.as_ref().to_str().unwrap(); +pub fn sign_command>(path: P, params: &SignParams) -> crate::Result { + match ¶ms.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>(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); - info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint); + Ok(()) +} +#[cfg(windows)] +pub fn sign_default>(path: P, params: &SignParams) -> crate::Result<()> { + let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; + let path = path.as_ref(); - let (mut cmd, signtool) = sign_command(path_str, params)?; - debug!("Running signtool {:?}", signtool); + 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(); - info!("{:?}", stdout); + log::info!("{:?}", stdout); 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>(path: P, params: &SignParams) -> crate::Result<()> { + match ¶ms.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() { - info!(action = "Signing"; "{}", tauri_utils::display_path(file_path)); + log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path)); sign(file_path, &settings.sign_params())?; } Ok(()) diff --git a/tooling/bundler/src/error.rs b/tooling/bundler/src/error.rs index f061d6bc8a61..6fbd4ca05803 100644 --- a/tooling/bundler/src/error.rs +++ b/tooling/bundler/src/error.rs @@ -91,6 +91,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, diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 8ad752c5e2c7..d774c4c4d263 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -155,6 +155,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -299,6 +300,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -1185,6 +1187,7 @@ "certificateThumbprint": null, "digestAlgorithm": null, "nsis": null, + "signCommand": null, "timestampUrl": null, "tsp": false, "webviewFixedRuntimePath": null, @@ -1560,6 +1563,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 diff --git a/tooling/cli/src/interface/rust.rs b/tooling/cli/src/interface/rust.rs index 6ee5de4aee25..a364efbd21a9 100644 --- a/tooling/cli/src/interface/rust.rs +++ b/tooling/cli/src/interface/rust.rs @@ -1195,6 +1195,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, }, updater: Some(UpdaterSettings { active: updater_config.active,