From 6c4a872f4e2144695a841dbb9cca74640795afa3 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 5 May 2021 19:16:58 +0800 Subject: [PATCH 1/2] feature: allow macOS code signing by including payload in str table Bug: #66, #128, #1023 --- lib/index.ts | 42 +++++++++++++++++++++++++++++++------ lib/mach-o.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 lib/mach-o.ts diff --git a/lib/index.ts b/lib/index.ts index ccd03fbbe..095778bc8 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,6 +1,15 @@ /* eslint-disable require-atomic-updates */ -import { existsSync, mkdirp, readFile, remove, stat, readFileSync } from 'fs-extra'; +import { execSync } from 'child_process'; +import { + existsSync, + mkdirp, + readFile, + remove, + stat, + readFileSync, + writeFileSync, +} from 'fs-extra'; import { need, system } from 'pkg-fetch'; import assert from 'assert'; import minimist from 'minimist'; @@ -17,6 +26,7 @@ import { shutdown } from './fabricator'; import walk, { Marker, WalkerParams } from './walker'; import { Target, NodeTarget, SymLinks } from './types'; import { CompressType } from './compress_type'; +import { patchMachOExecutable } from './mach-o'; const { version } = JSON.parse( readFileSync(path.join(__dirname, '../package.json'), 'utf-8') @@ -269,7 +279,6 @@ export async function exec(argv2: string[]) { // doCompress const algo = argv.C || argv.compress || 'None'; - let doCompress: CompressType = CompressType.None; switch (algo.toLowerCase()) { case 'brotli': @@ -284,8 +293,9 @@ export async function exec(argv2: string[]) { break; default: // eslint-disable-next-line no-console - throw wasReported(`Invalid compression algorithm ${algo} ( should be None, Brotli or Gzip)`); - + throw wasReported( + `Invalid compression algorithm ${algo} ( should be None, Brotli or Gzip)` + ); } if (doCompress !== CompressType.None) { // eslint-disable-next-line no-console @@ -350,7 +360,7 @@ export async function exec(argv2: string[]) { if (!existsSync(inputBin)) { throw wasReported( 'Bin file does not exist (taken from package.json ' + - "'bin' property)", + "'bin' property)", [inputBin] ); } @@ -632,8 +642,28 @@ export async function exec(argv2: string[]) { } const slash = target.platform === 'win' ? '\\' : '/'; - await producer({ backpack, bakes, slash, target: target as Target, symLinks, doCompress }); + await producer({ + backpack, + bakes, + slash, + target: target as Target, + symLinks, + doCompress, + }); + if (target.platform !== 'win' && target.output) { + if (target.platform === 'macos') { + // patch executable to allow code signing + const buf = patchMachOExecutable(readFileSync(target.output)); + writeFileSync(target.output, buf); + + if (hostPlatform === 'macos') { + // sign executable ad-hoc to workaround the new mandatory signing requirement + // users can always replace the signature if necessary + execSync(`codesign --sign - ${target.output}`); + } + } + await plusx(target.output); } } diff --git a/lib/mach-o.ts b/lib/mach-o.ts new file mode 100644 index 000000000..c0e1719bf --- /dev/null +++ b/lib/mach-o.ts @@ -0,0 +1,58 @@ +function parseCStr(buf: Buffer) { + for (let i = 0; i < buf.length; i += 1) { + if (buf[i] === 0) { + return buf.slice(0, i).toString(); + } + } +} + +function patchCommand(type: number, buf: Buffer, file: Buffer) { + // segment_64 + if (type === 0x19) { + const name = parseCStr(buf.slice(0, 16)); + + if (name === '__LINKEDIT') { + const fileoff = buf.readBigUInt64LE(32); + const vmsize_patched = BigInt(file.length) - fileoff; + const filesize_patched = vmsize_patched; + + buf.writeBigUInt64LE(vmsize_patched, 24); + buf.writeBigUInt64LE(filesize_patched, 40); + } + } + + // symtab + if (type === 0x2) { + const stroff = buf.readUInt32LE(8); + const strsize_patched = file.length - stroff; + + buf.writeUInt32LE(strsize_patched, 12); + } +} + +function patchMachOExecutable(file: Buffer) { + const align = 8; + const hsize = 32; + + const ncmds = file.readUInt32LE(16); + const buf = file.slice(hsize); + + for (let offset = 0, i = 0; i < ncmds; i += 1) { + const type = buf.readUInt32LE(offset); + + offset += 4; + const size = buf.readUInt32LE(offset) - 8; + + offset += 4; + patchCommand(type, buf.slice(offset, offset + size), file); + + offset += size; + if (offset & align) { + offset += align - (offset & align); + } + } + + return file; +} + +export { patchMachOExecutable }; From 1305a4e774416a01c3d4c552305d4d376e8b8f7f Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 5 May 2021 19:27:11 +0800 Subject: [PATCH 2/2] Ad-hoc sign the fabricator binary temporarily to generate bytecode on macOS The fetched/built base binaries MUST NOT have an existing signature if we want to sign the final executable. However, we do need to run the base binary to generate bytecode on macOS, and the binary has to be signed due to the new mandatory signing requirement. This change ad-hoc signs the base binary to allow pkg to generate bytecode on macOS. --- lib/index.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 095778bc8..968176bf7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,6 @@ /* eslint-disable require-atomic-updates */ +import assert from 'assert'; import { execSync } from 'child_process'; import { existsSync, @@ -9,10 +10,10 @@ import { stat, readFileSync, writeFileSync, + copyFileSync, } from 'fs-extra'; -import { need, system } from 'pkg-fetch'; -import assert from 'assert'; import minimist from 'minimist'; +import { need, system } from 'pkg-fetch'; import path from 'path'; import { log, wasReported } from './log'; @@ -564,6 +565,16 @@ export async function exec(argv2: string[]) { if (f && bytecode) { f.binaryPath = await needViaCache(f as NodeTarget); + if (f.platform === 'macos') { + // ad-hoc sign the base binary temporarily to generate bytecode + // due to the new mandatory signing requirement + const signedBinaryPath = `${f.binaryPath}-signed`; + await remove(signedBinaryPath); + copyFileSync(f.binaryPath, signedBinaryPath); + execSync(`codesign --sign - ${signedBinaryPath}`); + f.binaryPath = signedBinaryPath; + } + if (f.platform !== 'win') { await plusx(f.binaryPath); }