Skip to content

Commit 2440eee

Browse files
authored
Merge pull request #175 from switchifyapp/fix/updater-metadata-after-signing-174
Fix updater metadata after installer signing
2 parents 1bcf355 + f037c2a commit 2440eee

3 files changed

Lines changed: 216 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "electron-vite build",
99
"native:build": "node scripts/build-cursor-overlay-helper.cjs",
1010
"native:build-overlay": "npm run native:build",
11-
"package:win": "npm run build && npm run native:build && electron-builder --win --x64 --publish never",
11+
"package:win": "npm run build && npm run native:build && electron-builder --win --x64 --publish never && node scripts/sign-win-artifacts.cjs --update-latest-yml",
1212
"package:win:verify-uiaccess": "node scripts/verify-win-uiaccess-package.cjs",
1313
"signing:create-dev-cert": "node scripts/create-dev-signing-cert.cjs",
1414
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json",

scripts/sign-win-artifacts.cjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const fs = require('node:fs');
22
const path = require('node:path');
3+
const crypto = require('node:crypto');
34
const {
45
findWindowsSdkTool,
56
isWindows,
@@ -37,7 +38,85 @@ module.exports = async function signWindowsArtifacts(buildResult) {
3738
continue;
3839
}
3940
runTool(signtoolExe, signingArgs);
41+
updateLatestYmlForSignedInstaller(artifactPath);
4042
}
4143

4244
return artifacts;
4345
};
46+
47+
if (require.main === module) {
48+
const command = process.argv[2];
49+
if (command !== '--update-latest-yml') {
50+
throw new Error('Unsupported command. Use --update-latest-yml.');
51+
}
52+
53+
updateLatestYmlForReferencedInstaller(resolveProjectPath('dist', 'latest.yml'));
54+
}
55+
56+
function updateLatestYmlForSignedInstaller(installerPath) {
57+
const latestPath = path.join(path.dirname(installerPath), 'latest.yml');
58+
if (!fs.existsSync(latestPath)) {
59+
return;
60+
}
61+
62+
const installerName = path.basename(installerPath);
63+
const sha512 = createSha512Base64(installerPath);
64+
const content = fs.readFileSync(latestPath, 'utf8');
65+
66+
if (!referencesInstaller(content, installerName)) {
67+
console.warn(`WARNING: Skipping latest.yml update because it does not reference ${installerName}.`);
68+
return;
69+
}
70+
71+
const updated = content.replace(/^(\s*sha512:\s*).+$/gm, `$1${sha512}`);
72+
73+
if (updated === content) {
74+
throw new Error('latest.yml did not contain any sha512 entries to update.');
75+
}
76+
77+
fs.writeFileSync(latestPath, updated);
78+
console.log(`Updated latest.yml sha512 for ${installerName}.`);
79+
}
80+
81+
function updateLatestYmlForReferencedInstaller(latestPath) {
82+
if (!fs.existsSync(latestPath)) {
83+
return;
84+
}
85+
86+
const content = fs.readFileSync(latestPath, 'utf8');
87+
const installerName = findInstallerNameInLatestYml(content);
88+
const installerPath = path.join(path.dirname(latestPath), installerName);
89+
90+
if (!fs.existsSync(installerPath)) {
91+
throw new Error(`latest.yml references missing installer ${installerName}.`);
92+
}
93+
94+
updateLatestYmlForSignedInstaller(installerPath);
95+
}
96+
97+
function createSha512Base64(filePath) {
98+
return crypto.createHash('sha512').update(fs.readFileSync(filePath)).digest('base64');
99+
}
100+
101+
function findInstallerNameInLatestYml(content) {
102+
const match = content.match(/^\s*(?:url|path):\s*['"]?([^'"\r\n]+\.exe)['"]?\s*$/im);
103+
if (!match) {
104+
throw new Error('latest.yml does not reference a Windows installer.');
105+
}
106+
107+
return path.basename(decodeURI(match[1]));
108+
}
109+
110+
function referencesInstaller(content, installerName) {
111+
const encodedInstallerName = encodeURI(installerName);
112+
return (
113+
content.includes(`url: ${installerName}`) ||
114+
content.includes(`path: ${installerName}`) ||
115+
content.includes(`url: ${encodedInstallerName}`) ||
116+
content.includes(`path: ${encodedInstallerName}`)
117+
);
118+
}
119+
120+
module.exports.updateLatestYmlForSignedInstaller = updateLatestYmlForSignedInstaller;
121+
module.exports.updateLatestYmlForReferencedInstaller = updateLatestYmlForReferencedInstaller;
122+
module.exports.createSha512Base64 = createSha512Base64;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { createRequire } from 'node:module';
5+
import { afterEach, describe, expect, it } from 'vitest';
6+
7+
const require = createRequire(import.meta.url);
8+
const {
9+
createSha512Base64,
10+
updateLatestYmlForReferencedInstaller,
11+
updateLatestYmlForSignedInstaller
12+
} = require('../../scripts/sign-win-artifacts.cjs') as {
13+
createSha512Base64(filePath: string): string;
14+
updateLatestYmlForReferencedInstaller(latestPath: string): void;
15+
updateLatestYmlForSignedInstaller(installerPath: string): void;
16+
};
17+
18+
const tempDirs: string[] = [];
19+
20+
afterEach(() => {
21+
for (const tempDir of tempDirs.splice(0)) {
22+
fs.rmSync(tempDir, { recursive: true, force: true });
23+
}
24+
});
25+
26+
describe('sign-win-artifacts updater metadata', () => {
27+
it('updates latest.yml checksums after installer bytes change', () => {
28+
const distDir = createTempDir();
29+
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
30+
const latestPath = path.join(distDir, 'latest.yml');
31+
fs.writeFileSync(installerPath, 'unsigned installer bytes');
32+
fs.writeFileSync(
33+
latestPath,
34+
[
35+
'version: 0.1.1',
36+
'files:',
37+
' - url: Switchify-PC-Setup-0.1.1-x64.exe',
38+
' sha512: old-file-checksum',
39+
'path: Switchify-PC-Setup-0.1.1-x64.exe',
40+
'sha512: old-path-checksum'
41+
].join('\n')
42+
);
43+
44+
fs.writeFileSync(installerPath, 'signed installer bytes');
45+
updateLatestYmlForSignedInstaller(installerPath);
46+
47+
const sha512 = createSha512Base64(installerPath);
48+
const latest = fs.readFileSync(latestPath, 'utf8');
49+
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
50+
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
51+
});
52+
53+
it('ignores latest.yml when it does not reference the signed installer', () => {
54+
const distDir = createTempDir();
55+
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
56+
fs.writeFileSync(installerPath, 'signed installer bytes');
57+
fs.writeFileSync(
58+
path.join(distDir, 'latest.yml'),
59+
[
60+
'version: 0.1.1',
61+
'files:',
62+
' - url: Other-Setup.exe',
63+
' sha512: old-file-checksum',
64+
'path: Other-Setup.exe',
65+
'sha512: old-path-checksum'
66+
].join('\n')
67+
);
68+
69+
expect(() => updateLatestYmlForSignedInstaller(installerPath)).not.toThrow();
70+
expect(fs.readFileSync(path.join(distDir, 'latest.yml'), 'utf8')).toContain('old-file-checksum');
71+
});
72+
73+
it('updates latest.yml when the installer name is URL-encoded', () => {
74+
const distDir = createTempDir();
75+
const installerPath = path.join(distDir, 'Switchify PC Setup 0.1.1 x64.exe');
76+
const latestPath = path.join(distDir, 'latest.yml');
77+
fs.writeFileSync(installerPath, 'signed installer bytes');
78+
fs.writeFileSync(
79+
latestPath,
80+
[
81+
'version: 0.1.1',
82+
'files:',
83+
' - url: Switchify%20PC%20Setup%200.1.1%20x64.exe',
84+
' sha512: old-file-checksum',
85+
'path: Switchify%20PC%20Setup%200.1.1%20x64.exe',
86+
'sha512: old-path-checksum'
87+
].join('\n')
88+
);
89+
90+
updateLatestYmlForSignedInstaller(installerPath);
91+
92+
const sha512 = createSha512Base64(installerPath);
93+
const latest = fs.readFileSync(latestPath, 'utf8');
94+
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
95+
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
96+
});
97+
98+
it('does nothing when latest.yml is missing', () => {
99+
const distDir = createTempDir();
100+
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
101+
fs.writeFileSync(installerPath, 'signed installer bytes');
102+
103+
expect(() => updateLatestYmlForSignedInstaller(installerPath)).not.toThrow();
104+
});
105+
106+
it('updates the installer referenced by latest.yml', () => {
107+
const distDir = createTempDir();
108+
const installerPath = path.join(distDir, 'Switchify-PC-Setup-0.1.1-x64.exe');
109+
const latestPath = path.join(distDir, 'latest.yml');
110+
fs.writeFileSync(installerPath, 'signed installer bytes');
111+
fs.writeFileSync(
112+
latestPath,
113+
[
114+
'version: 0.1.1',
115+
'files:',
116+
' - url: Switchify-PC-Setup-0.1.1-x64.exe',
117+
' sha512: old-file-checksum',
118+
'path: Switchify-PC-Setup-0.1.1-x64.exe',
119+
'sha512: old-path-checksum'
120+
].join('\n')
121+
);
122+
123+
updateLatestYmlForReferencedInstaller(latestPath);
124+
125+
const sha512 = createSha512Base64(installerPath);
126+
const latest = fs.readFileSync(latestPath, 'utf8');
127+
expect(latest.match(/^sha512: .+$/gm)).toEqual([`sha512: ${sha512}`]);
128+
expect(latest.match(/^\s+sha512: .+$/gm)).toEqual([` sha512: ${sha512}`]);
129+
});
130+
});
131+
132+
function createTempDir(): string {
133+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'switchify-sign-test-'));
134+
tempDirs.push(tempDir);
135+
return tempDir;
136+
}

0 commit comments

Comments
 (0)