Skip to content

Commit

Permalink
feat(electron-updater): ensure that update only to the application si…
Browse files Browse the repository at this point in the history
…gned with same cert

Close #1187
  • Loading branch information
MariaDima authored and develar committed Jun 6, 2017
1 parent e8b703f commit 66771d3
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 87 deletions.
1 change: 1 addition & 0 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ Windows Specific Options ([win](#Config-win)).
| rfc3161TimeStampServer| <code>string</code> | <a name="WinBuildOptions-rfc3161TimeStampServer"></a>The URL of the RFC 3161 time stamp server. Defaults to `http://timestamp.comodoca.com/rfc3161`. |
| timeStampServer| <code>string</code> | <a name="WinBuildOptions-timeStampServer"></a>The URL of the time stamp server. Defaults to `http://timestamp.verisign.com/scripts/timstamp.dll`. |
| publisherName| <code>string</code> \| <code>Array&lt;string&gt;</code> \| <code>null</code> | <a name="WinBuildOptions-publisherName"></a>[The publisher name](https://github.com/electron-userland/electron-builder/issues/1187#issuecomment-278972073), exactly as in your code signed certificate. Several names can be provided. Defaults to common name from your code signing certificate. |
| forceCodeSigningVerification = <code>true</code>| <code>boolean</code> | <a name="WinBuildOptions-forceCodeSigningVerification"></a>Whether to verify the signature of an available update before installation. The [publisher name](WinBuildOptions#publisherName) will be used for the signature verification. |


<!-- end of generated block -->
21 changes: 0 additions & 21 deletions docs/api/electron-updater.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,18 +224,12 @@ Developer API only. See [[Auto Update]] for user documentation.
* [electron-updater/out/NsisUpdater](#module_electron-updater/out/NsisUpdater)
* [.NsisUpdater](#NsisUpdater) ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
* [`.quitAndInstall(isSilent)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall)
* [`.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate) ⇒ <code>Promise&lt;string&gt;</code>

<a name="NsisUpdater"></a>

### NsisUpdater ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
**Kind**: class of [<code>electron-updater/out/NsisUpdater</code>](#module_electron-updater/out/NsisUpdater)
**Extends**: <code>[AppUpdater](Auto-Update#AppUpdater)</code>

* [.NsisUpdater](#NsisUpdater) ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
* [`.quitAndInstall(isSilent)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall)
* [`.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate) ⇒ <code>Promise&lt;string&gt;</code>

<a name="module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall"></a>

#### `nsisUpdater.quitAndInstall(isSilent)`
Expand All @@ -245,21 +239,6 @@ Developer API only. See [[Auto Update]] for user documentation.
| --- | --- |
| isSilent | <code>boolean</code> |

<a name="module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate"></a>

#### `nsisUpdater.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)` ⇒ <code>Promise&lt;string&gt;</code>
Start downloading update manually. You can use this method if `autoDownload` option is set to `false`.

**Kind**: instance method of [<code>NsisUpdater</code>](#NsisUpdater)
**Returns**: <code>Promise&lt;string&gt;</code> - Path to downloaded file.
**Access**: protected

| Param | Type |
| --- | --- |
| versionInfo | <code>[VersionInfo](Publishing-Artifacts#VersionInfo)</code> |
| fileInfo | <code>[FileInfo](Auto-Update#FileInfo)</code> |
| cancellationToken | <code>[CancellationToken](electron-builder-http#CancellationToken)</code> |

<a name="module_electron-updater/out/PrivateGitHubProvider"></a>

## electron-updater/out/PrivateGitHubProvider
Expand Down
5 changes: 3 additions & 2 deletions packages/electron-builder-http/src/httpExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,9 @@ function configurePipes(options: DownloadOptions, response: any, destination: st
}
}

if (options.sha512 != null) {
streams.push(new DigestTransform(options.sha512, "sha512", "base64"))
const sha512 = options.sha512
if (sha512 != null) {
streams.push(new DigestTransform(sha512, "sha512", sha512.length === 128 && !sha512.includes("+") && !sha512.includes("Z") && !sha512.includes("=") ? "hex" : "base64"))
}
else if (options.sha2 != null) {
streams.push(new DigestTransform(options.sha2, "sha256", "hex"))
Expand Down
8 changes: 8 additions & 0 deletions packages/electron-builder/src/options/winOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions {
* Defaults to common name from your code signing certificate.
*/
readonly publisherName?: string | Array<string> | null

/**
* Whether to verify the signature of an available update before installation.
* The [publisher name](WinBuildOptions#publisherName) will be used for the signature verification.
*
* @default true
*/
readonly forceCodeSigningVerification?: boolean
}

export interface CommonNsisOptions {
Expand Down
9 changes: 6 additions & 3 deletions packages/electron-builder/src/publish/PublishManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ export class PublishManager implements PublishContext {
let publishConfig = publishConfigs[0]

if (packager.platform === Platform.WINDOWS) {
const publisherName = await (<WinPackager>packager).computedPublisherName.value
if (publisherName != null) {
publishConfig = Object.assign({publisherName: publisherName}, publishConfig)
const winPackager = <WinPackager>packager
if (winPackager.isForceCodeSigningVerification) {
const publisherName = await winPackager.computedPublisherName.value
if (publisherName != null) {
publishConfig = Object.assign({publisherName: publisherName}, publishConfig)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/electron-builder/src/winPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
return publisherName == null ? null : asArray(publisherName)
})

get isForceCodeSigningVerification(): boolean {
return this.platformSpecificBuildOptions.forceCodeSigningVerification !== false
}

constructor(info: BuildInfo) {
super(info)
}
Expand Down
75 changes: 70 additions & 5 deletions packages/electron-updater/src/NsisUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn } from "child_process"
import BluebirdPromise from "bluebird-lst"
import { execFile, spawn } from "child_process"
import { DownloadOptions } from "electron-builder-http"
import { CancellationError, CancellationToken } from "electron-builder-http/out/CancellationToken"
import { PublishConfiguration, VersionInfo } from "electron-builder-http/out/publishOptions"
Expand All @@ -18,10 +19,7 @@ export class NsisUpdater extends AppUpdater {
super(options, app)
}

/**
* Start downloading update manually. You can use this method if `autoDownload` option is set to `false`.
* @returns {Promise<string>} Path to downloaded file.
*/
/*** @private */
protected async doDownloadUpdate(versionInfo: VersionInfo, fileInfo: FileInfo, cancellationToken: CancellationToken) {
const downloadOptions: DownloadOptions = {
skipDirCreation: true,
Expand Down Expand Up @@ -57,6 +55,17 @@ export class NsisUpdater extends AppUpdater {
throw e
}

const signatureVerificationStatus = await this.verifySignature(tempFile)
if (signatureVerificationStatus != null) {
try {
await remove(tempDir)
}
finally {
// noinspection ThrowInsideFinallyBlockJS
throw new Error(`New version ${this.versionInfo!.version} is not signed by the application owner: ${signatureVerificationStatus}`)
}
}

if (logger != null) {
logger.info(`New version ${this.versionInfo!.version} has been downloaded to ${tempFile}`)
}
Expand All @@ -67,6 +76,62 @@ export class NsisUpdater extends AppUpdater {
return tempFile
}

// $certificateInfo = (Get-AuthenticodeSignature 'xxx\yyy.exe'
// | where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and $_.SignerCertificate.Subject.Contains("CN=siemens.com")})
// | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }
private async verifySignature(tempUpdateFile: string): Promise<string | null> {
const updateConfig = await this.loadUpdateConfig()
const publisherName = updateConfig.publisherName
if (publisherName == null) {
return null
}

return await new BluebirdPromise<string | null>((resolve, reject) => {
const commonNameConstraint = (Array.isArray(publisherName) ? <Array<string>>publisherName : [publisherName]).map(it => `$_.SignerCertificate.Subject.Contains('CN=${it},')`).join(" -or ")
const constraintCommand = `where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and (${commonNameConstraint})}`
const verifySignatureCommand = `Get-AuthenticodeSignature '${tempUpdateFile}' | ${constraintCommand}`
const powershellChild = spawn("powershell.exe", [(`$certificateInfo = (${verifySignatureCommand}) | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }`)])
powershellChild.on("error", reject)
powershellChild.on("exit", code => {
if (code !== 1) {
resolve(null)
return
}

execFile("powershell.exe", [`Get-AuthenticodeSignature '${tempUpdateFile}' | ConvertTo-Json -Compress`], {maxBuffer: 4 * 1024000}, (error, stdout, stderr) => {
if (error != null) {
reject(error)
return
}

if (stderr) {
reject(new Error(`Cannot execute Get-AuthenticodeSignature: ${stderr}`))
return
}

const data = JSON.parse(stdout)
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 data.Path

const result = JSON.stringify(data, (name, value) => name === "RawData" ? undefined : value, 2)
if (this.logger != null) {
this.logger.info(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
}
resolve(result)
})
})
})
}

private addQuitHandler() {
if (this.quitHandlerAdded) {
return
Expand Down
2 changes: 2 additions & 0 deletions test/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ skip.ifAll = skip
const isMac = process.platform === "darwin"
test.ifMac = isMac ? test : skip
test.ifNotWindows = isWindows ? skip : test
test.ifWindows = isWindows ? test : skip

skip.ifMac = skip
skip.ifLinux = skip
skip.ifWindows = skip
skip.ifNotWindows = skip
skip.ifCi = skip
skip.ifNotCi = skip
Expand Down
65 changes: 63 additions & 2 deletions test/out/__snapshots__/nsisUpdaterTest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,20 @@ Array [
exports[`file url github 1`] = `
Object {
"name": "TestApp-Setup-1.1.0.exe",
"sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
"sha512": undefined,
"sha2": undefined,
"sha512": "xrTrW8dzWYlPnu71Y4lpLIAuIurBZJvZmqEZyz1rzM3CbbE1Z+T+P5qYYZgwmhmXdYPOpvnmYKa0HGdgXggwtQ==",
"url": "https://github.com/develar/__test_nsis_release/releases/download/v1.1.0/TestApp-Setup-1.1.0.exe",
}
`;

exports[`file url github 2`] = `
Array [
"checking-for-update",
"update-available",
"update-downloaded",
]
`;

exports[`file url github pre-release 1`] = `
Object {
"name": "TestApp-Setup-1.5.2-beta.3.exe",
Expand All @@ -93,6 +101,14 @@ Object {
`;

exports[`file url github pre-release 2`] = `
Array [
"checking-for-update",
"update-available",
"update-downloaded",
]
`;

exports[`file url github pre-release 3`] = `
Object {
"path": "TestApp Setup 1.5.2-beta.3.exe",
"releaseName": "v1.5.2-beta.3",
Expand All @@ -114,6 +130,51 @@ Object {
}
`;

exports[`invalid signature 1`] = `
"New version 1.1.0 is not signed by the application owner: {
\\"SignerCertificate\\": {
\\"FriendlyName\\": \\"\\",
\\"IssuerName\\": {
\\"Name\\": \\"CN=StartCom Class 2 Object CA, OU=StartCom Certification Authority, O=StartCom Ltd., C=IL\\",
\\"Oid\\": \\"System.Security.Cryptography.Oid\\"
},
\\"NotAfter\\": \\"/Date(1516526235000)/\\",
\\"NotBefore\\": \\"/Date(1453367835000)/\\",
\\"PrivateKey\\": null,
\\"PublicKey\\": {
\\"Key\\": \\"System.Security.Cryptography.RSACryptoServiceProvider\\",
\\"Oid\\": \\"System.Security.Cryptography.Oid\\",
\\"EncodedKeyValue\\": \\"System.Security.Cryptography.AsnEncodedData\\",
\\"EncodedParameters\\": \\"System.Security.Cryptography.AsnEncodedData\\"
},
\\"SerialNumber\\": \\"18CB5EC53FB14EC2DBB44BD1518AF901\\",
\\"SubjectName\\": {
\\"Name\\": \\"CN=Vladimir Krivosheev, O=Vladimir Krivosheev, L=Grunwald, S=Bayern, C=DE\\",
\\"Oid\\": \\"System.Security.Cryptography.Oid\\"
},
\\"SignatureAlgorithm\\": {
\\"Value\\": \\"1.2.840.113549.1.1.11\\",
\\"FriendlyName\\": \\"sha256RSA\\"
},
\\"Thumbprint\\": \\"32F67D8F957E740C692ADD8CD5A5E463992193DD\\",
\\"Version\\": 3,
\\"Issuer\\": \\"CN=StartCom Class 2 Object CA, OU=StartCom Certification Authority, O=StartCom Ltd., C=IL\\",
\\"Subject\\": \\"CN=Vladimir Krivosheev, O=Vladimir Krivosheev, L=Grunwald, S=Bayern, C=DE\\"
},
\\"TimeStamperCertificate\\": null,
\\"Status\\": 0,
\\"StatusMessage\\": \\"Signature verified.\\"
}"
`;

exports[`invalid signature 2`] = `
Array [
"checking-for-update",
"update-available",
"error",
]
`;

exports[`sha512 mismatch error event 1`] = `
Object {
"name": "TestApp Setup 1.1.0.exe",
Expand Down
Loading

0 comments on commit 66771d3

Please sign in to comment.