Skip to content

Commit 66771d3

Browse files
MariaDimadevelar
authored andcommitted
feat(electron-updater): ensure that update only to the application signed with same cert
Close #1187
1 parent e8b703f commit 66771d3

File tree

11 files changed

+207
-87
lines changed

11 files changed

+207
-87
lines changed

docs/Options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ Windows Specific Options ([win](#Config-win)).
567567
| 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`. |
568568
| 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`. |
569569
| 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. |
570+
| 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. |
570571

571572

572573
<!-- end of generated block -->

docs/api/electron-updater.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,12 @@ Developer API only. See [[Auto Update]] for user documentation.
224224
* [electron-updater/out/NsisUpdater](#module_electron-updater/out/NsisUpdater)
225225
* [.NsisUpdater](#NsisUpdater) ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
226226
* [`.quitAndInstall(isSilent)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall)
227-
* [`.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate) ⇒ <code>Promise&lt;string&gt;</code>
228227

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

231230
### NsisUpdater ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
232231
**Kind**: class of [<code>electron-updater/out/NsisUpdater</code>](#module_electron-updater/out/NsisUpdater)
233232
**Extends**: <code>[AppUpdater](Auto-Update#AppUpdater)</code>
234-
235-
* [.NsisUpdater](#NsisUpdater) ⇐ <code>[AppUpdater](Auto-Update#AppUpdater)</code>
236-
* [`.quitAndInstall(isSilent)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall)
237-
* [`.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)`](#module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate) ⇒ <code>Promise&lt;string&gt;</code>
238-
239233
<a name="module_electron-updater/out/NsisUpdater.NsisUpdater+quitAndInstall"></a>
240234

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

248-
<a name="module_electron-updater/out/NsisUpdater.NsisUpdater+doDownloadUpdate"></a>
249-
250-
#### `nsisUpdater.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)` ⇒ <code>Promise&lt;string&gt;</code>
251-
Start downloading update manually. You can use this method if `autoDownload` option is set to `false`.
252-
253-
**Kind**: instance method of [<code>NsisUpdater</code>](#NsisUpdater)
254-
**Returns**: <code>Promise&lt;string&gt;</code> - Path to downloaded file.
255-
**Access**: protected
256-
257-
| Param | Type |
258-
| --- | --- |
259-
| versionInfo | <code>[VersionInfo](Publishing-Artifacts#VersionInfo)</code> |
260-
| fileInfo | <code>[FileInfo](Auto-Update#FileInfo)</code> |
261-
| cancellationToken | <code>[CancellationToken](electron-builder-http#CancellationToken)</code> |
262-
263242
<a name="module_electron-updater/out/PrivateGitHubProvider"></a>
264243

265244
## electron-updater/out/PrivateGitHubProvider

packages/electron-builder-http/src/httpExecutor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ function configurePipes(options: DownloadOptions, response: any, destination: st
229229
}
230230
}
231231

232-
if (options.sha512 != null) {
233-
streams.push(new DigestTransform(options.sha512, "sha512", "base64"))
232+
const sha512 = options.sha512
233+
if (sha512 != null) {
234+
streams.push(new DigestTransform(sha512, "sha512", sha512.length === 128 && !sha512.includes("+") && !sha512.includes("Z") && !sha512.includes("=") ? "hex" : "base64"))
234235
}
235236
else if (options.sha2 != null) {
236237
streams.push(new DigestTransform(options.sha2, "sha256", "hex"))

packages/electron-builder/src/options/winOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export interface WinBuildOptions extends PlatformSpecificBuildOptions {
7272
* Defaults to common name from your code signing certificate.
7373
*/
7474
readonly publisherName?: string | Array<string> | null
75+
76+
/**
77+
* Whether to verify the signature of an available update before installation.
78+
* The [publisher name](WinBuildOptions#publisherName) will be used for the signature verification.
79+
*
80+
* @default true
81+
*/
82+
readonly forceCodeSigningVerification?: boolean
7583
}
7684

7785
export interface CommonNsisOptions {

packages/electron-builder/src/publish/PublishManager.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ export class PublishManager implements PublishContext {
9191
let publishConfig = publishConfigs[0]
9292

9393
if (packager.platform === Platform.WINDOWS) {
94-
const publisherName = await (<WinPackager>packager).computedPublisherName.value
95-
if (publisherName != null) {
96-
publishConfig = Object.assign({publisherName: publisherName}, publishConfig)
94+
const winPackager = <WinPackager>packager
95+
if (winPackager.isForceCodeSigningVerification) {
96+
const publisherName = await winPackager.computedPublisherName.value
97+
if (publisherName != null) {
98+
publishConfig = Object.assign({publisherName: publisherName}, publishConfig)
99+
}
97100
}
98101
}
99102

packages/electron-builder/src/winPackager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
8282
return publisherName == null ? null : asArray(publisherName)
8383
})
8484

85+
get isForceCodeSigningVerification(): boolean {
86+
return this.platformSpecificBuildOptions.forceCodeSigningVerification !== false
87+
}
88+
8589
constructor(info: BuildInfo) {
8690
super(info)
8791
}

packages/electron-updater/src/NsisUpdater.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { spawn } from "child_process"
1+
import BluebirdPromise from "bluebird-lst"
2+
import { execFile, spawn } from "child_process"
23
import { DownloadOptions } from "electron-builder-http"
34
import { CancellationError, CancellationToken } from "electron-builder-http/out/CancellationToken"
45
import { PublishConfiguration, VersionInfo } from "electron-builder-http/out/publishOptions"
@@ -18,10 +19,7 @@ export class NsisUpdater extends AppUpdater {
1819
super(options, app)
1920
}
2021

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

58+
const signatureVerificationStatus = await this.verifySignature(tempFile)
59+
if (signatureVerificationStatus != null) {
60+
try {
61+
await remove(tempDir)
62+
}
63+
finally {
64+
// noinspection ThrowInsideFinallyBlockJS
65+
throw new Error(`New version ${this.versionInfo!.version} is not signed by the application owner: ${signatureVerificationStatus}`)
66+
}
67+
}
68+
6069
if (logger != null) {
6170
logger.info(`New version ${this.versionInfo!.version} has been downloaded to ${tempFile}`)
6271
}
@@ -67,6 +76,62 @@ export class NsisUpdater extends AppUpdater {
6776
return tempFile
6877
}
6978

79+
// $certificateInfo = (Get-AuthenticodeSignature 'xxx\yyy.exe'
80+
// | where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and $_.SignerCertificate.Subject.Contains("CN=siemens.com")})
81+
// | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }
82+
private async verifySignature(tempUpdateFile: string): Promise<string | null> {
83+
const updateConfig = await this.loadUpdateConfig()
84+
const publisherName = updateConfig.publisherName
85+
if (publisherName == null) {
86+
return null
87+
}
88+
89+
return await new BluebirdPromise<string | null>((resolve, reject) => {
90+
const commonNameConstraint = (Array.isArray(publisherName) ? <Array<string>>publisherName : [publisherName]).map(it => `$_.SignerCertificate.Subject.Contains('CN=${it},')`).join(" -or ")
91+
const constraintCommand = `where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and (${commonNameConstraint})}`
92+
const verifySignatureCommand = `Get-AuthenticodeSignature '${tempUpdateFile}' | ${constraintCommand}`
93+
const powershellChild = spawn("powershell.exe", [(`$certificateInfo = (${verifySignatureCommand}) | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }`)])
94+
powershellChild.on("error", reject)
95+
powershellChild.on("exit", code => {
96+
if (code !== 1) {
97+
resolve(null)
98+
return
99+
}
100+
101+
execFile("powershell.exe", [`Get-AuthenticodeSignature '${tempUpdateFile}' | ConvertTo-Json -Compress`], {maxBuffer: 4 * 1024000}, (error, stdout, stderr) => {
102+
if (error != null) {
103+
reject(error)
104+
return
105+
}
106+
107+
if (stderr) {
108+
reject(new Error(`Cannot execute Get-AuthenticodeSignature: ${stderr}`))
109+
return
110+
}
111+
112+
const data = JSON.parse(stdout)
113+
delete data.PrivateKey
114+
delete data.IsOSBinary
115+
delete data.SignatureType
116+
const signerCertificate = data.SignerCertificate
117+
if (signerCertificate != null) {
118+
delete signerCertificate.Archived
119+
delete signerCertificate.Extensions
120+
delete signerCertificate.Handle
121+
delete signerCertificate.HasPrivateKey
122+
}
123+
delete data.Path
124+
125+
const result = JSON.stringify(data, (name, value) => name === "RawData" ? undefined : value, 2)
126+
if (this.logger != null) {
127+
this.logger.info(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
128+
}
129+
resolve(result)
130+
})
131+
})
132+
})
133+
}
134+
70135
private addQuitHandler() {
71136
if (this.quitHandlerAdded) {
72137
return

test/jestSetup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ skip.ifAll = skip
2020
const isMac = process.platform === "darwin"
2121
test.ifMac = isMac ? test : skip
2222
test.ifNotWindows = isWindows ? skip : test
23+
test.ifWindows = isWindows ? test : skip
2324

2425
skip.ifMac = skip
2526
skip.ifLinux = skip
27+
skip.ifWindows = skip
2628
skip.ifNotWindows = skip
2729
skip.ifCi = skip
2830
skip.ifNotCi = skip

test/out/__snapshots__/nsisUpdaterTest.js.snap

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,20 @@ Array [
7777
exports[`file url github 1`] = `
7878
Object {
7979
"name": "TestApp-Setup-1.1.0.exe",
80-
"sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
81-
"sha512": undefined,
80+
"sha2": undefined,
81+
"sha512": "xrTrW8dzWYlPnu71Y4lpLIAuIurBZJvZmqEZyz1rzM3CbbE1Z+T+P5qYYZgwmhmXdYPOpvnmYKa0HGdgXggwtQ==",
8282
"url": "https://github.com/develar/__test_nsis_release/releases/download/v1.1.0/TestApp-Setup-1.1.0.exe",
8383
}
8484
`;
8585

86+
exports[`file url github 2`] = `
87+
Array [
88+
"checking-for-update",
89+
"update-available",
90+
"update-downloaded",
91+
]
92+
`;
93+
8694
exports[`file url github pre-release 1`] = `
8795
Object {
8896
"name": "TestApp-Setup-1.5.2-beta.3.exe",
@@ -93,6 +101,14 @@ Object {
93101
`;
94102

95103
exports[`file url github pre-release 2`] = `
104+
Array [
105+
"checking-for-update",
106+
"update-available",
107+
"update-downloaded",
108+
]
109+
`;
110+
111+
exports[`file url github pre-release 3`] = `
96112
Object {
97113
"path": "TestApp Setup 1.5.2-beta.3.exe",
98114
"releaseName": "v1.5.2-beta.3",
@@ -114,6 +130,51 @@ Object {
114130
}
115131
`;
116132

133+
exports[`invalid signature 1`] = `
134+
"New version 1.1.0 is not signed by the application owner: {
135+
\\"SignerCertificate\\": {
136+
\\"FriendlyName\\": \\"\\",
137+
\\"IssuerName\\": {
138+
\\"Name\\": \\"CN=StartCom Class 2 Object CA, OU=StartCom Certification Authority, O=StartCom Ltd., C=IL\\",
139+
\\"Oid\\": \\"System.Security.Cryptography.Oid\\"
140+
},
141+
\\"NotAfter\\": \\"/Date(1516526235000)/\\",
142+
\\"NotBefore\\": \\"/Date(1453367835000)/\\",
143+
\\"PrivateKey\\": null,
144+
\\"PublicKey\\": {
145+
\\"Key\\": \\"System.Security.Cryptography.RSACryptoServiceProvider\\",
146+
\\"Oid\\": \\"System.Security.Cryptography.Oid\\",
147+
\\"EncodedKeyValue\\": \\"System.Security.Cryptography.AsnEncodedData\\",
148+
\\"EncodedParameters\\": \\"System.Security.Cryptography.AsnEncodedData\\"
149+
},
150+
\\"SerialNumber\\": \\"18CB5EC53FB14EC2DBB44BD1518AF901\\",
151+
\\"SubjectName\\": {
152+
\\"Name\\": \\"CN=Vladimir Krivosheev, O=Vladimir Krivosheev, L=Grunwald, S=Bayern, C=DE\\",
153+
\\"Oid\\": \\"System.Security.Cryptography.Oid\\"
154+
},
155+
\\"SignatureAlgorithm\\": {
156+
\\"Value\\": \\"1.2.840.113549.1.1.11\\",
157+
\\"FriendlyName\\": \\"sha256RSA\\"
158+
},
159+
\\"Thumbprint\\": \\"32F67D8F957E740C692ADD8CD5A5E463992193DD\\",
160+
\\"Version\\": 3,
161+
\\"Issuer\\": \\"CN=StartCom Class 2 Object CA, OU=StartCom Certification Authority, O=StartCom Ltd., C=IL\\",
162+
\\"Subject\\": \\"CN=Vladimir Krivosheev, O=Vladimir Krivosheev, L=Grunwald, S=Bayern, C=DE\\"
163+
},
164+
\\"TimeStamperCertificate\\": null,
165+
\\"Status\\": 0,
166+
\\"StatusMessage\\": \\"Signature verified.\\"
167+
}"
168+
`;
169+
170+
exports[`invalid signature 2`] = `
171+
Array [
172+
"checking-for-update",
173+
"update-available",
174+
"error",
175+
]
176+
`;
177+
117178
exports[`sha512 mismatch error event 1`] = `
118179
Object {
119180
"name": "TestApp Setup 1.1.0.exe",

0 commit comments

Comments
 (0)