diff --git a/.changes/nsis-sign-plugins.md b/.changes/nsis-sign-plugins.md new file mode 100644 index 000000000..8e3e37892 --- /dev/null +++ b/.changes/nsis-sign-plugins.md @@ -0,0 +1,5 @@ +--- +'tauri-bundler': 'patch:enhance' +--- + +Sign NSIS and WiX DLLs when bundling diff --git a/.changes/sign-dlls.md b/.changes/sign-dlls.md new file mode 100644 index 000000000..043a37bc6 --- /dev/null +++ b/.changes/sign-dlls.md @@ -0,0 +1,5 @@ +--- +'tauri-bundler': 'patch:enhance' +--- + +Sign DLLs from resources. diff --git a/Cargo.lock b/Cargo.lock index d1ed22b51..e3fc631ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8503,6 +8503,7 @@ dependencies = [ "url", "uuid", "walkdir", + "which", "windows-registry 0.5.0", "windows-sys 0.59.0", "zip 2.3.0", diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index c66d1fb97..432ecd40d 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -65,6 +65,9 @@ ar = "0.9" md5 = "0.7" rpm = { version = "0.16", features = ["bzip2-compression"] } +[target."cfg(unix)".dependencies] +which = "7" + [lib] name = "tauri_bundler" path = "src/lib.rs" diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index c3d4cd6ac..b7ed6a309 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -472,6 +472,16 @@ pub fn build_wix_app_installer( } fs::create_dir_all(&output_path)?; + // when we're performing code signing, we'll sign some WiX DLLs, so we make a local copy + let wix_toolset_path = if settings.can_sign() { + let wix_path = output_path.join("wix"); + crate::utils::fs_utils::copy_dir(&wix_toolset_path, &wix_path) + .context("failed to copy wix directory")?; + wix_path + } else { + wix_toolset_path.to_path_buf() + }; + let mut data = BTreeMap::new(); let silent_webview_install = if let WebviewInstallMode::DownloadBootstrapper { silent } @@ -763,7 +773,11 @@ pub fn build_wix_app_installer( let fragment = fragment_handlebars.render_template(&fragment_content, &data)?; let mut extensions = Vec::new(); for cap in extension_regex.captures_iter(&fragment) { - extensions.push(wix_toolset_path.join(format!("Wix{}.dll", &cap[1]))); + let path = wix_toolset_path.join(format!("Wix{}.dll", &cap[1])); + if settings.can_sign() { + try_sign(&path, settings)?; + } + extensions.push(path); } candle_inputs.push((fragment_path, extensions)); } @@ -773,11 +787,18 @@ pub fn build_wix_app_installer( fragment_extensions.insert(wix_toolset_path.join("WixUIExtension.dll")); fragment_extensions.insert(wix_toolset_path.join("WixUtilExtension.dll")); + // sign default extensions + if settings.can_sign() { + for path in &fragment_extensions { + try_sign(&path, settings)?; + } + } + for (path, extensions) in candle_inputs { for ext in &extensions { fragment_extensions.insert(ext.clone()); } - run_candle(settings, wix_toolset_path, &output_path, path, extensions)?; + run_candle(settings, &wix_toolset_path, &output_path, path, extensions)?; } let mut output_paths = Vec::new(); @@ -853,7 +874,7 @@ pub fn build_wix_app_installer( log::info!(action = "Running"; "light to produce {}", display_path(&msi_path)); run_light( - wix_toolset_path, + &wix_toolset_path, &output_path, arguments, &(fragment_extensions.clone().into_iter().collect()), @@ -968,9 +989,12 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { if added_resources.contains(&resource_path) { continue; } - added_resources.push(resource_path.clone()); + if settings.can_sign() { + try_sign(&resource_path, settings)?; + } + let resource_entry = ResourceFile { id: format!("I{}", Uuid::new_v4().as_simple()), guid: Uuid::new_v4().to_string(), @@ -1055,6 +1079,10 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { .to_string_lossy() .into_owned(); if !added_resources.iter().any(|r| r.ends_with(&relative_path)) { + if settings.can_sign() { + try_sign(resource_path, settings)?; + } + dlls.push(ResourceFile { id: format!("I{}", Uuid::new_v4().as_simple()), guid: Uuid::new_v4().to_string(), diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi index 47ca20f0c..4bccb0d9e 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi +++ b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi @@ -47,7 +47,7 @@ ${StrLoc} !define COPYRIGHT "{{copyright}}" !define OUTFILE "{{out_file}}" !define ARCH "{{arch}}" -!define PLUGINSPATH "{{additional_plugins_path}}" +!define ADDITIONALPLUGINSPATH "{{additional_plugins_path}}" !define ALLOWDOWNGRADES "{{allow_downgrades}}" !define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}" !define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}" @@ -85,10 +85,8 @@ VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" VIAddVersionKey "FileVersion" "${VERSION}" VIAddVersionKey "ProductVersion" "${VERSION}" -; Plugins path, currently exists for linux only -!if "${PLUGINSPATH}" != "" - !addplugindir "${PLUGINSPATH}" -!endif +# additional plugins +!addplugindir "${ADDITIONALPLUGINSPATH}" ; Uninstaller signing command !if "${UNINSTALLERSIGNCOMMAND}" != "" diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index 7c94ffbf7..3992f590f 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -55,11 +55,18 @@ const NSIS_REQUIRED_FILES: &[&str] = &[ "Include/nsDialogs.nsh", "Include/WinMessages.nsh", ]; +const NSIS_PLUGIN_FILES: &[&str] = &[ + "NSISdl.dll", + "StartMenu.dll", + "System.dll", + "nsDialogs.dll", + "additional/nsis_tauri_utils.dll", +]; #[cfg(not(target_os = "windows"))] -const NSIS_REQUIRED_FILES: &[&str] = &["Plugins/x86-unicode/nsis_tauri_utils.dll"]; +const NSIS_REQUIRED_FILES: &[&str] = &["Plugins/x86-unicode/additional/nsis_tauri_utils.dll"]; const NSIS_REQUIRED_FILES_HASH: &[(&str, &str, &str, HashAlgorithm)] = &[( - "Plugins/x86-unicode/nsis_tauri_utils.dll", + "Plugins/x86-unicode/additional/nsis_tauri_utils.dll", NSIS_TAURI_UTILS_URL, NSIS_TAURI_UTILS_SHA1, HashAlgorithm::Sha1, @@ -96,7 +103,10 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result c fs::rename(_tauri_tools_path.join("nsis-3.08"), nsis_toolset_path)?; } + // download additional plugins let nsis_plugins = nsis_toolset_path.join("Plugins"); let data = download_and_verify( @@ -124,7 +135,7 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c HashAlgorithm::Sha1, )?; - let target_folder = nsis_plugins.join("x86-unicode"); + let target_folder = nsis_plugins.join("x86-unicode").join("additional"); fs::create_dir_all(&target_folder)?; fs::write(target_folder.join("nsis_tauri_utils.dll"), data)?; @@ -156,7 +167,7 @@ fn try_add_numeric_build_number(version_str: &str) -> anyhow::Result { fn build_nsis_app_installer( settings: &Settings, - _nsis_toolset_path: &Path, + #[allow(unused_variables)] nsis_toolset_path: &Path, tauri_tools_path: &Path, updater: bool, ) -> crate::Result> { @@ -180,6 +191,65 @@ fn build_nsis_app_installer( } fs::create_dir_all(&output_path)?; + // we make a copy of the NSIS directory if we're going to sign its DLLs + // because we don't want to change the DLL hashes so the cache can reuse it + let maybe_plugin_copy_path = if settings.can_sign() { + // find nsis path + #[cfg(target_os = "linux")] + let system_nsis_toolset_path = std::env::var_os("NSIS_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/usr/share/nsis")); + #[cfg(target_os = "macos")] + let system_nsis_toolset_path = std::env::var_os("NSIS_PATH") + .map(PathBuf::from) + .ok_or_else(|| anyhow::anyhow!("failed to resolve NSIS path")) + .or_else(|_| { + let mut makensis_path = + which::which("makensis").context("failed to resolve `makensis`; did you install nsis? See https://tauri.app/distribute/windows-installer/#install-nsis for more information")?; + // homebrew installs it as a symlink + if makensis_path.is_symlink() { + // read_link might return a path relative to makensis_path so we must use join() and canonicalize + makensis_path = makensis_path + .parent() + .context("missing makensis parent")? + .join(std::fs::read_link(&makensis_path).context("failed to resolve makensis symlink")?) + .canonicalize() + .context("failed to resolve makensis path")?; + } + // file structure: + // ├── bin + // │ ├── makensis + // ├── share + // │ ├── nsis + let bin_folder = makensis_path.parent().context("missing makensis parent")?; + let root_folder = bin_folder.parent().context("missing makensis root")?; + crate::Result::Ok(root_folder.join("share").join("nsis")) + })?; + #[cfg(windows)] + let system_nsis_toolset_path = nsis_toolset_path.to_path_buf(); + + let plugins_path = output_path.join("Plugins"); + // copy system plugins (we don't want to modify system installed DLLs, and on some systems there will even be permission errors if we try) + crate::utils::fs_utils::copy_dir( + &system_nsis_toolset_path.join("Plugins").join("x86-unicode"), + &plugins_path.join("x86-unicode"), + ) + .context("failed to copy system NSIS Plugins folder to local copy")?; + // copy our downloaded DLLs + crate::utils::fs_utils::copy_dir( + &nsis_toolset_path + .join("Plugins") + .join("x86-unicode") + .join("additional"), + &plugins_path.join("x86-unicode").join("additional"), + ) + .context("failed to copy additional NSIS Plugins folder to local copy")?; + Some(plugins_path) + } else { + // in this case plugin_copy_path can be None, we'll use the system default path + None + }; + let mut data = BTreeMap::new(); let bundle_id = settings.bundle_identifier(); @@ -187,12 +257,17 @@ fn build_nsis_app_installer( .publisher() .unwrap_or_else(|| bundle_id.split('.').nth(1).unwrap_or(bundle_id)); - #[cfg(not(target_os = "windows"))] - { - let mut dir = dirs::cache_dir().unwrap(); - dir.extend(["tauri", "NSIS", "Plugins", "x86-unicode"]); - data.insert("additional_plugins_path", to_json(dir)); - } + let additional_plugins_path = maybe_plugin_copy_path + .clone() + .unwrap_or_else(|| nsis_toolset_path.join("Plugins")) + .join("x86-unicode") + .join("additional"); + + data.insert( + "additional_plugins_path", + // either our Plugins copy (when signing) or the cache/Plugins/x86-unicode path + to_json(&additional_plugins_path), + ); data.insert("arch", to_json(arch)); data.insert("bundle_id", to_json(bundle_id)); @@ -526,13 +601,29 @@ fn build_nsis_app_installer( )); fs::create_dir_all(nsis_installer_path.parent().unwrap())?; - log::info!(action = "Running"; "makensis.exe to produce {}", display_path(&nsis_installer_path)); + if settings.can_sign() { + log::info!("Signing NSIS plugins"); + for dll in NSIS_PLUGIN_FILES { + let path = additional_plugins_path.join(dll); + if path.exists() { + try_sign(&path, settings)?; + } else { + log::warn!("Could not find {}, skipping signing", path.display()); + } + } + } + + log::info!(action = "Running"; "makensis to produce {}", display_path(&nsis_installer_path)); #[cfg(target_os = "windows")] - let mut nsis_cmd = Command::new(_nsis_toolset_path.join("makensis.exe")); + let mut nsis_cmd = Command::new(nsis_toolset_path.join("makensis.exe")); #[cfg(not(target_os = "windows"))] let mut nsis_cmd = Command::new("makensis"); + if let Some(plugins_path) = &maybe_plugin_copy_path { + nsis_cmd.env("NSISPLUGINS", plugins_path); + } + nsis_cmd .args(["-INPUTCHARSET", "UTF8", "-OUTPUTCHARSET", "UTF8"]) .arg(match settings.log_level() { @@ -628,6 +719,9 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { let loader_path = dunce::simplified(&settings.project_out_directory().join("WebView2Loader.dll")).to_path_buf(); if loader_path.exists() { + if settings.can_sign() { + try_sign(&loader_path, settings)?; + } added_resources.push(loader_path.clone()); resources.insert( loader_path, @@ -650,6 +744,10 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } added_resources.push(resource_path.clone()); + if settings.can_sign() { + try_sign(&resource_path, settings)?; + } + let target_path = resource.target(); resources.insert( resource_path, diff --git a/crates/tauri-bundler/src/bundle/windows/sign.rs b/crates/tauri-bundler/src/bundle/windows/sign.rs index 61024436e..3c4cd9573 100644 --- a/crates/tauri-bundler/src/bundle/windows/sign.rs +++ b/crates/tauri-bundler/src/bundle/windows/sign.rs @@ -241,9 +241,9 @@ pub fn sign>(path: P, params: &SignParams) -> crate::Result<()> { } } -pub fn try_sign(file_path: &std::path::PathBuf, settings: &Settings) -> crate::Result<()> { +pub fn try_sign>(file_path: P, settings: &Settings) -> crate::Result<()> { if settings.can_sign() { - log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path)); + log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path.as_ref())); sign(file_path, &settings.sign_params())?; } Ok(()) diff --git a/crates/tauri-bundler/src/utils/fs_utils.rs b/crates/tauri-bundler/src/utils/fs_utils.rs index ef8233ed3..7527e6331 100644 --- a/crates/tauri-bundler/src/utils/fs_utils.rs +++ b/crates/tauri-bundler/src/utils/fs_utils.rs @@ -111,9 +111,6 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> { "{from:?} is not a Directory" ))); } - if to.exists() { - return Err(crate::Error::GenericError(format!("{to:?} already exists"))); - } let parent = to.parent().expect("No data in parent"); fs::create_dir_all(parent)?; for entry in walkdir::WalkDir::new(from) { @@ -129,7 +126,7 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> { symlink_file(&target, &dest_path)?; } } else if entry.file_type().is_dir() { - fs::create_dir(dest_path)?; + fs::create_dir_all(dest_path)?; } else { fs::copy(entry.path(), dest_path)?; } diff --git a/examples/api/src-tauri/build.rs b/examples/api/src-tauri/build.rs index 5e24a778f..441e6ea24 100644 --- a/examples/api/src-tauri/build.rs +++ b/examples/api/src-tauri/build.rs @@ -22,16 +22,20 @@ fn main() { ) .expect("failed to run tauri-build"); - // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests - // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - let target_env = std::env::var("CARGO_CFG_TARGET_ENV"); - let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true"); - if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() { - embed_manifest_for_tests(); + #[cfg(windows)] + { + // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests + // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV"); + let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true"); + if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() { + embed_manifest_for_tests(); + } } } +#[cfg(windows)] fn embed_manifest_for_tests() { static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml";