Sfoglia il codice sorgente

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

muwoo 1 anno fa
parent
commit
5909662766

+ 5 - 0
.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.

+ 5 - 0
.changes/utils-sign-command.md

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

+ 10 - 0
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

+ 15 - 0
core/tauri-utils/src/config.rs

@@ -674,6 +674,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 {
@@ -688,6 +702,7 @@ impl Default for WindowsConfig {
       allow_downgrades: true,
       wix: None,
       nsis: None,
+      sign_command: None,
     }
   }
 }

+ 1 - 1
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]

+ 12 - 7
tooling/bundler/src/bundle.rs

@@ -63,8 +63,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
     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<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 && 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 {

+ 15 - 0
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<String>,
 }
 
 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,
     }
   }
 }

+ 0 - 1
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;

+ 3 - 1
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);
   }
 

+ 13 - 23
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<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()
@@ -115,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(())
 }
@@ -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])
 }
 

+ 174 - 111
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<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> {
-  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,
+#[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)
     })
-    .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> {
   // 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<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(&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<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]);
@@ -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<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),
+
+    // 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);
 
-  info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint);
+  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();
 
-  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<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() {
-    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(())

+ 3 - 0
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,

+ 10 - 0
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

+ 1 - 0
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,