import { asArray, InvalidConfigurationError, log, retry } from "builder-util"
import { MemoLazy, parseDn } from "builder-util-runtime"
import { rename } from "fs-extra"
import { Lazy } from "lazy-val"
import * as os from "os"
import * as path from "path"
import { getBin } from "../binDownload"
import { Target } from "../core"
import { WindowsConfiguration } from "../options/winOptions"
import AppXTarget from "../targets/AppxTarget"
import { executeAppBuilderAsJson } from "../util/appBuilder"
import { computeToolEnv, ToolInfo } from "../util/bundledTool"
import { isUseSystemSigncode } from "../util/flags"
import { resolveFunction } from "../util/resolve"
import { VmManager } from "../vm/vm"
import { WinPackager } from "../winPackager"
import { importCertificate } from "./codesign"
import { SignManager } from "./signManager"
import { WindowsSignOptions } from "./windowsCodeSign"
export function getSignVendorPath() {
return getBin("winCodeSign")
}
export type CustomWindowsSign = (configuration: CustomWindowsSignTaskConfiguration, packager?: WinPackager) => Promise<any>
export interface WindowsSignToolOptions extends WindowsSignOptions {
readonly name: string
readonly site: string | null
}
export interface FileCodeSigningInfo {
readonly file: string
readonly password: string | null
}
export interface WindowsSignTaskConfiguration extends WindowsSignToolOptions {
readonly cscInfo: FileCodeSigningInfo | CertificateFromStoreInfo | null
resultOutputPath?: string
hash: string
isNest: boolean
}
export interface CustomWindowsSignTaskConfiguration extends WindowsSignTaskConfiguration {
computeSignToolArgs(isWin: boolean): Array<string>
}
export interface CertificateInfo {
readonly commonName: string
readonly bloodyMicrosoftSubjectDn: string
}
export interface CertificateFromStoreInfo {
thumbprint: string
subject: string
store: string
isLocalMachineStore: boolean
}
interface CertInfo {
Subject: string
Thumbprint: string
PSParentPath: string
}
export class WindowsSignToolManager implements SignManager {
private readonly platformSpecificBuildOptions: WindowsConfiguration
constructor(private readonly packager: WinPackager) {
this.platformSpecificBuildOptions = packager.platformSpecificBuildOptions
}
readonly computedPublisherName = new Lazy<Array<string> | null>(async () => {
const publisherName = this.platformSpecificBuildOptions.signtoolOptions?.publisherName
if (publisherName === null) {
return null
} else if (publisherName != null) {
return asArray(publisherName)
}
const certInfo = await this.lazyCertInfo.value
return certInfo == null ? null : [certInfo.commonName]
})
readonly lazyCertInfo = new MemoLazy<MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>, CertificateInfo | null>(
() => this.cscInfo,
async csc => {
const cscInfo = await csc.value
if (cscInfo == null) {
return null
}
if ("subject" in cscInfo) {
const bloodyMicrosoftSubjectDn = cscInfo.subject
return {
commonName: parseDn(bloodyMicrosoftSubjectDn).get("CN")!,
bloodyMicrosoftSubjectDn,
}
}
const cscFile = cscInfo.file
if (cscFile == null) {
return null
}
return await this.getCertInfo(cscFile, cscInfo.password || "")
}
)
readonly cscInfo = new MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>(
() => this.platformSpecificBuildOptions,
platformSpecificBuildOptions => {
const subjectName = platformSpecificBuildOptions.signtoolOptions?.certificateSubjectName
const shaType = platformSpecificBuildOptions.signtoolOptions?.certificateSha1
if (subjectName != null || shaType != null) {
return this.packager.vm.value
.then(vm => this.getCertificateFromStoreInfo(platformSpecificBuildOptions, vm))
.catch((e: any) => {
if (platformSpecificBuildOptions.signtoolOptions?.sign == null) {
throw e
} else {
log.debug({ error: e }, "getCertificateFromStoreInfo error")
return null
}
})
}
const certificateFile = platformSpecificBuildOptions.signtoolOptions?.certificateFile
if (certificateFile != null) {
const certificatePassword = this.packager.getCscPassword()
return Promise.resolve({
file: certificateFile,
password: certificatePassword == null ? null : certificatePassword.trim(),
})
}
const cscLink = this.packager.getCscLink("WIN_CSC_LINK")
if (cscLink == null || cscLink === "") {
return Promise.resolve(null)
}
return (
importCertificate(cscLink, this.packager.info.tempDirManager, this.packager.projectDir)
.catch((e: any) => {
if (e instanceof InvalidConfigurationError) {
throw new InvalidConfigurationError(`Env WIN_CSC_LINK is not correct, cannot resolve: ${e.message}`)
} else {
throw e
}
})
.then(path => {
return {
file: path,
password: this.packager.getCscPassword(),
}
})
)
}
)
initialize(): Promise<void> {
return Promise.resolve()
}
async computePublisherName(target: Target, publisherName: string) {
if (target instanceof AppXTarget && (await this.cscInfo.value) == null) {
log.info({ reason: "Windows Store only build" }, "AppX is not signed")
return publisherName || "CN=ms"
}
const certInfo = await this.lazyCertInfo.value
const publisher = publisherName || (certInfo == null ? null : certInfo.bloodyMicrosoftSubjectDn)
if (publisher == null) {
throw new Error("Internal error: cannot compute subject using certificate info")
}
return publisher
}
async signFile(options: WindowsSignOptions): Promise<boolean> {
let hashes = options.options.signtoolOptions?.signingHashAlgorithms
if (options.path.endsWith(".msi")) {
hashes = [hashes != null && !hashes.includes("sha1") ? "sha256" : "sha1"]
} else if (options.path.endsWith(".appx")) {
hashes = ["sha256"]
} else if (hashes == null) {
hashes = ["sha1", "sha256"]
} else {
hashes = Array.isArray(hashes) ? hashes : [hashes]
}
const name = this.packager.appInfo.productName
const site = await this.packager.appInfo.computePackageUrl()
const customSign = await resolveFunction(this.packager.appInfo.type, options.options.signtoolOptions?.sign, "sign")
const cscInfo = await this.cscInfo.value
if (cscInfo) {
let logInfo: any = {
file: log.filePath(options.path),
}
if ("file" in cscInfo) {
logInfo = {
...logInfo,
certificateFile: cscInfo.file,
}
} else {
logInfo = {
...logInfo,
subject: cscInfo.subject,
thumbprint: cscInfo.thumbprint,
store: cscInfo.store,
user: cscInfo.isLocalMachineStore ? "local machine" : "current user",
}
}
log.info(logInfo, "signing")
} else if (!customSign) {
log.debug({ signHook: !!customSign, cscInfo }, "no signing info identified, signing is skipped")
return false
}
const executor = customSign || ((config: CustomWindowsSignTaskConfiguration, packager: WinPackager) => this.doSign(config, packager))
let isNest = false
for (const hash of hashes) {
const taskConfiguration: WindowsSignTaskConfiguration = { ...options, name, site, cscInfo, hash, isNest }
await Promise.resolve(
executor(
{
...taskConfiguration,
computeSignToolArgs: isWin => this.computeSignToolArgs(taskConfiguration, isWin),
},
this.packager
)
)
isNest = true
if (taskConfiguration.resultOutputPath != null) {
await rename(taskConfiguration.resultOutputPath, options.path)
}
}
return true
}
async getCertInfo(file: string, password: string): Promise<CertificateInfo> {
let result: any = null
const errorMessagePrefix = "Cannot extract publisher name from code signing certificate. As workaround, set win.publisherName. Error: "
try {
result = await executeAppBuilderAsJson<any>(["certificate-info", "--input", file, "--password", password])
} catch (e: any) {
throw new Error(`${errorMessagePrefix}${e.stack || e}`)
}
if (result.error != null) {
throw new InvalidConfigurationError(`${errorMessagePrefix}${result.error}`)
}
return result
}
computeSignToolArgs(options: WindowsSignTaskConfiguration, isWin: boolean, vm: VmManager = new VmManager()): Array<string> {
const inputFile = vm.toVmFile(options.path)
const outputPath = isWin ? inputFile : this.getOutputPath(inputFile, options.hash)
if (!isWin) {
options.resultOutputPath = outputPath
}
const args = isWin ? ["sign"] : ["-in", inputFile, "-out", outputPath]
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
const timestampingServiceUrl = options.options.signtoolOptions?.timeStampServer || "http://timestamp.digicert.com"
if (isWin) {
args.push(
options.isNest || options.hash === "sha256" ? "/tr" : "/t",
options.isNest || options.hash === "sha256" ? options.options.signtoolOptions?.rfc3161TimeStampServer || "http://timestamp.digicert.com" : timestampingServiceUrl
)
} else {
args.push("-t", timestampingServiceUrl)
}
}
const certificateFile = (options.cscInfo as FileCodeSigningInfo).file
if (certificateFile == null) {
const cscInfo = options.cscInfo as CertificateFromStoreInfo
const subjectName = cscInfo.thumbprint
if (!isWin) {
throw new Error(`${subjectName == null ? "certificateSha1" : "certificateSubjectName"} supported only on Windows`)
}
args.push("/sha1", cscInfo.thumbprint)
args.push("/s", cscInfo.store)
if (cscInfo.isLocalMachineStore) {
args.push("/sm")
}
} else {
const certExtension = path.extname(certificateFile)
if (certExtension === ".p12" || certExtension === ".pfx") {
args.push(isWin ? "/f" : "-pkcs12", vm.toVmFile(certificateFile))
} else {
throw new Error(`Please specify pkcs12 (.p12/.pfx) file, ${certificateFile} is not correct`)
}
}
if (!isWin || options.hash !== "sha1") {
args.push(isWin ? "/fd" : "-h", options.hash)
if (isWin && process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
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 (options.isNest) {
args.push(isWin ? "/as" : "-nest")
}
const password = options.cscInfo == null ? null : (options.cscInfo as FileCodeSigningInfo).password
if (password) {
args.push(isWin ? "/p" : "-pass", password)
}
const additionalCert = options.options.signtoolOptions?.additionalCertificateFile
if (additionalCert) {
args.push(isWin ? "/ac" : "-ac", vm.toVmFile(additionalCert))
}
const httpsProxyFromEnv = process.env.HTTPS_PROXY
if (!isWin && httpsProxyFromEnv != null && httpsProxyFromEnv.length) {
args.push("-p", httpsProxyFromEnv)
}
if (isWin) {
args.push("/debug")
args.push(inputFile)
}
return args
}
getOutputPath(inputPath: string, hash: string) {
const extension = path.extname(inputPath)
return path.join(path.dirname(inputPath), `${path.basename(inputPath, extension)}-signed-${hash}${extension}`)
}
getWinSignTool(vendorPath: string): string {
if (isOldWin6()) {
return path.join(vendorPath, "windows-6", "signtool.exe")
} else {
return path.join(vendorPath, "windows-10", process.arch, "signtool.exe")
}
}
async getToolPath(isWin = process.platform === "win32"): Promise<ToolInfo> {
if (isUseSystemSigncode()) {
return { path: "osslsigncode" }
}
const result = process.env.SIGNTOOL_PATH
if (result) {
return { path: result }
}
const vendorPath = await getSignVendorPath()
if (isWin) {
return { path: this.getWinSignTool(vendorPath) }
} else if (process.platform === "darwin") {
const toolDirPath = path.join(vendorPath, process.platform, "10.12")
return {
path: path.join(toolDirPath, "osslsigncode"),
env: computeToolEnv([path.join(toolDirPath, "lib")]),
}
} else {
return { path: path.join(vendorPath, process.platform, "osslsigncode") }
}
}
async getCertificateFromStoreInfo(options: WindowsConfiguration, vm: VmManager): Promise<CertificateFromStoreInfo> {
const certificateSubjectName = options.signtoolOptions?.certificateSubjectName
const certificateSha1 = options.signtoolOptions?.certificateSha1?.toUpperCase()
const ps = await vm.powershellCommand.value
const rawResult = await vm.exec(ps, [
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-ChildItem -Recurse Cert: -CodeSigningCert | Select-Object -Property Subject,PSParentPath,Thumbprint | ConvertTo-Json -Compress",
])
const certList = rawResult.length === 0 ? [] : asArray<CertInfo>(JSON.parse(rawResult))
for (const certInfo of certList) {
if (
(certificateSubjectName != null && !certInfo.Subject.includes(certificateSubjectName)) ||
(certificateSha1 != null && certInfo.Thumbprint.toUpperCase() !== certificateSha1)
) {
continue
}
const parentPath = certInfo.PSParentPath
const store = parentPath.substring(parentPath.lastIndexOf("\\") + 1)
log.debug({ store, PSParentPath: parentPath }, "auto-detect certificate store")
const isLocalMachineStore = parentPath.includes("Certificate::LocalMachine")
log.debug(null, "auto-detect using of LocalMachine store")
return {
thumbprint: certInfo.Thumbprint,
subject: certInfo.Subject,
store,
isLocalMachineStore,
}
}
throw new Error(`Cannot find certificate ${certificateSubjectName || certificateSha1}, all certs: ${rawResult}`)
}
async doSign(configuration: CustomWindowsSignTaskConfiguration, packager: WinPackager) {
const timeout = parseInt(process.env.SIGNTOOL_TIMEOUT as any, 10) || 10 * 60 * 1000
let args: Array<string>
let env = process.env
let vm: VmManager
const useVmIfNotOnWin = configuration.path.endsWith(".appx") || !("file" in configuration.cscInfo!)
const isWin = process.platform === "win32" || useVmIfNotOnWin
const toolInfo = await this.getToolPath(isWin)
const tool = toolInfo.path
if (useVmIfNotOnWin) {
vm = await packager.vm.value
args = this.computeSignToolArgs(configuration, isWin, vm)
} else {
vm = new VmManager()
args = configuration.computeSignToolArgs(isWin)
if (toolInfo.env != null) {
env = toolInfo.env
}
}
await retry(
() => vm.exec(tool, args, { timeout, env }),
2,
15000,
10000,
0,
(e: any) => {
if (
e.message.includes("The file is being used by another process") ||
e.message.includes("The specified timestamp server either could not be reached") ||
e.message.includes("No certificates were found that met all the given criteria.")
) {
log.warn(`Attempt to code sign failed, another attempt will be made in 15 seconds: ${e.message}`)
return true
}
return false
}
)
}
}
export function isOldWin6() {
const winVersion = os.release()
return winVersion.startsWith("6.") && !winVersion.startsWith("6.3")
}