diff --git a/Dockerfile.builder b/Dockerfile.builder index 4941d22..b99fd84 100644 --- a/Dockerfile.builder +++ b/Dockerfile.builder @@ -31,7 +31,7 @@ RUN npm install COPY . . -RUN test -n "$TAURI_SIGNING_PRIVATE_KEY" +RUN node scripts/check-signing.mjs RUN nice -n 10 npm run bundle:windows RUN set -eux; \ diff --git a/Dockerfile.builder-linux b/Dockerfile.builder-linux index 45f28a6..6c37c59 100644 --- a/Dockerfile.builder-linux +++ b/Dockerfile.builder-linux @@ -26,8 +26,8 @@ RUN npm install COPY . . RUN npm run icons +RUN node scripts/check-signing.mjs RUN cargo test --manifest-path src-tauri/Cargo.toml --lib -RUN test -n "$TAURI_SIGNING_PRIVATE_KEY" RUN nice -n 10 npm run bundle:linux RUN set -eux; \ diff --git a/scripts/check-signing.mjs b/scripts/check-signing.mjs new file mode 100644 index 0000000..77c5de3 --- /dev/null +++ b/scripts/check-signing.mjs @@ -0,0 +1,40 @@ +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { createHash, createPublicKey, verify } from 'node:crypto'; + +const temp = mkdtempSync(join(tmpdir(), 'fieldlens-signing-')); +try { + if (!process.env.TAURI_SIGNING_PRIVATE_KEY) throw new Error('missing key'); + const probe = join(temp, 'probe.txt'); + writeFileSync(probe, 'FieldLens updater signing preflight\n', { mode: 0o600 }); + const result = spawnSync(process.execPath, [resolve('node_modules/@tauri-apps/cli/tauri.js'), 'signer', 'sign', probe], { + env: process.env, stdio: 'pipe', timeout: 30000, + }); + if (result.status !== 0) throw new Error('signing failed'); + const config = JSON.parse(readFileSync('src-tauri/tauri.conf.json', 'utf8')); + const publicLines = Buffer.from(config.plugins.updater.pubkey, 'base64').toString().trim().split(/\r?\n/); + const signatureLines = Buffer.from(readFileSync(probe + '.sig', 'utf8').trim(), 'base64').toString().trim().split(/\r?\n/); + const pub = Buffer.from(publicLines[1], 'base64'); + const sig = Buffer.from(signatureLines[1], 'base64'); + if (pub.length !== 42 || sig.length !== 74 || !pub.subarray(2, 10).equals(sig.subarray(2, 10))) throw new Error('key mismatch'); + const key = createPublicKey({ + key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), pub.subarray(10)]), + format: 'der', type: 'spki', + }); + const algorithm = sig.subarray(0, 2).toString(); + if (!['ED', 'Ed'].includes(algorithm)) throw new Error('unsupported signature'); + const message = readFileSync(probe); + const signed = algorithm === 'ED' ? createHash('blake2b512').update(message).digest() : message; + if (!verify(null, signed, key, sig.subarray(10))) throw new Error('invalid signature'); + if (!signatureLines[2].startsWith('trusted comment: ')) throw new Error('invalid comment'); + const comment = Buffer.from(signatureLines[2].slice('trusted comment: '.length)); + if (!verify(null, Buffer.concat([sig.subarray(10), comment]), key, Buffer.from(signatureLines[3], 'base64'))) throw new Error('invalid trusted comment'); + console.log('Updater signing preflight passed: key, password and public key match.'); +} catch { + console.error('Updater signing preflight failed: check the signing key/password and configured public key.'); + process.exitCode = 1; +} finally { + rmSync(temp, { recursive: true, force: true }); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3e655cf..296af3e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -47,11 +47,18 @@ "policykit-1" ] } - } + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] }, "plugins": { "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDAwQURCMzYxRTNGQTJBODgKUldTSUt2cmpZYk90QU1KNEtLVmgzZ1FFd1FmMnY0SU1ETVZMMVVQSE9TM2VsS1RJdDlTZHFFS3UK", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYwNzYwMEYwN0YwNjI4QTEKUldTaEtBWi84QUIyWUxYUVg3V0RqSDlJbVc4Ymh2TEh5dDFHZnNDUURlTjVuc09WZzdmajM1VzEK", "endpoints": [ "https://fieldlens.arrowstack.dev/downloads/latest.json" ],