Files

50 lines
6.9 KiB
JavaScript
Raw Permalink Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
import fs,{mkdtemp,writeFile,rm,symlink,mkdir} from 'node:fs/promises';
import {generateKeyPairSync} from 'node:crypto';
import {tmpdir} from 'node:os';
import path from 'node:path';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),{invokeHelper,createServiceStoreClient,readEnvelope}=require('../apps/network-broker/service-store-client.cjs');
const {signManifest,verifyReleaseEnvelope}=require('../packages/core/windows-trust.cjs');
function launchScript(code,observe=()=>{}){return (file,args,options)=>{observe(file,args,options);return spawn(process.execPath,['-e',code],{stdio:['pipe','pipe','ignore']});};}
const emit="const b=Buffer.from(JSON.stringify({persisted:true,stateSha256:'a'.repeat(64)}));const h=Buffer.alloc(4);h.writeUInt32LE(b.length);process.stdout.write(Buffer.concat([h,b]));";
test('actual owned subprocess one framed reply is accepted only after terminal close',async()=>{
let settled=false;const work=invokeHelper('--commit-acceptance',{expectedPreviousSHA:'b'.repeat(64),candidateState:{}},{launch:launchScript(emit+'setTimeout(()=>process.exit(0),150);',(_,args,options)=>{assert.deepEqual(args,['--commit-acceptance']);assert.equal(options.shell,false);assert.equal(options.windowsHide,true);assert.deepEqual(Object.keys(options.env).sort(),['SystemRoot','WINDIR']);})});work.then(()=>{settled=true;});await new Promise(r=>setTimeout(r,75));assert.equal(settled,false);assert.equal((await work).persisted,true);
});
test('native-minted canonical SID alone is preserved to actual helper subprocess',async()=>{
const old=process.env.SAFETYNET_SERVICE_CLIENT_SID;process.env.SAFETYNET_SERVICE_CLIENT_SID='S-1-12-1-1-2-3-4';
try{const value=await invokeHelper('--trust-snapshot',undefined,{launch:(file,args,options)=>{assert.deepEqual(Object.keys(options.env).sort(),['SAFETYNET_SERVICE_CLIENT_SID','SystemRoot','WINDIR']);return spawn(process.execPath,['-e',"const b=Buffer.from(JSON.stringify({sid:process.env.SAFETYNET_SERVICE_CLIENT_SID}));const h=Buffer.alloc(4);h.writeUInt32LE(b.length);process.stdout.write(Buffer.concat([h,b]));"],{stdio:['pipe','pipe','ignore'],env:options.env});}});assert.equal(value.sid,'S-1-12-1-1-2-3-4');process.env.SAFETYNET_SERVICE_CLIENT_SID='S-1-12-1-01-2-3-4';await assert.rejects(invokeHelper('--trust-snapshot',undefined),{message:'broker-bootstrap-invalid'});}finally{if(old===undefined)delete process.env.SAFETYNET_SERVICE_CLIENT_SID;else process.env.SAFETYNET_SERVICE_CLIENT_SID=old;}
});
test('large genuinely signed release survives bounded partial reads and verifies cryptographically',async()=>{
const root=await mkdtemp(path.join(tmpdir(),'safetynet-large-release-')),keys=generateKeyPairSync('ec',{namedCurve:'prime256v1'});
try{
const paths=['SafetyNet.exe','resources/app.asar','resources/broker/node.exe','resources/inspector/server.py','resources/python/python.exe','resources/semantic/model.bin','safetynet-service-host.exe',...Array.from({length:6000},(_,i)=>'payload/'+String(i).padStart(5,'0')+'x'.repeat(205)+'.bin')].sort();
const manifest={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:1,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:keys.publicKey.export({type:'spki',format:'pem'}),files:paths.map(path=>({path,bytes:1,sha256:'a'.repeat(64)}))};
const envelope=signManifest(manifest,keys.privateKey.export({type:'pkcs8',format:'pem'}),'release');assert.ok(Buffer.byteLength(JSON.stringify(envelope))>2097152);await writeFile(path.join(root,'release-envelope.json'),JSON.stringify(envelope));
const partial={...fs,async open(...args){const h=await fs.open(...args);return {stat:()=>h.stat(),close:()=>h.close(),read:(b,o,n,p)=>h.read(b,o,Math.min(n,4096),p)};}};
const actual=await readEnvelope(root,'release-envelope.json',partial);assert.equal(verifyReleaseEnvelope(actual,{publicKey:keys.publicKey.export({type:'spki',format:'pem'})}).manifest.files.length,6007);
const changed={...fs,async open(...args){const h=await fs.open(...args);let stats=0;return {close:()=>h.close(),read:(...a)=>h.read(...a),async stat(){const s=await h.stat();if(++stats>1)s.mtimeMs++;return s;}};}};await assert.rejects(readEnvelope(root,'release-envelope.json',changed),{message:'broker-bootstrap-invalid'});
}finally{await rm(root,{recursive:true,force:true});}
});
test('physical framing rejects duplicate, trailing, invalid UTF8 and nonzero exit',async()=>{
for(const suffix of ['process.stdout.write(Buffer.from([0]));','process.stdout.write(Buffer.from([2,0,0,0,123,125]));','process.exitCode=5;'])await assert.rejects(invokeHelper('--trust-snapshot',undefined,{launch:launchScript(emit+suffix)}),{message:'broker-bootstrap-invalid'});
await assert.rejects(invokeHelper('--trust-snapshot',undefined,{launch:launchScript("process.stdout.write(Buffer.from([2,0,0,0,255,255]));")}),{message:'broker-bootstrap-invalid'});
});
test('physical hanging helper deadline kills its owned process, no retry or acceptance',async()=>{
let calls=0;await assert.rejects(invokeHelper('--trust-snapshot',undefined,{deadlineMs:50,launch:launchScript('setInterval(()=>{},1000);',()=>calls++)}),{message:'broker-bootstrap-invalid'});assert.equal(calls,1);
});
test('commit has smaller reply limit and invalid snapshot cannot become trust',async()=>{
await assert.rejects(invokeHelper('--commit-acceptance',{}, {launch:launchScript('process.stdout.write(Buffer.from([1,0,1,0]));setInterval(()=>{},1000);')}),{message:'broker-bootstrap-invalid'});
const client=createServiceStoreClient({launch:launchScript(emit)});await assert.rejects(client.snapshot(),{message:'broker-bootstrap-invalid'});
});
test('fixed envelope files are bounded and junction ancestors cannot select another trust store',async()=>{
const root=await mkdtemp(path.join(tmpdir(),'safetynet-store-'));
try{
const envelope={payload:'synthetic signed payload',signature:'synthetic signature'};await writeFile(path.join(root,'release-envelope.json'),JSON.stringify(envelope));await writeFile(path.join(root,'deployment-envelope.json'),JSON.stringify(envelope));
assert.deepEqual((await createServiceStoreClient({root}).envelopes()).releaseEnvelope,envelope);
await writeFile(path.join(root,'deployment-envelope.json'),'x'.repeat(2097153));await assert.rejects(createServiceStoreClient({root}).envelopes(),{message:'broker-bootstrap-invalid'});
const target=path.join(root,'target'),alias=path.join(root,'alias');await mkdir(target);await writeFile(path.join(target,'release-envelope.json'),JSON.stringify(envelope));await writeFile(path.join(target,'deployment-envelope.json'),JSON.stringify(envelope));await symlink(target,alias,'junction');await assert.rejects(createServiceStoreClient({root:alias}).envelopes(),{message:'broker-bootstrap-invalid'});
}finally{await rm(root,{recursive:true,force:true});}
});