Skip to content

Commit

Permalink
feat: EV certificate code signing (custom /n and /tr)
Browse files Browse the repository at this point in the history
Closes #627 #590
  • Loading branch information
develar committed Aug 9, 2016
1 parent 6a906ac commit e008c19
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 53 deletions.
2 changes: 2 additions & 0 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Windows specific build options.
| signingHashAlgorithms | <a name="WinBuildOptions-signingHashAlgorithms"></a>Array of signing algorithms used. Defaults to `['sha1', 'sha256']`
| icon | <a name="WinBuildOptions-icon"></a>The path to application icon. Defaults to `build/icon.ico` (consider using this convention instead of complicating your configuration).
| legalTrademarks | <a name="WinBuildOptions-legalTrademarks"></a>The trademarks and registered trademarks.
| certificateSubjectName | <a name="WinBuildOptions-certificateSubjectName"></a>The name of the subject of the signing certificate. Required only for EV Code Signing and works only on Windows.
| rfc3161TimeStampServer | <a name="WinBuildOptions-rfc3161TimeStampServer"></a>The URL of the RFC 3161 time stamp server. Defaults to `http://timestamp.comodoca.com/rfc3161`.

<a name="NsisOptions"></a>
### `.build.nsis`
Expand Down
16 changes: 13 additions & 3 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -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
}

/*
Expand Down
64 changes: 39 additions & 25 deletions src/winPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WinBuildOptions> {
readonly cscInfo: Promise<FileCodeSigningInfo | null>
readonly cscInfo: Promise<FileCodeSigningInfo | null> | null

private readonly iconPath: Promise<string>

constructor(info: BuildInfo, cleanupTasks: Array<() => Promise<any>>) {
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()
Expand Down Expand Up @@ -121,18 +131,22 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
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<any> {
return sign(opts)
protected async doSign(options: SignOptions): Promise<any> {
return sign(options)
}

async signAndEditResources(file: string) {
Expand Down
48 changes: 25 additions & 23 deletions src/windowsCodeSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ export function getSignVendorPath() {
}

export interface SignOptions {
path: string
cert: string
name?: string | null
password: string
site?: string | null
hash?: Array<string> | 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<string> | null

readonly tr?: string | null
}

export async function sign(options: SignOptions) {
Expand All @@ -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") {
Expand All @@ -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) {
Expand All @@ -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
Expand Down
15 changes: 13 additions & 2 deletions test/src/winPackagerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ".+/))

Expand All @@ -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
Expand Down

0 comments on commit e008c19

Please sign in to comment.