From 903368a2141f286d8718027049751f25021496d2 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Thu, 26 Oct 2023 11:11:35 -0400 Subject: [PATCH] feat: Teach the tool to sign all kinds of files --- README.md | 26 +++++++++----- package.json | 2 +- src/files.ts | 63 +++++++++++++++++++++++++++++++++ src/index.ts | 3 +- src/sign.ts | 80 ++++-------------------------------------- src/types.ts | 50 ++++++++++++++++++++++++++ src/utils/parse-env.ts | 23 ++++++++++++ 7 files changed, 163 insertions(+), 84 deletions(-) create mode 100644 src/files.ts create mode 100644 src/types.ts create mode 100644 src/utils/parse-env.ts diff --git a/README.md b/README.md index 7c5fe27..75722eb 100644 --- a/README.md +++ b/README.md @@ -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" @@ -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 @@ -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. @@ -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 \ No newline at end of file diff --git a/package.json b/package.json index 2edd8c2..c3ae17e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..5cb9a6a --- /dev/null +++ b/src/files.ts @@ -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 { + dir = dir || options.appDirectory; + + // Array of file paths to sign + const result: Array = []; + + // 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; +} diff --git a/src/index.ts b/src/index.ts index c8f27bd..be497be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ -import { sign, SignOptions } from './sign'; +import { sign } from './sign'; +import { SignOptions } from './types'; export { sign, SignOptions }; diff --git a/src/sign.ts b/src/sign.ts index 3f83966..458f6ae 100644 --- a/src/sign.ts +++ b/src/sign.ts @@ -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; - 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 @@ -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 = []; - - // 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); @@ -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(); @@ -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, @@ -183,6 +114,7 @@ export async function sign(options: SignOptions) { description, timestampServer, website, + signJavaScript, files }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..af0f36b --- /dev/null +++ b/src/types.ts @@ -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; + 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 +} diff --git a/src/utils/parse-env.ts b/src/utils/parse-env.ts new file mode 100644 index 0000000..dbad038 --- /dev/null +++ b/src/utils/parse-env.ts @@ -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; +}