Files
safetynet/scripts/verified-installer.mjs
T

31 lines
2.8 KiB
JavaScript

import {lstat,open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
function fail(code){throw Object.assign(new Error(code),{code});}
function same(expected,actual){return actual&&['sourceSha256','runtimeSha256','packageSha256'].every(key=>actual[key]===expected[key]);}
async function stat(file){try{return await lstat(file);}catch(error){if(error.code==='ENOENT')return null;throw error;}}
// Guard actual invocation and result. Caller owns/protects the build directory.
export async function buildVerifiedInstaller({plan,expectedIdentity,readIdentity,verify,run,installerPath}){
if(typeof run!=='function'||!plan||!path.isAbsolute(installerPath)||path.dirname(installerPath)!==path.dirname(plan.unpacked.directory)||!installerPath.endsWith('.exe'))fail('installer_configuration_invalid');
const stage=await plan.installer({expectedIdentity,readIdentity,verify});
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const previous=await stat(installerPath);if(previous?.isSymbolicLink())fail('installer_artifact_invalid');
const result=await run(stage.args);
if(result?.status!==0)fail('installer_build_failed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const receipt=await verify(stage.directory,stage.identity);
if(!receipt||!same(stage.identity,receipt.identity)||!['fuses','resources','broker','runtimeAcl'].every(key=>receipt[key]===true))fail('installer_verification_failed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const produced=await stat(installerPath);
if(!produced||!produced.isFile()||produced.isSymbolicLink()||produced.size<1024||produced.size>4294967295||previous&&previous.mtimeMs===produced.mtimeMs&&previous.ctimeMs===produced.ctimeMs&&previous.size===produced.size)fail('installer_artifact_invalid');
const file=await open(installerPath,'r');
try{
const magic=Buffer.alloc(2);await file.read(magic,0,2,0);if(magic.toString('ascii')!=='MZ')fail('installer_artifact_invalid');
const hash=createHash('sha256');for await(const chunk of file.createReadStream({start:0,autoClose:false}))hash.update(chunk);
const after=await file.stat();if(after.size!==produced.size||after.mtimeMs!==produced.mtimeMs||after.ctimeMs!==produced.ctimeMs)fail('installer_artifact_changed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const final=await file.stat();if(final.size!==after.size||final.mtimeMs!==after.mtimeMs||final.ctimeMs!==after.ctimeMs)fail('installer_artifact_changed');
return Object.freeze({path:installerPath,bytes:after.size,sha256:hash.digest('hex'),identity:stage.identity});
}finally{await file.close();}
}