Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11215 -60

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
// Preparation for a separately packaged broker; NOT wired into production yet.
// The expected manifest must be embedded in the integrity-checked application,
// never accepted from the writable broker directory. Hashes alone do not replace
// installation ACLs, signed distribution, or a handle-bound process launch.
import {lstat, readdir, open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
const MAX_FILES=4096;
function fail(code,failureFacts){throw Object.assign(new Error(code),{code,...(failureFacts?{failureFacts}:{})});}
function version(value){return typeof value==='string'&&/^\d{1,5}\.\d{1,5}\.\d{1,5}$/.test(value);}
function relative(value){
return typeof value==='string'&&value.length<=240&&!value.includes('\\')&&value.split('/').every(segment=>
/^[a-zA-Z0-9_.-]+$/.test(segment)&&segment!=='.'&&segment!=='..'&&!/[. ]$/.test(segment)&&
!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment));
}
function validate(manifest,expected){
if(!manifest||manifest.format!=='safetynet-broker-package-v1'||!version(manifest.version)||
!Number.isSafeInteger(manifest.protocol)||manifest.protocol<1||manifest.protocol>65535||
!relative(manifest.entrypoint)||!manifest.entrypoint.endsWith('.exe')||
!Array.isArray(manifest.files)||!manifest.files.length||manifest.files.length>MAX_FILES)fail('broker_manifest_invalid');
if(expected&&(manifest.version!==expected.version||manifest.protocol!==expected.protocol))fail('broker_package_incompatible');
const names=new Set();
for(const file of manifest.files){
if(!file||!relative(file.path)||!Number.isSafeInteger(file.bytes)||file.bytes<0||
!/^[a-f0-9]{64}$/.test(file.sha256)||names.has(file.path.toLowerCase()))fail('broker_manifest_invalid');
names.add(file.path.toLowerCase());
}
if(!names.has(manifest.entrypoint.toLowerCase()))fail('broker_manifest_invalid');
}
async function inventory(root){
const files=[];let entries=0;
// Reject symlinks/junctions at every ancestor, including the supplied root.
for(let current=path.resolve(root);;current=path.dirname(current)){
const stat=await lstat(current);
if(stat.isSymbolicLink()||!stat.isDirectory())fail('broker_path_unsafe');
if(path.dirname(current)===current)break;
}
async function walk(directory,prefix=''){
for(const item of await readdir(directory,{withFileTypes:true})){
if(++entries>MAX_FILES)fail('broker_package_too_large');
const name=prefix+item.name;
if(!relative(name))fail('broker_path_unsafe');
const stat=await lstat(path.join(root,...name.split('/')));
if(stat.isSymbolicLink())fail('broker_path_unsafe');
if(stat.isDirectory())await walk(path.join(directory,item.name),name+'/');
else if(stat.isFile()){
files.push(name);if(files.length>MAX_FILES)fail('broker_package_too_large');
}else fail('broker_path_unsafe');
}
}
await walk(root);
if(new Set(files.map(file=>file.toLowerCase())).size!==files.length)fail('broker_path_unsafe');
return files.sort();
}
async function digest(root,name){
const handle=await open(path.join(root,...name.split('/')),'r');
try{
const before=await handle.stat();if(!before.isFile())fail('broker_path_unsafe');
const hash=createHash('sha256');let bytes=0;
for await(const chunk of handle.createReadStream({autoClose:false})){hash.update(chunk);bytes+=chunk.length;}
const after=await handle.stat();
const changed=[];
if(before.size!==bytes)changed.push('size-before-read');
if(after.size!==bytes)changed.push('size-after-read');
if(before.mtimeMs!==after.mtimeMs)changed.push('modification-time');
if(before.ctimeMs!==after.ctimeMs)changed.push('change-time');
if(before.dev!==after.dev||before.ino!==after.ino)changed.push('file-identity');
// Fixed bounded facts preserve the reason for a failed check without paths,
// file content, raw timestamps, OS error text or automatic retry.
if(changed.length)fail('broker_package_changed',Object.freeze({changed:Object.freeze(changed)}));
return {path:name,bytes,sha256:hash.digest('hex')};
}finally{await handle.close();}
}
export async function buildBrokerPackageManifest(root,{version:release,protocol,entrypoint}){
try{
const manifest={format:'safetynet-broker-package-v1',version:release,protocol,entrypoint,files:[]};
// Validate metadata before touching executable payloads.
validate({...manifest,files:[{path:entrypoint,bytes:0,sha256:'0'.repeat(64)}]});
for(const name of await inventory(root))manifest.files.push(await digest(root,name));
validate(manifest);return manifest;
}catch(error){if(error.code?.startsWith('broker_'))throw error;fail('broker_package_unavailable');}
}
export async function verifyBrokerPackage(root,trustedManifest,{version:release,protocol}){
try{
// Copy before await so a caller cannot mutate the trusted contract mid-check.
const manifest=structuredClone(trustedManifest);validate(manifest,{version:release,protocol});
const actual=await inventory(root),expected=manifest.files.map(file=>file.path).sort();
if(JSON.stringify(actual)!==JSON.stringify(expected))fail('broker_package_inventory_mismatch');
for(const file of manifest.files){const observed=await digest(root,file.path);
if(observed.bytes!==file.bytes||observed.sha256!==file.sha256)fail('broker_package_integrity_mismatch');}
// Verification is an installation/startup check, not an atomic launch lease.
return Object.freeze({version:manifest.version,protocol:manifest.protocol,entrypoint:path.join(path.resolve(root),...manifest.entrypoint.split('/')),files:manifest.files.length});
}catch(error){if(error.code?.startsWith('broker_'))throw error;fail('broker_package_unavailable');}
}
+85
View File
@@ -0,0 +1,85 @@
// Compile an owned native SCM helper; never installs services or Windows rules.
import {spawn,execFileSync} from 'node:child_process';
import {readFile,writeFile,mkdir,lstat,realpath} from 'node:fs/promises';
import {createHash,generateKeyPairSync,sign} from 'node:crypto';
import {createRequire} from 'node:module';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
const args=process.argv.slice(2),compileOnly=args.length===1&&args[0]==='--compile-only',selfTest=args.length===1&&(args[0]==='--self-test'||compileOnly);
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
const fail=()=>{throw Error('native-service-build-invalid');};
const {signManifest,publicKeyCoordinates}=createRequire(import.meta.url)('../packages/core/windows-trust.cjs');
const {encodeAcceptanceState}=createRequire(import.meta.url)('../apps/network-broker/service-acceptance.cjs');
async function safe(directory){const info=await lstat(directory);if(info.isSymbolicLink()||await realpath(directory)!==directory)fail();}
async function run(executable,arguments_,expectedCode=0){const started=performance.now();return await new Promise((resolve,reject)=>{const child=spawn(executable,arguments_,{cwd:root,windowsHide:true,stdio:['ignore','pipe','pipe'],shell:false});let bytes=0;for(const stream of [child.stdout,child.stderr])stream.on('data',chunk=>{bytes+=chunk.length;if(bytes>65536){child.kill();reject(Error('native-build-output-limit'));}else process.stdout.write(chunk);});const timer=setTimeout(()=>{child.kill();reject(Error('native-build-timeout'));},60000);child.once('error',error=>{error.nativeStage=path.basename(executable);reject(error);});child.once('exit',code=>{clearTimeout(timer);(code===expectedCode)?resolve({code,milliseconds:performance.now()-started}):reject(Error('native-build-failed'));});});}
async function main(){
if(process.platform!=='win32'||!selfTest)fail();
const output=path.join(root,'artifacts/native-service-host');await mkdir(output,{recursive:true});await safe(path.join(root,'artifacts'));await safe(output);
const vswhere='C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe';
const installation=execFileSync(vswhere,['-latest','-products','*','-requires','Microsoft.VisualStudio.Component.VC.Tools.x86.x64','-property','installationPath'],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();if(!installation)fail();
const version=(await readFile(path.join(installation,'VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt'),'utf8')).trim();if(!/^\d+\.\d+\.\d+$/.test(version))fail();
const tool=path.join(installation,'VC/Tools/MSVC',version),sdk='C:/Program Files (x86)/Windows Kits/10',sdkVersion='10.0.26100.0';
const vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),policy=generateKeyPairSync('ed25519');
const publicPem=key=>key.export({format:'pem',type:'spki'}),privatePem=key=>key.export({format:'pem',type:'pkcs8'});
const header='static constexpr char RELEASE_ANCHOR_XY[]="'+publicKeyCoordinates(publicPem(vendor.publicKey)).toString('hex')+'";';
await writeFile(path.join(output,'service-pins.h'),header);
const source=path.join(root,'apps/network-broker/native/service-host.cpp'),nativeHeader=path.join(root,'apps/network-broker/native/signed-manifest.h'),stateHeader=path.join(root,'apps/network-broker/native/acceptance-state.h'),topologyHeader=path.join(root,'apps/network-broker/native/wfp-topology.h'),credentialHeader=path.join(root,'apps/network-broker/native/management-credential.h'),beforeCredentialHeader=hash(await readFile(credentialHeader)),before=hash(await readFile(source)),beforeHeader=hash(await readFile(nativeHeader)),beforeStateHeader=hash(await readFile(stateHeader)),beforeTopologyHeader=hash(await readFile(topologyHeader));
const executable=path.join(output,'safetynet-service-host-test.exe');
const compiler=path.join(tool,'bin/Hostx64/x64/cl.exe');
const arguments_=['/nologo','/std:c++17','/W4','/WX','/EHsc','/O2','/MT','/guard:cf','/DUNICODE','/D_UNICODE','/DSAFETYNET_NATIVE_SELF_TEST',... [output,path.join(tool,'include'),...['ucrt','um','shared'].map(v=>path.join(sdk,'Include',sdkVersion,v))].map(v=>'/I'+v),'/Fo'+path.join(output,'service-host.obj'),'/Fe'+executable,source,'/link','/DYNAMICBASE','/NXCOMPAT','/HIGHENTROPYVA',... [path.join(tool,'lib/x64'),...['ucrt','um'].map(v=>path.join(sdk,'Lib',sdkVersion,v,'x64'))].map(v=>'/LIBPATH:'+v)];
const stages={};stages.compile=await run(compiler,arguments_);process.stdout.write('native.compile.completed\n');const compiledStat=await lstat(executable);process.stdout.write('native.compile.output.bytes='+compiledStat.size+'\n');const compiledHash=hash(await readFile(executable));process.stdout.write('native.compile.output.sha256='+compiledHash+'\n');if(hash(await readFile(source))!==before||hash(await readFile(nativeHeader))!==beforeHeader||hash(await readFile(stateHeader))!==beforeStateHeader||hash(await readFile(topologyHeader))!==beforeTopologyHeader||hash(await readFile(credentialHeader))!==beforeCredentialHeader)fail();if(compileOnly){await writeFile(path.join(output,'current-compile-only-proof.json'),JSON.stringify({compiled:true,selfTestExecuted:false,installed:false,wfpActivated:false,helperSha256:compiledHash,sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,stages},null,2));return;}process.stdout.write('native.selftest.starting\n');stages.pipeJob=await run(executable,['--self-test']);stages.credentialSelection=await run(executable,['--verify-credential-selection']);
const helper=await readFile(executable),releaseId='11111111-1111-1111-1111-111111111111',deploymentTenant='22222222-2222-2222-2222-222222222222';
const fixturePaths=['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py','resources/semantic/synthetic.bin'];
const files=[{path:'safetynet-service-host-test.exe',bytes:helper.length,sha256:hash(helper)}];
for(const name of fixturePaths){const destination=path.join(output,name);await mkdir(path.dirname(destination),{recursive:true});const synthetic=Buffer.from('fixed native signed inventory fixture');await writeFile(destination,synthetic);files.push({path:name,bytes:synthetic.length,sha256:hash(synthetic)});}
files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
const release={format:'safetynet-windows-release-v1',releaseId,releaseSequence:2,serviceName:'SafetyNetNetworkBroker',deploymentTenant,deploymentPublicKey:publicPem(customer.publicKey),files};
const deployment={format:'safetynet-windows-deployment-v1',releaseId,deploymentTenant,deploymentSequence:2,policyPublicKey:publicPem(policy.publicKey),expiresAt:Date.now()+60000,users:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',minimumPolicyVersion:3}]};
const releaseEnvelope=signManifest(release,privatePem(vendor.privateKey),'release'),deploymentEnvelope=signManifest(deployment,privatePem(customer.privateKey),'deployment');
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));
stages.trustPositive=await run(executable,['--verify-fixture']);
// Native negatives are genuinely customer-signed, intentionally bypassing
// JS schema validation so the independent C++ parser must reject them.
const canonical=value=>Array.isArray(value)?'['+value.map(canonical).join(',')+']':value&&typeof value==='object'?'{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}':JSON.stringify(value);
const rawDeployment=value=>{const bytes=Buffer.from(canonical(value));return {payload:bytes.toString('base64url'),signature:sign('sha256',Buffer.concat([Buffer.from('SafetyNet Windows deployment v1\0'),bytes]),{key:customer.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64url')};};
const pq=generateKeyPairSync('ml-dsa-87'),wrongPq=generateKeyPairSync('ml-dsa-65');
const management={origin:'https://management.example',signatureKey:publicPem(pq.publicKey),protocol:'safetynet-policy-mlkem1024-mldsa87-v1'};
const deploymentV2={...deployment,format:'safetynet-windows-deployment-v2',management};
stages.managementV2={};
for(const [index,origin] of ['https://management.example','https://management.example:8443','https://127.0.0.1','https://[::1]','https://[::ffff:c0a8:1]'].entries()){
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deploymentV2,management:{...management,origin}},privatePem(customer.privateKey),'deployment')));
stages.managementV2['positive'+index]=await run(executable,['--verify-fixture']);
}
const invalidManagement=[...['http://management.example','https://management.example/','https://management.example/path','https://user:[email protected]','https://management.example?query=1','https://management.example#fragment','https://MANAGEMENT.example','https://management.example:443','https://management.example.','https://management.example:','https://[::1]:','https://[0:0:0:0:0:0:0:1]','https://[::ffff:192.168.0.1]','https://127.1','https://bad_host.example'].map(origin=>({...management,origin})),{...management,protocol:'safetynet-policy-mlkem768-mldsa65-v1'},{...management,signatureKey:publicPem(wrongPq.publicKey)},{...management,signatureKey:publicPem(policy.publicKey)},{...management,signatureKey:management.signatureKey.trim()},{...management,token:'synthetic-denied'}];
for(const [index,value] of invalidManagement.entries()){
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,management:value})));
stages.managementV2['invalid'+index]=await run(executable,['--verify-fixture'],86);
}
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v3'})));stages.managementV2.unknownFormat=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v1'})));stages.managementV2.v1ExtraCapability=await run(executable,['--verify-fixture'],86);
const signedV2=signManifest(deploymentV2,privatePem(customer.privateKey),'deployment');
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify({...signedV2,payload:Buffer.from(canonical({...deploymentV2,management:{...management,origin:'https://attacker.example'}})).toString('base64url')}));stages.managementV2.signatureTamper=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.managementV2.v1HistoricalPreserved=await run(executable,['--verify-fixture']);
const changedPayload=JSON.parse(Buffer.from(releaseEnvelope.payload,'base64url'));changedPayload.releaseSequence=3;
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify({...releaseEnvelope,payload:Buffer.from(JSON.stringify(changedPayload)).toString('base64url')}));stages.signatureTamper=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,deploymentPublicKey:publicPem(vendor.publicKey)},privatePem(vendor.privateKey),'release')));stages.wrongDelegatedKey=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
stages.uninstalledSnapshotRejected=await run(executable,['--trust-snapshot'],5);
stages.uninstalledCommitRejected=await run(executable,['--commit-acceptance'],5);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deployment,expiresAt:Date.now()-1},privatePem(customer.privateKey),'deployment')));stages.expiredDeployment=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.uninstalledClientRejected=await run(executable,['--client-stdio'],5);
stages.sameSequenceDigestRejected=await run(executable,['--verify-equivocation'],86);
const manyFiles=[...files,...Array.from({length:500},(_,i)=>({path:`resources/semantic/synthetic-${String(i).padStart(4,'0')}.bin`,bytes:0,sha256:'0'.repeat(64)}))].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,files:manyFiles},privatePem(vendor.privateKey),'release')));stages.largeSignedInventory=await run(executable,['--verify-fixture']);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
const oldState={format:'safetynet-service-floors-v1',release:{minimumSequence:2,acceptedDigest:hash(Buffer.from(releaseEnvelope.payload,'base64url'))},deployment:{tenant:deploymentTenant,minimumSequence:2,acceptedDigest:hash(Buffer.from(deploymentEnvelope.payload,'base64url'))},policies:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',policyKeySha256:hash(policy.publicKey.export({format:'der',type:'spki'})),minimumVersion:3,acceptedDigest:'a'.repeat(64)}]};
await writeFile(path.join(output,'acceptance-old.json'),encodeAcceptanceState(oldState));
const nextState=structuredClone(oldState);nextState.policies[0].minimumVersion=4;nextState.policies[0].acceptedDigest='b'.repeat(64);
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(nextState));stages.monotonicState=await run(executable,['--verify-state-fixture']);
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState({...nextState,policies:[]}));stages.policyDropRejected=await run(executable,['--verify-state-fixture'],86);
const lowered=structuredClone(nextState);lowered.policies[0].minimumVersion=2;await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(lowered));stages.policyRollbackRejected=await run(executable,['--verify-state-fixture'],86);
const equivocation=structuredClone(oldState);equivocation.policies[0].acceptedDigest='b'.repeat(64);await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(equivocation));stages.policyEquivocationRejected=await run(executable,['--verify-state-fixture'],86);
await writeFile(path.join(output,'proof.json'),JSON.stringify({sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,helperSha256:hash(await readFile(executable)),selfTest:true,installed:false,wfpActivated:false,msvc:version,sdk:sdkVersion,stages},null,2));
}
main().catch(error=>{process.stderr.write('native-service-build-failed'+(typeof error?.nativeStage==='string'&&/^[a-zA-Z0-9.-]+$/.test(error.nativeStage)?':'+error.nativeStage:'')+(typeof error?.code==='string'&&/^[A-Z_]+$/.test(error.code)?':'+error.code:'')+(typeof error?.syscall==='string'&&/^[a-z_]+$/.test(error.syscall)?':'+error.syscall:'')+'\n');process.exitCode=1;});
+14
View File
@@ -0,0 +1,14 @@
; Called after resources are copied into the protected per-machine location.
; nsExec hides the console and bounds captured output by NSIS_MAX_STRLEN.
; Its timeout is an inactivity timeout; the trusted Python helper also owns
; a hard wall-clock deadline and kill-on-close Job for its grant descendants.
!macro customInstall
nsExec::ExecToStack /TIMEOUT=245000 '"$INSTDIR\resources\python\python.exe" -I -B "$INSTDIR\resources\inspector\provision_runtime_acl.py" --provision'
Pop $0
Pop $1
StrCpy $1 ""
StrCmp $0 "0" safetynet_runtime_acl_ready
SetErrorLevel 1
Abort "SafetyNet could not establish the required scanner security permissions. Installation stopped."
safetynet_runtime_acl_ready:
!macroend
+69
View File
@@ -0,0 +1,69 @@
// Mandatory component packaging only; this does not activate a broker or
// install/attest its Windows network boundary.
import {createRequire} from 'node:module';
import {randomUUID} from 'node:crypto';
import {createHash} from 'node:crypto';
import {createReadStream} from 'node:fs';
import {readdir,lstat} from 'node:fs/promises';
import path from 'node:path';
import {stageBrokerPayload,BROKER_PAYLOAD,verifySourceSnapshot} from './stage-broker-payload.mjs';
import {verifyBrokerRuntime} from './provision-broker-runtime.mjs';
import {verifyBrokerPackage} from './broker-package-contract.mjs';
const require=createRequire(import.meta.url);
const RESOURCE_FROM='.runtime/windows/broker-payload',MANIFEST_FROM=RESOURCE_FROM+'.manifest.json';
const EMBEDDED_MANIFEST='apps/browser/broker-package-manifest.json';
function fail(code){throw Object.assign(new Error(code),{code});}
export function brokerResourceConfiguration(build,{payloadRoot,manifestPath}){
if(!build||!Array.isArray(build.extraResources)||!Array.isArray(build.files))fail('broker_package_configuration_missing');
const destination=value=>typeof value==='string'?value.replaceAll('\\','/').replace(/\/+$/,'').toLowerCase():'';
const resources=build.extraResources.filter(item=>item&&typeof item==='object'&&(destination(item.to)==='broker'||destination(item.to).startsWith('broker/')));
const manifests=build.files.filter(item=>item&&typeof item==='object'&&destination(item.to)===EMBEDDED_MANIFEST);
if(resources.length!==1||resources[0].from!==RESOURCE_FROM||manifests.length!==1||manifests[0].from!==MANIFEST_FROM)fail('broker_package_configuration_missing');
if(!path.isAbsolute(payloadRoot)||!path.isAbsolute(manifestPath))fail('broker_package_configuration_invalid');
return {...structuredClone(build),extraResources:build.extraResources.map(item=>item===resources[0]?{from:payloadRoot,to:'broker',filter:['**/*']}:structuredClone(item)),files:build.files.map(item=>item===manifests[0]?{from:manifestPath,to:EMBEDDED_MANIFEST}:structuredClone(item))};
}
export async function prepareBrokerResources({root,build,dependencyArchive}){
await verifyBrokerRuntime(path.join(root,'.runtime/windows/broker'));
const destination=path.join(root,'.runtime/windows','broker-payload-package-'+randomUUID());
const staged=await stageBrokerPayload({root,destination,dependencyArchive});
const configuration=brokerResourceConfiguration(build,{payloadRoot:staged.root,manifestPath:staged.root+'.manifest.json'});
return {configuration,manifest:staged.manifest,sourceSnapshot:staged.sources,
verifySources:()=>verifySourceSnapshot(root,staged.sources)};
}
export async function verifyPackagedBroker({resources,expectedManifest,readEmbeddedManifest}){
if(typeof readEmbeddedManifest!=='function')fail('broker_package_manifest_reader_required');
// Production supplies the ASAR extractor after embedded ASAR integrity/fuse
// verification. Never read expected state from resources/broker metadata.
let embedded;try{const bytes=await readEmbeddedManifest(EMBEDDED_MANIFEST);if(!Buffer.isBuffer(bytes)||bytes.length>1048576)fail('broker_package_manifest_invalid');embedded=JSON.parse(bytes.toString('utf8'));}catch{fail('broker_package_manifest_invalid');}
if(JSON.stringify(embedded)!==JSON.stringify(expectedManifest))fail('broker_package_manifest_mismatch');
return verifyBrokerPackage(path.join(resources,'broker'),embedded,BROKER_PAYLOAD);
}
export function embeddedBrokerManifestReader(archive){
const {extractFile}=require('@electron/asar');
return relative=>extractFile(archive,path.normalize(relative));
}
const scannerRoots=[['apps/inspector','inspector'],['.runtime/windows/python','python'],['.runtime/windows/semantic','semantic']];
async function fileDigest(file){const stat=await lstat(file);if(stat.isSymbolicLink()||!stat.isFile())fail('scanner_package_path_unsafe');const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);const after=await lstat(file);if(stat.size!==after.size||stat.mtimeMs!==after.mtimeMs||stat.ctimeMs!==after.ctimeMs)fail('scanner_package_changed');return {bytes:stat.size,sha256:hash.digest('hex')};}
async function scannerInventory(base,component,filter){
const records=[];
async function walk(directory,prefix=''){
for(const entry of await readdir(directory,{withFileTypes:true})){
if(filter&&(entry.name==='__pycache__'||component==='inspector'&&(entry.name.startsWith('test')||entry.name==='appcontainer_probe.py')))continue;
const file=path.join(directory,entry.name),relative=prefix+entry.name;
if(entry.isSymbolicLink())fail('scanner_package_path_unsafe');
if(entry.isDirectory())await walk(file,relative+'/');else if(entry.isFile())records.push({path:component+'/'+relative,...await fileDigest(file)});
else fail('scanner_package_path_unsafe');
if(records.length>20000)fail('scanner_package_too_large');
}
}
const stat=await lstat(base);if(stat.isSymbolicLink()||!stat.isDirectory())fail('scanner_package_path_unsafe');
await walk(base);return records.sort((a,b)=>a.path.localeCompare(b.path));
}
export async function snapshotScannerResources(root){
const records=[];for(const [from,to] of scannerRoots)records.push(...await scannerInventory(path.join(root,...from.split('/')),to,to!=='semantic'));
return records;
}
export async function verifyScannerResources(resources,expected){
const actual=[];for(const [,component] of scannerRoots)actual.push(...await scannerInventory(path.join(resources,component),component,false));
if(JSON.stringify(actual)!==JSON.stringify(expected))fail('scanner_package_integrity_mismatch');return true;
}
+60 -17
View File
@@ -1,26 +1,69 @@
import {hardenedElectronFuses,verifyPackagedFuses} from './package-fuses.mjs';
import { spawnSync } from 'node:child_process';
import {readFileSync,readdirSync} from 'node:fs';
import {createHash} from 'node:crypto';
import {spawnSync} from 'node:child_process';
import {readFileSync,readdirSync,writeFileSync,createReadStream} from 'node:fs';
import {createHash,randomUUID} from 'node:crypto';
import {createRequire} from 'node:module';
import path from 'node:path';
const platform=process.argv[2];if(platform!=='win'||process.platform!=='win32')throw Error('Windows is the active build target; other platforms are deferred');
const runtime=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-windows-runtime.py'],{stdio:'inherit'});if(runtime.status!==0)process.exit(runtime.status??1);
const semantic=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-semantic-runtime.py','--verify'],{stdio:'inherit'});if(semantic.status!==0)process.exit(semantic.status??1);
import {provisionBrokerRuntime,BROKER_RUNTIME} from './provision-broker-runtime.mjs';
import {prepareBrokerResources,verifyPackagedBroker,embeddedBrokerManifestReader,snapshotScannerResources,verifyScannerResources} from './package-broker-resources.mjs';
import {createWindowsPackagePlan} from './windows-package-plan.mjs';
import {buildVerifiedInstaller} from './verified-installer.mjs';
const platform=process.argv[2];
if(platform!=='win'||process.platform!=='win32'||process.argv.slice(3).some(value=>value!=='--unpacked-only'))throw Error('Windows is the active build target; only --unpacked-only is additionally supported');
const workspace=path.resolve('.');
for(const args of [['scripts/provision-windows-runtime.py'],['scripts/provision-semantic-runtime.py','--verify']]){
const result=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',args,{stdio:'inherit'});if(result.status!==0)process.exit(result.status??1);
}
await provisionBrokerRuntime(workspace);
const name=process.env.BRAND_NAME||'SafetyNet',id=process.env.BRAND_APP_ID||'enterprise.safetynet.browser';
if(!/^[\w .-]{1,40}$/.test(name)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(id))throw Error('Invalid white-label product name or app ID');
const configuration=JSON.parse(readFileSync('package.json','utf8')),workspace=path.resolve('.'),output=path.resolve(configuration.build.directories.output);
if(!output.startsWith(workspace+path.sep))throw Error('Build output must stay inside the workspace');
const configuration=JSON.parse(readFileSync('package.json','utf8')),output=path.resolve(configuration.build.directories.output);
// Reuse the pinned installed distribution already exercised by native tests.
// This avoids a second archive extraction and Windows directory-rename locks.
const electronDirectory=path.dirname(createRequire(import.meta.url)('electron'));
if(readFileSync(path.join(electronDirectory,'version'),'utf8').trim().replace(/^v/,'')!==configuration.devDependencies.electron)throw Error('Installed Electron does not match the build pin');
function sourceSnapshot(){const entries=[];function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}for(const directory of ['apps/browser','packages/core'])walk(directory);entries.push(['package.json',createHash('sha256').update(readFileSync('package.json')).digest('hex')]);return JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])));}
const plan=createWindowsPackagePlan({workspace,outputDirectory:output,electronDirectory,productName:name,appId:id});
const broker=await prepareBrokerResources({root:workspace,build:configuration.build});
const scannerSnapshot=await snapshotScannerResources(workspace);
const artifactName=`${name}-Setup-${configuration.version}.exe`;
if(!/^[\w .-]+\.exe$/.test(artifactName))throw Error('Invalid installer artifact name');
const buildConfiguration={...broker.configuration,artifactName,electronFuses:hardenedElectronFuses};
const configPath=path.join(workspace,'.runtime/windows',`package-build-${randomUUID()}.json`);
writeFileSync(configPath,JSON.stringify(buildConfiguration,null,2)+'\n',{flag:'wx'});
const configArgs=[`--config=${configPath}`];
function sourceSnapshot(){
const entries=[];
function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isSymbolicLink())throw Error('Build source reparse point rejected');if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}
for(const directory of ['apps/browser','apps/network-broker','apps/inspector','packages/core','scripts'])walk(directory);
for(const file of ['package.json','package-lock.json',configuration.build.win.icon,configPath])entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);
return createHash('sha256').update(JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])))).digest('hex');
}
const sourcesBefore=sourceSnapshot();
const r=spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',`--${platform}`,'--dir',`--config.electronDist=${electronDirectory}`,`--config.productName=${name}`,`--config.appId=${id}`,...Object.entries(hardenedElectronFuses).map(([key,value])=>`--config.electronFuses.${key}=${value}`)],{stdio:'inherit',env:process.env});
if(r.status!==0)process.exit(r.status??1);
if(sourceSnapshot()!==sourcesBefore)throw Error('Browser sources changed during packaging; discard this artifact and rebuild from stable sources');
const resources=path.join(output,'win-unpacked/resources');
await verifyPackagedFuses(path.join(output,'win-unpacked',`${name}.exe`),path.join(resources,'app.asar'));
console.log('Packaged Electron security fuses and embedded ASAR integrity verified.');
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});process.exit(acl.status??1);
const run=args=>spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',...args,...configArgs],{stdio:'inherit',env:process.env});
const result=run(plan.unpacked.args);if(result.status!==0)process.exit(result.status??1);
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
const resources=path.join(plan.unpacked.directory,'resources'),archive=path.join(resources,'app.asar'),executable=path.join(plan.unpacked.directory,`${name}.exe`);
async function verify(){
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
await verifyPackagedFuses(executable,archive);
await verifyPackagedBroker({resources,expectedManifest:broker.manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)});
await verifyScannerResources(resources,scannerSnapshot);
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py')],{stdio:'inherit',windowsHide:true});if(acl.status!==0)throw Error('Packaged scanner ACL verification failed');
return {identity:await readIdentity(),fuses:true,resources:true,broker:true,runtimeAcl:true};
}
// This verifies/provisions the unpacked build location only. NSIS extraction
// does not preserve these ACEs: a reviewed installer post-install ACL step and
// actual standard-user Program Files acceptance remain required. Browser
// startup must NEVER silently provision or broaden installation permissions.
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});if(acl.status!==0)process.exit(acl.status??1);
async function treeHash(directory){
const hash=createHash('sha256');
async function walk(current){for(const item of readdirSync(current,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const file=path.join(current,item.name);if(item.isSymbolicLink())throw Error('Packaged reparse point rejected');if(item.isDirectory())await walk(file);else if(item.isFile()){const content=createHash('sha256');let bytes=0;for await(const chunk of createReadStream(file)){content.update(chunk);bytes+=chunk.length;}hash.update(JSON.stringify([path.relative(directory,file).replaceAll('\\','/'),bytes,content.digest('hex')])+'\n');}}}
await walk(directory);return hash.digest('hex');
}
async function readIdentity(){await broker.verifySources();const scanners=await snapshotScannerResources(workspace);if(JSON.stringify(scanners)!==JSON.stringify(scannerSnapshot))throw Error('Scanner sources changed; discard this package');return {sourceSha256:sourceSnapshot(),runtimeSha256:createHash('sha256').update(JSON.stringify({electron:configuration.devDependencies.electron,broker:BROKER_RUNTIME,scanners})).digest('hex'),packageSha256:await treeHash(plan.unpacked.directory)};}
await verify();
console.log('Unpacked browser, embedded broker manifest, complete broker payload, security fuses and scanner ACLs verified.');
if(!process.argv.includes('--unpacked-only')){
const installer=await buildVerifiedInstaller({plan,expectedIdentity:await readIdentity(),readIdentity,verify,run,installerPath:path.join(output,artifactName)});
console.log(JSON.stringify({installer:installer.path,bytes:installer.bytes,sha256:installer.sha256,brokerIncluded:true,productionActivationVerified:false}));
}
+96
View File
@@ -0,0 +1,96 @@
// Official standalone Node executable gives the broker a distinct Windows
// application identity; Electron utilityProcess shares the browser executable.
// This provisioning helper is not yet wired into production packaging.
import {createHash} from 'node:crypto';
import {createReadStream,createWriteStream} from 'node:fs';
import {lstat,mkdir,mkdtemp,readFile,rename,writeFile,readdir} from 'node:fs/promises';
import {pipeline} from 'node:stream/promises';
import {Readable,Transform} from 'node:stream';
import {spawn} from 'node:child_process';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
export const BROKER_RUNTIME=Object.freeze({version:'24.21.0',platform:'win32',arch:'x64',
archive:'node-v24.21.0-win-x64.zip',
archiveSha256:'158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541',
executableSha256:'ba4e6d110e8c1592a1ecd390f6b05f3da124b13871a5be62b341a07a853c6c32',
licenseSha256:'ed34dd8e3f0a78dbaf00d0444ce8e285b015b765379c2e17880455f70370f8e9',
baseUrl:'https://nodejs.org/dist/v24.21.0/'});
function fail(code){throw Object.assign(new Error(code),{code});}
async function sha256(file){const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);return hash.digest('hex');}
export function verifyOfficialChecksums(text){
if(typeof text!=='string'||text.length>65536)fail('broker_runtime_checksums_invalid');
for(const [name,expected] of [[BROKER_RUNTIME.archive,BROKER_RUNTIME.archiveSha256],['win-x64/node.exe',BROKER_RUNTIME.executableSha256]]){
const rows=text.split(/\r?\n/).filter(row=>row.endsWith(' '+name));
if(rows.length!==1||rows[0]!==expected+' '+name)fail('broker_runtime_checksums_mismatch');
}
}
export async function downloadOfficialArchive(destination){
const checksumResponse=await fetch(BROKER_RUNTIME.baseUrl+'SHASUMS256.txt',{redirect:'error',signal:AbortSignal.timeout(30000)});
if(!checksumResponse.ok)fail('broker_runtime_download_failed');
const chunks=[];let checksumBytes=0;
for await(const chunk of checksumResponse.body){checksumBytes+=chunk.length;if(checksumBytes>65536)fail('broker_runtime_checksums_invalid');chunks.push(Buffer.from(chunk));}
const checksumText=Buffer.concat(chunks).toString();verifyOfficialChecksums(checksumText);
const response=await fetch(BROKER_RUNTIME.baseUrl+BROKER_RUNTIME.archive,{redirect:'error',signal:AbortSignal.timeout(120000)});
if(!response.ok)fail('broker_runtime_download_failed');
let bytes=0;const hash=createHash('sha256');
const bounded=new Transform({transform(chunk,_encoding,callback){bytes+=chunk.length;if(bytes>67108864){callback(new Error('broker_runtime_archive_too_large'));return;}hash.update(chunk);callback(null,chunk);}});
await pipeline(Readable.fromWeb(response.body),bounded,createWriteStream(destination,{flags:'wx',mode:0o600}));
if(hash.digest('hex')!==BROKER_RUNTIME.archiveSha256)fail('broker_runtime_archive_mismatch');
return {bytes,archiveSha256:BROKER_RUNTIME.archiveSha256,checksumText};
}
function psQuote(value){return "'"+value.replaceAll("'","''")+"'";}
async function extractSelected(archive,destination){
// Extract only exact reviewed members from the already hash-verified ZIP.
const script=`$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem; $zip=[IO.Compression.ZipFile]::OpenRead(${psQuote(archive)}); try { foreach($name in @('node.exe','LICENSE')) { $member='node-v${BROKER_RUNTIME.version}-win-x64/'+$name; $entries=@($zip.Entries | Where-Object { $_.FullName -ceq $member }); if($entries.Count -ne 1 -or $entries[0].Length -gt 100000000) { throw 'broker_runtime_archive_members_invalid' }; $inputStream=$entries[0].Open(); $outputStream=[IO.File]::Open([IO.Path]::Combine(${psQuote(destination)},$name),[IO.FileMode]::CreateNew); try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose(); $inputStream.Dispose() } } } finally { $zip.Dispose() }`;
const child=spawn('powershell.exe',['-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:'ignore'});
const code=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',resolve);});
if(code!==0)fail('broker_runtime_extraction_failed');
}
async function verifyDirectoryAncestors(directory){
for(let current=path.resolve(directory);;current=path.dirname(current)){
const stat=await lstat(current);if(stat.isSymbolicLink()||!stat.isDirectory())fail('broker_runtime_path_unsafe');
if(path.dirname(current)===current)break;
}
}
export async function verifyBrokerRuntime(directory){
await verifyDirectoryAncestors(directory);
const manifestPath=path.join(directory,'runtime-manifest.json'),manifestStat=await lstat(manifestPath);
if(manifestStat.isSymbolicLink()||!manifestStat.isFile()||manifestStat.size>16384)fail('broker_runtime_manifest_invalid');
let manifest;try{manifest=JSON.parse(await readFile(manifestPath,'utf8'));}catch{fail('broker_runtime_manifest_invalid');}
if(!manifest||typeof manifest!=='object')fail('broker_runtime_manifest_invalid');
if(manifest.format!=='safetynet-node-broker-runtime-v1'||manifest.version!==BROKER_RUNTIME.version||manifest.arch!=='x64'||manifest.platform!=='win32'||manifest.archiveSha256!==BROKER_RUNTIME.archiveSha256)fail('broker_runtime_manifest_invalid');
if(!Array.isArray(manifest.files)||manifest.files.length!==2||manifest.files.some(file=>!file||typeof file.path!=='string')||new Set(manifest.files.map(file=>file.path)).size!==2)fail('broker_runtime_manifest_invalid');
const files=await readdir(directory);if(JSON.stringify(files.sort())!==JSON.stringify(['LICENSE','node.exe','runtime-manifest.json'].sort()))fail('broker_runtime_inventory_mismatch');
for(const name of ['node.exe','LICENSE']){
const stat=await lstat(path.join(directory,name));if(stat.isSymbolicLink()||!stat.isFile())fail('broker_runtime_path_unsafe');
const record=manifest.files?.find(item=>item.path===name);
if(!record||record.bytes!==stat.size||!/^[a-f0-9]{64}$/.test(record.sha256)||await sha256(path.join(directory,name))!==record.sha256)fail('broker_runtime_integrity_mismatch');
if(record.sha256!==(name==='node.exe'?BROKER_RUNTIME.executableSha256:BROKER_RUNTIME.licenseSha256))fail('broker_runtime_integrity_mismatch');
}
return manifest;
}
export async function provisionBrokerRuntime(workspace){
if(process.platform!=='win32'||process.arch!=='x64')fail('broker_runtime_platform_unsupported');
const parent=path.join(path.resolve(workspace),'.runtime/windows');await mkdir(parent,{recursive:true});
await verifyDirectoryAncestors(parent);
const destination=path.join(parent,'broker');
try{await lstat(destination);return await verifyBrokerRuntime(destination);}catch(error){if(error.code!=='ENOENT')throw error;}
const staging=await mkdtemp(path.join(parent,'broker-provision-'));
const archive=path.join(staging,BROKER_RUNTIME.archive),payload=path.join(staging,'payload');await mkdir(payload);
// Failed and verified archives/proof remain ignored for diagnosis; no
// unverified executable is started and no global Node installation changes.
const download=await downloadOfficialArchive(archive);
await writeFile(path.join(staging,'SHASUMS256.txt'),download.checksumText);
await extractSelected(archive,payload);
const manifest={format:'safetynet-node-broker-runtime-v1',version:BROKER_RUNTIME.version,platform:'win32',arch:'x64',archiveSha256:BROKER_RUNTIME.archiveSha256,files:[]};
for(const name of ['node.exe','LICENSE'])manifest.files.push({path:name,bytes:(await lstat(path.join(payload,name))).size,sha256:await sha256(path.join(payload,name))});
await writeFile(path.join(payload,'runtime-manifest.json'),JSON.stringify(manifest,null,2)+'\n');
await verifyBrokerRuntime(payload);await rename(payload,destination);
return await verifyBrokerRuntime(destination);
}
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
const workspace=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
const result=process.argv.includes('--verify')?await verifyBrokerRuntime(path.join(workspace,'.runtime/windows/broker')):await provisionBrokerRuntime(workspace);
console.log(JSON.stringify({version:result.version,files:result.files.length,verified:true}));
}
+55
View File
@@ -0,0 +1,55 @@
// Standalone payload preparation only. No browser wiring, installer or service activation.
import {readFile,writeFile,lstat,mkdir,mkdtemp,rename} from 'node:fs/promises';
import {createReadStream} from 'node:fs';
import {createHash} from 'node:crypto';
import {builtinModules} from 'node:module';
import {gunzipSync} from 'node:zlib';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {BROKER_RUNTIME} from './provision-broker-runtime.mjs';
import {buildBrokerPackageManifest,verifyBrokerPackage} from './broker-package-contract.mjs';
export const BROKER_PAYLOAD=Object.freeze({version:'0.1.0',protocol:1,entrypoint:'node.exe'});
const BROKER_FILES=Object.freeze(['audit-gate.cjs','bounded-audit.cjs','connect-inspection-proxy.cjs','diagnostics-adapter.cjs','dns-policy-route.cjs','download-response-gate.cjs','http-request-gate.cjs','http-response-gate.cjs','https-forwarder.cjs','https-inspection-server.cjs','lifecycle-audit.cjs','managed-proxy.cjs','management-identity-operation.cjs','multipart.cjs','policy-authority.cjs','process-entry.cjs','production-entry.cjs','production-factory.cjs','proxy-forwarder.cjs','service-acceptance.cjs','service-bootstrap.cjs','service-commit-gate.cjs','service-entry.cjs','service-store-client.cjs','framed-channel.cjs','tls-authority.cjs','tls-certificates.cjs','windows-wfp-plan.cjs','wfp-topology.cjs','wfp-revalidation.cjs','work-admission.cjs']);
const BROWSER_HELPERS=new Set(['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs']);
const builtins=new Set(builtinModules.flatMap(name=>[name,name.replace(/^node:/,''),'node:'+name.replace(/^node:/,'')]));
function fail(code){throw Object.assign(new Error(code),{code});}
async function safe(file,directory=false){
for(let current=path.resolve(file);;current=path.dirname(current)){
const stat=await lstat(current);if(stat.isSymbolicLink())fail('broker_stage_path_unsafe');
if(current===path.resolve(file)&&((directory&&!stat.isDirectory())||(!directory&&!stat.isFile())))fail('broker_stage_path_unsafe');
if(path.dirname(current)===current)break;
}
}
async function digest(file){await safe(file);const before=await lstat(file);if(before.size>100000000)fail('broker_stage_file_limit');const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);const after=await lstat(file);if(before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)fail('broker_stage_source_changed');return {bytes:after.size,sha256:hash.digest('hex')};}
export async function snapshotSources(root,names){const records=[];for(const name of names){if(!/^[a-zA-Z0-9_./-]+$/.test(name)||name.split('/').some(p=>p==='..'||p==='.'||!p))fail('broker_stage_path_unsafe');records.push({path:name,...await digest(path.join(root,...name.split('/')))});}return records;}
export async function verifySourceSnapshot(root,records){for(const record of records){const actual=await digest(path.join(root,...record.path.split('/')));if(actual.bytes!==record.bytes||actual.sha256!==record.sha256)fail('broker_stage_source_changed');}}
function allowedSource(name){return /^packages\/core\/[a-z0-9-]+\.(?:cjs|mjs)$/.test(name)||(name.startsWith('apps/network-broker/')&&BROKER_FILES.includes(name.slice(20)))||(name.startsWith('apps/browser/')&&BROWSER_HELPERS.has(name.slice(13)));}
export async function reviewedSourceClosure(root){
const queue=BROKER_FILES.map(name=>'apps/network-broker/'+name);queue.push('packages/core/policy.mjs');const seen=new Set(),external=new Set();
while(queue.length){const name=queue.shift();if(seen.has(name))continue;if(!allowedSource(name))fail('broker_stage_unreviewed_source');await safe(path.join(root,...name.split('/')));const source=await readFile(path.join(root,...name.split('/')),'utf8');if(Buffer.byteLength(source)>1048576)fail('broker_stage_source_limit');seen.add(name);
for(const call of source.matchAll(/\b(?:require|import)\s*\(([^)]*)\)/g))if(!/^\s*['"][^'"]+['"]\s*$/.test(call[1]))fail('broker_stage_dynamic_dependency');
const imports=[...source.matchAll(/(?:require\s*\(|import\s*\()\s*['"]([^'"]+)['"]/g),...source.matchAll(/\bfrom\s*['"]([^'"]+)['"]/g),...source.matchAll(/\bimport\s*['"]([^'"]+)['"]/g)].map(match=>match[1]);
for(const specifier of imports){if(builtins.has(specifier))continue;if(specifier.startsWith('.')){const next=path.posix.normalize(path.posix.join(path.posix.dirname(name),specifier));if(!allowedSource(next))fail('broker_stage_unreviewed_source');queue.push(next);}else external.add(specifier);}
}
if([...external].some(name=>name!=='zod'))fail('broker_stage_unreviewed_dependency');return {files:[...seen].sort(),dependencies:[...external].sort()};
}
export function extractVerifiedZod(bytes,locked){
if(!Buffer.isBuffer(bytes)||bytes.length>10485760||locked?.version!=='4.6.7'||locked.dev||locked.resolved!==`https://registry.npmjs.org/zod/-/zod-${locked.version}.tgz`||!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(locked.integrity||'')||'sha512-'+createHash('sha512').update(bytes).digest('base64')!==locked.integrity)fail('broker_stage_dependency_integrity');
let tar;try{tar=gunzipSync(bytes,{maxOutputLength:67108864});}catch{fail('broker_stage_dependency_archive');}
const files=[];const names=new Set();for(let offset=0;offset+512<=tar.length;){const header=tar.subarray(offset,offset+512);if(header.every(byte=>byte===0))break;const text=buffer=>buffer.toString('utf8').replace(/\0.*$/s,'');const name=text(header.subarray(0,100)),prefix=text(header.subarray(345,500));const full=prefix?prefix+'/'+name:name;const size=parseInt(text(header.subarray(124,136)).trim(),8),type=header[156];const checksum=parseInt(text(header.subarray(148,156)).trim(),8);let sum=0;for(let i=0;i<512;i++)sum+=i>=148&&i<156?32:header[i];if(sum!==checksum||!Number.isSafeInteger(size)||size<0||offset+512+size>tar.length||![0,48,53].includes(type)||!full.startsWith('package/')||full.includes('\\')||full.split('/').filter(Boolean).some(part=>! /^[a-zA-Z0-9_.-]+$/.test(part)||part==='..'||part==='.'||/[. ]$/.test(part)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))||names.has(full.toLowerCase()))fail('broker_stage_dependency_archive');names.add(full.toLowerCase());if(names.size>4096)fail('broker_stage_dependency_archive');
const relative=full.slice(8);if(type!==53&&relative&&!relative.startsWith('src/')&&!/(^|\/)(tests?|fixtures?|benchmarks?)(\/|$)/i.test(relative)&&(relative==='package.json'||/^license(?:\.[a-z]+)?$/i.test(relative)||/\.(?:js|cjs|mjs)$/.test(relative)))files.push({path:'node_modules/zod/'+relative,bytes:Buffer.from(tar.subarray(offset+512,offset+512+size))});offset+=512+Math.ceil(size/512)*512;
}
if(files.length>2048||!files.some(file=>file.path==='node_modules/zod/package.json')||!files.some(file=>/\/license(?:\.[a-z]+)?$/i.test(file.path)))fail('broker_stage_dependency_archive');let metadata;try{metadata=JSON.parse(files.find(file=>file.path==='node_modules/zod/package.json').bytes.toString());}catch{fail('broker_stage_dependency_archive');}if(metadata.name!=='zod'||metadata.version!==locked.version)fail('broker_stage_dependency_integrity');return files;
}
async function archive(locked){const response=await fetch(locked.resolved,{redirect:'error',signal:AbortSignal.timeout(30000)});if(!response.ok)fail('broker_stage_dependency_download');const chunks=[];let count=0;for await(const chunk of response.body){count+=chunk.length;if(count>10485760)fail('broker_stage_dependency_limit');chunks.push(Buffer.from(chunk));}return Buffer.concat(chunks);}
export async function stageBrokerPayload({root=fileURLToPath(new URL('../',import.meta.url)),destination,dependencyArchive}={}){
root=path.resolve(root);const parent=path.join(root,'.runtime','windows');destination=path.resolve(destination||path.join(parent,'broker-payload'));if(path.dirname(destination)!==parent||!/^broker-payload(?:-[a-zA-Z0-9_-]+)?$/.test(path.basename(destination)))fail('broker_stage_destination_unsafe');await safe(root,true);await safe(parent,true);try{await lstat(destination);fail('broker_stage_destination_exists');}catch(error){if(error.code!=='ENOENT')throw error;}
const closure=await reviewedSourceClosure(root);const runtimeNames=['node.exe','LICENSE'];const sourceNames=[...closure.files,'package-lock.json',...runtimeNames.map(name=>'.runtime/windows/broker/'+name)];const sources=await snapshotSources(root,sourceNames);for(const [name,expected] of [['node.exe',BROKER_RUNTIME.executableSha256],['LICENSE',BROKER_RUNTIME.licenseSha256]])if(sources.find(item=>item.path==='.runtime/windows/broker/'+name).sha256!==expected)fail('broker_stage_runtime_integrity');
const lock=JSON.parse(await readFile(path.join(root,'package-lock.json'),'utf8'));const locked=lock.packages?.['node_modules/zod'];if(!locked)fail('broker_stage_dependency_missing');const dependencies=extractVerifiedZod(dependencyArchive||await archive(locked),locked);
const temporary=await mkdtemp(path.join(parent,'broker-payload-stage-'));async function put(name,bytes){const target=path.join(temporary,...name.split('/'));await mkdir(path.dirname(target),{recursive:true});await writeFile(target,bytes,{flag:'wx'});}
for(const name of runtimeNames)await put(name,await readFile(path.join(root,'.runtime/windows/broker',name)));for(const name of closure.files)await put(name,await readFile(path.join(root,...name.split('/'))));for(const file of dependencies)await put(file.path,file.bytes);
await verifySourceSnapshot(root,sources);const manifest=await buildBrokerPackageManifest(temporary,BROKER_PAYLOAD);for(const source of sources.filter(item=>item.path!=='package-lock.json')){const target=source.path.startsWith('.runtime/windows/broker/')?source.path.slice('.runtime/windows/broker/'.length):source.path;const copied=manifest.files.find(item=>item.path===target);if(!copied||copied.bytes!==source.bytes||copied.sha256!==source.sha256)fail('broker_stage_source_changed');}await verifyBrokerPackage(temporary,manifest,BROKER_PAYLOAD);await verifySourceSnapshot(root,sources);await rename(temporary,destination);
// Adjacent metadata is evidence; only an embedded/signed installer copy is trusted.
await writeFile(destination+'.manifest.json',JSON.stringify(manifest,null,2)+'\n',{flag:'wx'});return {root:destination,manifest,sources,sourceFiles:closure.files.length,dependencies:closure.dependencies,productionReady:false};
}
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){try{const result=await stageBrokerPayload();console.log(JSON.stringify({files:result.manifest.files.length,dependencies:result.dependencies,productionReady:result.productionReady}));}catch(error){console.error(error.code?.startsWith('broker_')?error.code:'broker_stage_failed');process.exitCode=1;}}
+30
View File
@@ -0,0 +1,30 @@
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();}
}
+38
View File
@@ -0,0 +1,38 @@
// Not wired into package.mjs until a real packaged broker is available.
// Official CLI: https://www.electron.build/docs/cli/
// --dir produces only an unpacked app; --win nsis --prepackaged distributes
// that verified directory without silently rebuilding its application payload.
import path from 'node:path';
function fail(code){throw Object.assign(new Error(code),{code});}
function identity(value){
if(!value||!['sourceSha256','runtimeSha256','packageSha256'].every(key=>/^[a-f0-9]{64}$/.test(value[key])))fail('package_identity_invalid');
return Object.freeze(Object.fromEntries(['sourceSha256','runtimeSha256','packageSha256'].map(key=>[key,value[key]])));
}
function same(left,right){return Object.keys(left).every(key=>left[key]===right[key]);}
export function createWindowsPackagePlan({workspace,outputDirectory,electronDirectory,productName='SafetyNet',appId='enterprise.safetynet.browser',platform=process.platform}){
if(platform!=='win32')fail('package_platform_unsupported');
if(![workspace,outputDirectory,electronDirectory].every(value=>typeof value==='string'&&path.isAbsolute(value)))fail('package_path_invalid');
const root=path.resolve(workspace),output=path.resolve(outputDirectory),runtime=path.resolve(electronDirectory);
const relative=path.relative(root,output);
if(!relative||relative.startsWith('..'+path.sep)||relative==='..'||path.isAbsolute(relative))fail('package_output_unsafe');
if(!/^[\w .-]{1,40}$/.test(productName)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(appId))fail('package_brand_invalid');
const directory=path.join(output,'win-unpacked');
const common=[`--config.productName=${productName}`,`--config.appId=${appId}`,`--config.directories.output=${output}`,'--publish','never'];
return Object.freeze({
unpacked:Object.freeze({stage:'build-unpacked',args:Object.freeze(['--win','--dir',`--config.electronDist=${runtime}`,...common]),directory}),
verification:Object.freeze({stage:'verify-unpacked',required:Object.freeze(['fuses','resources','broker','runtimeAcl']),directory}),
async installer({expectedIdentity,readIdentity,verify}){
if(typeof readIdentity!=='function'||typeof verify!=='function')fail('package_verifier_required');
const expected=identity(expectedIdentity);
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
const receipt=await verify(directory,expected);
if(!receipt||!same(expected,identity(receipt.identity))||!['fuses','resources','broker','runtimeAcl'].every(key=>receipt[key]===true))fail('package_verification_incomplete');
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
// This plans arguments only. Caller must protect the verified directory
// against changes through invocation and inspect the real installer result.
return Object.freeze({stage:'build-installer',directory,identity:expected,
args:Object.freeze(['--win','nsis','--prepackaged',directory,...common])});
}
});
}
+33
View File
@@ -0,0 +1,33 @@
// Build the inventory AFTER final binaries exist; signing is performed by an
// external trusted release signer. No private key is generated or stored here.
import {lstat,readdir,open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
const OMIT=new Set(['release-envelope.json','deployment-envelope.json']);
function fail(code){throw Object.assign(new Error(code),{code});}
async function ancestors(root){for(let current=path.resolve(root);;current=path.dirname(current)){const stat=await lstat(current);if(!stat.isDirectory()||stat.isSymbolicLink())fail('windows-release-path-unsafe');if(path.dirname(current)===current)break;}}
async function hash(file){const input=await open(file,'r');try{const before=await input.stat(),sha=createHash('sha256');let bytes=0;for await(const chunk of input.createReadStream({autoClose:false})){bytes+=chunk.length;sha.update(chunk);}const after=await input.stat();if(!before.isFile()||before.size!==bytes||after.size!==bytes||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs||before.ino!==after.ino||before.dev!==after.dev)fail('windows-release-source-changed');return {bytes,sha256:sha.digest('hex')};}finally{await input.close();}}
export async function buildReleaseInventory(root,{browserPath='SafetyNet.exe',nativeHostPath='safetynet-service-host.exe'}={}){
await ancestors(root);const files=[],seen=new Set();let count=0;
async function walk(directory,prefix=''){
for(const entry of await readdir(directory,{withFileTypes:true})){
const relative=prefix+entry.name;
if(++count>15000||!relative.split('/').every(part=>/^[A-Za-z0-9_.-]+$/.test(part)&&part!=='.'&&part!=='..'&&!/[. ]$/.test(part)&&!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))||relative.length>240||entry.isSymbolicLink())fail('windows-release-path-unsafe');
const target=path.join(directory,entry.name),stat=await lstat(target);if(stat.isSymbolicLink())fail('windows-release-path-unsafe');
if(stat.isDirectory())await walk(target,relative+'/');else if(stat.isFile()){
if(OMIT.has(relative))continue;
if(seen.has(relative.toLowerCase())||files.length>=10000)fail('windows-release-inventory-invalid');seen.add(relative.toLowerCase());files.push({path:relative,...await hash(target)});
}else fail('windows-release-path-unsafe');
}
}
await walk(root);files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
for(const required of [browserPath,nativeHostPath,'resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py'])if(!files.some(file=>file.path===required))fail('windows-release-component-missing');
if(!files.some(file=>file.path.startsWith('resources/semantic/')))fail('windows-release-component-missing');
return files;
}
export async function verifyReleaseInventory(root,verifiedRelease,options){
// Cryptographic authenticity is established separately by windows-trust.cjs.
// This API only compares an already trusted immutable inventory to disk.
const actual=await buildReleaseInventory(root,options),expected=verifiedRelease?.manifest?.files;
if(!Array.isArray(expected)||actual.length!==expected.length||actual.some((file,index)=>file.path!==expected[index]?.path||file.bytes!==expected[index]?.bytes||file.sha256!==expected[index]?.sha256))fail('windows-release-inventory-mismatch');return true;
}