diff --git a/docs/Options.md b/docs/Options.md index 871ac1defe1..73cd30c4bb8 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -121,6 +121,8 @@ Windows specific build options. | signingHashAlgorithms | Array of signing algorithms used. Defaults to `['sha1', 'sha256']` | icon | The path to application icon. Defaults to `build/icon.ico` (consider using this convention instead of complicating your configuration). | legalTrademarks | The trademarks and registered trademarks. +| certificateSubjectName | The name of the subject of the signing certificate. Required only for EV Code Signing and works only on Windows. +| rfc3161TimeStampServer | The URL of the RFC 3161 time stamp server. Defaults to `http://timestamp.comodoca.com/rfc3161`. ### `.build.nsis` diff --git a/src/metadata.ts b/src/metadata.ts index 08ac327770b..1e004f6e9a2 100755 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -297,9 +297,6 @@ export interface MasBuildOptions extends MacOptions { Windows specific build options. */ export interface WinBuildOptions extends PlatformSpecificBuildOptions { - readonly certificateFile?: string - readonly certificatePassword?: string - /* Target package type: list of `squirrel`, `nsis`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`. Defaults to `squirrel`. */ @@ -352,6 +349,19 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions { The trademarks and registered trademarks. */ readonly legalTrademarks?: string | null + + readonly certificateFile?: string + readonly certificatePassword?: string + + /* + The name of the subject of the signing certificate. Required only for EV Code Signing and works only on Windows. + */ + readonly certificateSubjectName?: string + + /* + The URL of the RFC 3161 time stamp server. Defaults to `http://timestamp.comodoca.com/rfc3161`. + */ + readonly rfc3161TimeStampServer?: string } /* diff --git a/src/winPackager.ts b/src/winPackager.ts index 0bcdf57ca44..168e282a8e7 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -16,41 +16,51 @@ import { rename } from "fs-extra-p" const __awaiter = require("./util/awaiter") export interface FileCodeSigningInfo { - readonly file: string + readonly file?: string | null readonly password?: string | null + + readonly subjectName?: string | null } export class WinPackager extends PlatformPackager { - readonly cscInfo: Promise + readonly cscInfo: Promise | null private readonly iconPath: Promise constructor(info: BuildInfo, cleanupTasks: Array<() => Promise>) { super(info) - const certificateFile = this.platformSpecificBuildOptions.certificateFile - const cscLink = this.options.cscLink - if (certificateFile != null) { - const certificatePassword = this.platformSpecificBuildOptions.certificatePassword || this.getCscPassword() - this.cscInfo = BluebirdPromise.resolve({ - file: certificateFile, - password: certificatePassword == null ? null : certificatePassword.trim(), - }) - } - else if (cscLink != null) { - this.cscInfo = downloadCertificate(cscLink) - .then(path => { - if (cscLink.startsWith("https://")) { - cleanupTasks.push(() => deleteFile(path, true)) - } - return { - file: path, - password: this.getCscPassword(), - } + const subjectName = this.platformSpecificBuildOptions.certificateSubjectName + if (subjectName == null) { + const certificateFile = this.platformSpecificBuildOptions.certificateFile + const cscLink = this.options.cscLink + if (certificateFile != null) { + const certificatePassword = this.platformSpecificBuildOptions.certificatePassword || this.getCscPassword() + this.cscInfo = BluebirdPromise.resolve({ + file: certificateFile, + password: certificatePassword == null ? null : certificatePassword.trim(), }) + } + else if (cscLink != null) { + this.cscInfo = downloadCertificate(cscLink) + .then(path => { + if (cscLink.startsWith("https://")) { + cleanupTasks.push(() => deleteFile(path, true)) + } + return { + file: path, + password: this.getCscPassword(), + } + }) + } + else { + this.cscInfo = BluebirdPromise.resolve(null) + } } else { - this.cscInfo = BluebirdPromise.resolve(null) + this.cscInfo = BluebirdPromise.resolve({ + subjectName: subjectName + }) } this.iconPath = this.getValidIconPath() @@ -121,18 +131,22 @@ export class WinPackager extends PlatformPackager { log(`Signing ${path.basename(file)} (certificate file "${cscInfo.file}")`) await this.doSign({ path: file, + cert: cscInfo.file, - password: cscInfo.password!, + subjectName: cscInfo.subjectName, + + password: cscInfo.password, name: this.appInfo.productName, site: await this.appInfo.computePackageUrl(), hash: this.platformSpecificBuildOptions.signingHashAlgorithms, + tr: this.platformSpecificBuildOptions.rfc3161TimeStampServer, }) } } //noinspection JSMethodCanBeStatic - protected async doSign(opts: SignOptions): Promise { - return sign(opts) + protected async doSign(options: SignOptions): Promise { + return sign(options) } async signAndEditResources(file: string) { diff --git a/src/windowsCodeSign.ts b/src/windowsCodeSign.ts index b0fd87b98b2..2a1dd4c63f9 100644 --- a/src/windowsCodeSign.ts +++ b/src/windowsCodeSign.ts @@ -13,12 +13,17 @@ export function getSignVendorPath() { } export interface SignOptions { - path: string - cert: string - name?: string | null - password: string - site?: string | null - hash?: Array | null + readonly path: string + + readonly cert?: string | null + readonly subjectName?: string | null + + readonly name?: string | null + readonly password?: string | null + readonly site?: string | null + readonly hash?: Array | null + + readonly tr?: string | null } export async function sign(options: SignOptions) { @@ -45,26 +50,30 @@ export async function sign(options: SignOptions) { } // on windows be aware of http://stackoverflow.com/a/32640183/1910191 -async function spawnSign(options: any, inputPath: string, outputPath: string, hash: string, nest: boolean) { +async function spawnSign(options: SignOptions, 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 + nest || hash === "sha256" ? "/tr" : "/t", nest || hash === "sha256" ? (options.tr || "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) + const certificateFile = options.cert + if (certificateFile == null) { + if (process.platform !== "win32") { + throw new Error("certificateSubjectName supported only on Windows") + } + args.push("/n", options.subjectName!) } else { - args.push(isWin ? "/f" : "-certs", options.cert) - // todo win maybe incorrect - args.push(isWin ? "/csp" : "-key", options.key) + const certExtension = path.extname(certificateFile) + if (certExtension === ".p12" || certExtension === ".pfx") { + args.push(isWin ? "/f" : "-pkcs12", certificateFile) + } } if (!isWin || hash !== "sha1") { @@ -90,19 +99,12 @@ async function spawnSign(options: any, inputPath: string, outputPath: string, ha 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) + return await spawn(await getToolPath(), args) } // async function verify(options: any) { @@ -124,7 +126,7 @@ function getOutputPath(inputPath: string, hash: string) { return path.join(path.dirname(inputPath), `${path.basename(inputPath, extension)}-signed-${hash}${extension}`) } -async function getToolPath(options: any) { +async function getToolPath() { let result = process.env.SIGNTOOL_PATH if (result) { return result diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts index 427114a3a1b..aecd893edad 100755 --- a/test/src/winPackagerTest.ts +++ b/test/src/winPackagerTest.ts @@ -93,11 +93,11 @@ test("detect install-spinner, certificateFile/password", () => { }) }) -test.ifNotCiOsx("icon < 256", (t: any) => t.throws(assertPack("test-app-one", platform(Platform.WINDOWS), { +test.ifNotCiOsx("icon < 256", t => t.throws(assertPack("test-app-one", platform(Platform.WINDOWS), { tempDirCreated: projectDir => rename(path.join(projectDir, "build", "incorrect.ico"), path.join(projectDir, "build", "icon.ico")) }), /Windows icon size must be at least 256x256, please fix ".+/)) -test.ifNotCiOsx("icon not an image", (t: any) => t.throws(assertPack("test-app-one", platform(Platform.WINDOWS), { +test.ifNotCiOsx("icon not an image", t => t.throws(assertPack("test-app-one", platform(Platform.WINDOWS), { tempDirCreated: projectDir => outputFile(path.join(projectDir, "build", "icon.ico"), "foo") }), /Windows icon is not valid ico file, please fix ".+/)) @@ -122,6 +122,17 @@ test.ifOsx("custom icon", () => { }) }) +test.ifNotWindows("ev", t => t.throws(assertPack("test-app-one", { + targets: Platform.WINDOWS.createTarget(["dir"]), + devMetadata: { + build: { + win: { + certificateSubjectName: "ev", + } + } + } +}), /certificateSubjectName supported only on Windows/)) + class CheckingWinPackager extends WinPackager { effectiveDistOptions: any signOptions: SignOptions | null