import { parseDn } from "builder-util-runtime"
import { execFile, execFileSync } from "child_process"
import * as os from "os"
import { Logger } from "./types"
import * as path from "path"
export function verifySignature(publisherNames: Array<string>, unescapedTempUpdateFile: string, logger: Logger): Promise<string | null> {
return new Promise<string | null>((resolve, reject) => {
const tempUpdateFile = unescapedTempUpdateFile.replace(/'/g, "''")
logger.info(`Verifying signature ${tempUpdateFile}`)
execFile(
`set "PSModulePath=" & chcp 65001 >NUL & powershell.exe`,
["-NoProfile", "-NonInteractive", "-InputFormat", "None", "-Command", `"Get-AuthenticodeSignature -LiteralPath '${tempUpdateFile}' | ConvertTo-Json -Compress"`],
{
shell: true,
timeout: 20 * 1000,
},
(error, stdout, stderr) => {
try {
if (error != null || stderr) {
handleError(logger, error, stderr, reject)
resolve(null)
return
}
const data = parseOut(stdout)
if (data.Status === 0) {
try {
const normlaizedUpdateFilePath = path.normalize(data.Path)
const normalizedTempUpdateFile = path.normalize(unescapedTempUpdateFile)
logger.info(`LiteralPath: ${normlaizedUpdateFilePath}. Update Path: ${normalizedTempUpdateFile}`)
if (normlaizedUpdateFilePath !== normalizedTempUpdateFile) {
handleError(logger, new Error(`LiteralPath of ${normlaizedUpdateFilePath} is different than ${normalizedTempUpdateFile}`), stderr, reject)
resolve(null)
return
}
} catch (error: any) {
logger.warn(`Unable to verify LiteralPath of update asset due to missing data.Path. Skipping this step of validation. Message: ${error.message ?? error.stack}`)
}
const subject = parseDn(data.SignerCertificate.Subject)
let match = false
for (const name of publisherNames) {
const dn = parseDn(name)
if (dn.size) {
const allKeys = Array.from(dn.keys())
match = allKeys.every(key => {
return dn.get(key) === subject.get(key)
})
} else if (name === subject.get("CN")!) {
logger.warn(`Signature validated using only CN ${name}. Please add your full Distinguished Name (DN) to publisherNames configuration`)
match = true
}
if (match) {
resolve(null)
return
}
}
}
const result = `publisherNames: ${publisherNames.join(" | ")}, raw info: ` + JSON.stringify(data, (name, value) => (name === "RawData" ? undefined : value), 2)
logger.warn(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
resolve(result)
} catch (e: any) {
handleError(logger, e, null, reject)
resolve(null)
return
}
}
)
})
}
function parseOut(out: string): any {
const data = JSON.parse(out)
delete data.PrivateKey
delete data.IsOSBinary
delete data.SignatureType
const signerCertificate = data.SignerCertificate
if (signerCertificate != null) {
delete signerCertificate.Archived
delete signerCertificate.Extensions
delete signerCertificate.Handle
delete signerCertificate.HasPrivateKey
delete signerCertificate.SubjectName
}
return data
}
function handleError(logger: Logger, error: Error | null, stderr: string | null, reject: (reason: any) => void): void {
if (isOldWin6()) {
logger.warn(
`Cannot execute Get-AuthenticodeSignature: ${error || stderr}. Ignoring signature validation due to unsupported powershell version. Please upgrade to powershell 3 or higher.`
)
return
}
try {
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "ConvertTo-Json test"], { timeout: 10 * 1000 } as any)
} catch (testError: any) {
logger.warn(
`Cannot execute ConvertTo-Json: ${testError.message}. Ignoring signature validation due to unsupported powershell version. Please upgrade to powershell 3 or higher.`
)
return
}
if (error != null) {
reject(error)
}
if (stderr) {
reject(new Error(`Cannot execute Get-AuthenticodeSignature, stderr: ${stderr}. Failing signature validation due to unknown stderr.`))
}
}
function isOldWin6(): boolean {
const winVersion = os.release()
return winVersion.startsWith("6.") && !winVersion.startsWith("6.3")
}