Repository: electron-userland/electron-builder (TypeScript)

Issue:
>26.0.1 gets stuck on Windows signing the squirrel exe
Before update (26.0.1) https://github.com/element-hq/element-desktop/actions/runs/13175306673/job/36773250686
After update to 26.0.6 https://github.com/element-hq/element-desktop/actions/runs/13196581336/job/36839196683 it errors when calling upon a vendored signtool.exe from electron-winstaller/electron-windows-sign - the signing we use via eSignerCKA requires a rather modern signtool so we prefer the system one by use of the `SIGNTOOL_PATH` envvar which electron-builder respects.

`electron-win-sign` however does not respect it, it instead reads `WINDOWS_SIGNTOOL_PATH` upon changing to which signing just seems to hang. https://github.com/element-hq/element-desktop/actions/runs/13197854827/job/36843095558

I'm surprised to see `D:\a\element-desktop\element-desktop\node_modules\electron-winstaller\vendor\signtool.exe` being called given e-b is passing a signing hookFunction so should be using its own signtool handling - https://github.com/electron-userland/electron-builder/blob/f4fc04acf3f206f01a5cf3030ff89684f136a0c0/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts#L144-L150

Maybe that if-statement isn't being entered due to the condition not doing the same thing as the old signing code - looks like other areas in the code just look at `options.sign` potentially https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/platformPackager.ts#L370


Discussion:
On a retry with debug logging enabled it seems to hang directly after signing the MSI https://github.com/element-hq/element-desktop/actions/runs/13200748516/job/36851975119
electron-builder run with debugging and crash from electron-windows-sign due to it not using e-b's `packager.sign`: https://github.com/element-hq/element-desktop/actions/runs/13204321507/job/36863804553

the reason I believe `WINDOWS_SIGNTOOL_PATH` to not work is because the other parameters (subject name & certificate sha) are not being passed by e-b as its relying on handling signing itself but fails to do so.
I've had to roll back to get code signing working again which is a shame as it means I lost ARM64 support. I will set up a better environment to test including code signing before the next e-b upgrade -= https://github.com/element-hq/element-desktop/pull/2131 should be that
@t3chguy Do you use a custom sign function? 
if CSC_LINK is used, electron-builder does have unit tests (UT) to ensure that signing can pass. 
```
test.ifAll.ifNotCiMac(
  "Squirrel.Windows",
  app(
    {
      targets: Platform.WINDOWS.createTarget(["squirrel"]),
      config: {
        win: {
          compression: "normal",
        },
        executableName: " test with spaces",
        electronFuses: {
          runAsNode: true,
          enableCookieEncryption: true,
          enableNodeOptionsEnvironmentVariable: true,
          enableNodeCliInspectArguments: true,
          enableEmbeddedAsarIntegrityValidation: true,
          onlyLoadAppFromAsar: true,
          loadBrowserProcessSpecificV8Snapshot: true,
          grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests
        },
      },
    },
    { signedWin: true }
  )
)
```

![Image](https://github.com/user-attachments/assets/543f8974-c7db-438f-908b-34f81f94ad1b)
> @t3chguy Do you use a custom sign function? 

No, we pass signtool options of subject Name and sha1. We also specify the signtool executable via environment as we need a modern one which is compatible with the eSignerCKA
This issue can be reproduced in the GitHub Action. However, signing works fine locally on Windows 11, and the problem cannot be reproduced there. The machines in GitHub Actions run on Windows Server 2022. It's still unclear why it fails in GitHub Actions, and it will take some time to investigate the cause.

The fact that the vendored signtool is being executed rather than the one specified in `SIGNTOOL_PATH` is issue enough. Based on the debug logs looks like the sign hook isn't being passed due to the if-statement not triggering.
However, when I tested it locally, I was able to use the `electron-builder` sign function without any issues. From the error logs, it seems that the process did enter the `electron-builder` sign function, but the failure was caused by an issue with the `win option` parameter at the end.
@beyondkmp if you look at https://github.com/element-hq/element-desktop/actions/runs/13204321507/job/36863804553#step:17:599 

```
...
• signing with signtool.exe  path=dist\Element Nightly 0.0.1-nightly.2025020701.msi
  • signing         file=dist\Element Nightly 0.0.1-nightly.2025020701.msi subject=OID.1.3.6.1.4.1.311.60.2.1.3=GB, OID.2.5.4.15=Private Organization, CN=NEW VECTOR LTD, SERIALNUMBER=10873661, O=NEW VECTOR LTD, L=London, C=GB thumbprint=ED8CD4DF9BD2ED1B68CD98F28333B33E755BB911 store=My user=current user
  • executing       file=C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe args=sign /tr http://timestamp.digicert.com/ /sha1 ED8CD4DF9BD2ED1B68CD98F28333B33E755BB911 /s My /fd sha256 /td sha256 /d Element Nightly /du https://element.io/ /debug D:\a\element-desktop\element-desktop\dist\Element Nightly 0.0.1-nightly.2025020701.msi env={}
  • executed        file=C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe stdout=
The following certificates were considered:
    Issued to: NEW VECTOR LTD

    Issued by: SSL.com EV Code Signing Intermediate CA RSA R3

    Expires:   Mon Mar 16 15:47:01 2026

    SHA1 hash: ED8CD4DF9BD2ED1B68CD98F28333B33E755BB911

                      After EKU filter, 1 certs were left.
After expiry filter, 1 certs were left.
After Hash filter, 1 certs were left.
After Private Key filter, 1 certs were left.
The following certificate was selected:
    Issued to: NEW VECTOR LTD

    Issued by: SSL.com EV Code Signing Intermediate CA RSA R3

    Expires:   Mon Mar 16 15:47:01 2026

    SHA1 hash: ED8CD4DF9BD2ED1B68CD98F28333B33E755BB911

                      Done Adding Additional Store
Successfully signed: D:\a\element-desktop\element-desktop\dist\Element Nightly 0.0.1-nightly.2025020701.msi

                      Number of files successfully Signed: 1

Number of warnings: 0

Number of errors: 0


  ⨯ Failed with exit code: 4294967295
Output:
System.AggregateException: One or more errors occurred. ---> System.Exception: Failed to sign, command invoked was: 'D:\a\element-desktop\element-desktop\node_modules\electron-winstaller\vendor\signtool.exe sign windows-sign C:\Users\runneradmin\AppData\Local\SquirrelTemp\tempa\lib\net45\Element Nightly_ExecutionStub.exe'
   at Squirrel.Update.Program.<signPEFile>d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
...
```

You can see a successful electron-builder sign using `C:/Program Files (x86)/Windows Kits/10/bin/10.0.22000.0/x86/signtool.exe` on `dist\Element Nightly 0.0.1-nightly.2025020701.msi`
Followed by a failure using `D:\a\element-desktop\element-desktop\node_modules\electron-winstaller\vendor\signtool.exe` on `C:\Users\runneradmin\AppData\Local\SquirrelTemp\tempa\lib\net45\Element Nightly_ExecutionStub.exe`

> From the error logs, it seems that the process did enter the electron-builder sign function

It looks like for squirrel signing this function is not entered as there's no `signing` log block for it.
https://github.com/electron/windows-installer/blob/e722a519f8625c939087de2602b2b5b594886bc4/src/sign.ts#L13

Based on the source code of the Windows installer, if the `windowsSign` parameter is set, it will replace the `signtool.exe` in the vendor directory with a fake sign tool to support this parameter. Currently, it seems that the replacement did not succeed on GitHub Actions, and the real `signtool.exe` is still being used, which does not support the `windowsSign` parameter, leading to the signing failure.

In my local tests, it succeeded because everything was on the same C drive. However, I noticed that on GitHub Actions, the data and `node_modules` are on different drives — one on the C drive and the other on the D drive. This difference might be the cause of the issue.
@beyondkmp where are you seeing the fake sign tool? I see it calling an earlier code path if `hookFunction` is passed: https://github.com/electron/windows-sign/blob/main/src/sign.ts#L37-L41
This looks to be similar if not the same as https://github.com/electron/windows-installer/issues/404 & more concretely https://github.com/electron/windows-installer/issues/508

Definitely seems like the bug is upstream of e-b but it is a monumental bug to be shipping either way. I'm going to look into a patch-package fix workaround in the coming week, as I do want the other benefits of the major upgrade.
@t3chguy https://github.com/electron/windows-sign/issues/12#issuecomment-1985409461

It seems that `hookFunction` cannot be used, and we can only use `hookModulePath`, which is more troublesome.

@beyondkmp what's the plan then? to switch to `hookModulePath` or maybe to try fix `hookFunction` upstream to work correctly given its more versatile?
I personally we should fix upstream, as I'm getting more involved with working with the electron `wg-ecosystem` group so I just need to understand how to set up an env for this issue to debug further locally.

Is there a minimum repro repo for this or do I need to use the full project you're using, the latter will prob be more difficult to set up on my arm64 Windows VM, so just wondering if we can consolidate the work to a minimum repro. Seems like your signing env might be something we'll need to repro locally with though?
I have eSignerCKA working with demo credentials so it should be easier to spin up a minimum repro, but I'm currently sick and have severe brain fog so don't think I'll be able to get you a minimum repro.

1. electron-builder 26.0.6 project with modern electron
2. Workflow like https://github.com/element-hq/element-desktop/blob/t3chguy/test-windows-signing/.github/workflows/build_windows.yaml#L120-L161
3. ENV `ED_SIGNTOOL_THUMBPRINT` & `ED_SIGNTOOL_SUBJECT_NAME` need passing to electron-builder via something like https://github.com/element-hq/element-desktop/blob/develop/electron-builder.ts#L185-L186

Hope that helps @mmaietta 
@t3chguy I'll try to test that out locally. Best wishes on getting better and feeling healthy soon!
@mmaietta oh almost forgot, we tell electron-builder to use the system signtool explicitly https://github.com/element-hq/element-desktop/blob/t3chguy/test-windows-signing/.github/workflows/build_windows.yaml#L39 as eSignerCKA needs a fairly modern signtool
Weird, I get `404 - page not found. Cannot find a valid ref in <path>` with the links
> It seems that hookFunction cannot be used, and we can only use hookModulePath, which is more troublesome.

Wait, I don't understand this. Looking at the code, it should just execute our hook, no additional logic
https://github.com/electron/windows-sign/blob/2419f57fb9ce432078855e3de6f610400e8588fd/src/sign-with-hook.ts#L37-L47
> > It seems that hookFunction cannot be used, and we can only use hookModulePath, which is more troublesome.
> 
> Wait, I don't understand this. Looking at the code, it should just execute our hook, no additional logic https://github.com/electron/windows-sign/blob/2419f57fb9ce432078855e3de6f610400e8588fd/src/sign-with-hook.ts#L37-L47


The `windows-installer` does not directly use the `window-sign` sign method for signing. Instead, it uses the `getCreateSeaSignTool` method(https://github.com/electron/windows-installer/blob/e722a519f8625c939087de2602b2b5b594886bc4/src/sign.ts#L74-L89), which first replaces the original `signtool.exe` with `node.exe`. This allows the execution of JavaScript code,  and then it uses Squirrel.Windows to run node.exe for signing.

Inside `sea.ts`, the `windowsSign` is directly serialized into JSON(https://github.com/electron/windows-sign/blob/2419f57fb9ce432078855e3de6f610400e8588fd/src/sea.ts#L107). If a `hookFunction` is passed, this JSON serialization will fail, causing the issue. However, if a `hookModulePath` is passed, it can be serialized into JSON, and the code inside it can be executed.


![Image](https://github.com/user-attachments/assets/86cab8d2-f417-40ab-a706-acd46bc9d88b)
@t3chguy can you try out this `patch-package` and see if it resolves your issue?
`electron-builder-squirrel-windows+26.0.6.patch`
```
diff --git a/node_modules/electron-builder-squirrel-windows/out/SquirrelWindowsTarget.js b/node_modules/electron-builder-squirrel-windows/out/SquirrelWindowsTarget.js
index 15312f4..07e8c52 100644
--- a/node_modules/electron-builder-squirrel-windows/out/SquirrelWindowsTarget.js
+++ b/node_modules/electron-builder-squirrel-windows/out/SquirrelWindowsTarget.js
@@ -93,24 +93,26 @@ class SquirrelWindowsTarget extends app_builder_lib_1.Target {
         const appInfo = packager.appInfo;
         // If not specified will use the Squirrel.Windows that is shipped with electron-installer(https://github.com/electron/windows-installer/tree/main/vendor)
         // After https://github.com/electron-userland/electron-builder-binaries/pull/56 merged, will add `electron-builder-binaries` to get the latest version of squirrel.
-        let vendorDirectory = this.options.customSquirrelVendorDir;
+        let vendorDirectory = this.options.customSquirrelVendorDir || path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor");
         if ((0, builder_util_1.isEmptyOrSpaces)(vendorDirectory) || !fs.existsSync(vendorDirectory)) {
             builder_util_1.log.warn({ vendorDirectory }, "unable to access Squirrel.Windows vendor directory, falling back to default electron-winstaller");
-            vendorDirectory = undefined;
+            vendorDirectory = path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor");
         }
         const options = {
             appDirectory: this.appDirectory,
             outputDirectory: this.outputDirectory,
             name: this.options.useAppIdAsId ? appInfo.id : this.appName,
+            title: appInfo.productName || appInfo.name,
             version: appInfo.version,
             description: appInfo.description,
-            exe: `${this.packager.platformSpecificBuildOptions.executableName || this.options.name || appInfo.productName}.exe`,
+            exe: `${appInfo.productFilename || this.options.name || appInfo.productName}.exe`,
             authors: appInfo.companyName || "",
             iconUrl,
             copyright: appInfo.copyright,
             vendorDirectory,
             nuspecTemplate: path.join(__dirname, "..", "template.nuspectemplate"),
             noMsi: !this.options.msi,
+            usePackageJson: false,
         };
         const projectUrl = await appInfo.computePackageUrl();
         if (projectUrl != null) {
@@ -121,13 +123,20 @@ class SquirrelWindowsTarget extends app_builder_lib_1.Target {
             await (0, promises_1.writeFile)(nuspecTemplate, templateContent);
             options.nuspecTemplate = nuspecTemplate;
         }
-        if (await (await packager.signingManager.value).cscInfo.value) {
-            options.windowsSign = {
-                hookFunction: async (file) => {
-                    await packager.sign(file);
-                },
-            };
+        const tmpVendorDirectory = await packager.info.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" });
+        // Copy entire vendor directory to temp directory
+        await fs.promises.cp(vendorDirectory, tmpVendorDirectory, { recursive: true });
+        builder_util_1.log.debug({ from: vendorDirectory, to: tmpVendorDirectory }, "copied vendor directory");
+        // Find and sign all executables in the temp vendor directory
+        const files = await fs.promises.readdir(tmpVendorDirectory);
+        for (const file of files) {
+            if (file.endsWith(".exe") || file.endsWith(".dll")) {
+                const filePath = path.join(tmpVendorDirectory, file);
+                builder_util_1.log.debug({ file: filePath }, "signing vendor executable");
+                await packager.sign(filePath);
+            }
         }
+        options.vendorDirectory = tmpVendorDirectory;
         if ((0, builder_util_1.isEmptyOrSpaces)(options.description)) {
             options.description = this.options.name || appInfo.productName;
         }
```
@mmaietta https://github.com/element-hq/element-desktop/actions/runs/13290420431/job/37109610542?pr=2132#step:17:1456

```
...
The following certificate was selected:
    Issued to: Esigner LLC

    Issued by: SSL.com EV Code Signing Intermediate CA RSA R2

    Expires:   Sat Jun 28 17:00:31 2025

    SHA1 hash: 247D3EE6F29D11968B33B29A23063F3721ADC3E4

                      Done Adding Additional Store
Successfully signed: C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\WriteZipToSetup.exe

                      Number of files successfully Signed: 1

Number of warnings: 0

Number of errors: 0


  • async task error  error=ENOENT: no such file or directory, copyfile 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z--ia32.exe' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z.exe'
  ⨯ ENOENT: no such file or directory, copyfile 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z--ia32.exe' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z.exe'  failedTask=build stackTrace=Error: ENOENT: no such file or directory, copyfile 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z--ia32.exe' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\t-7bl6yJ\squirrel-windows-vendor-3\7z.exe'
    at Object.copyFileSync (node:fs:3086:11)
    at SquirrelWindowsTarget.select7zipArch (D:\a\element-desktop\element-desktop\node_modules\electron-builder-squirrel-windows\src\SquirrelWindowsTarget.ts:90:8)
    at SquirrelWindowsTarget.build (D:\a\element-desktop\element-desktop\node_modules\electron-builder-squirrel-windows\src\SquirrelWindowsTarget.ts:46:12)
    at async Promise.all (index 0)
    at AsyncTaskManager.awaitTasks (D:\a\element-desktop\element-desktop\node_modules\builder-util\src\asyncTaskManager.ts:65:25)
    at Packager.doBuild (D:\a\element-desktop\element-desktop\node_modules\app-builder-lib\src\packager.ts:475:5)
    at executeFinally (D:\a\element-desktop\element-desktop\node_modules\builder-util\src\promise.ts:12:14)
    at Packager.build (D:\a\element-desktop\element-desktop\node_modules\app-builder-lib\src\packager.ts:393:31)
    at executeFinally (D:\a\element-desktop\element-desktop\node_modules\builder-util\src\promise.ts:12:14)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error: Process completed with exit code 1.
Though it did seemingly pass on the x64 build https://github.com/element-hq/element-desktop/actions/runs/13290932109/job/37111358677?pr=2132 - so the other error may be yet another major issue 🙈 

I loaded up the x64 zip from github actions into a Windows box and:

![Image](https://github.com/user-attachments/assets/89dd8218-7f22-4b89-991d-ea84ed0f12e0)

![Image](https://github.com/user-attachments/assets/9faa58f0-c344-4b4e-9d89-a7320340e2bb)

Looks like the Squirrel Setup file is missing signature

![Image](https://github.com/user-attachments/assets/f16481ba-3c29-45cc-930c-d0041dead611)

vs one made using older e-b

![Image](https://github.com/user-attachments/assets/6c250ba9-53ab-475e-91e0-88740c6572bf)

Also looks like the icon of the Setup file is incorrect, and the name is just `Setup` rather than `Element Setup <version>.exe` which is also concerning.

Concerningly, I also see a bunch of things getting signed which are not part of the package but rather look like build/packaging utilities:

![Image](https://github.com/user-attachments/assets/d5436511-6334-4f5b-b7b9-9f4cf5ec0d08)

eSigner costs per-signature so signing additional files would increase expenditure *significantly* given we do at least 1 build a day across 2 architectures and want to add arm64.

![Image](https://github.com/user-attachments/assets/cf39662d-3083-4188-bb10-fa55342df96a)
Oomph, yeah, we definitely can't cause that impact on signing costs. Thanks for calling it out!

CC @beyondkmp looks like we need an alternative way to handle this signing logic and/or open a PR upstream for allowing more custom logic flows (e.g. Azure signing) that can't leverage `electron/windows-sign` package
@mmaietta  https://github.com/electron/windows-installer/issues/548

However, based on my investigation, under the current architecture, implementing hookFunction in the upstream is not feasible. Moreover, Electron Forge also only supports hookModulePath.
@beyondkmp & @mmaietta thank you for persevering with this, great work.



Patch:
diff --git a/.changeset/beige-dolls-boil.md b/.changeset/beige-dolls-boil.md
new file mode 100644
index 00000000000..2cc2eba61da
--- /dev/null
+++ b/.changeset/beige-dolls-boil.md
@@ -0,0 +1,5 @@
+---
+"electron-builder-squirrel-windows": patch
+---
+
+Sign the vendor directory instead of using Squirrel.Windows' signing method
diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts
index 6e23c93778b..0d83498b26e 100644
--- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts
+++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts
@@ -1,17 +1,14 @@
 import { InvalidConfigurationError, log, isEmptyOrSpaces } from "builder-util"
-import { Arch, getArchSuffix, SquirrelWindowsOptions, Target } from "app-builder-lib"
-import { WinPackager } from "app-builder-lib/out/winPackager"
 import { sanitizeFileName } from "builder-util/out/filename"
+import { Arch, getArchSuffix, SquirrelWindowsOptions, Target, WinPackager } from "app-builder-lib"
 import * as path from "path"
 import * as fs from "fs"
-import { readFile, writeFile } from "fs/promises"
+import * as os from "os"
 import { Options as SquirrelOptions, createWindowsInstaller, convertVersion } from "electron-winstaller"
 
 export default class SquirrelWindowsTarget extends Target {
   //tslint:disable-next-line:no-object-literal-type-assertion
   readonly options: SquirrelWindowsOptions = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config.squirrelWindows } as SquirrelWindowsOptions
-  private appDirectory: string = ""
-  private outputDirectory: string = ""
 
   constructor(
     private readonly packager: WinPackager,
@@ -20,6 +17,32 @@ export default class SquirrelWindowsTarget extends Target {
     super("squirrel")
   }
 
+  private async prepareSignedVendorDirectory(): Promise<string> {
+    // If not specified will use the Squirrel.Windows that is shipped with electron-installer(https://github.com/electron/windows-installer/tree/main/vendor)
+    // After https://github.com/electron-userland/electron-builder-binaries/pull/56 merged, will add `electron-builder-binaries` to get the latest version of squirrel.
+    let vendorDirectory = this.options.customSquirrelVendorDir || path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor")
+    if (isEmptyOrSpaces(vendorDirectory) || !fs.existsSync(vendorDirectory)) {
+      log.warn({ vendorDirectory }, "unable to access Squirrel.Windows vendor directory, falling back to default electron-winstaller")
+      vendorDirectory = path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor")
+    }
+
+    const tmpVendorDirectory = await this.packager.info.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" })
+    // Copy entire vendor directory to temp directory
+    await fs.promises.cp(vendorDirectory, tmpVendorDirectory, { recursive: true })
+    log.debug({ from: vendorDirectory, to: tmpVendorDirectory }, "copied vendor directory")
+
+    const files = await fs.promises.readdir(tmpVendorDirectory)
+    for (const file of files) {
+      if (["Squirrel.exe", "StubExecutable.exe"].includes(file)) {
+        const filePath = path.join(tmpVendorDirectory, file)
+        log.debug({ file: filePath }, "signing vendor executable")
+        await this.packager.sign(filePath)
+      }
+    }
+
+    return tmpVendorDirectory
+  }
+
   async build(appOutDir: string, arch: Arch) {
     const packager = this.packager
     const version = packager.appInfo.version
@@ -28,6 +51,7 @@ export default class SquirrelWindowsTarget extends Target {
     const setupFile = packager.expandArtifactNamePattern(this.options, "exe", arch, "${productName} Setup ${version}.${ext}")
     const installerOutDir = path.join(this.outDir, `squirrel-windows${getArchSuffix(arch)}`)
     const artifactPath = path.join(installerOutDir, setupFile)
+    const msiArtifactPath = path.join(installerOutDir, packager.expandArtifactNamePattern(this.options, "msi", arch, "${productName} Setup ${version}.${ext}"))
 
     await packager.info.callArtifactBuildStarted({
       targetPresentableName: "Squirrel.Windows",
@@ -35,27 +59,34 @@ export default class SquirrelWindowsTarget extends Target {
       arch,
     })
 
-    if (arch === Arch.ia32) {
-      log.warn("For windows consider only distributing 64-bit or use nsis target, see https://github.com/electron-userland/electron-builder/issues/359#issuecomment-214851130")
-    }
+    const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFile)
+    await createWindowsInstaller(distOptions)
 
-    this.appDirectory = appOutDir
-    this.outputDirectory = installerOutDir
-    const distOptions = await this.computeEffectiveDistOptions()
-    if (distOptions.vendorDirectory) {
-      this.select7zipArch(distOptions.vendorDirectory, arch)
+    await packager.signAndEditResources(artifactPath, arch, installerOutDir)
+    if (this.options.msi) {
+      await packager.sign(msiArtifactPath)
     }
 
-    await createWindowsInstaller(distOptions)
+    const safeArtifactName = (ext: string) => `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.${ext}`
 
     await packager.info.callArtifactBuildCompleted({
       file: artifactPath,
       target: this,
       arch,
-      safeArtifactName: `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.exe`,
+      safeArtifactName: safeArtifactName("exe"),
       packager: this.packager,
     })
 
+    if (this.options.msi) {
+      await packager.info.callArtifactBuildCompleted({
+        file: msiArtifactPath,
+        target: this,
+        arch,
+        safeArtifactName: safeArtifactName("msi"),
+        packager: this.packager,
+      })
+    }
+
     const packagePrefix = `${this.appName}-${convertVersion(version)}-`
     packager.info.dispatchArtifactCreated({
       file: path.join(installerOutDir, `${packagePrefix}full.nupkg`),
@@ -84,14 +115,30 @@ export default class SquirrelWindowsTarget extends Target {
     return this.options.name || this.packager.appInfo.name
   }
 
-  private select7zipArch(vendorDirectory: string, arch: Arch) {
-    // Copy the 7-Zip executable for the configured architecture.
-    const resolvedArch = getArchSuffix(arch) === "" ? process.arch : getArchSuffix(arch)
+  private select7zipArch(vendorDirectory: string) {
+    // https://github.com/electron/windows-installer/blob/main/script/select-7z-arch.js
+    // Even if we're cross-compiling for a different arch like arm64,
+    // we still need to use the 7-Zip executable for the host arch
+    const resolvedArch = os.arch
     fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.exe`), path.join(vendorDirectory, "7z.exe"))
     fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.dll`), path.join(vendorDirectory, "7z.dll"))
   }
 
-  async computeEffectiveDistOptions(): Promise<SquirrelOptions> {
+  private async createNuspecTemplateWithProjectUrl() {
+    const templatePath = path.resolve(__dirname, "..", "template.nuspectemplate")
+    const projectUrl = await this.packager.appInfo.computePackageUrl()
+    if (projectUrl != null) {
+      const nuspecTemplate = await this.packager.info.tempDirManager.getTempFile({ prefix: "template", suffix: ".nuspectemplate" })
+      let templateContent = await fs.promises.readFile(templatePath, "utf8")
+      const searchString = "<copyright><%- copyright %></copyright>"
+      templateContent = templateContent.replace(searchString, `${searchString}\n    <projectUrl>${projectUrl}</projectUrl>`)
+      await fs.promises.writeFile(nuspecTemplate, templateContent)
+      return nuspecTemplate
+    }
+    return templatePath
+  }
+
+  async computeEffectiveDistOptions(appDirectory: string, outputDirectory: string, setupFile: string): Promise<SquirrelOptions> {
     const packager = this.packager
     let iconUrl = this.options.iconUrl
     if (iconUrl == null) {
@@ -106,47 +153,29 @@ export default class SquirrelWindowsTarget extends Target {
     }
 
     checkConflictingOptions(this.options)
-
     const appInfo = packager.appInfo
-    // If not specified will use the Squirrel.Windows that is shipped with electron-installer(https://github.com/electron/windows-installer/tree/main/vendor)
-    // After https://github.com/electron-userland/electron-builder-binaries/pull/56 merged, will add `electron-builder-binaries` to get the latest version of squirrel.
-    let vendorDirectory = this.options.customSquirrelVendorDir
-    if (isEmptyOrSpaces(vendorDirectory) || !fs.existsSync(vendorDirectory)) {
-      log.warn({ vendorDirectory }, "unable to access Squirrel.Windows vendor directory, falling back to default electron-winstaller")
-      vendorDirectory = undefined
-    }
-
     const options: SquirrelOptions = {
-      appDirectory: this.appDirectory,
-      outputDirectory: this.outputDirectory,
+      appDirectory: appDirectory,
+      outputDirectory: outputDirectory,
       name: this.options.useAppIdAsId ? appInfo.id : this.appName,
+      title: appInfo.productName || appInfo.name,
       version: appInfo.version,
       description: appInfo.description,
-      exe: `${this.packager.platformSpecificBuildOptions.executableName || this.options.name || appInfo.productName}.exe`,
+      exe: `${appInfo.productFilename || this.options.name || appInfo.productName}.exe`,
       authors: appInfo.companyName || "",
+      nuspecTemplate: await this.createNuspecTemplateWithProjectUrl(),
       iconUrl,
       copyright: appInfo.copyright,
-      vendorDirectory,
-      nuspecTemplate: path.join(__dirname, "..", "template.nuspectemplate"),
       noMsi: !this.options.msi,
+      usePackageJson: false,
     }
 
-    const projectUrl = await appInfo.computePackageUrl()
-    if (projectUrl != null) {
-      const nuspecTemplate = await this.packager.info.tempDirManager.getTempFile({ prefix: "template", suffix: ".nuspectemplate" })
-      let templateContent = await readFile(path.resolve(__dirname, "..", "template.nuspectemplate"), "utf8")
-      const searchString = "<copyright><%- copyright %></copyright>"
-      templateContent = templateContent.replace(searchString, `${searchString}\n    <projectUrl>${projectUrl}</projectUrl>`)
-      await writeFile(nuspecTemplate, templateContent)
-      options.nuspecTemplate = nuspecTemplate
-    }
-
-    if (await (await packager.signingManager.value).cscInfo.value) {
-      options.windowsSign = {
-        hookFunction: async (file: string) => {
-          await packager.sign(file)
-        },
-      }
+    options.vendorDirectory = await this.prepareSignedVendorDirectory()
+    this.select7zipArch(options.vendorDirectory)
+    options.fixUpPaths = true
+    options.setupExe = setupFile
+    if (this.options.msi) {
+      options.setupMsi = setupFile.replace(".exe", ".msi")
     }
 
     if (isEmptyOrSpaces(options.description)) {


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 95e37acbaad..fc67b27a5e7 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -169,7 +169,7 @@ jobs:
       fail-fast: false
       matrix:
         testFiles:
-          - winCodeSignTest,differentialUpdateTest
+          - winCodeSignTest,differentialUpdateTest,squirrelWindowsTest
           - appxTest,msiTest,portableTest,assistedInstallerTest,protonTest
           - BuildTest,oneClickInstallerTest,winPackagerTest,nsisUpdaterTest,webInstallerTest
     steps:
diff --git a/test/snapshots/windows/squirrelWindowsTest.js.snap b/test/snapshots/windows/squirrelWindowsTest.js.snap
index fd8244e4c63..065e4078b90 100644
--- a/test/snapshots/windows/squirrelWindowsTest.js.snap
+++ b/test/snapshots/windows/squirrelWindowsTest.js.snap
@@ -22,6 +22,8 @@ exports[`Squirrel.Windows 1`] = `
 
 exports[`Squirrel.Windows 2`] = `
 [
+  "lib/",
+  "lib/net45/",
   "lib/net45/chrome_100_percent.pak",
   "lib/net45/chrome_200_percent.pak",
   "lib/net45/d3dcompiler_47.dll",
@@ -33,19 +35,23 @@ exports[`Squirrel.Windows 2`] = `
   "lib/net45/LICENSES.chromium.html",
   "lib/net45/resources.pak",
   "lib/net45/snapshot_blob.bin",
-  "lib/net45/Test App ßW.exe",
-  "lib/net45/Test App ßW_ExecutionStub.exe",
-  "lib/net45/Update.exe",
+  "lib/net45/squirrel.exe",
+  "lib/net45/test with spaces.exe",
+  "lib/net45/test with spaces_ExecutionStub.exe",
   "lib/net45/v8_context_snapshot.bin",
   "lib/net45/vk_swiftshader.dll",
   "lib/net45/vk_swiftshader_icd.json",
   "lib/net45/vulkan-1.dll",
   "lib/net45/locales/en-US.pak",
+  "lib/net45/resources/",
   "lib/net45/resources/app.asar",
-  "lib/net45/swiftshader/libEGL.dll",
-  "lib/net45/swiftshader/libGLESv2.dll",
+  "package/",
+  "package/services/",
+  "package/services/metadata/",
+  "package/services/metadata/core-properties/",
   "TestApp.nuspec",
   "[Content_Types].xml",
+  "_rels/",
   "_rels/.rels",
 ]
 `;
@@ -77,6 +83,190 @@ exports[`artifactName 1`] = `
 
 exports[`artifactName 2`] = `
 [
+  "lib/",
+  "lib/net45/",
+  "lib/net45/chrome_100_percent.pak",
+  "lib/net45/chrome_200_percent.pak",
+  "lib/net45/d3dcompiler_47.dll",
+  "lib/net45/ffmpeg.dll",
+  "lib/net45/icudtl.dat",
+  "lib/net45/libEGL.dll",
+  "lib/net45/libGLESv2.dll",
+  "lib/net45/LICENSE.electron.txt",
+  "lib/net45/LICENSES.chromium.html",
+  "lib/net45/resources.pak",
+  "lib/net45/snapshot_blob.bin",
+  "lib/net45/squirrel.exe",
+  "lib/net45/Test App ßW.exe",
+  "lib/net45/Test App ßW_ExecutionStub.exe",
+  "lib/net45/v8_context_snapshot.bin",
+  "lib/net45/vk_swiftshader.dll",
+  "lib/net45/vk_swiftshader_icd.json",
+  "lib/net45/vulkan-1.dll",
+  "lib/net45/locales/en-US.pak",
+  "lib/net45/resources/",
+  "lib/net45/resources/app.asar",
+  "package/",
+  "package/services/",
+  "package/services/metadata/",
+  "package/services/metadata/core-properties/",
+  "TestApp.nuspec",
+  "[Content_Types].xml",
+  "_rels/",
+  "_rels/.rels",
+]
+`;
+
+exports[`squirrel window arm64 msi 1`] = `
+{
+  "win": [
+    {
+      "arch": "arm64",
+      "file": "RELEASES",
+    },
+    {
+      "arch": "arm64",
+      "file": "Test App ßW Setup 1.1.0.exe",
+      "safeArtifactName": "TestApp-Setup-1.1.0-arm64.exe",
+    },
+    {
+      "arch": "arm64",
+      "file": "Test App ßW Setup 1.1.0.msi",
+      "safeArtifactName": "TestApp-Setup-1.1.0-arm64.msi",
+    },
+    {
+      "arch": "arm64",
+      "file": "TestApp-1.1.0-full.nupkg",
+    },
+  ],
+}
+`;
+
+exports[`squirrel window arm64 msi 2`] = `
+[
+  "lib/",
+  "lib/net45/",
+  "lib/net45/chrome_100_percent.pak",
+  "lib/net45/chrome_200_percent.pak",
+  "lib/net45/ffmpeg.dll",
+  "lib/net45/icudtl.dat",
+  "lib/net45/libEGL.dll",
+  "lib/net45/libGLESv2.dll",
+  "lib/net45/LICENSE.electron.txt",
+  "lib/net45/LICENSES.chromium.html",
+  "lib/net45/resources.pak",
+  "lib/net45/snapshot_blob.bin",
+  "lib/net45/squirrel.exe",
+  "lib/net45/Test App ßW.exe",
+  "lib/net45/Test App ßW_ExecutionStub.exe",
+  "lib/net45/v8_context_snapshot.bin",
+  "lib/net45/vk_swiftshader.dll",
+  "lib/net45/vk_swiftshader_icd.json",
+  "lib/net45/vulkan-1.dll",
+  "lib/net45/locales/en-US.pak",
+  "lib/net45/resources/",
+  "lib/net45/resources/app.asar",
+  "package/",
+  "package/services/",
+  "package/services/metadata/",
+  "package/services/metadata/core-properties/",
+  "TestApp.nuspec",
+  "[Content_Types].xml",
+  "_rels/",
+  "_rels/.rels",
+]
+`;
+
+exports[`squirrel window ia32 msi 1`] = `
+{
+  "win": [
+    {
+      "arch": "ia32",
+      "file": "RELEASES",
+    },
+    {
+      "arch": "ia32",
+      "file": "Test App ßW Setup 1.1.0.exe",
+      "safeArtifactName": "TestApp-Setup-1.1.0-ia32.exe",
+    },
+    {
+      "arch": "ia32",
+      "file": "Test App ßW Setup 1.1.0.msi",
+      "safeArtifactName": "TestApp-Setup-1.1.0-ia32.msi",
+    },
+    {
+      "arch": "ia32",
+      "file": "TestApp-1.1.0-full.nupkg",
+    },
+  ],
+}
+`;
+
+exports[`squirrel window ia32 msi 2`] = `
+[
+  "lib/",
+  "lib/net45/",
+  "lib/net45/chrome_100_percent.pak",
+  "lib/net45/chrome_200_percent.pak",
+  "lib/net45/d3dcompiler_47.dll",
+  "lib/net45/ffmpeg.dll",
+  "lib/net45/icudtl.dat",
+  "lib/net45/libEGL.dll",
+  "lib/net45/libGLESv2.dll",
+  "lib/net45/LICENSE.electron.txt",
+  "lib/net45/LICENSES.chromium.html",
+  "lib/net45/resources.pak",
+  "lib/net45/snapshot_blob.bin",
+  "lib/net45/squirrel.exe",
+  "lib/net45/Test App ßW.exe",
+  "lib/net45/Test App ßW_ExecutionStub.exe",
+  "lib/net45/v8_context_snapshot.bin",
+  "lib/net45/vk_swiftshader.dll",
+  "lib/net45/vk_swiftshader_icd.json",
+  "lib/net45/vulkan-1.dll",
+  "lib/net45/locales/en-US.pak",
+  "lib/net45/resources/",
+  "lib/net45/resources/app.asar",
+  "package/",
+  "package/services/",
+  "package/services/metadata/",
+  "package/services/metadata/core-properties/",
+  "TestApp.nuspec",
+  "[Content_Types].xml",
+  "_rels/",
+  "_rels/.rels",
+]
+`;
+
+exports[`squirrel window x64 msi 1`] = `
+{
+  "win": [
+    {
+      "arch": "x64",
+      "file": "RELEASES",
+    },
+    {
+      "arch": "x64",
+      "file": "Test App ßW Setup 1.1.0.exe",
+      "safeArtifactName": "TestApp-Setup-1.1.0.exe",
+    },
+    {
+      "arch": "x64",
+      "file": "Test App ßW Setup 1.1.0.msi",
+      "safeArtifactName": "TestApp-Setup-1.1.0.msi",
+    },
+    {
+      "arch": "x64",
+      "file": "TestApp-1.1.0-full.nupkg",
+    },
+  ],
+}
+`;
+
+exports[`squirrel window x64 msi 2`] = `
+[
+  "lib/",
+  "lib/net45/",
   "lib/net45/chrome_100_percent.pak",
   "lib/net45/chrome_200_percent.pak",
   "lib/net45/d3dcompiler_47.dll",
@@ -88,19 +278,23 @@ exports[`artifactName 2`] = `
   "lib/net45/LICENSES.chromium.html",
   "lib/net45/resources.pak",
   "lib/net45/snapshot_blob.bin",
+  "lib/net45/squirrel.exe",
   "lib/net45/Test App ßW.exe",
   "lib/net45/Test App ßW_ExecutionStub.exe",
-  "lib/net45/Update.exe",
   "lib/net45/v8_context_snapshot.bin",
   "lib/net45/vk_swiftshader.dll",
   "lib/net45/vk_swiftshader_icd.json",
   "lib/net45/vulkan-1.dll",
   "lib/net45/locales/en-US.pak",
+  "lib/net45/resources/",
   "lib/net45/resources/app.asar",
-  "lib/net45/swiftshader/libEGL.dll",
-  "lib/net45/swiftshader/libGLESv2.dll",
+  "package/",
+  "package/services/",
+  "package/services/metadata/",
+  "package/services/metadata/core-properties/",
   "TestApp.nuspec",
   "[Content_Types].xml",
+  "_rels/",
   "_rels/.rels",
 ]
 `;
diff --git a/test/src/helpers/CheckingPackager.ts b/test/src/helpers/CheckingPackager.ts
index c039dcd5ca4..e3a3faf87fd 100644
--- a/test/src/helpers/CheckingPackager.ts
+++ b/test/src/helpers/CheckingPackager.ts
@@ -1,12 +1,12 @@
 import { SignOptions as MacSignOptions } from "@electron/osx-sign/dist/cjs/types"
 import { Identity } from "app-builder-lib/out/codeSign/macCodeSign"
-import { MacPackager } from "app-builder-lib/out/macPackager"
 import { DoPackOptions } from "app-builder-lib/out/platformPackager"
-import { WinPackager } from "app-builder-lib/out/winPackager"
+import { WinPackager, getArchSuffix, MacPackager } from "app-builder-lib"
 import { AsyncTaskManager } from "builder-util"
 import { DmgTarget } from "dmg-builder"
 import { Arch, MacConfiguration, Packager, Target } from "electron-builder"
 import SquirrelWindowsTarget from "electron-builder-squirrel-windows"
+import * as path from "path"
 
 export class CheckingWinPackager extends WinPackager {
   effectiveDistOptions: any
@@ -19,8 +19,10 @@ export class CheckingWinPackager extends WinPackager {
   async pack(outDir: string, arch: Arch, targets: Array<Target>, taskManager: AsyncTaskManager): Promise<any> {
     // skip pack
     const helperClass: typeof SquirrelWindowsTarget = require("electron-builder-squirrel-windows").default
-    this.effectiveDistOptions = await new helperClass(this, outDir).computeEffectiveDistOptions()
-
+    const newClass = new helperClass(this, outDir)
+    const setupFile = this.expandArtifactNamePattern(newClass.options, "exe", arch, "${productName} Setup ${version}.${ext}")
+    const installerOutDir = path.join(outDir, `squirrel-windows${getArchSuffix(arch)}`)
+    this.effectiveDistOptions = await newClass.computeEffectiveDistOptions(installerOutDir, outDir, setupFile)
     await this.sign(this.computeAppOutDir(outDir, arch))
   }
 
diff --git a/test/src/windows/squirrelWindowsTest.ts b/test/src/windows/squirrelWindowsTest.ts
index 25cb0a7a601..f87342006a7 100644
--- a/test/src/windows/squirrelWindowsTest.ts
+++ b/test/src/windows/squirrelWindowsTest.ts
@@ -12,7 +12,7 @@ test.ifAll.ifNotCiMac(
         win: {
           compression: "normal",
         },
-        executableName: " test with spaces",
+        executableName: "test with spaces",
         electronFuses: {
           runAsNode: true,
           enableCookieEncryption: true,
@@ -56,6 +56,51 @@ test.skip(
   })
 )
 
+test.ifAll(
+  "squirrel window arm64 msi",
+  app(
+    {
+      targets: Platform.WINDOWS.createTarget("squirrel", Arch.arm64),
+      config: {
+        squirrelWindows: {
+          msi: true,
+        },
+      },
+    },
+    { signedWin: true }
+  )
+)
+
+test.ifAll(
+  "squirrel window x64 msi",
+  app(
+    {
+      targets: Platform.WINDOWS.createTarget("squirrel", Arch.x64),
+      config: {
+        squirrelWindows: {
+          msi: true,
+        },
+      },
+    },
+    { signedWin: true }
+  )
+)
+
+test.ifAll(
+  "squirrel window ia32 msi",
+  app(
+    {
+      targets: Platform.WINDOWS.createTarget("squirrel", Arch.ia32),
+      config: {
+        squirrelWindows: {
+          msi: true,
+        },
+      },
+    },
+    { signedWin: true }
+  )
+)
+
 test.ifAll("detect install-spinner", () => {
   let platformPackager: CheckingWinPackager | null = null
   let loadingGifPath: string | null = null