-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add functionality to create a "signtool.exe" (#7)
This PR adds functionality to have `@electron/windows-sign` create a `signtool.exe` that signs any given file with the same options passed to `@electron/windows-sign` in the first place. That's helpful for tools like Squirrel - which will _only_ sign with `signtool.exe`. It's a bit of a clever hack to make `@electron/windows-sign` compatible with any tool that'll let users call `signtool.exe`. Here's how it works: - On the user's machine, we use Node's "single executable binary" feature to create a custom version of `node.exe` that contains signing options. We'll call that newly minted binary `signtool.exe`. - When our `signtool.exe` is executed, it spawns user's `node.exe`, requires `@electron/windows-sign`, and signs the file originally passed to it with the options already embedded in the binary. - Since we _create_ the `signtool.exe` on the local machine, users can use this tool with confidence - there is no mysterious "signtool.exe" that'd be able to exfiltrate secrets. I don't expect users to actually use this feature directly. It'll be consumed by `@electron/windows-installer`, for which I'll open another PR.
- Loading branch information
1 parent
efbe255
commit 8b23eaa
Showing
11 changed files
with
752 additions
and
432 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,6 @@ | |
|
||
/dist/ | ||
|
||
yarn-error.log | ||
yarn-error.log | ||
|
||
tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
declare module 'postject' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import { sign } from './sign'; | ||
import { OptionalHookOptions, OptionalSignToolOptions, SignOptions } from './types'; | ||
import { createSeaSignTool } from './sea'; | ||
import { OptionalHookOptions, OptionalSignToolOptions, SignOptions, SignToolOptions } from './types'; | ||
|
||
export { sign, SignOptions, OptionalSignToolOptions, OptionalHookOptions }; | ||
export { sign, SignOptions, SignToolOptions, OptionalSignToolOptions, OptionalHookOptions, createSeaSignTool }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import { getDirname } from 'cross-dirname'; | ||
import path from 'path'; | ||
import os from 'os'; | ||
import fs from 'fs-extra'; | ||
import postject from 'postject'; | ||
|
||
import { spawnPromise } from './spawn'; | ||
import { log } from './utils/log'; | ||
import { SignToolOptions } from './types'; | ||
|
||
interface SeaOptions { | ||
// Full path to the sea. Needs to end with .exe | ||
path: string | ||
// Optional: A binary to use. Will use the current executable | ||
// (process.execPath) by default. | ||
bin?: string; | ||
// Sign options | ||
windowsSign: SignToolOptions | ||
} | ||
|
||
interface InternalSeaOptions extends Required<SeaOptions> { | ||
dir: string | ||
filename: string | ||
} | ||
|
||
/** | ||
* cross-dir uses new Error() stacks | ||
* to figure out our directory in a way | ||
* that's somewhat cross-compatible. | ||
* | ||
* We can't just use __dirname because it's | ||
* undefined in ESM - and we can't use import.meta.url | ||
* because TypeScript won't allow usage unless you're | ||
* _only_ compiling for ESM. | ||
*/ | ||
export const DIRNAME = getDirname(); | ||
|
||
const FILENAMES = { | ||
SEA_CONFIG: 'sea-config.json', | ||
SEA_MAIN: 'sea.js', | ||
SEA_BLOB: 'sea.blob', | ||
SEA_RECEIVER: 'receiver.mjs' | ||
}; | ||
|
||
const SEA_MAIN_SCRIPT = ` | ||
const bin = "%PATH_TO_BIN%"; | ||
const script = "%PATH_TO_SCRIPT%"; | ||
const options = %WINDOWS_SIGN_OPTIONS% | ||
const { spawnSync } = require('child_process'); | ||
function main() { | ||
console.log("@electron/windows-sign sea"); | ||
console.log({ bin, script }); | ||
try { | ||
const spawn = spawnSync( | ||
bin, | ||
[ script, JSON.stringify(options), JSON.stringify(process.argv.slice(1)) ], | ||
{ stdio: ['inherit', 'inherit', 'pipe'] } | ||
); | ||
const stderr = spawn.stderr.toString().trim(); | ||
if (stderr) { | ||
throw new Error(stderr); | ||
} | ||
} catch (error) { | ||
process.exitCode = 1; | ||
throw new Error(error); | ||
} | ||
} | ||
main(); | ||
`; | ||
|
||
const SEA_RECEIVER_SCRIPT = ` | ||
import { sign } from '@electron/windows-sign'; | ||
import fs from 'fs-extra'; | ||
import path from 'path'; | ||
const logPath = path.join('electron-windows-sign.log'); | ||
const options = JSON.parse(process.argv[2]); | ||
const signArgv = JSON.parse(process.argv[3]); | ||
const files = signArgv.slice(-1); | ||
fs.appendFileSync(logPath, \`\\n\${files}\`); | ||
sign({ ...options, files }) | ||
.then((result) => { | ||
fs.appendFileSync(logPath, \`\\n\${result}\`); | ||
console.log(\`Successfully signed \${files}\`, result); | ||
}) | ||
.catch((error) => { | ||
fs.appendFileSync(logPath, \`\\n\${error}\`); | ||
throw new Error(error); | ||
}); | ||
`; | ||
|
||
/** | ||
* Uses Node's "Single Executable App" functionality | ||
* to create a Node-driven signtool.exe that calls this | ||
* module. | ||
* | ||
* This is useful with other tooling that _always_ calls | ||
* a signtool.exe to sign. Some of those tools cannot be | ||
* easily configured, but we _can_ override their signtool.exe. | ||
*/ | ||
export async function createSeaSignTool(options: Partial<SeaOptions> = {}): Promise<InternalSeaOptions> { | ||
checkCompatibility(); | ||
|
||
const requiredOptions = await getOptions(options); | ||
await createFiles(requiredOptions); | ||
await createBlob(requiredOptions); | ||
await createBinary(requiredOptions); | ||
await createSeaReceiver(requiredOptions); | ||
await cleanup(requiredOptions); | ||
|
||
return requiredOptions; | ||
} | ||
|
||
async function createSeaReceiver(options: InternalSeaOptions) { | ||
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER); | ||
|
||
await fs.ensureFile(receiverPath); | ||
await fs.writeFile(receiverPath, SEA_RECEIVER_SCRIPT); | ||
} | ||
|
||
async function createFiles(options: InternalSeaOptions) { | ||
const { dir, bin } = options; | ||
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER); | ||
|
||
// sea-config.json | ||
await fs.outputJSON(path.join(dir, FILENAMES.SEA_CONFIG), { | ||
main: FILENAMES.SEA_MAIN, | ||
output: FILENAMES.SEA_BLOB, | ||
disableExperimentalSEAWarning: true | ||
}, { | ||
spaces: 2 | ||
}); | ||
|
||
// signtool.js | ||
const binPath = bin || process.execPath; | ||
const script = SEA_MAIN_SCRIPT | ||
.replace('%PATH_TO_BIN%', escapeMaybe(binPath)) | ||
.replace('%PATH_TO_SCRIPT%', escapeMaybe(receiverPath)) | ||
.replace('%WINDOWS_SIGN_OPTIONS%', JSON.stringify(options.windowsSign)); | ||
|
||
await fs.outputFile(path.join(dir, FILENAMES.SEA_MAIN), script); | ||
} | ||
|
||
async function createBlob(options: InternalSeaOptions) { | ||
const args = ['--experimental-sea-config', 'sea-config.json']; | ||
const bin = process.execPath; | ||
const cwd = options.dir; | ||
|
||
log(`Calling ${bin} with options:`, args); | ||
|
||
return spawnPromise(bin, args, { | ||
cwd | ||
}); | ||
} | ||
|
||
async function createBinary(options: InternalSeaOptions) { | ||
const { dir, filename } = options; | ||
|
||
log(`Creating ${filename} in ${dir}`); | ||
|
||
// Copy Node over | ||
const seaPath = path.join(dir, filename); | ||
await fs.copyFile(process.execPath, seaPath); | ||
|
||
// Remove the Node signature | ||
const signtool = path.join(DIRNAME, '../../vendor/signtool.exe'); | ||
await spawnPromise(signtool, [ | ||
'remove', | ||
'/s', | ||
seaPath | ||
]); | ||
|
||
// Inject the blob | ||
const blob = await fs.readFile(path.join(dir, FILENAMES.SEA_BLOB)); | ||
await postject.inject(seaPath, 'NODE_SEA_BLOB', blob, { | ||
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2' | ||
}); | ||
} | ||
|
||
async function cleanup(options: InternalSeaOptions) { | ||
const { dir } = options; | ||
const toRemove = [ | ||
FILENAMES.SEA_BLOB, | ||
FILENAMES.SEA_MAIN, | ||
FILENAMES.SEA_CONFIG | ||
]; | ||
|
||
for (const file of toRemove) { | ||
try { | ||
await fs.remove(path.join(dir, file)); | ||
} catch (error) { | ||
console.warn(`Tried and failed to remove ${file}. Continuing.`, error); | ||
} | ||
} | ||
} | ||
|
||
async function getOptions(options: Partial<SeaOptions>): Promise<InternalSeaOptions> { | ||
const cloned = { ...options }; | ||
|
||
if (!cloned.path) { | ||
cloned.path = path.join(os.homedir(), '.electron', 'windows-sign', 'sea.exe'); | ||
await fs.ensureFile(cloned.path); | ||
} | ||
|
||
if (!cloned.bin) { | ||
cloned.bin = process.execPath; | ||
} | ||
|
||
if (!cloned.windowsSign) { | ||
throw new Error('Did not find windowsSign options, which are required'); | ||
} | ||
|
||
return { | ||
path: cloned.path, | ||
dir: path.dirname(cloned.path), | ||
filename: path.basename(cloned.path), | ||
bin: cloned.bin, | ||
windowsSign: cloned.windowsSign | ||
}; | ||
} | ||
|
||
/** | ||
* Ensures that the current Node.js version supports SEA app generation and errors if not. | ||
*/ | ||
function checkCompatibility() { | ||
const version = process.versions.node; | ||
const split = version.split('.'); | ||
const major = parseInt(split[0], 10); | ||
const minor = parseInt(split[1], 10); | ||
|
||
if (major >= 20) { | ||
return true; | ||
} | ||
|
||
if (major === 19 && minor >= 7) { | ||
return true; | ||
} | ||
|
||
if (major === 18 && minor >= 16) { | ||
return true; | ||
} | ||
|
||
throw new Error(`Your Node.js version (${process.version}) does not support Single Executable Applications. Please upgrade your version of Node.js.`); | ||
} | ||
|
||
/** Make sure that the input string has escaped backwards slashes | ||
* - but never double-escaped backwards slashes. | ||
*/ | ||
function escapeMaybe(input: string): string { | ||
const result = input.split(path.sep).join('\\\\'); | ||
|
||
if (result.includes('\\\\\\\\')) { | ||
throw new Error(`Your passed input ${input} contains escaped slashes. Please do not escape them`); | ||
} | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.