Skip to content

Commit

Permalink
feat: Teach the tool to sign all kinds of files
Browse files Browse the repository at this point in the history
  • Loading branch information
felixrieseberg committed Oct 26, 2023
1 parent 063801a commit 903368a
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 84 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ electron-windows-sign $PATH_TO_APP_DIRECTORY --certificate-file=$PATH_TO_CERT --
```

### Full configuration
```
```ts
// Path to a timestamp server. Defaults to http://timestamp.digicert.com
// Can also be passed as process.env.WINDOWS_TIMESTAMP_SERVER
timestampServer = "http://timestamp.digicert.com"
Expand All @@ -53,6 +53,8 @@ description = "My content"
// URL of the signed content. Will be passed to signtool.exe as /du
// Can also be passed as process.env.WINDOWS_SIGN_WEBSITE
website = "https://mywebsite.com"
// If enabled, attempt to sign .js JavaScript files. Disabled by default
signJavaScript = true
```

## With a custom signtool.exe or custom parameters
Expand Down Expand Up @@ -133,13 +135,20 @@ DESCRIPTION
```

# File Types
PE files (.exe, .dll, .sys, .efi, .scr)
Microsoft installers (.msi)
APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
Catalog files (.cat)
Cabinet files (.cab)
Scripts (.js, .vbs, .wsf, .ps1)
Silverlight applications (.xap)
This tool will aggressively attempt to sign all files that _can_
be signed, excluding scripts.

- [Portable executable files][pe] (.exe, .dll, .sys, .efi, .scr, .node)
- Microsoft installers (.msi)
- APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
- Catalog files (.cat)
- Cabinet files (.cab)
- Silverlight applications (.xap)
- Scripts (.vbs, .wsf, .ps1)

If you do want to sign JavaScript, please enable it with the `signJavaScript`
parameter. As far as we are aware, there are no benefits to signing
JavaScript files, so we do not by default.

# License
BSD 2-Clause "Simplified". Please see LICENSE for details.
Expand All @@ -148,3 +157,4 @@ BSD 2-Clause "Simplified". Please see LICENSE for details.
[electron-windows-sign]: https://github.com/electron-windows-sign
[npm_img]: https://img.shields.io/npm/v/electron-windows-sign.svg
[npm_url]: https://npmjs.org/package/electron-windows-sign
[pe]: https://en.wikipedia.org/wiki/Portable_Executable
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"scripts": {
"build": "tsc && tsc -p tsconfig.esm.json",
"lint": "eslint --ext .ts,.js src bin test",
"lint": "eslint --ext .ts,.js src bin",
"prepublishOnly": "yarn build"
},
"engines": {
Expand Down
63 changes: 63 additions & 0 deletions src/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import path from 'path';
import fs from 'fs-extra';

import { SignOptions } from './types';

const IS_PE_REGEX = /\.(exe|dll|sys|efi|scr|node)$/i;
const IS_MSI_REGEX = /\.msi$/i;
const IS_PACKAGE_REGEX = /\.(appx|appxbundle|msix|msixbundle)$/i;
const IS_CATCAB_REGEX = /\.(cat|cab)$/i;
const IS_SILVERLIGHT_REGEX = /\.xap$/i;
const IS_SCRIPT_REGEX = /\.(vbs|wsf|ps1)$/i;
const IS_JS_REGEX = /\.js$/i;

/**
* Recursively goes through an entire directory and returns an array
* of full paths for files ot sign.
*
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
* - Microsoft installers (.msi)
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
* - Catalog files (.cat)
* - Cabinet files (.cab)
* - Silverlight applications (.xap)
* - Scripts (.vbs, .wsf, .ps1)
* If configured:
* - JavaScript files (.js)
*/
export function getFilesToSign(options: SignOptions, dir?: string): Array<string> {
dir = dir || options.appDirectory;

// Array of file paths to sign
const result: Array<string> = [];

// Iterate over the app directory, looking for files to sign
const files = fs.readdirSync(dir);

const regexes = [
IS_PE_REGEX,
IS_MSI_REGEX,
IS_PACKAGE_REGEX,
IS_CATCAB_REGEX,
IS_SILVERLIGHT_REGEX,
IS_SCRIPT_REGEX
];

if (options.signJavaScript) {
regexes.push(IS_JS_REGEX);
}

for (const file of files) {
const fullPath = path.resolve(dir, file);

if (fs.statSync(fullPath).isDirectory()) {
// If it's a directory, recurse
result.push(...getFilesToSign(options, fullPath));
} else if (regexes.some((regex) => regex.test(file))) {
// If it's a match, add it to the list
result.push(fullPath);
}
}

return result;
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sign, SignOptions } from './sign';
import { sign } from './sign';
import { SignOptions } from './types';

export { sign, SignOptions };
80 changes: 6 additions & 74 deletions src/sign.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,9 @@
import path from 'path';
import fs from 'fs-extra';
import { enableDebugging, log } from './utils';
import { spawnPromise } from './spawn';

// SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
// https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
const enum HASHES {
sha1 = 'sha1',
sha256 = 'sha256',
}
export interface SignOptions extends OptionalSignOptions {
// Path to the application directory. We will scan this
// directory for any .dll, .exe, .msi, or .node files and
// codesign them with signtool.exe
appDirectory: string;
// Path to a .pfx code signing certificate. Will use
// process.env.WINDOWS_CERTIFICATE_FILE if not provided
certificateFile?: string;
// Password to said certificate. If you don't provide this,
// you need to provide a `signWithParams` option. Will use
// process.env.WINDOWS_CERTIFICATE_PASSWORD if not provided
certificatePassword?: string;
}

interface InternalOptions extends OptionalSignOptions {
certificateFile: string;
certificatePassword?: string;
signToolPath: string;
timestampServer: string;
files: Array<string>;
hash: HASHES;
appendSignature?: boolean;
}

interface OptionalSignOptions {
// Path to a timestamp server. Defaults to http://timestamp.digicert.com
timestampServer?: string;
// Description of the signed content. Will be passed to signtool.exe as /d
description?: string;
// URL of the signed content. Will be passed to signtool.exe as /du
website?: string;
// Path to signtool.exe. Will use vendor/signtool.exe if not provided
signToolPath?: string;
// Additional parameters to pass to signtool.exe.
signWithParams?: string;
// Enable debug logging
debug?: boolean;
// Automatically select the best signing certificate, passed as
// /a to signtool.exe, on by default
automaticallySelectCertificate?: boolean;
}
import { getFilesToSign } from './files';
import { HASHES, InternalOptions, SignOptions } from './types';
import { booleanFromEnv } from './utils/parse-env';

function getSigntoolArgs(options: InternalOptions) {
// See the following url for docs
Expand Down Expand Up @@ -110,30 +64,6 @@ function getSigntoolArgs(options: InternalOptions) {
return args;
}

const IS_BINARY_REGEX = /\.(exe|msi|dll|node)$/i;

function getFilesToSign(dir: string) {
// Array of file paths to sign
const result: Array<string> = [];

// Iterate over the app directory, looking for files to sign
const files = fs.readdirSync(dir);

for (const file of files) {
const fullPath = path.resolve(dir, file);

if (fs.statSync(fullPath).isDirectory()) {
// If it's a directory, recurse
result.push(...getFilesToSign(fullPath));
} else if (IS_BINARY_REGEX.test(file)) {
// If it's a binary, add it to the list
result.push(fullPath);
}
}

return result;
}

async function execute(options: InternalOptions) {
const { signToolPath, files } = options;
const args = getSigntoolArgs(options);
Expand All @@ -157,6 +87,7 @@ export async function sign(options: SignOptions) {
const signToolPath = options.signToolPath || process.env.WINDOWS_SIGNTOOL_PATH || path.join(__dirname, '../../vendor/signtool.exe');
const description = options.description || process.env.WINDOWS_SIGN_DESCRIPTION;
const website = options.website || process.env.WINDOWS_SIGN_WEBSITE;
const signJavaScript = options.signJavaScript || booleanFromEnv('WINDOWS_SIGN_JAVASCRIPT');

if (options.debug) {
enableDebugging();
Expand All @@ -172,7 +103,7 @@ export async function sign(options: SignOptions) {
throw new Error('You must provide a certificatePassword or signing parameters');
}

const files = getFilesToSign(options.appDirectory);
const files = getFilesToSign(options);

const internalOptions = {
...options,
Expand All @@ -183,6 +114,7 @@ export async function sign(options: SignOptions) {
description,
timestampServer,
website,
signJavaScript,
files
};

Expand Down
50 changes: 50 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
// https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
export const enum HASHES {
sha1 = 'sha1',
sha256 = 'sha256',
}

export interface SignOptions extends OptionalSignOptions {
// Path to the application directory. We will scan this
// directory for any .dll, .exe, .msi, or .node files and
// codesign them with signtool.exe
appDirectory: string;
// Path to a .pfx code signing certificate. Will use
// process.env.WINDOWS_CERTIFICATE_FILE if not provided
certificateFile?: string;
// Password to said certificate. If you don't provide this,
// you need to provide a `signWithParams` option. Will use
// process.env.WINDOWS_CERTIFICATE_PASSWORD if not provided
certificatePassword?: string;
}

export interface InternalOptions extends OptionalSignOptions {
certificateFile: string;
certificatePassword?: string;
signToolPath: string;
timestampServer: string;
files: Array<string>;
hash: HASHES;
appendSignature?: boolean;
}

export interface OptionalSignOptions {
// Path to a timestamp server. Defaults to http://timestamp.digicert.com
timestampServer?: string;
// Description of the signed content. Will be passed to signtool.exe as /d
description?: string;
// URL of the signed content. Will be passed to signtool.exe as /du
website?: string;
// Path to signtool.exe. Will use vendor/signtool.exe if not provided
signToolPath?: string;
// Additional parameters to pass to signtool.exe.
signWithParams?: string;
// Enable debug logging
debug?: boolean;
// Automatically select the best signing certificate, passed as
// /a to signtool.exe, on by default
automaticallySelectCertificate?: boolean;
// Should we sign JavaScript files? Defaults to false
signJavaScript?: boolean
}
23 changes: 23 additions & 0 deletions src/utils/parse-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Tries to parse an process.env string to a boolean.
* Will understand undefined as the default value
* Will understand "false", "False", "fAlse", or "0" as `false`
* Will understand everything else as true
*
* @export
* @param {string} name
* @return {*} {boolean}
*/
export function booleanFromEnv(name: string): boolean | undefined {
const value = process.env[name];

if (value === undefined) {
return undefined;
}

if (value.toLowerCase() === 'false' || value === '0') {
return false;
}

return !!value;
}

0 comments on commit 903368a

Please sign in to comment.