ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
78 lines
3.3 KiB
JavaScript
78 lines
3.3 KiB
JavaScript
import { createHash, generateKeyPairSync, verify } from 'node:crypto'
|
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import { spawnSync } from 'node:child_process'
|
|
|
|
function runGenerator(script, args, expectedSuccess) {
|
|
const result = spawnSync(process.execPath, [script, ...args], {
|
|
encoding: 'utf8',
|
|
windowsHide: true,
|
|
})
|
|
if ((result.status === 0) !== expectedSuccess) {
|
|
throw new Error(`generator exit ${result.status}: ${result.stderr || result.stdout}`)
|
|
}
|
|
}
|
|
|
|
const root = await mkdtemp(path.join(tmpdir(), 'remotedesk-update-manifest-'))
|
|
try {
|
|
const script = path.resolve('packaging/windows/create-update-manifest.mjs')
|
|
const installer = path.join(root, 'RemoteDesk-M0-9.8.7-windows-x64.msi')
|
|
const privateKeyPath = path.join(root, 'private.pem')
|
|
const output = path.join(root, 'stable.json')
|
|
const publicOutput = path.join(root, 'public.txt')
|
|
const installerBytes = Buffer.from('deterministic MSI fixture bytes', 'utf8')
|
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
|
|
await writeFile(installer, installerBytes)
|
|
await writeFile(privateKeyPath, privateKey.export({ type: 'pkcs8', format: 'pem' }))
|
|
|
|
const validArgs = [
|
|
'--installer', installer,
|
|
'--installer-url', 'https://updates.example.test/releases/RemoteDesk-M0-9.8.7-windows-x64.msi',
|
|
'--private-key', privateKeyPath,
|
|
'--version', '9.8.7',
|
|
'--channel', 'stable',
|
|
'--target', 'windows-x64',
|
|
'--output', output,
|
|
'--public-key-output', publicOutput,
|
|
]
|
|
runGenerator(script, validArgs, true)
|
|
|
|
const envelope = JSON.parse(await readFile(output, 'utf8'))
|
|
if (envelope.schema !== 1 || typeof envelope.payload !== 'string' || typeof envelope.signature !== 'string') {
|
|
throw new Error('generated envelope schema is invalid')
|
|
}
|
|
const payloadBytes = Buffer.from(envelope.payload, 'base64')
|
|
const signature = Buffer.from(envelope.signature, 'base64')
|
|
if (signature.length !== 64 || !verify(null, payloadBytes, publicKey, signature)) {
|
|
throw new Error('generated Ed25519 signature is invalid')
|
|
}
|
|
const payload = JSON.parse(payloadBytes.toString('utf8'))
|
|
const expectedHash = createHash('sha256').update(installerBytes).digest('hex')
|
|
if (payload.product !== 'remotedesk'
|
|
|| payload.channel !== 'stable'
|
|
|| payload.version !== '9.8.7'
|
|
|| payload.target !== 'windows-x64'
|
|
|| payload.installer.sha256 !== expectedHash
|
|
|| payload.installer.size_bytes !== installerBytes.length) {
|
|
throw new Error('signed payload does not bind the installer and release metadata')
|
|
}
|
|
const expectedPublicKey = Buffer.from(publicKey.export({ format: 'jwk' }).x, 'base64url').toString('base64')
|
|
if ((await readFile(publicOutput, 'ascii')).trim() !== expectedPublicKey) {
|
|
throw new Error('published raw update public key does not match the signing key')
|
|
}
|
|
|
|
runGenerator(script, validArgs, false)
|
|
runGenerator(script, [
|
|
'--installer', installer,
|
|
'--installer-url', 'http://updates.example.test/RemoteDesk.msi',
|
|
'--private-key', privateKeyPath,
|
|
'--version', '9.8.7',
|
|
'--output', path.join(root, 'invalid.json'),
|
|
], false)
|
|
process.stdout.write('update manifest generator contract: OK\n')
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|