56 lines
7.0 KiB
JavaScript
56 lines
7.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {EventEmitter} from 'node:events';
|
|
import {generateKeyPairSync,randomUUID} from 'node:crypto';
|
|
import {createRequire} from 'node:module';
|
|
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
|
const require=createRequire(import.meta.url),{runServiceProcess}=require('../apps/network-broker/service-entry.cjs');
|
|
const {signManifest,verifyReleaseEnvelope,verifyDeploymentEnvelope}=require('../packages/core/windows-trust.cjs');
|
|
const {acceptanceStateDigest}=require('../apps/network-broker/service-acceptance.cjs');
|
|
const pub=k=>k.publicKey.export({type:'spki',format:'pem'}),priv=k=>k.privateKey.export({type:'pkcs8',format:'pem'});
|
|
function fixture(){
|
|
const vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),key=generateKeyPairSync('ed25519'),sid='S-1-12-1-1-2-3-4';
|
|
const release={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:1,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:pub(customer),files:['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'].map(path=>({path,bytes:1,sha256:'a'.repeat(64)}))};
|
|
const releaseEnvelope=signManifest(release,priv(vendor),'release'),r=verifyReleaseEnvelope(releaseEnvelope,{publicKey:pub(vendor)});
|
|
const deployment={format:'safetynet-windows-deployment-v1',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:1,policyPublicKey:pub(key),expiresAt:Date.now()+60000,users:[{sid,deviceId:'device',minimumPolicyVersion:7}]};
|
|
const deploymentEnvelope=signManifest(deployment,priv(customer),'deployment'),d=verifyDeploymentEnvelope(deploymentEnvelope,{release:r,authenticatedSid:sid});
|
|
const policy=defaultPolicy();policy.version=7;policy.assignment={deviceId:'device',groups:[]};
|
|
const snapshot={version:1,authenticatedSid:sid,releaseAnchor:pub(vendor),acceptanceState:{format:'safetynet-service-floors-v1',release:{minimumSequence:1,acceptedDigest:r.digest},deployment:{tenant:release.deploymentTenant,minimumSequence:1,acceptedDigest:d.digest},policies:[]}};
|
|
return {snapshot,envelopes:{releaseEnvelope,deploymentEnvelope},config:{publicKey:pub(key),deviceId:'device',minimumVersion:7,envelope:signPolicy(policy,priv(key)),brokerPort:43120,network:{mode:'direct',dns:'system'}}};
|
|
}
|
|
function channel(){const c=new EventEmitter();c.connected=true;c.send=(v,callback)=>{c.emit('sent',v);callback?.();};c.disconnect=()=>{if(c.connected){c.connected=false;c.emit('disconnect');}};return c;}
|
|
const verifiedTopology=async()=>({bfeRunning:true,ownedSublayer:'ours',enumerationComplete:true,allObjectsReadable:true,sublayers:[{key:'ours',weight:65535}],filters:Array.from({length:8},(_,i)=>({key:String(i),sublayer:'ours',layer:'c38d57d1-05a7-4c33-904f-7fbceee60e82',ownedVerified:true}))});
|
|
test('default unavailable native topology constructs no factory and opens no listener',async()=>{
|
|
const f=fixture(),c=channel();let factories=0;
|
|
const store={async snapshot(){return f.snapshot;},async envelopes(){return f.envelopes;},async commit(value){return {persisted:true,stateSha256:acceptanceStateDigest(value.candidateState)};}};
|
|
const sent=new Promise(resolve=>{c.once('sent',resolve);c.once('disconnect',()=>resolve({type:'quarantined'}));});
|
|
const handle=runServiceProcess({channel:c,store,factory:async()=>{factories++;throw Error('must not construct');}});
|
|
c.emit('message',{type:'start',version:1,generation:randomUUID(),policyVersion:7,config:f.config});
|
|
assert.ok(['startup-fault','quarantined'].includes((await sent).type));assert.equal(factories,0);await handle.close();assert.equal(c.connected,false);
|
|
});
|
|
test('runtime topology change waits original broker cleanup before disconnect',async()=>{
|
|
const f=fixture(),c=channel();let checks=0,closeCalls=0,releaseClose,closeStarted;
|
|
const started=new Promise(resolve=>{closeStarted=resolve;});
|
|
const cleanup=new Promise(resolve=>{releaseClose=resolve;});
|
|
const store={async snapshot(){return f.snapshot;},async envelopes(){return f.envelopes;},async commit(value){return {persisted:true,stateSha256:acceptanceStateDigest(value.candidateState)};}};
|
|
const ready=new Promise(resolve=>c.once('sent',resolve));
|
|
const handle=runServiceProcess({channel:c,store,readTopology:async()=>({...await verifiedTopology(),bfeRunning:++checks===1}),factory:async()=>({authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'a'.repeat(43)},async listen(){return 43120;},close(){closeCalls++;closeStarted();return cleanup;}})});
|
|
c.emit('message',{type:'start',version:1,generation:randomUUID(),policyVersion:7,config:f.config});
|
|
assert.equal((await ready).type,'ready');
|
|
await started;assert.equal(closeCalls,1);assert.equal(c.connected,true);
|
|
const original=handle.close();assert.equal(handle.close(),original);
|
|
releaseClose();await original;assert.equal(c.connected,false);assert.equal(closeCalls,1);
|
|
});
|
|
test('signed service entry commits exact verified state before factory or listener',async()=>{
|
|
const f=fixture(),c=channel(),order=[];let request;
|
|
const store={async snapshot(){order.push('snapshot');return f.snapshot;},async envelopes(){order.push('files');return f.envelopes;},async commit(value){order.push('commit');request=value;return {persisted:true,stateSha256:acceptanceStateDigest(value.candidateState)};}};
|
|
const finished=new Promise(resolve=>c.once('sent',resolve));const handle=runServiceProcess({channel:c,store,readTopology:verifiedTopology,factory:async(config)=>{order.push('factory');assert.equal(config.minimumVersion,7);return {authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'a'.repeat(43)},async listen(){order.push('listen');return 43120;},async close(){order.push('close');}};}});
|
|
c.emit('message',{type:'start',version:1,generation:randomUUID(),policyVersion:7,config:f.config});assert.equal((await finished).type,'ready');assert.deepEqual(order,['snapshot','files','commit','factory','listen']);assert.equal(request.expectedPreviousSHA,acceptanceStateDigest(f.snapshot.acceptanceState));await handle.close();assert.equal(c.connected,false);
|
|
});
|
|
test('failed durable commit and modified policy cannot construct the real factory',async()=>{
|
|
for(const tamper of [false,true]){const f=fixture(),c=channel();let called=0,commits=0;if(tamper)f.config.envelope.payload+=' ';
|
|
const sent=new Promise(resolve=>c.once('sent',resolve));const handle=runServiceProcess({channel:c,store:{async snapshot(){return f.snapshot;},async envelopes(){return f.envelopes;},async commit(){commits++;throw Error('sensitive native error');}},factory:async()=>{called++;}});
|
|
c.emit('message',{type:'start',version:1,generation:randomUUID(),policyVersion:7,config:f.config});const result=await sent;assert.equal(result.type,'startup-fault');assert.equal(result.code,'broker-bootstrap-invalid');assert.equal(called,0);assert.equal(commits,tamper?0:1);await handle.close();
|
|
}
|
|
});
|