Skip to content

Commit

Permalink
feat: Allow @electron/windows-sign to take over Squirrel codesigning (#…
Browse files Browse the repository at this point in the history
…501)

* feat: Allow @electron/windows-sign to take over Squirrel codesigning

* test: Remove test for Node 14

* fix: Tests, correct node version

* fix: We actually need Node v20

* fix: Update @electron/windows-sign

* build: Update node-orb

* docs: Add documentation
  • Loading branch information
felixrieseberg authored Mar 4, 2024
1 parent b133e78 commit 598aa70
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 297 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ lib/
.idea/
npm-debug.log
SquirrelSetup.log
electron-windows-sign.log
receiver.mjs
signtool-original.exe
Squirrel-Releasify.log
.node-version
.DS_Store
spec/fixtures/app/Update.exe
vendor/7z.dll
vendor/7z.exe
hook.log
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ There are several configuration settings supported:
| `remoteReleases` | No | A URL to your existing updates. If given, these will be downloaded to create delta updates |
| `remoteToken` | No | Authentication token for remote updates |
| `frameworkVersion` | No | Set the required .NET framework version, e.g. `net461` |
| `windowsSign` | No | Use [@electron/windows-sign][@electron/windows-sign] for advanced codesigning. See [documentation](#advanced-codesigning-with-electronwindows-sign) for details. |

## Sign your installer or else bad things will happen

Expand Down Expand Up @@ -169,10 +170,22 @@ function handleSquirrelEvent() {

Notice that the first time the installer launches your app, your app will see a `--squirrel-firstrun` flag. This allows you to do things like showing up a splash screen or presenting a settings UI. Another thing to be aware of is that, since the app is spawned by squirrel and squirrel acquires a file lock during installation, you won't be able to successfully check for app updates till a few seconds later when squirrel releases the lock.

## Advanced codesigning with [@electron/windows-sign][@electron/windows-sign]

This package supports two different ways to codesign your application and the installer:

1) Modern: By passing a `windowsSign` option, which will be passed to [@electron/windows-sign]. This method allows full customization of the code-signing process - and supports more complicated scenarios like cloud-hosted EV certificates, custom sign pipelines, and per-file overrides. It also supports all existing "simple" codesigning scenarios, including just passing a certificate file and password. Please see https://github.com/@electron/windows-sign for all possible configuration options.

When passing `windowsSign`, do not pass any other available parameters at the top level (like `certificateFile`, `certificatePassword`, or `signWithParams`).

2) Legacy: By passing the top-level settings (`certificateFile`, `certificatePassword`, and `signWithParams`). For simple codesigning scenarios, there's no reason not to use this method - it'll work just as fine as the modern method.

## Debugging this package

You can get debug messages from this package by running with the environment variable `DEBUG=electron-windows-installer:main` e.g.

```shell
DEBUG=electron-windows-installer:main node tasks/electron-winstaller.js
```

[@electron/windows-sign]: https://github.com/electron/windows-sign/
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"build": "tsc",
"prepublish": "npm run build",
"lint": "eslint --ext .ts src spec",
"test": "npm run lint && ava --timeout=30s",
"ava": "ava --timeout=60s",
"test": "npm run lint && npm run ava",
"tdd": "ava --watch"
},
"dependencies": {
Expand All @@ -46,6 +47,9 @@
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
},
"optionalDependencies": {
"@electron/windows-sign": "^1.1.2"
},
"engines": {
"node": ">=8.0.0"
},
Expand Down
12 changes: 12 additions & 0 deletions spec/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import path from 'path';
import fs from 'fs-extra';

import { createTempDir } from '../../src/temp-utils';

export const FIXTURE_APP_DIR = path.join(__dirname, '../fixtures/app');

export async function createTempAppDirectory(): Promise<string> {
const appDirectory = await createTempDir('electron-winstaller-ad-');
await fs.copy(FIXTURE_APP_DIR, appDirectory);
return appDirectory;
}
8 changes: 8 additions & 0 deletions spec/helpers/windowsSignHook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const fs = require('fs-extra');
const path = require('path');

module.exports = function(args) {
console.log(...args);

fs.appendFileSync(path.join(__dirname, 'hook.log'), `${JSON.stringify(args)}\n`);
};
8 changes: 1 addition & 7 deletions spec/installer-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { createTempDir } from '../src/temp-utils';
import fs from 'fs-extra';
import { createWindowsInstaller } from '../src';
import spawn from '../src/spawn-promise';
import { createTempAppDirectory } from './helpers/helpers';

const log = require('debug')('electron-windows-installer:spec');

const fixtureAppDirectory = path.join(__dirname, 'fixtures/app');

function spawn7z(args: string[]): Promise<string> {
const sevenZipPath = path.join(__dirname, '..', 'vendor', '7z.exe');
const wineExe = ['arm64', 'x64'].includes(process.arch) ? 'wine64' : 'wine';
Expand All @@ -17,11 +16,6 @@ function spawn7z(args: string[]): Promise<string> {
: spawn(sevenZipPath, args);
}

async function createTempAppDirectory(): Promise<string> {
const appDirectory = await createTempDir('electron-winstaller-ad-');
await fs.copy(fixtureAppDirectory, appDirectory);
return appDirectory;
}

test.serial('creates a nuget package and installer', async (t): Promise<void> => {
const outputDirectory = await createTempDir('ei-');
Expand Down
48 changes: 48 additions & 0 deletions spec/sign-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import test from 'ava';
import path from 'path';
import { createTempDir } from '../src/temp-utils';
import fs from 'fs-extra';
import { createWindowsInstaller } from '../src';
import { createTempAppDirectory } from './helpers/helpers';
import { SignToolOptions } from '@electron/windows-sign';
import semver from 'semver';

const log = require('debug')('electron-windows-installer:spec');

if (process.platform === 'win32' && semver.gte(process.version, '20.0.0')) {
test.serial('creates a signtool.exe and uses it to sign', async (t): Promise<void> => {

const outputDirectory = await createTempDir('ei-');
const appDirectory = await createTempAppDirectory();
const hookLogPath = path.join(__dirname, './helpers/hook.log');
const hookModulePath = path.join(__dirname, './helpers/windowsSignHook.js');
const windowsSign: SignToolOptions = { hookModulePath };
const options = { appDirectory, outputDirectory, windowsSign };

// Reset
await fs.remove(hookLogPath);

// Test
await createWindowsInstaller(options);

log(`Verifying assertions on ${outputDirectory}`);
log(JSON.stringify(await fs.readdir(outputDirectory)));

const nupkgPath = path.join(outputDirectory, 'myapp-1.0.0-full.nupkg');

t.true(await fs.pathExists(nupkgPath));
t.true(await fs.pathExists(path.join(outputDirectory, 'MyAppSetup.exe')));

if (process.platform === 'win32') {
t.true(await fs.pathExists(path.join(outputDirectory, 'MyAppSetup.msi')));
}

log('Verifying Update.exe');
t.true(await fs.pathExists(path.join(appDirectory, 'Squirrel.exe')));

log('Verifying that our hook got to "sign" all files');
const hookLog = await fs.readFile(hookLogPath, { encoding: 'utf8' });
const filesLogged = hookLog.split('\n').filter(v => !!v.trim()).length;
t.is(filesLogged, 8);
});
}
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as os from 'os';
import { exec } from 'child_process';
import spawn from './spawn-promise';
import template from 'lodash.template';
import { createSignTool, resetSignTool } from './sign';

export { SquirrelWindowsOptions } from './options';
export { SquirrelWindowsOptions as Options} from './options';
Expand Down Expand Up @@ -82,7 +83,7 @@ export async function createWindowsInstaller(options: SquirrelWindowsOptions): P
const defaultLoadingGif = path.join(__dirname, '..', 'resources', 'install-spinner.gif');
loadingGif = loadingGif ? path.resolve(loadingGif) : defaultLoadingGif;

const { certificateFile, certificatePassword, remoteReleases, signWithParams, remoteToken } = options;
const { certificateFile, certificatePassword, remoteReleases, signWithParams, remoteToken, windowsSign } = options;

const metadata: Metadata = {
description: '',
Expand Down Expand Up @@ -193,6 +194,8 @@ export async function createWindowsInstaller(options: SquirrelWindowsOptions): P
cmd = monoExe;
}

// Legacy codesign options
await resetSignTool();
if (signWithParams) {
args.push('--signWithParams');
if (!signWithParams.includes('/f') && !signWithParams.includes('/p') && certificateFile && certificatePassword) {
Expand All @@ -203,6 +206,11 @@ export async function createWindowsInstaller(options: SquirrelWindowsOptions): P
} else if (certificateFile && certificatePassword) {
args.push('--signWithParams');
args.push(`/a /f "${path.resolve(certificateFile)}" /p "${certificatePassword}"`);
// @electron/windows-sign options
} else if (windowsSign) {
args.push('--signWithParams');
args.push('windows-sign');
await createSignTool(options);
}

if (options.setupIcon) {
Expand Down Expand Up @@ -244,4 +252,6 @@ export async function createWindowsInstaller(options: SquirrelWindowsOptions): P
}
}
}

await resetSignTool();
}
30 changes: 29 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Original definitions by: Brendan Forster <https://github.com/shiftkey>, Daniel Perez Alvarez <https://github.com/unindented>
// Original definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

import { SignToolOptions } from '@electron/windows-sign';

export interface SquirrelWindowsOptions {
/**
* The folder path of your Electron app
Expand Down Expand Up @@ -72,17 +74,29 @@ export interface SquirrelWindowsOptions {
*/
name?: string;
/**
* The path to an Authenticode Code Signing Certificate
* The path to an Authenticode Code Signing Certificate.
*
* This is a legacy parameter provided for backwards compatibility.
* For more comprehensive support of various codesigning scenarios
* like EV certificates, see the "windowsSign" parameter.
*/
certificateFile?: string;
/**
* The password to decrypt the certificate given in `certificateFile`
*
* This is a legacy parameter provided for backwards compatibility.
* For more comprehensive support of various codesigning scenarios
* like EV certificates, see the "windowsSign" parameter.
*/
certificatePassword?: string;
/**
* Params to pass to signtool.
*
* Overrides `certificateFile` and `certificatePassword`.
*
* This is a legacy parameter provided for backwards compatibility.
* For more comprehensive support of various codesigning scenarios
* like EV certificates, see the "windowsSign" parameter.
*/
signWithParams?: string;
/**
Expand Down Expand Up @@ -131,6 +145,20 @@ export interface SquirrelWindowsOptions {
fixUpPaths?: boolean;

skipUpdateIcon?: boolean;

/**
* Requires Node.js 18 or newer.
*
* Sign your app with @electron/windows-sign, allowing for full customization
* of the code-signing process - and supports more complicated scenarios like
* cloud-hosted EV certificates, custom sign pipelines, and per-file overrides.
* It also supports all existing "simple" codesigning scenarios, including
* just passing a certificate file and password.
*
* Please see https://github.com/@electron/windows-sign for all possible
* configuration options.
*/
windowsSign?: SignToolOptions;
}

export interface PersonMetadata {
Expand Down
84 changes: 84 additions & 0 deletions src/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { createSeaSignTool as createSeaSignToolType } from '@electron/windows-sign';
import path from 'path';
import semver from 'semver';
import fs from 'fs-extra';

import { SquirrelWindowsOptions } from './options';

const VENDOR_PATH = path.join(__dirname, '..', 'vendor');
const ORIGINAL_SIGN_TOOL_PATH = path.join(VENDOR_PATH, 'signtool.exe');
const BACKUP_SIGN_TOOL_PATH = path.join(VENDOR_PATH, 'signtool-original.exe');
const SIGN_LOG_PATH = path.join(VENDOR_PATH, 'electron-windows-sign.log');

/**
* This method uses @electron/windows-sign to create a fake signtool.exe
* that can be called by Squirrel - but then just calls @electron/windows-sign
* to actually perform the signing.
*
* That's useful for users who need a high degree of customization of the signing
* process but still want to use @electron/windows-installer.
*/
export async function createSignTool(options: SquirrelWindowsOptions): Promise<void> {
if (!options.windowsSign) {
throw new Error('Signtool should only be created if windowsSign options are set');
}

const createSeaSignTool = await getCreateSeaSignTool();

await resetSignTool();
await fs.remove(SIGN_LOG_PATH);

// Make a backup of signtool.exe
await fs.copy(ORIGINAL_SIGN_TOOL_PATH, BACKUP_SIGN_TOOL_PATH, { overwrite: true });

// Create a new signtool.exe using @electron/windows-sign
await createSeaSignTool({
path: ORIGINAL_SIGN_TOOL_PATH,
windowsSign: options.windowsSign
});
}

/**
* Ensure that signtool.exe is actually the "real" signtool.exe, not our
* fake substitute.
*/
export async function resetSignTool() {
if (fs.existsSync(BACKUP_SIGN_TOOL_PATH)) {
// Reset the backup of signtool.exe
await fs.copy(BACKUP_SIGN_TOOL_PATH, ORIGINAL_SIGN_TOOL_PATH, { overwrite: true });
await fs.remove(BACKUP_SIGN_TOOL_PATH);
}
}

/**
* @electron/windows-installer only requires Node.js >= 8.0.0.
* @electron/windows-sign requires Node.js >= 16.0.0.
* @electron/windows-sign's "fake signtool.exe" feature requires
* Node.js >= 20.0.0, the first version to contain the "single
* executable" feature with proper support.
*
* Since this is overall a very niche feature and only benefits
* consumers with rather advanced codesigning needs, we did not
* want to make Node.js v18 a hard requirement for @electron/windows-installer.
*
* Instead, @electron/windows-sign is an optional dependency - and
* if it didn't install, we'll throw a useful error here.
*
* @returns
*/
async function getCreateSeaSignTool(): Promise<typeof createSeaSignToolType> {
try {
const { createSeaSignTool } = await import('@electron/windows-sign');
return createSeaSignTool;
} catch(error) {
let message = 'In order to use windowsSign options, @electron/windows-sign must be installed as a dependency.';

if (semver.lte(process.version, '20.0.0')) {
message += ` You are currently using Node.js ${process.version}. Please upgrade to Node.js 19 or later and reinstall all dependencies to ensure that @electron/windows-sign is available.`;
} else {
message += ` ${error}`;
}

throw new Error(message);
}
}
Loading

0 comments on commit 598aa70

Please sign in to comment.