From a6a0cd645f5c053d818fee9765f4fb3aee5ecd2a Mon Sep 17 00:00:00 2001 From: develar Date: Mon, 1 Aug 2016 10:15:22 +0200 Subject: [PATCH] fix: Failing to sign the Windows build on Linux Closes #578 --- .idea/dictionaries/develar.xml | 3 + docker/winSign.sh | 5 ++ package.json | 4 +- src/asarUtil.ts | 19 +++-- src/builder.ts | 2 +- src/cliOptions.ts | 2 +- src/macPackager.ts | 2 +- src/metadata.ts | 5 +- src/winPackager.ts | 5 +- src/windowsCodeSign.ts | 142 +++++++++++++++++++++++++++++++++ test/src/winPackagerTest.ts | 21 ++--- tslint.json | 4 +- typings/signcode.d.ts | 13 --- 13 files changed, 178 insertions(+), 49 deletions(-) create mode 100755 docker/winSign.sh create mode 100644 src/windowsCodeSign.ts delete mode 100644 typings/signcode.d.ts diff --git a/.idea/dictionaries/develar.xml b/.idea/dictionaries/develar.xml index 4aa9c9a9966..72dd05273a1 100644 --- a/.idea/dictionaries/develar.xml +++ b/.idea/dictionaries/develar.xml @@ -73,6 +73,7 @@ nuget nupkg nuspec + osslsigncode packagejson pacman passin @@ -83,9 +84,11 @@ progexe promisify psmdcp + readpass repos rimraf semver + signtool templating testapp tsconfig diff --git a/docker/winSign.sh b/docker/winSign.sh new file mode 100755 index 00000000000..9a48e8bc86a --- /dev/null +++ b/docker/winSign.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +dir=${PWD##*/} +rm -rf ../${dir}.7z +7za a -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on ../winCodeSign.7z . diff --git a/package.json b/package.json index 325a3bab425..85b804bc446 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "compile": "npm run compile-production && npm run compile-test", "compile-production": "ts-babel", "compile-test": "ts-babel test", - "lint": "tslint src/**/*.ts test/src/**/*.ts", + "lint": "tslint 'src/**/*.ts' 'test/src/**/*.ts'", "pretest": "npm run compile && npm run lint", "test": "node ./test/out/helpers/runTests.js", "semantic-release": "semantic-release pre && npm publish && semantic-release post", @@ -87,8 +87,8 @@ "rcedit": "^0.5.1", "sanitize-filename": "^1.6.0", "semver": "^5.3.0", - "signcode-tf": "~0.7.5", "source-map-support": "^0.4.2", + "tslint": "^3.14.0-dev.1", "typescript": "2.0.0-dev.20160705", "update-notifier": "^1.0.2", "uuid-1345": "^0.99.6", diff --git a/src/asarUtil.ts b/src/asarUtil.ts index 32489c233e9..c8d0a273907 100644 --- a/src/asarUtil.ts +++ b/src/asarUtil.ts @@ -6,7 +6,6 @@ import { } from "fs-extra-p" import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" -import pathSorter = require("path-sort") import { log } from "./util/log" import { Minimatch } from "minimatch" import { deepAssign } from "./util/deepAssign" @@ -85,16 +84,16 @@ export async function createAsarArchive(src: string, resourcesPath: string, opti } function isUnpackDir(path: string, pattern: Minimatch, rawPattern: string): boolean { - return path.indexOf(rawPattern) === 0 || pattern.match(path) + return path.startsWith(rawPattern) || pattern.match(path) } async function order(src: string, filenames: Array, options: any) { - const orderingFiles = (await readFile(options.ordering, "utf8")).split("\n").map(function (line) { - if (line.indexOf(':') !== -1) { - line = line.split(':').pop()! + const orderingFiles = (await readFile(options.ordering, "utf8")).split("\n").map(line => { + if (line.indexOf(":") !== -1) { + line = line.split(":").pop()! } line = line.trim() - if (line[0] === '/') { + if (line[0] === "/") { line = line.slice(1) } return line @@ -121,7 +120,7 @@ async function order(src: string, filenames: Array, options: any) { for (let file of filenames) { if (!filenamesSorted.includes(file)) { filenamesSorted.push(file) - missing += 1; + missing += 1 } } log(`Ordering file has ${((total - missing) / total * 100)}% coverage.`) @@ -149,7 +148,7 @@ async function detectUnpackedDirs(src: string, files: Array, metadata: M const nodeModuleDir = file.substring(0, nextSlashIndex) - if (file.length == (nodeModuleDir.length + 1 + packageJsonStringLength) && file.endsWith("package.json")) { + if (file.length === (nodeModuleDir.length + 1 + packageJsonStringLength) && file.endsWith("package.json")) { const promise = readJson(file) if (readPackageJsonPromises.length > MAX_FILE_REQUESTS) { @@ -162,7 +161,7 @@ async function detectUnpackedDirs(src: string, files: Array, metadata: M if (autoUnpackDirs.has(nodeModuleDir)) { const fileParent = path.dirname(file) - if (fileParent != nodeModuleDir && !autoUnpackDirs.has(fileParent)) { + if (fileParent !== nodeModuleDir && !autoUnpackDirs.has(fileParent)) { autoUnpackDirs.add(fileParent) if (createDirPromises.length > MAX_FILE_REQUESTS) { @@ -190,7 +189,7 @@ async function detectUnpackedDirs(src: string, files: Array, metadata: M log(`${path.relative(src, nodeModuleDir)} is not packed into asar archive - contains executable code`) autoUnpackDirs.add(nodeModuleDir) const fileParent = path.dirname(file) - if (fileParent != nodeModuleDir) { + if (fileParent !== nodeModuleDir) { autoUnpackDirs.add(fileParent) // create parent dir to be able to copy file later without directory existence check createDirPromises.push(ensureDir(path.join(unpackedDest, path.relative(src, fileParent)))) diff --git a/src/builder.ts b/src/builder.ts index cc0d45ffd32..a130536176b 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -168,7 +168,7 @@ export function normalizeOptions(args: CliOptions): BuildOptions { delete r.windows delete r.osx delete r.macos - delete r["$0"] + delete r.$0 delete r._ delete r.version delete r.help diff --git a/src/cliOptions.ts b/src/cliOptions.ts index 9b57b0743b4..9ae31c4f1df 100644 --- a/src/cliOptions.ts +++ b/src/cliOptions.ts @@ -74,7 +74,7 @@ export function createYargs(): any { choices: ["ia32", "x64", "all"], }) .option("extraMetadata", { - alias: ["em",], + alias: ["em"], group: buildGroup, describe: "Inject properties to application package.json (asar only)", }) diff --git a/src/macPackager.ts b/src/macPackager.ts index 1473b7dc295..11710908fb0 100644 --- a/src/macPackager.ts +++ b/src/macPackager.ts @@ -86,7 +86,7 @@ export default class MacPackager extends PlatformPackager { if (hasMas) { // osx-sign - disable warning const appOutDir = path.join(outDir, "mas") - const masBuildOptions = deepAssign({}, this.platformSpecificBuildOptions, (this.devMetadata.build)["mas"]) + const masBuildOptions = deepAssign({}, this.platformSpecificBuildOptions, (this.devMetadata.build).mas) //noinspection JSUnusedGlobalSymbols await this.doPack(packOptions, outDir, appOutDir, "mas", arch, masBuildOptions) await this.sign(appOutDir, masBuildOptions) diff --git a/src/metadata.ts b/src/metadata.ts index a6b3c93c556..2ce230676a9 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -559,9 +559,10 @@ export class Platform { case Platform.LINUX.nodeName: return Platform.LINUX - } - throw new Error(`Unknown platform: ${name}`) + default: + throw new Error(`Unknown platform: ${name}`) + } } } diff --git a/src/winPackager.ts b/src/winPackager.ts index 2320c64eedf..abd4ebce838 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -5,7 +5,7 @@ import { Platform, WinBuildOptions, Arch } from "./metadata" import * as path from "path" import { log, task } from "./util/log" import { deleteFile, open, close, read } from "fs-extra-p" -import { sign, SignOptions } from "signcode-tf" +import { sign, SignOptions } from "./windowsCodeSign" import SquirrelWindowsTarget from "./targets/squirrelWindows" import NsisTarget from "./targets/nsis" import { DEFAULT_TARGET, createCommonTarget, DIR_TARGET } from "./targets/targetFactory" @@ -123,7 +123,6 @@ export class WinPackager extends PlatformPackager { password: cscInfo.password!, name: this.appInfo.productName, site: await this.appInfo.computePackageUrl(), - overwrite: true, hash: this.platformSpecificBuildOptions.signingHashAlgorithms, }) } @@ -131,7 +130,7 @@ export class WinPackager extends PlatformPackager { //noinspection JSMethodCanBeStatic protected async doSign(opts: SignOptions): Promise { - return BluebirdPromise.promisify(sign)(opts) + return sign(opts) } protected packageInDistributableFormat(outDir: string, appOutDir: string, arch: Arch, targets: Array, promises: Array>): void { diff --git a/src/windowsCodeSign.ts b/src/windowsCodeSign.ts new file mode 100644 index 00000000000..9b33c4744f2 --- /dev/null +++ b/src/windowsCodeSign.ts @@ -0,0 +1,142 @@ +import { spawn } from "./util/util" +import { rename } from "fs-extra-p" +import * as path from "path" +import { release } from "os" +import { getBin } from "./util/binDownload" +//noinspection JSUnusedLocalSymbols +const __awaiter = require("./util/awaiter") + +const TOOLS_VERSION = "winCodeSign-1.0.0" + +export interface SignOptions { + path: string + cert: string + name?: string | null + password: string + site?: string | null + hash?: Array | null +} + +export async function sign(options: SignOptions) { + let hashes = options.hash + if (hashes == null) { + hashes = ["sha1", "sha256"] + } + else { + hashes = Array.isArray(hashes) ? hashes.slice() : [hashes] + } + + const isWin = process.platform === "win32" + let nest = false + //noinspection JSUnusedAssignment + let outputPath = "" + for (let hash of hashes) { + outputPath = isWin ? options.path : getOutputPath(options.path, hash) + await spawnSign(options, options.path, outputPath, hash, nest) + nest = true + if (!isWin) { + await rename(outputPath, options.path) + } + } +} + +// on windows be aware of http://stackoverflow.com/a/32640183/1910191 +async function spawnSign(options: any, inputPath: string, outputPath: string, hash: string, nest: boolean) { + const timestampingServiceUrl = "http://timestamp.verisign.com/scripts/timstamp.dll" + const isWin = process.platform === "win32" + const args = isWin ? [ + "sign", + nest || hash === "sha256" ? "/tr" : "/t", nest || hash === "sha256" ? "http://timestamp.comodoca.com/rfc3161" : timestampingServiceUrl + ] : [ + "-in", inputPath, + "-out", outputPath, + "-t", timestampingServiceUrl + ] + + const certExtension = path.extname(options.cert) + if (certExtension === ".p12" || certExtension === ".pfx") { + args.push(isWin ? "/f" : "-pkcs12", options.cert) + } + else { + args.push(isWin ? "/f" : "-certs", options.cert) + // todo win maybe incorrect + args.push(isWin ? "/csp" : "-key", options.key) + } + + if (!isWin || hash !== "sha1") { + args.push(isWin ? "/fd" : "-h", hash) + if (isWin) { + args.push("/td", "sha256") + } + } + + if (options.name) { + args.push(isWin ? "/d" : "-n", options.name) + } + + if (options.site) { + args.push(isWin ? "/du" : "-i", options.site) + } + + if (nest) { + args.push(isWin ? "/as" : "-nest") + } + + if (options.password) { + args.push(isWin ? "/p" : "-pass", options.password) + } + + if (options.passwordPath) { + if (isWin) { + throw new Error("-readpass is not supported on Windows") + } + args.push("-readpass", options.passwordPath) + } + + if (isWin) { + // must be last argument + args.push(inputPath) + } + + return await spawn(await getToolPath(options), args, { + stdio: ["ignore", "ignore", "inherit"], + }) +} + +// async function verify(options: any) { +// const out = await exec(await getToolPath(options), [ +// "verify", +// "-in", options.path, +// "-require-leaf-hash", options.hash +// ]) +// if (out.includes("No signature found.")) { +// throw new Error("No signature found") +// } +// else if (out.includes("Leaf hash match: failed")) { +// throw new Error("Leaf hash match failed") +// } +// } + +function getOutputPath(inputPath: string, hash: string) { + const extension = path.extname(inputPath) + return path.join(path.dirname(inputPath), `${path.basename(inputPath, extension)}-signed-${hash}${extension}`) +} + +async function getToolPath(options: any) { + let result = options.signcodePath || process.env.SIGNTOOL_PATH + if (result) { + return result + } + + if (process.env.USE_SYSTEM_SIGNCODE || process.platform === "linux") { + return "osslsigncode" + } + + const vendorPath = await getBin("winCodeSign", TOOLS_VERSION, `https://dl.bintray.com/electron-userland/bin/${TOOLS_VERSION}.7z`, "0e524943dd6f03a9cee4cdaaa235e27d4945a1c9dc80ccee8ff1219e7b73ad88") + if (process.platform === "win32") { + return path.join(vendorPath, `windows-${(release().startsWith("6.") ? "6" : "10")}`, "signtool.exe") + } + else { + return path.join(vendorPath, process.platform, "osslsigncode") + } +} diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts index 3d510269a7f..8c4c6fd4942 100755 --- a/test/src/winPackagerTest.ts +++ b/test/src/winPackagerTest.ts @@ -1,4 +1,4 @@ -import { Platform, Arch, BuildInfo, PackagerOptions } from "out" +import { Platform, Arch, BuildInfo } from "out" import test from "./helpers/avaEx" import { assertPack, platform, modifyPackageJson, signed, getTestAsset } from "./helpers/packTester" import { outputFile, rename, copy } from "fs-extra-p" @@ -6,7 +6,7 @@ import * as path from "path" import { WinPackager } from "out/winPackager" import { Promise as BluebirdPromise } from "bluebird" import { assertThat } from "./helpers/fileAssert" -import { SignOptions } from "signcode-tf" +import { SignOptions } from "out/windowsCodeSign" import SquirrelWindowsTarget from "out/targets/squirrelWindows" import { Target } from "out/platformPackager" import { ElectronPackagerOptions } from "out/packager/dirPackager" @@ -14,23 +14,15 @@ import { ElectronPackagerOptions } from "out/packager/dirPackager" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/util/awaiter") -function _signed(packagerOptions: PackagerOptions): PackagerOptions { - if (process.platform !== "win32") { - // todo Linux Signing failed with SIGBUS - return packagerOptions - } - return signed(packagerOptions) -} - -test.ifNotCiOsx("win", () => assertPack("test-app-one", _signed({ +test.ifNotCiOsx("win", () => assertPack("test-app-one", signed({ targets: Platform.WINDOWS.createTarget(["default", "zip"]), }) )) -test("nsis", () => assertPack("test-app-one", _signed({ +test("nsis", () => assertPack("test-app-one", signed({ targets: Platform.WINDOWS.createTarget(["nsis"], Arch.ia32, Arch.x64), }), { - useTempDir: true, + useTempDir: true, } )) @@ -52,7 +44,7 @@ test.ifDevOrLinuxCi("nsis 32 perMachine, no run after finish", () => assertPack( } })) -test.ifNotCiOsx("nsis boring", () => assertPack("test-app-one", _signed({ +test.ifNotCiOsx("nsis boring", () => assertPack("test-app-one", signed({ targets: Platform.WINDOWS.createTarget(["nsis"]), devMetadata: { build: { @@ -62,7 +54,6 @@ test.ifNotCiOsx("nsis boring", () => assertPack("test-app-one", _signed({ } } }))) - test.ifNotCiOsx("nsis, installerHeaderIcon", () => { let headerIconPath: string | null = null return assertPack("test-app-one", { diff --git a/tslint.json b/tslint.json index ca7b1797ad5..cca20347622 100644 --- a/tslint.json +++ b/tslint.json @@ -56,6 +56,8 @@ "check-operator", "check-separator", "check-type" - ] + ], + "no-bitwise": false, + "jsdoc-format": false } } \ No newline at end of file diff --git a/typings/signcode.d.ts b/typings/signcode.d.ts deleted file mode 100644 index 930b847ea2b..00000000000 --- a/typings/signcode.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module "signcode-tf" { - export interface SignOptions { - path: string - cert: string - name?: string | null - password: string - site?: string | null - hash?: Array | null - overwrite?: boolean - } - - export function sign(options: SignOptions, callback: (error: Error | null) => void): void -} \ No newline at end of file