Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {createServer,request as httpRequest} from 'node:http';
|
||||
import {once} from 'node:events';
|
||||
import {mkdtemp,open,readFile,rm} from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
const require=createRequire(import.meta.url),{createBrokerAudit,BrokerAuditEventSchema}=require('../apps/network-broker/audit-gate.cjs');
|
||||
const {gateRequest}=require('../apps/network-broker/http-request-gate.cjs');
|
||||
async function fixture(t,configure={}){
|
||||
const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-broker-audit-')),journal=path.join(root,'synthetic-events.jsonl');
|
||||
const received=[];let connections=0;
|
||||
const receiver=createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.end('received');});receiver.on('connection',()=>connections++);receiver.listen(0,'127.0.0.1');await once(receiver,'listening');
|
||||
let valid=true,persistenceCalls=0;
|
||||
const durable=async event=>{
|
||||
persistenceCalls++;BrokerAuditEventSchema.parse(event);
|
||||
await configure.beforePersist?.(event);
|
||||
// Synthetic metadata only. Production requires the existing protected,
|
||||
// encrypted durable journal, not this test's plain fsync demonstration.
|
||||
const file=await open(journal,'a');try{await file.writeFile(JSON.stringify(event)+'\n');await file.sync();}finally{await file.close();}
|
||||
return {persisted:true,eventId:event.eventId};
|
||||
};
|
||||
const audit=createBrokerAudit({current:()=>valid,policyVersion:3,persist:configure.persist||durable,correlation:()=>({traceId:'a'.repeat(32),spanId:'b'.repeat(16)})});
|
||||
const destination=`http://127.0.0.1:${receiver.address().port}/upload`;
|
||||
const proxy=createServer(async(req,res)=>{
|
||||
try{
|
||||
await gateRequest(req,{authority:'policy-3',current:()=>valid,deadlineMs:1000,inspect:async({body})=>{
|
||||
const decision=body.toString().includes('PRIVATE_TEST_SECRET')?'block':'allow';
|
||||
await audit.record(decision,{bytes:body.length,detectedType:'text',host:'127.0.0.1'});return {decision};
|
||||
},forward:({body})=>new Promise((resolve,reject)=>{const outgoing=httpRequest(destination,{method:'POST',headers:{'content-length':body.length}},response=>{response.resume();response.once('end',resolve);});outgoing.once('error',reject);outgoing.end(body);})});
|
||||
res.end('allowed');
|
||||
}catch{res.writeHead(403);res.end('blocked');}
|
||||
});proxy.listen(0,'127.0.0.1');await once(proxy,'listening');
|
||||
t.after(async()=>{proxy.closeAllConnections();receiver.closeAllConnections();await Promise.all([new Promise(resolve=>proxy.close(resolve)),new Promise(resolve=>receiver.close(resolve))]);await rm(root,{force:true,recursive:true});});
|
||||
const send=body=>new Promise((resolve,reject)=>{const outgoing=httpRequest({host:'127.0.0.1',port:proxy.address().port,path:destination,method:'POST',headers:{host:new URL(destination).host,'content-length':Buffer.byteLength(body)}},response=>{response.resume();response.once('end',()=>resolve(response.statusCode));});outgoing.once('error',reject);outgoing.end(body);});
|
||||
return {send,audit,received,revoke:()=>{valid=false;},get connections(){return connections;},get persistenceCalls(){return persistenceCalls;},events:async()=>String(await readFile(journal)).trim().split('\n').map(line=>JSON.parse(line))};
|
||||
}
|
||||
test('pending durable allow audit releases no TCP connection and fsync receipt precedes release',async t=>{
|
||||
let entered,release;const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve);
|
||||
const f=await fixture(t,{beforePersist:async()=>{entered();await hold;}});
|
||||
const pending=f.send('public payload');await started;assert.equal(f.connections,0);release();assert.equal(await pending,200);
|
||||
assert.deepEqual(f.received,['public payload']);const events=await f.events();assert.equal(events.length,1);assert.equal(events[0].kind,'upload.authorized');assert.equal(events[0].policyVersion,3);assert.equal(events[0].traceId,'a'.repeat(32));
|
||||
});
|
||||
test('durable block records safe facts without original secret or request fingerprint',async t=>{
|
||||
const f=await fixture(t);assert.equal(await f.send('PRIVATE_TEST_SECRET'),403);assert.equal(f.connections,0);
|
||||
const events=await f.events();assert.equal(events[0].kind,'upload.block');assert.equal(events[0].request.bytes,19);assert.equal(events[0].request.detectedType,'text');
|
||||
const text=JSON.stringify(events);assert.equal(text.includes('PRIVATE_TEST_SECRET'),false);assert.equal(text.includes('sha256'),false);
|
||||
});
|
||||
test('audit failure and uncertain receipt block without dial or automatic retry',async t=>{
|
||||
for(const persist of [async()=>{throw Error('private persistence details');},async()=>({persisted:true,eventId:'wrong-id'})]){
|
||||
let calls=0;const f=await fixture(t,{persist:async event=>{calls++;return persist(event);}});
|
||||
assert.equal(await f.send('public'),403);assert.equal(f.connections,0);assert.equal(calls,1);
|
||||
}
|
||||
});
|
||||
test('revocation while audit waits prevents release even after durable write succeeds',async t=>{
|
||||
let entered,release;const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve);
|
||||
const f=await fixture(t,{beforePersist:async()=>{entered();await hold;}}),pending=f.send('public');await started;f.revoke();release();
|
||||
assert.equal(await pending,403);assert.equal(f.connections,0);assert.equal((await f.events()).length,1);assert.equal(f.persistenceCalls,1);
|
||||
});
|
||||
test('missing mandatory persistence and malformed security facts cannot authorize transfer',async()=>{
|
||||
const audit=createBrokerAudit({current:()=>true,policyVersion:1});
|
||||
await assert.rejects(audit.record('allow',{bytes:2,detectedType:'text'}),{code:'audit-unavailable'});
|
||||
for(const facts of [{bytes:-1,detectedType:'text'},{bytes:2,detectedType:'text',body:'private'},{bytes:2,detectedType:'text',sha256:'a'.repeat(64)},{bytes:2,detectedType:'text',host:'https://company.example/private?q=secret'},{bytes:2,detectedType:'text',reason:'broker-inspection-fault'}])await assert.rejects(audit.record('allow',facts),{code:'audit-facts-invalid'});
|
||||
});
|
||||
test('fault event uses existing schema fixed reason without error detail',async()=>{
|
||||
const events=[];const audit=createBrokerAudit({current:()=>false,policyVersion:7,persist:async event=>{events.push(BrokerAuditEventSchema.parse(event));return {persisted:true,eventId:event.eventId};}});
|
||||
await audit.record('fault',{bytes:0,detectedType:'unknown'});assert.equal(events[0].kind,'upload.block');assert.equal(events[0].ruleId,'broker-inspection-fault');
|
||||
});
|
||||
test('decision reason mismatch and accessors fail with fixed errors without reading secret getters',async()=>{
|
||||
let evaluated=false;const audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>({persisted:true,eventId:event.eventId})});
|
||||
for(const [decision,reason] of [['allow','broker-policy-block'],['block','broker-approved'],['block','broker-audit-failed'],['fault','broker-authority-expired']])await assert.rejects(audit.record(decision,{bytes:1,detectedType:'text',reason}),{code:'audit-facts-invalid'});
|
||||
await assert.rejects(audit.record('allow',{get bytes(){evaluated=true;throw Error('private secret');},detectedType:'text'}),{message:'audit-facts-invalid'});assert.equal(evaluated,false);
|
||||
});
|
||||
test('only trusted policy and fixed built-in matched IDs enter security events',async()=>{
|
||||
const events=[],ids=['company-secret'];
|
||||
const audit=createBrokerAudit({current:()=>true,policyVersion:1,allowedRuleIds:ids,persist:async event=>{events.push(event);return {persisted:true,eventId:event.eventId};}});
|
||||
ids.push('RAW PRIVATE RULE TEXT');
|
||||
await audit.record('block',{bytes:4,detectedType:'text',matchedRuleId:'company-secret'});
|
||||
await audit.record('block',{bytes:4,detectedType:'text',matchedRuleId:'api-secret'});
|
||||
assert.deepEqual(events.map(event=>event.ruleId),['company-secret','api-secret']);
|
||||
for(const [decision,id] of [['allow','company-secret'],['block','RAW PRIVATE RULE TEXT'],['fault','unknown-rule']])await assert.rejects(audit.record(decision,{bytes:4,detectedType:'text',matchedRuleId:id}),{code:'audit-facts-invalid'});
|
||||
assert.equal(events.length,2);assert.equal(JSON.stringify(events).includes('RAW PRIVATE RULE TEXT'),false);
|
||||
});
|
||||
test('canonical bare IPv6 destination is valid security metadata without an URL or port',async()=>{
|
||||
const events=[],audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>{events.push(event);return {persisted:true,eventId:event.eventId};}});
|
||||
await audit.record('allow',{bytes:1,detectedType:'text',host:'::1'});assert.equal(events[0].host,'::1');
|
||||
await assert.rejects(audit.record('allow',{bytes:1,detectedType:'text',host:'[::1]:443'}),{code:'audit-facts-invalid'});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createBrokerAuditPersistence}=require('../apps/browser/broker-audit-persistence.cjs');
|
||||
const enabled=process.platform==='win32'&&process.env.SAFETYNET_NATIVE_BROKER_AUDIT==='1';
|
||||
test('actual Windows DPAPI encrypted commit preserves broker identity before receipt',{skip:!enabled},()=>{
|
||||
const root=path.resolve('artifacts/broker-parent-audit');fs.mkdirSync(root,{recursive:true});
|
||||
const directory=fs.mkdtempSync(path.join(root,'proof-'));
|
||||
const python=path.resolve('.runtime/windows/python/python.exe'),helper=path.resolve('apps/inspector/dpapi.py');
|
||||
const convert=(action,input)=>{const result=spawnSync(python,[helper,action],{input,windowsHide:true,timeout:10000,maxBuffer:65536});assert.equal(result.status,0,'Private Windows DPAPI operation failed');return result.stdout;};
|
||||
const queue=[],file=path.join(directory,'queue.bin'),temporary=path.join(directory,'queue.tmp');let committed=false;
|
||||
const sink=createBrokerAuditPersistence({policyVersion:4,current:()=>true,getQueue:()=>queue,onFailure:()=>assert.fail('Parent audit was quarantined'),persistQueue:()=>{
|
||||
const protectedBytes=convert('protect',Buffer.from(JSON.stringify(queue)));
|
||||
const fd=fs.openSync(temporary,'wx',0o600);try{fs.writeFileSync(fd,protectedBytes);fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
|
||||
fs.renameSync(temporary,file);committed=true;
|
||||
}});
|
||||
const input={eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:4,request:{bytes:6,detectedType:'text'}};
|
||||
const receipt=sink.persist(input);assert.equal(committed,true);assert.equal(receipt.eventId,input.eventId);
|
||||
const encrypted=fs.readFileSync(file);assert.equal(encrypted.includes(Buffer.from(input.eventId)),false);
|
||||
const decrypted=convert('unprotect',encrypted);try{assert.deepEqual(JSON.parse(decrypted.toString()),[input]);}finally{decrypted.fill(0);}
|
||||
fs.writeFileSync(path.join(directory,'proof.json'),JSON.stringify({outcome:'passed',nativeProtection:'Windows current-user DPAPI',fsyncBeforeReceipt:true,identityPreserved:true,plaintextIdentityAbsent:true,productionMainWired:false},null,2)+'\n');
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createBrokerAuditPersistence}=require('../apps/browser/broker-audit-persistence.cjs');
|
||||
const event=()=>({eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:4,request:{bytes:6,detectedType:'text'}});
|
||||
function fixture(overrides={}){const queue=[];let writes=0,failures=0;const sink=createBrokerAuditPersistence({policyVersion:4,current:()=>true,getQueue:()=>queue,persistQueue:()=>{writes++;},onFailure:()=>{failures++;},...overrides});return {sink,queue,writes:()=>writes,failures:()=>failures};}
|
||||
test('receipt preserves broker event identity only after synchronous parent commit',()=>{
|
||||
const f=fixture(),input=event(),receipt=f.sink.persist(input);
|
||||
assert.deepEqual(receipt,{persisted:true,eventId:input.eventId});assert.equal(f.writes(),1);
|
||||
assert.notEqual(f.queue[0],input);assert.ok(Object.isFrozen(f.queue[0]));assert.equal(f.queue[0].eventId,input.eventId);
|
||||
});
|
||||
test('write failure is sticky and never issues a receipt or retries uncertain commit',()=>{
|
||||
let writes=0;const f=fixture({persistQueue:()=>{writes++;throw Error('private filesystem detail');}});
|
||||
assert.throws(()=>f.sink.persist(event()),/^Error: broker-audit-parent-unavailable$/);
|
||||
assert.throws(()=>f.sink.persist(event()),/broker-audit-parent-unavailable/);assert.equal(writes,1);
|
||||
});
|
||||
test('invalid policy, duplicate ID and queue capacity fail before write',()=>{
|
||||
for(const mode of ['policy','duplicate','capacity']){const f=fixture(),input=event();if(mode==='policy')input.policyVersion=3;if(mode==='duplicate')f.queue.push({...input});if(mode==='capacity')f.queue.push(...Array.from({length:10000},()=>({eventId:randomUUID()})));
|
||||
assert.throws(()=>f.sink.persist(input),/broker-audit-parent-unavailable/);assert.equal(f.writes(),0);
|
||||
}
|
||||
});
|
||||
test('revocation during commit prevents acknowledgment and asynchronous enqueue is rejected',()=>{
|
||||
let active=true;const f=fixture({current:()=>active,persistQueue:()=>{active=false;}});
|
||||
assert.throws(()=>f.sink.persist(event()),/broker-audit-parent-unavailable/);
|
||||
const asyncFixture=fixture({persistQueue:()=>Promise.resolve()});assert.throws(()=>asyncFixture.sink.persist(event()),/broker-audit-parent-unavailable/);
|
||||
for(const result of [false,true,{queued:true}]){const uncertain=fixture({persistQueue:()=>result});assert.throws(()=>uncertain.sink.persist(event()),/broker-audit-parent-unavailable/);}
|
||||
});
|
||||
test('raw extra fields and top-level getters cannot become an audit channel',()=>{
|
||||
const f=fixture();let read=false;const input=event();Object.defineProperty(input,'secret',{enumerable:true,get(){read=true;return 'private';}});
|
||||
assert.throws(()=>f.sink.persist(input),/broker-audit-parent-unavailable/);assert.equal(read,false);assert.equal(f.writes(),0);
|
||||
const raw=fixture();assert.throws(()=>raw.sink.persist({...event(),body:'private'}),/broker-audit-parent-unavailable/);assert.equal(raw.writes(),0);
|
||||
const nested=fixture(),nestedInput=event();Object.defineProperty(nestedInput.request,'bytes',{enumerable:true,get(){read=true;return 6;}});
|
||||
assert.throws(()=>nested.sink.persist(nestedInput),/broker-audit-parent-unavailable/);assert.equal(read,false);assert.equal(nested.writes(),0);
|
||||
});
|
||||
test('real download facts are immutable and linked without a generic request hash',()=>{
|
||||
const input={eventId:randomUUID(),kind:'download.block',ruleId:'synthetic-malware',policyVersion:4,file:{bytes:128,detectedType:'pdf',sha256:'a'.repeat(64)},broker:{requestId:randomUUID(),phase:'response.block',reason:'broker-download-inspection-block',authorizedEventId:randomUUID()}};
|
||||
const f=fixture();assert.equal(f.sink.persist(input).eventId,input.eventId);assert.ok(Object.isFrozen(f.queue[0].file));assert.equal(f.queue[0].request,undefined);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {fork} from 'node:child_process';
|
||||
import {once} from 'node:events';
|
||||
import https from 'node:https';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createSessionCertificates}=require('../apps/network-broker/tls-certificates.cjs');
|
||||
const enabled=process.platform==='win32'&&process.env.SAFETYNET_NATIVE_CERTIFICATE_PINS==='1';
|
||||
test('actual stock Electron accepts exact session leaf and rejects unregistered TLS leaf',{skip:!enabled,timeout:40000},async()=>{
|
||||
const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
const servers=[],root=path.resolve('artifacts/broker-certificate-pins');fs.mkdirSync(root,{recursive:true});const profile=fs.mkdtempSync(path.join(root,'profile-'));let child;
|
||||
try{
|
||||
async function server(){const initial=issuer.resolve({authority:'localhost:443'});let requests=0;
|
||||
const site=https.createServer({key:initial.key,cert:initial.cert},(_request,response)=>{requests++;response.end('synthetic public page');});servers.push(site);site.listen(0,'127.0.0.1');await once(site,'listening');
|
||||
const authority='localhost:'+site.address().port,leaf=issuer.resolve({authority});site.setSecureContext({key:leaf.key,cert:leaf.cert});
|
||||
return {url:'https://'+authority+'/',leaf:issuer.publicMetadata({authority}),requests:()=>requests};
|
||||
}
|
||||
const publicSite=await server(),untrustedSite=await server();
|
||||
const env=Object.fromEntries(['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','APPDATA','LOCALAPPDATA','PATH'].filter(key=>process.env[key]).map(key=>[key,process.env[key]]));
|
||||
child=fork(path.resolve('tests/fixtures/broker-certificate-pins-electron.cjs'),[],{execPath:require('electron'),execArgv:[],env,windowsHide:true,stdio:['ignore','ignore','ignore','ipc']});
|
||||
const result=once(child,'message',{signal:AbortSignal.timeout(20000)});child.send({type:'start',profile,leaf:publicSite.leaf,publicURL:publicSite.url,untrustedURL:untrustedSite.url});
|
||||
const [proof]=await result;assert.equal(proof.type,'proof');assert.equal(proof.publicStatus,200);assert.equal(proof.publicBody,'synthetic public page');assert.equal(proof.untrustedRejected,true);assert.equal(publicSite.requests(),1);assert.equal(untrustedSite.requests(),0);
|
||||
fs.writeFileSync(path.join(root,'proof.json'),JSON.stringify({...proof,untrustedApplicationRequests:0,productionBrowserWired:false},null,2)+'\n');
|
||||
}finally{
|
||||
if(child&&child.exitCode===null&&child.signalCode===null){
|
||||
const exited=once(child,'exit',{signal:AbortSignal.timeout(6000)}),limit=setTimeout(()=>child.kill(),3000);
|
||||
try{if(child.connected)child.send({type:'stop'},error=>{if(error)child.kill();});else child.kill();await exited;}finally{clearTimeout(limit);}
|
||||
}
|
||||
for(const site of servers){site.closeAllConnections();await new Promise(resolve=>site.close(resolve));}issuer.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createBrokerCertificatePins,authorityFacts}=require('../apps/browser/broker-certificate-pins.cjs');
|
||||
const {createSessionCertificates}=require('../apps/network-broker/tls-certificates.cjs');
|
||||
test('exact session leaf accepted; other certificates use platform trust and revocation rejects known leaf',async()=>{
|
||||
let current=true;const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
const pins=createBrokerCertificatePins({generation:'synthetic-generation',policyVersion:4,current:()=>current,authorizedAuthority:()=>true});
|
||||
try{
|
||||
const leaf=issuer.publicMetadata({authority:'example.com:443'});
|
||||
assert.equal(pins.accept({...leaf,generation:'synthetic-generation',policyVersion:4}),true);
|
||||
const verify=(hostname,data)=>{let result;pins.verify({hostname,certificate:{data,fingerprint:leaf.fingerprint256}},code=>result=code);return result;};
|
||||
assert.equal(verify('example.com',leaf.cert),0);
|
||||
assert.equal(verify('attacker.test',leaf.cert),-3);
|
||||
assert.equal(verify('example.com','unrelated-public-certificate'),-3);
|
||||
current=false;assert.equal(verify('example.com',leaf.cert),-2);
|
||||
current=true;pins.revoke();assert.equal(verify('example.com',leaf.cert),-2);
|
||||
}finally{issuer.close();}
|
||||
});
|
||||
test('wrong generation, policy, SAN, fingerprint and unauthorized destination cannot publish pins',async()=>{
|
||||
const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
try{const leaf=issuer.publicMetadata({authority:'example.com:443'}),input={...leaf,generation:'one',policyVersion:4};
|
||||
for(const patch of [{generation:'two'},{policyVersion:3},{authority:'attacker.test:443'},{fingerprint256:'wrong'},{cert:leaf.cert+'private trailing payload'},{cert:leaf.cert+leaf.cert}]){
|
||||
const pins=createBrokerCertificatePins({generation:'one',policyVersion:4,current:()=>true,authorizedAuthority:()=>true});
|
||||
assert.throws(()=>pins.accept({...input,...patch}),/broker-certificate-pin-rejected/);assert.equal(pins.status().pins,0);
|
||||
}
|
||||
const denied=createBrokerCertificatePins({generation:'one',policyVersion:4,current:()=>true,authorizedAuthority:()=>false});assert.throws(()=>denied.accept(input));
|
||||
}finally{issuer.close();}
|
||||
});
|
||||
test('noncanonical authority and pin capacity cannot weaken trust',async()=>{
|
||||
for(const value of ['127.1:443','EXAMPLE.com:443','example.com:0443','example.com:65536','example.com:[email protected]','[0:0:0:0:0:0:0:1]:443'])assert.throws(()=>authorityFacts(value));
|
||||
assert.equal(authorityFacts('[::1]:443').hostname,'::1');
|
||||
const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
const pins=createBrokerCertificatePins({generation:'one',policyVersion:4,current:()=>true,authorizedAuthority:()=>true,maxPins:1});
|
||||
try{for(const [index,authority]of ['example.com:443','other.test:443'].entries()){
|
||||
const input={...issuer.publicMetadata({authority}),generation:'one',policyVersion:4};if(index===0)assert.equal(pins.accept(input),true);else assert.throws(()=>pins.accept(input));
|
||||
}}finally{issuer.close();}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import tls from 'node:tls';
|
||||
import fs from 'node:fs';
|
||||
import {once} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
const {createConnectProxy}=createRequire(import.meta.url)('../apps/network-broker/connect-inspection-proxy.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
|
||||
async function fixture(t,config={}) {
|
||||
let connections=0,resolutions=0;
|
||||
const received=[];
|
||||
const target=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.writeHead(201,{'content-type':'text/plain'});res.end('accepted');});
|
||||
target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
|
||||
const destination=`localhost:${target.address().port}`;
|
||||
const proxy=createConnectProxy({authority:'policy-1',current:()=>true,resolveCertificate:async binding=>{resolutions++;if(binding.authority!==destination)return undefined;return {authority:destination,key,cert};},inspect:async({body})=>({decision:body.toString().includes('SECRET')?'block':'allow'}),forward:({request,body,signal})=>new Promise((resolve,reject)=>{
|
||||
const req=https.request({host:'127.0.0.1',port:target.address().port,path:new URL(request.url).pathname,method:request.method,servername:'localhost',ca:cert,rejectUnauthorized:true,signal,headers:{host:destination,'content-length':body.length}},res=>{const chunks=[];res.on('data',c=>chunks.push(c));res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));});req.on('error',reject);req.end(body);
|
||||
}),deadlineMs:1000,authMode:'bearer',...config});
|
||||
const port=await proxy.listen();
|
||||
t.after(async()=>{await proxy.close();target.closeAllConnections();await new Promise(r=>target.close(r));});
|
||||
async function send(body,{token=proxy.capability,authorization=`Bearer ${token}`,connectAuthority=destination,host=destination,servername='localhost'}={}) {
|
||||
const result=await new Promise((resolve,reject)=>{
|
||||
const req=http.request({host:'127.0.0.1',port,method:'CONNECT',path:connectAuthority,headers:{host:connectAuthority,'proxy-authorization':authorization}});
|
||||
req.setTimeout(2000,()=>req.destroy(new Error('connect-test-deadline')));req.on('error',reject);
|
||||
req.on('connect',(res,socket,head)=>{if(res.statusCode!==200){socket.destroy();resolve({status:res.statusCode,challenge:res.headers['proxy-authenticate']});return;}assert.equal(head.length,0);
|
||||
const secure=tls.connect({socket,servername,ca:cert,rejectUnauthorized:true});let text='';
|
||||
secure.setTimeout(2000,()=>secure.destroy(new Error('tls-test-deadline')));secure.on('error',reject);
|
||||
secure.on('secureConnect',()=>secure.write(`POST /submit HTTP/1.1\r\nHost: ${host}\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`));
|
||||
secure.on('data',c=>text+=c);secure.on('end',()=>{secure.destroy();resolve({status:Number(/^HTTP\/1.1 (\d+)/.exec(text)?.[1]),text});});
|
||||
});req.end();
|
||||
});return result;
|
||||
}
|
||||
return {send,received,credentials:proxy.credentials,get connections(){return connections;},get resolutions(){return resolutions;}};
|
||||
}
|
||||
test('real authenticated CONNECT wraps verified TLS; no opaque target dial for secret',async t=>{
|
||||
const f=await fixture(t);
|
||||
assert.equal((await f.send('SECRET')).status,403);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
const allowed=await f.send('public');assert.equal(allowed.status,201);assert.match(allowed.text,/accepted$/);assert.equal(f.connections,1);assert.deepEqual(f.received,['public']);
|
||||
});
|
||||
test('incorrect token prevents certificate resolution, TLS establishment and target dial',async t=>{
|
||||
const f=await fixture(t);assert.equal((await f.send('public',{token:'incorrect'})).status,407);assert.equal(f.resolutions,0);assert.equal(f.connections,0);
|
||||
});
|
||||
test('unapproved CONNECT authority, inner mismatch and absent SNI never dial target',async t=>{
|
||||
const f=await fixture(t);
|
||||
assert.equal((await f.send('public',{connectAuthority:'evil.example:443'})).status,403);
|
||||
assert.equal((await f.send('public',{host:'evil.example:443'})).status,403);
|
||||
assert.equal((await f.send('public',{servername:''})).status,403);
|
||||
assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
});
|
||||
test('shared CONNECT work budget remains consumed when timed-out inspector ignores cancellation',async t=>{
|
||||
const retained=[];
|
||||
const f=await fixture(t,{maxClients:1,deadlineMs:100,inspect:({body})=>{retained.push(body);return new Promise(()=>{});}});
|
||||
assert.equal((await f.send('public')).status,403);
|
||||
for(let index=0;index<3;index++)assert.equal((await f.send('public')).status,403);
|
||||
assert.equal(retained.length,1);assert.equal(retained[0].toString(),'public');assert.equal(f.connections,0);
|
||||
});
|
||||
test('shared CONNECT work budget retains an ignored forward promise after timeout',async t=>{
|
||||
let calls=0;
|
||||
const f=await fixture(t,{maxClients:1,deadlineMs:100,forward:()=>{calls++;return new Promise(()=>{});}});
|
||||
assert.equal((await f.send('public')).status,502);
|
||||
for(let index=0;index<3;index++)assert.equal((await f.send('public')).status,403);
|
||||
assert.equal(calls,1);assert.equal(f.connections,0);
|
||||
});
|
||||
test('timed-out certificate resolver retains shared capacity across new CONNECT sockets',async t=>{
|
||||
let calls=0,aborted=false;
|
||||
const f=await fixture(t,{maxClients:1,deadlineMs:100,resolveCertificate:(_binding,{signal})=>{calls++;signal.addEventListener('abort',()=>aborted=true,{once:true});return new Promise(()=>{});}});
|
||||
for(let index=0;index<4;index++)assert.equal((await f.send('public')).status,403);
|
||||
assert.equal(calls,1);assert.equal(aborted,true);assert.equal(f.connections,0);
|
||||
});
|
||||
test('Basic mode challenges and rejects Bearer without downgrade then accepts exact random credentials',async t=>{
|
||||
const f=await fixture(t,{authMode:'basic'});
|
||||
const refused=await f.send('public');assert.equal(refused.status,407);assert.equal(refused.challenge,'Basic realm="SafetyNet private broker", charset="UTF-8"');assert.equal(f.connections,0);assert.equal(f.resolutions,0);
|
||||
const authorization=`Basic ${Buffer.from(`${f.credentials.username}:${f.credentials.password}`).toString('base64')}`;
|
||||
assert.equal((await f.send('public',{authorization})).status,201);assert.deepEqual(f.received,['public']);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {createServer} from 'node:http';
|
||||
import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createDiagnostics}=require('../apps/browser/diagnostics.cjs');
|
||||
const {createBrokerDiagnostics}=require('../apps/network-broker/diagnostics-adapter.cjs');
|
||||
async function fixture(t){
|
||||
const payloads=[];
|
||||
const server=createServer((request,response)=>{
|
||||
const chunks=[];let bytes=0;
|
||||
request.on('data',chunk=>{bytes+=chunk.length;if(bytes>1048576){request.destroy();return;}chunks.push(chunk);});
|
||||
request.on('end',()=>{payloads.push(JSON.parse(Buffer.concat(chunks).toString()));response.setHeader('content-type','application/json');response.end('{}');});
|
||||
}).listen(0,'127.0.0.1');
|
||||
await new Promise(resolve=>server.once('listening',resolve));
|
||||
const diagnostics=createDiagnostics();
|
||||
diagnostics.configure(DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:`http://127.0.0.1:${server.address().port}`,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000}));
|
||||
t.after(async()=>{await diagnostics.shutdown();server.closeAllConnections();await new Promise(resolve=>server.close(resolve));});
|
||||
return {diagnostics,adapter:createBrokerDiagnostics(diagnostics),payloads,spans:()=>payloads.flatMap(p=>p.resourceSpans||[]).flatMap(p=>p.scopeSpans||[]).flatMap(p=>p.spans||[])};
|
||||
}
|
||||
test('broker stages export actual correlated OTLP timing without payloads or authority',async t=>{
|
||||
const f=await fixture(t),secret='PRIVATE_BROKER_SECRET',attributes={bytes:12,policyVersion:4,url:'http://private-company.test/',authority:secret,token:secret,content:secret};
|
||||
await f.adapter.wrap('request',async()=>{
|
||||
for(const stage of ['framing','buffer','inspect','authorize','forward'])assert.equal(await f.adapter.wrap(stage,async()=>17,attributes),17);
|
||||
});
|
||||
await f.diagnostics.flush();
|
||||
const spans=f.spans();assert.equal(spans.length,6);
|
||||
const operations=spans.map(span=>span.attributes.find(attribute=>attribute.key==='safetynet.operation')?.value.stringValue).sort();
|
||||
assert.deepEqual(operations,['broker.authorize','broker.buffer','broker.forward','broker.framing','broker.inspect','broker.request'].sort());
|
||||
const parent=spans.find(span=>!span.parentSpanId);assert.ok(parent);
|
||||
for(const child of spans.filter(span=>span!==parent)){
|
||||
assert.equal(child.parentSpanId,parent.spanId);assert.equal(child.traceId,parent.traceId);
|
||||
assert.ok(BigInt(child.endTimeUnixNano)>=BigInt(child.startTimeUnixNano));
|
||||
assert.ok(child.attributes.some(attribute=>attribute.key==='safetynet.duration.ms'));
|
||||
}
|
||||
const text=JSON.stringify(f.payloads);assert.equal(text.includes(secret),false);assert.equal(text.includes('private-company'),false);
|
||||
assert.equal(f.diagnostics.status().lifetimeFailedExports,0);
|
||||
});
|
||||
test('disabled diagnostics preserve the exact enforcement failure with no exports',async t=>{
|
||||
const f=await fixture(t);f.diagnostics.configure({enabled:false});
|
||||
const failure=new Error('PRIVATE_ENFORCEMENT_FAILURE');
|
||||
await assert.rejects(f.adapter.wrap('inspect',async()=>{throw failure;}),error=>error===failure);
|
||||
assert.equal(await f.adapter.wrap('forward',async()=>23),23);
|
||||
await f.diagnostics.flush();assert.equal(f.spans().length,0);
|
||||
});
|
||||
test('enabled failure tracing preserves error identity without exporting error text',async t=>{
|
||||
const f=await fixture(t),failure=new Error('PRIVATE_ERROR_DETAIL');
|
||||
await assert.rejects(f.adapter.wrap('inspect',async()=>{throw failure;}),error=>error===failure);
|
||||
await f.diagnostics.flush();assert.equal(f.spans().length,1);
|
||||
assert.equal(JSON.stringify(f.payloads).includes('PRIVATE_ERROR_DETAIL'),false);
|
||||
assert.equal(f.spans()[0].status.code,2);
|
||||
});
|
||||
test('adapter rejects unknown stages and never reads telemetry accessors',()=>{
|
||||
let touched=false,seen;
|
||||
const adapter=createBrokerDiagnostics({span:(_name,attributes,fn)=>{seen=attributes;return fn();}});
|
||||
const attributes={get bytes(){touched=true;throw Error('private');},count:2,url:'private',operation:'private'};
|
||||
assert.equal(adapter.wrap('buffer',()=>true,attributes),true);
|
||||
assert.equal(touched,false);assert.deepEqual(seen,{operation:'broker.buffer',count:2});
|
||||
assert.throws(()=>adapter.wrap('private',()=>true),{message:'broker_diagnostics_stage_invalid'});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import https from 'node:https';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {dnsAddressBlocked} from '../packages/core/security.mjs';
|
||||
const require=createRequire(import.meta.url),{createDnsPolicyRoute,assertSupportedDns}=require('../apps/network-broker/dns-policy-route.cjs');
|
||||
const policy={security:{dns:{enabled:true,blockedIPs:['127.0.0.1','::1']}}};
|
||||
test('blocked literal including mapped IPv6 is denied before any DNS lookup',async()=>{
|
||||
let calls=0;const resolve=createDnsPolicyRoute({current:()=>true,blocked:a=>dnsAddressBlocked(a,policy),lookup:async()=>{calls++;throw Error();}});
|
||||
for(const hostname of ['127.0.0.1','[::1]','::ffff:127.0.0.1'])await assert.rejects(resolve({hostname,port:443}),{message:'broker-route-denied'});assert.equal(calls,0);
|
||||
});
|
||||
test('real OS DNS blocked route produces zero TCP at controlled HTTPS receiver',async()=>{
|
||||
const cert=await readFile(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=await readFile(new URL('./fixtures/icap/key.pem',import.meta.url));let connections=0;
|
||||
const server=https.createServer({cert,key},(_q,r)=>r.end('public'));server.on('connection',()=>connections++);await new Promise(r=>server.listen(0,'::',r));const port=server.address().port;
|
||||
try{const {createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs'),resolveRoute=createDnsPolicyRoute({current:()=>true,blocked:a=>dnsAddressBlocked(a,policy)}),forward=createHttpsForwarder({authority:'localhost:'+port,current:()=>true,resolveRoute,ca:cert});await assert.rejects(forward({request:{url:`https://localhost:${port}/`,method:'GET',headers:{}},body:Buffer.alloc(0),signal:new AbortController().signal}));assert.equal(connections,0);}finally{server.closeAllConnections();await new Promise(r=>server.close(r));}
|
||||
});
|
||||
test('system/VPN DNS all-address check refuses mixed blocked records and binds approved numeric route',async()=>{
|
||||
let response=[{address:'192.0.2.1',family:4},{address:'127.0.0.1',family:4}];const resolve=createDnsPolicyRoute({current:()=>true,blocked:a=>dnsAddressBlocked(a,policy),lookup:async(host,options)=>{assert.equal(host,'intranet.example');assert.deepEqual(options,{all:true,verbatim:true});return response;}});
|
||||
await assert.rejects(resolve({hostname:'intranet.example',port:443}),{message:'broker-route-denied'});response=[{address:'192.0.2.1',family:4}];const route=await resolve({hostname:'intranet.example',port:443});response[0].address='127.0.0.1';assert.equal(route.address,'192.0.2.1');assert.ok(Object.isFrozen(route));
|
||||
});
|
||||
test('revocation during original lookup and malformed results never produce approved route',async()=>{
|
||||
let active=true;const resolve=createDnsPolicyRoute({current:()=>active,blocked:()=>false,lookup:async()=>{active=false;return [{address:'192.0.2.1'}];}});await assert.rejects(resolve({hostname:'example.test',port:443}),{message:'broker-route-denied'});
|
||||
for(const response of [[],[{address:'hostname'}],null])await assert.rejects(createDnsPolicyRoute({current:()=>true,blocked:()=>false,lookup:async()=>response})({hostname:'example.test',port:443}),{message:'broker-route-denied'});
|
||||
});
|
||||
test('secure DNS and hostname proxy with active IP block policy refuse unsupported downgrade',()=>{
|
||||
assert.throws(()=>assertSupportedDns({enterprise:{dns:{mode:'secure'}}},{mode:'direct'}),/broker-route-unsupported/);assert.throws(()=>assertSupportedDns(policy,{mode:'fixed_servers'}),/broker-route-unsupported/);assert.doesNotThrow(()=>assertSupportedDns(policy,{mode:'direct'}));assert.doesNotThrow(()=>assertSupportedDns({security:{dns:{enabled:false,blockedIPs:['127.0.0.1']}}},{mode:'fixed_servers'}));
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {defaultPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createDownloadResponseGate}=require('../apps/network-broker/download-response-gate.cjs');
|
||||
const input={request:{url:'https://intranet.example/document.pdf'},body:Buffer.alloc(0)};
|
||||
const response=(body,headers={},statusCode=200)=>({statusCode,headers,body:Buffer.from(body)});
|
||||
test('incoming confidential HTML/API is not outbound DLP; magic document requires actual scanner',async()=>{
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;let calls=0;
|
||||
const page=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response('company internal text',{'content-type':'text/html'}),inspector:{inspect:async()=>{calls++;assert.fail('incoming page not outgoingDLP');}}});
|
||||
assert.equal((await page(input)).body.toString(),'company internal text');assert.equal(calls,0);
|
||||
const document=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response('%PDF-1.4 public',{'content-type':'application/pdf'})});await assert.rejects(document(input),{code:'download-inspection-blocked'});
|
||||
});
|
||||
test('signed explicit no-malware-scanner still requires complete document parsing and preserves disposition',async()=>{
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.allowWithoutMalwareScanner=true;
|
||||
let calls=0;const gate=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response('%PDF-1.4 public',{'content-type':'application/pdf','content-disposition':'inline; filename="public.pdf"'}),inspector:{inspect:async()=>{calls++;return {decision:'allow',coverage:{complete:true}};}}});
|
||||
const value=await gate(input);assert.equal(calls,1);assert.equal(value.headers['content-disposition'],'inline; filename="public.pdf"');assert.equal(value.body.toString(),'%PDF-1.4 public');
|
||||
});
|
||||
test('malware/DLP block, incomplete coverage, scanner mutation and revocation release no response',async()=>{
|
||||
for(const inspect of [async()=>({decision:'block',ruleId:'registered'}),async()=>({decision:'allow',coverage:{complete:false}}),async bytes=>{bytes.fill(0);return {decision:'allow',coverage:{complete:true}};}]){
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.allowWithoutMalwareScanner=true;
|
||||
const gate=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response('%PDF-1.4 public'),inspector:{inspect}});await assert.rejects(gate(input),{code:'download-inspection-blocked'});
|
||||
}
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.allowWithoutMalwareScanner=true;let current=true;
|
||||
const gate=createDownloadResponseGate({auditRequired:false,policy,current:()=>current,forward:async()=>response('%PDF-1.4 public'),inspector:{inspect:async()=>{current=false;return {decision:'allow',coverage:{complete:true}};}}});await assert.rejects(gate(input),{code:'authority-expired'});
|
||||
});
|
||||
test('magic mismatch, partial206 and compression cannot bypass full download inspection',async()=>{
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.blockMimeMismatch=true;
|
||||
for(const record of [response('%PDF-1.4 public',{'content-type':'text/plain'}),response('%PDF-1.4 public',{},206),response('%PDF-1.4 public',{'content-encoding':'gzip'})]){
|
||||
const gate=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>record,inspector:{inspect:async()=>assert.fail('invalidrepresentation must not scan')}});await assert.rejects(gate(input));
|
||||
}
|
||||
});
|
||||
import {randomUUID,createHash} from 'node:crypto';
|
||||
import {createSyntheticAudit} from './fixtures/broker-durable-audit.cjs';
|
||||
const {createBrokerAudit}=require('../apps/network-broker/audit-gate.cjs'),{createBrokerLifecycle}=require('../apps/network-broker/lifecycle-audit.cjs');
|
||||
test('download verdict commits typed immutable real-file facts and registered detector before release',async t=>{
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.allowWithoutMalwareScanner=true;policy.dlp.rules=[{id:'registered-secret',label:'Synthetic',kind:'literal',value:'synthetic',action:'block'}];
|
||||
const journal=createSyntheticAudit(t),audit=createBrokerAudit({persist:journal.persist,current:()=>true,policyVersion:1,allowedRuleIds:['registered-secret']}),context=createBrokerLifecycle(audit).begin();await context.decision('allow',{bytes:0,detectedType:'text'});
|
||||
const body=Buffer.from('%PDF-1.4 encodedsynthetic'),gate=createDownloadResponseGate({policy,current:()=>true,forward:async()=>response(body),inspector:{inspect:async()=>({decision:'block',ruleId:'registered-secret',coverage:{complete:true}})}});
|
||||
await assert.rejects(gate({...input,auditContext:context}),{code:'download-inspection-blocked'});await context.reject('upstream-failed',{bytes:0,detectedType:'text'});
|
||||
const events=journal.events();assert.equal(events.length,2);assert.equal(events[1].kind,'download.block');assert.equal(events[1].ruleId,'registered-secret');assert.equal(events[1].broker.phase,'response.block');assert.equal(events[1].broker.authorizedEventId,events[0].eventId);assert.equal(events[1].file.sha256,createHash('sha256').update(body).digest('hex'));assert.equal(events[1].request,undefined);
|
||||
});
|
||||
test('pending or failed mandatory download commit cannot return document bytes',async()=>{
|
||||
const policy=defaultPolicy();policy.browser.allowDownloads=true;policy.fileTransfers.download.allowWithoutMalwareScanner=true;let release,entered;const hold=new Promise(resolve=>release=resolve),started=new Promise(resolve=>entered=resolve);let completed=false;
|
||||
const audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>{if(event.kind==='download.scan'){entered();await hold;}return {persisted:true,eventId:event.eventId};}}),context=createBrokerLifecycle(audit).begin();await context.decision('allow',{bytes:0,detectedType:'text'});
|
||||
const gate=createDownloadResponseGate({policy,current:()=>true,forward:async()=>response('%PDF-1.4 public'),inspector:{inspect:async()=>({decision:'allow',coverage:{complete:true}})}});
|
||||
const pending=gate({...input,auditContext:context}).then(()=>completed=true);await started;assert.equal(completed,false);release();await pending;assert.equal(completed,true);
|
||||
const fail=createBrokerAudit({current:()=>true,policyVersion:1,persist:async event=>{if(event.kind==='download.scan')throw Error('private journal detail');return {persisted:true,eventId:event.eventId};}}),denied=createBrokerLifecycle(fail).begin();await denied.decision('allow',{bytes:0,detectedType:'text'});await assert.rejects(gate({...input,auditContext:denied}),{code:'audit-persistence-failed'});
|
||||
const missing=createDownloadResponseGate({policy,current:()=>true,forward:async()=>assert.fail('no unaudited dial')});await assert.rejects(missing(input),{code:'audit-unavailable'});
|
||||
});
|
||||
test('download causation refuses missing authorization and unknown matched detector',async()=>{
|
||||
const audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async()=>assert.fail('invalid facts never persist')});
|
||||
for(const broker of [{requestId:randomUUID(),phase:'response.block',reason:'broker-download-inspection-block'},{requestId:randomUUID(),phase:'response.scanned',reason:'broker-download-approved',authorizedEventId:randomUUID()}])await assert.rejects(audit.recordDownload('block',{bytes:3,detectedType:'pdf',sha256:'a'.repeat(64),broker}),{code:'audit-facts-invalid'});
|
||||
});
|
||||
|
||||
test('OS save prohibition does not bypass or deny complete inline asset scans',async()=>{
|
||||
const policy=defaultPolicy();policy.fileTransfers.download.allowWithoutMalwareScanner=true;const bytes=Buffer.from('89504e470d0a1a0a','hex');let scans=0;
|
||||
const gate=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response(bytes,{'content-type':'image/png'}),inspector:{inspect:async()=>{scans++;return {decision:'allow',coverage:{complete:true}};}}});
|
||||
assert.deepEqual((await gate({...input,request:{url:'https://intranet.example/icon.png'}})).body,bytes);assert.equal(scans,1);
|
||||
const attachment=createDownloadResponseGate({auditRequired:false,policy,current:()=>true,forward:async()=>response(bytes,{'content-type':'image/png','content-disposition':'attachment; filename=icon.png'}),inspector:{inspect:async()=>assert.fail('policy denies before scan')}});await assert.rejects(attachment(input),{code:'download-policy-blocked'});
|
||||
});
|
||||
|
||||
test('late original classifier cannot commit another decision after a terminal scan timeout',async t=>{
|
||||
const journal=createSyntheticAudit(t),audit=createBrokerAudit({persist:journal.persist,current:()=>true,policyVersion:1}),context=createBrokerLifecycle(audit).begin();
|
||||
await context.reject('inspection-deadline',{bytes:6,detectedType:'text'});assert.throws(()=>context.decision('allow',{bytes:6,detectedType:'text'}),{code:'aborted'});assert.throws(()=>context.response('allow',{bytes:6,detectedType:'pdf',sha256:'a'.repeat(64)}),{code:'aborted'});
|
||||
const events=journal.events();assert.equal(events.length,1);assert.equal(events[0].broker.reason,'broker-scan-timeout');
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {fork} from 'node:child_process';
|
||||
import {once} from 'node:events';
|
||||
import fs from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import https from 'node:https';
|
||||
import {X509Certificate,generateKeyPairSync} from 'node:crypto';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createManagedProxy}=require('../apps/network-broker/managed-proxy.cjs'),{createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs'),{createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs');
|
||||
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
function bounded(promise,ms=15000){let timer;return Promise.race([promise,new Promise((_,reject)=>timer=setTimeout(()=>reject(Error('interop-deadline')),ms))]).finally(()=>clearTimeout(timer));}
|
||||
test('stock Electron authenticates Basic proxy and sends only signed-policy approved browser content',{
|
||||
skip:process.platform!=='win32'||process.env.SAFETYNET_NATIVE_BROKER_ELECTRON!=='1',timeout:60000
|
||||
},async t=>{
|
||||
assert.equal(process.execPath.toLowerCase(),fileURLToPath(new URL('../.runtime/windows/broker/node.exe',import.meta.url)).toLowerCase());
|
||||
const received=[];let leakedProxyHeader=false,connections=0;
|
||||
const site=https.createServer({key,cert},async(req,res)=>{
|
||||
leakedProxyHeader ||= !!req.headers['proxy-authorization'];
|
||||
const chunks=[];for await(const chunk of req)chunks.push(chunk);
|
||||
if(req.method==='POST'){received.push(Buffer.concat(chunks).toString());res.writeHead(201,{'content-type':'text/plain'});res.end('accepted');}
|
||||
else {res.setHeader('content-type','text/html');res.end('<!doctype html><link rel="icon" href="data:,"><h1>Local interop fixture</h1>');}
|
||||
});site.on('connection',()=>connections++);site.listen(0,'127.0.0.1');await once(site,'listening');
|
||||
const port=site.address().port,destination=`localhost:${port}`,origin=`https://${destination}`;
|
||||
t.after(async()=>{site.closeAllConnections();await new Promise(resolve=>site.close(resolve));});
|
||||
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'});
|
||||
const p=defaultPolicy();p.assignment={deviceId:'interop-device',groups:[]};p.dlp.rules=[{id:'company-secret',label:'Sensitive',kind:'literal',value:'PROJECT SECRET',action:'block'}];
|
||||
const authority=await createPolicyAuthority({publicKey,deviceId:'interop-device'}),snapshot=authority.accept(signPolicy(p,privateKey));
|
||||
const forward=createHttpsForwarder({authority:snapshot.token,current:authority.current,resolveRoute:()=>({approved:true,address:'127.0.0.1',port}),ca:cert,maxResponseBytes:16777216});
|
||||
const audit=createSyntheticAudit(t);
|
||||
const proxy=await createManagedProxy({policyAuthority:authority,persistAudit:audit.persist,resolveCertificate:binding=>binding.authority===destination?{authority:destination,key,cert}:undefined,forward});
|
||||
const proxyPort=await proxy.listen();t.after(()=>proxy.close());
|
||||
const artifactRoot=fileURLToPath(new URL('../artifacts/broker-electron-interop/',import.meta.url));fs.mkdirSync(artifactRoot,{recursive:true});
|
||||
const userData=fs.mkdtempSync(artifactRoot+'profile-');
|
||||
const cleanEnvironment=Object.fromEntries(['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','HOMEDRIVE','HOMEPATH','LOCALAPPDATA','APPDATA','PATH'].filter(name=>process.env[name]!==undefined).map(name=>[name,process.env[name]]));
|
||||
const child=fork(fileURLToPath(new URL('./fixtures/broker-electron-interop.cjs',import.meta.url)),[],{execPath:require('electron'),execArgv:[],env:cleanEnvironment,windowsHide:true,stdio:['ignore','ignore','ignore','ipc']});
|
||||
t.after(()=>{if(child.exitCode===null)child.kill();});
|
||||
const ready=bounded(once(child,'message'));child.send({type:'initialize',config:{proxyPort,origin,userData,credentials:proxy.credentials,fingerprint:new X509Certificate(cert).fingerprint256}});
|
||||
const [initialized]=await ready;assert.equal(initialized.type,'ready');assert.ok(initialized.loginCount>=1);assert.match(initialized.electron,/^44\./);
|
||||
const operation=async type=>{const result=bounded(once(child,'message'));child.send({type});const [message]=await result;assert.equal(message.type,'result');return message.result;};
|
||||
assert.deepEqual(await operation('public'),{status:201,body:'accepted'});assert.deepEqual(received,['public browser content']);
|
||||
const beforeSecret=connections;
|
||||
const blocked=await operation('secret');assert.ok(blocked.blocked||blocked.status===403);assert.deepEqual(received,['public browser content']);assert.equal(connections,beforeSecret);
|
||||
const next=structuredClone(p);next.version=2;authority.accept(signPolicy(next,privateKey));
|
||||
const revoked=await operation('revoked');assert.ok(revoked.blocked||revoked.status===403);assert.deepEqual(received,['public browser content']);assert.equal(leakedProxyHeader,false);assert.equal(connections,beforeSecret);
|
||||
const exit=bounded(once(child,'exit'));child.send({type:'stop'});await exit;
|
||||
fs.mkdirSync('artifacts/broker-electron-interop',{recursive:true});
|
||||
fs.writeFileSync(`artifacts/broker-electron-interop/proof-${new Date().toISOString().replace(/:/g,'-')}.json`,JSON.stringify({schemaVersion:1,electron:initialized.electron,chromium:initialized.chromium,brokerNode:process.version,proxyAuthentication:'Basic',loginEvents:initialized.loginCount,authorizedPosts:received.length,authorizedBytes:Buffer.byteLength(received[0]),secretPosts:0,revokedPosts:0,upstreamConnectionsBeforeSecret:beforeSecret,upstreamConnectionsAfterBlocks:connections,proxyCredentialsLeaked:leakedProxyHeader,isolatedFixture:true},null,2));
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
import {PassThrough} from 'node:stream';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
const {createFramedChannel}=createRequire(import.meta.url)('../apps/network-broker/framed-channel.cjs');
|
||||
function frame(value){const bytes=Buffer.from(typeof value==='string'?value:JSON.stringify(value)),result=Buffer.alloc(bytes.length+4);result.writeUInt32LE(bytes.length);bytes.copy(result,4);return result;}
|
||||
function fixture(options={}){
|
||||
const read=new EventEmitter(),write=new EventEmitter(),writes=[],callbacks=[];
|
||||
read.destroy=()=>{};write.destroy=()=>{};write.write=(bytes,callback)=>{writes.push(Buffer.from(bytes));callbacks.push(callback);};
|
||||
const channel=createFramedChannel({readable:read,writable:write,...options}),messages=[];let disconnected=0;
|
||||
channel.on('message',value=>messages.push(value));channel.on('disconnect',()=>disconnected++);
|
||||
return {read,write,channel,writes,callbacks,messages,disconnected:()=>disconnected};
|
||||
}
|
||||
test('fragmented headers and UTF8 plus coalesced frames preserve exact bounded objects',()=>{
|
||||
const f=fixture(),bytes=Buffer.concat([frame({text:'ä漢字'}),frame({type:'stop'})]);
|
||||
for(const byte of bytes)f.read.emit('data',Buffer.from([byte]));
|
||||
assert.deepEqual(f.messages,[{text:'ä漢字'},{type:'stop'}]);assert.equal(f.channel.connected,true);
|
||||
f.read.emit('data',Buffer.concat([frame({n:1}),frame({n:2})]));assert.equal(f.messages.length,4);
|
||||
f.channel.disconnect();assert.equal(f.disconnected(),1);
|
||||
});
|
||||
test('first bootstrap alone may exceed64KiB; later oversize rejected before allocation',()=>{
|
||||
const f=fixture({inboundFirstLimit:2097152});f.read.emit('data',frame({payload:'x'.repeat(70000)}));assert.equal(f.messages.length,1);
|
||||
const header=Buffer.alloc(4);header.writeUInt32LE(65537);f.read.emit('data',header);assert.equal(f.channel.connected,false);assert.equal(f.messages.length,1);
|
||||
for(const length of [0,2097153,0xffffffff]){const g=fixture({inboundFirstLimit:2097152}),h=Buffer.alloc(4);h.writeUInt32LE(length);g.read.emit('data',h);assert.equal(g.disconnected(),1);}
|
||||
});
|
||||
test('invalid UTF8, primitive JSON and malformed JSON disconnect without message or raw error',()=>{
|
||||
const invalid=Buffer.from([2,0,0,0,0xc0,0xaf]);
|
||||
for(const bytes of [invalid,frame('null'),frame('[]'),frame('"secret"'),frame('{broken')]){const f=fixture();f.read.emit('data',bytes);assert.equal(f.messages.length,0);assert.equal(f.disconnected(),1);}
|
||||
const f=fixture();f.read.emit('data',frame({valid:true}).subarray(0,6));f.read.emit('end');assert.equal(f.disconnected(),1);assert.equal(f.messages.length,0);
|
||||
});
|
||||
test('writes are framed with first-only bootstrap limit and at most8 unsettled writes',()=>{
|
||||
const f=fixture({outboundFirstLimit:2097152}),results=[];
|
||||
f.channel.send({payload:'x'.repeat(70000)},error=>results.push(error));assert.equal(f.writes[0].readUInt32LE(0),70014);f.callbacks.shift()();assert.equal(results[0],undefined);
|
||||
f.channel.send({payload:'x'.repeat(70000)},error=>results.push(error));assert.equal(f.disconnected(),1);assert.equal(results[1].message,'broker-channel-unavailable');assert.equal(f.writes.length,1);
|
||||
const g=fixture();for(let i=0;i<8;i++)g.channel.send({n:i});assert.equal(g.writes.length,8);let failure;
|
||||
g.channel.send({n:8},error=>{failure=error;});assert.equal(failure.message,'broker-channel-unavailable');assert.equal(g.disconnected(),1);assert.equal(g.writes.length,8);
|
||||
});
|
||||
test('stream failure yields fixed callback error and exactly one disconnect',()=>{
|
||||
const f=fixture();let failure;f.channel.send({type:'ready'},error=>{failure=error;});f.callbacks[0](Error('PRIVATE_FAILURE'));assert.equal(failure.message,'broker-channel-unavailable');
|
||||
f.read.emit('error',Error('PRIVATE_FAILURE'));f.write.emit('error',Error('PRIVATE_FAILURE'));assert.equal(f.disconnected(),1);
|
||||
});
|
||||
test('disconnect fails pending callbacks exactly once; late write success grants no success',()=>{
|
||||
const f=fixture(),results=[];f.channel.send({type:'audit'},error=>results.push(error?.message||'success'));
|
||||
f.channel.disconnect();assert.deepEqual(results,['broker-channel-unavailable']);
|
||||
f.callbacks[0]();f.callbacks[0](Error('late'));assert.deepEqual(results,['broker-channel-unavailable']);assert.equal(f.disconnected(),1);
|
||||
});
|
||||
test('throwing write callbacks cannot prevent fail-closed disconnection',()=>{
|
||||
const f=fixture();f.channel.send({type:'audit'},()=>{throw Error('private callback');});
|
||||
assert.doesNotThrow(()=>f.callbacks[0]());assert.equal(f.channel.connected,false);
|
||||
assert.doesNotThrow(()=>f.channel.send({type:'audit'},()=>{throw Error('private callback');}));assert.equal(f.disconnected(),1);
|
||||
});
|
||||
test('actual duplex framing carries production protocol and exact durable receipt',async t=>{
|
||||
const {runProductionProcess}=createRequire(import.meta.url)('../apps/network-broker/production-entry.cjs');
|
||||
const toChild=new PassThrough(),toParent=new PassThrough();
|
||||
const child=createFramedChannel({readable:toChild,writable:toParent,inboundFirstLimit:2097152});
|
||||
const parent=createFramedChannel({readable:toParent,writable:toChild,outboundFirstLimit:2097152});
|
||||
t.after(()=>parent.disconnect());
|
||||
let callbacks,closed=0;
|
||||
const processControl=runProductionProcess(async(_config,value)=>{callbacks=value;return {authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)},listen:async()=>43100,close:async()=>closed++};},child);
|
||||
const received=[];parent.on('message',message=>received.push(message));
|
||||
const context={version:1,generation:randomUUID(),policyVersion:1};
|
||||
parent.send({type:'start',...context,config:{brokerPort:43100}});
|
||||
await new Promise(resolve=>setImmediate(resolve));assert.equal(received[0].type,'ready');
|
||||
const event={eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:1,request:{bytes:3,detectedType:'text'}};
|
||||
let committed=false;const pending=callbacks.persistAudit(event).then(receipt=>{committed=true;return receipt;});
|
||||
assert.equal(committed,false);assert.equal(received[1].type,'audit');
|
||||
parent.send({type:'audit-receipt',...context,requestId:received[1].requestId,sequence:received[1].sequence,receipt:{persisted:true,eventId:event.eventId}});
|
||||
assert.deepEqual(await pending,{persisted:true,eventId:event.eventId});
|
||||
// Receipt replay is invalid even though its frame, generation and UUID match.
|
||||
parent.send({type:'audit-receipt',...context,requestId:received[1].requestId,sequence:received[1].sequence,receipt:{persisted:true,eventId:event.eventId}});
|
||||
await processControl.close();assert.equal(child.connected,false);assert.equal(closed,1);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import {once} from 'node:events';
|
||||
import {PassThrough} from 'node:stream';
|
||||
import {createRequire} from 'node:module';
|
||||
const {gateRequest}=createRequire(import.meta.url)('../apps/network-broker/http-request-gate.cjs');
|
||||
|
||||
async function fixture(t,inspect,options={}) {
|
||||
const received=[];
|
||||
let connections=0;
|
||||
const receiver=http.createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.end('ok');});
|
||||
receiver.on('connection',()=>connections++);
|
||||
receiver.listen(0,'127.0.0.1');await once(receiver,'listening');
|
||||
const target=`http://127.0.0.1:${receiver.address().port}/submit`;
|
||||
const decisions=[];
|
||||
const broker=http.createServer((req,res)=>{
|
||||
gateRequest(req,{authority:'policy-1',current:()=>true,inspect,...options,forward:({request,body})=>new Promise((resolve,reject)=>{
|
||||
const outgoing=http.request(request.url,{method:request.method,headers:{'content-length':body.length}},response=>{response.resume();response.on('end',resolve);});
|
||||
outgoing.on('error',reject);outgoing.end(body);
|
||||
})}).then(()=>{decisions.push('allow');res.end('ok');},error=>{decisions.push(error.code);res.statusCode=403;res.end(error.code);});
|
||||
});
|
||||
broker.listen(0,'127.0.0.1');await once(broker,'listening');
|
||||
t.after(async()=>{broker.closeAllConnections();receiver.closeAllConnections();await Promise.all([new Promise(r=>broker.close(r)),new Promise(r=>receiver.close(r))]);});
|
||||
const send=body=>new Promise((resolve,reject)=>{const req=http.request({host:'127.0.0.1',port:broker.address().port,path:target,method:'POST',headers:{host:new URL(target).host,'content-length':Buffer.byteLength(body)}},res=>{res.resume();res.on('end',()=>resolve(res.statusCode));});req.on('error',reject);req.end(body);});
|
||||
return {received,decisions,send,target,broker,get connections(){return connections;}};
|
||||
}
|
||||
|
||||
async function rawRequest(t,broker,payload) {
|
||||
const socket=net.connect(broker.address().port,'127.0.0.1');
|
||||
t.after(()=>socket.destroy());
|
||||
return await new Promise((resolve,reject)=>{
|
||||
let text='',finished=false;
|
||||
const timer=setTimeout(()=>finish(new Error('raw-request-deadline')),2000);
|
||||
const finish=error=>{if(finished)return;finished=true;clearTimeout(timer);socket.destroy();if(error)reject(error);else resolve(text);};
|
||||
socket.on('connect',()=>socket.end(payload));
|
||||
socket.on('data',chunk=>text+=chunk);
|
||||
socket.on('error',finish);
|
||||
socket.on('end',()=>finish());
|
||||
socket.on('close',()=>finish());
|
||||
});
|
||||
}
|
||||
|
||||
test('controlled HTTP receiver sees benign original only; inspector mutation cannot alter release',async t=>{
|
||||
const f=await fixture(t,async({body})=>{const secret=body.toString().includes('SECRET');body.fill(120);return {decision:secret?'block':'allow'};});
|
||||
assert.equal(await f.send('public text'),200);assert.deepEqual(f.received,['public text']);
|
||||
assert.equal(await f.send('SECRET'),403);assert.deepEqual(f.received,['public text']);
|
||||
});
|
||||
test('no upstream bytes or dial before delayed allow',async t=>{
|
||||
let entered,release;const waiting=new Promise(r=>entered=r),hold=new Promise(r=>release=r);
|
||||
const f=await fixture(t,async()=>{entered();await hold;return {decision:'allow'};});
|
||||
const request=f.send('public');await waiting;assert.deepEqual(f.received,[]);assert.equal(f.connections,0);release();assert.equal(await request,200);assert.equal(f.connections,1);
|
||||
});
|
||||
test('policy revoked while inspection waits prevents release',async t=>{
|
||||
let valid=true;
|
||||
const f=await fixture(t,async()=>{valid=false;return {decision:'allow'};},{current:()=>valid});
|
||||
assert.equal(await f.send('public'),403);assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['authority-expired']);
|
||||
});
|
||||
test('bounded body and inspection deadline fail closed',async t=>{
|
||||
const f=await fixture(t,()=>new Promise(()=>{}),{maxBytes:8,deadlineMs:30});
|
||||
assert.equal(await f.send('too long for this bound'),403);assert.equal(await f.send('short'),403);
|
||||
assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['body-too-large','deadline']);
|
||||
});
|
||||
test('abort during inspection prevents release',async t=>{
|
||||
const controller=new AbortController();
|
||||
const f=await fixture(t,async()=>{controller.abort();return {decision:'allow'};},{signal:controller.signal});
|
||||
assert.equal(await f.send('public'),403);assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['aborted']);
|
||||
});
|
||||
test('actual duplicate Host is denied, no upstream request',async t=>{
|
||||
const f=await fixture(t,async()=>({decision:'allow'}));
|
||||
const result=await rawRequest(t,f.broker,`POST ${f.target} HTTP/1.1\r\nHost: ${new URL(f.target).host}\r\nHost: attacker.invalid\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`);
|
||||
assert.match(result,/403/);assert.deepEqual(f.received,[]);assert.equal(f.connections,0);assert.deepEqual(f.decisions,['duplicate-header']);
|
||||
});
|
||||
test('physical chunked and CL+TE requests never inspect or dial',async t=>{
|
||||
let inspected=0;
|
||||
const f=await fixture(t,async()=>{inspected++;return {decision:'allow'};});
|
||||
for(const length of ['', 'Content-Length: 6\r\n']) {
|
||||
const result=await rawRequest(t,f.broker,`POST ${f.target} HTTP/1.1\r\nHost: ${new URL(f.target).host}\r\n${length}Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n6\r\nSECRET\r\n0\r\n\r\n`);
|
||||
assert.match(result,/HTTP\/1\.1 (400|403)/);
|
||||
}
|
||||
assert.equal(inspected,0);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
});
|
||||
test('ambiguous framing and unsupported transports rejected before inspection',async()=>{
|
||||
for(const [headers,method,url] of [
|
||||
[['Host','example.test','Content-Length','1','Content-Length','1'],'POST','http://example.test/'],
|
||||
[['Host','example.test','Content-Length','1','Transfer-Encoding','chunked'],'POST','http://example.test/'],
|
||||
[['Host','example.test','Upgrade','websocket'],'GET','http://example.test/'],
|
||||
[['Host','example.test'],'CONNECT','example.test:443'],
|
||||
[['Host','example.test','Content-Encoding','gzip'],'POST','http://example.test/'],
|
||||
[['Host','evil.test'],'GET','http://example.test/']
|
||||
]) {
|
||||
const req=new PassThrough();Object.assign(req,{rawHeaders:headers,method,url,complete:true});
|
||||
await assert.rejects(gateRequest(req,{authority:'p',current:()=>true,inspect:()=>assert.fail('inspection'),forward:()=>assert.fail('forward')}));
|
||||
}
|
||||
});
|
||||
test('truncated body and stream error return fixed errors without release',async()=>{
|
||||
for(const kind of ['end','error','aborted']) {
|
||||
const req=new PassThrough();Object.assign(req,{rawHeaders:['Host','example.test','Content-Length','3'],method:'POST',url:'http://example.test/',complete:false});
|
||||
const promise=gateRequest(req,{authority:'p',current:()=>true,inspect:()=>assert.fail('inspection'),forward:()=>assert.fail('forward')});
|
||||
if(kind==='end')req.end('x');else req.emit(kind, new Error('confidential error message'));
|
||||
await assert.rejects(promise,error=>['incomplete-body','request-error','aborted'].includes(error.code)&&!error.message.includes('confidential'));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const {validateResponse}=createRequire(import.meta.url)('../apps/network-broker/http-response-gate.cjs');
|
||||
test('private response preserves status/body and separate cookies; strips hop by hop',()=>{
|
||||
const body=Buffer.from('accepted'),cookies=['a=1; Secure','b=2; HttpOnly'];
|
||||
const result=validateResponse({statusCode:201,headers:{'Content-Type':'text/plain','Set-Cookie':cookies,Connection:'keep-alive','Transfer-Encoding':'chunked','Content-Length':'999','Proxy-Authenticate':'private'},body});
|
||||
body.fill(0);cookies[0]='changed';
|
||||
assert.equal(result.body.toString(),'accepted');assert.equal(result.headers['content-length'],'8');assert.deepEqual(result.headers['set-cookie'],['a=1; Secure','b=2; HttpOnly']);assert.equal(result.headers.connection,'close');assert.equal(result.headers['transfer-encoding'],undefined);assert.equal(result.headers['proxy-authenticate'],undefined);
|
||||
});
|
||||
test('invalid status, injected headers, overflow and unsupported encoding fail closed',()=>{
|
||||
const valid={statusCode:200,headers:{},body:Buffer.from('public')};
|
||||
for(const change of [{statusCode:101},{statusCode:999},{statusCode:204},{headers:{'set-cookie':['a=1\r\nInjected: yes']}},{headers:{'Content-Type':'text/plain','content-type':'other'}},{headers:{'content-encoding':'gzip'}},{headers:{location:['one','two']}}])assert.throws(()=>validateResponse({...valid,...change}));
|
||||
assert.throws(()=>validateResponse(valid,3));
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import https from 'node:https';
|
||||
import fs from 'node:fs';
|
||||
import {once} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
async function fixture(t,handler,configuration={}){
|
||||
const received=[];let connections=0;
|
||||
const server=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push({body:Buffer.concat(chunks).toString(),headers:req.headers,path:req.url});handler?.(req,res)||(!handler&&res.end('public response'));});
|
||||
server.on('connection',()=>connections++);server.on('tlsClientError',()=>{});
|
||||
server.listen(0,'127.0.0.1');await once(server,'listening');
|
||||
t.after(async()=>{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));});
|
||||
const port=server.address().port;
|
||||
const forward=createHttpsForwarder({authority:'policy-1',current:()=>true,resolveRoute:async destination=>({approved:true,address:'127.0.0.1',port:destination.port}),ca:cert,deadlineMs:1000,...configuration});
|
||||
const request={method:'POST',url:`https://localhost:${port}/submit`,headers:{'content-type':'text/plain'}};
|
||||
return {request,forward,received,get connections(){return connections;}};
|
||||
}
|
||||
test('numeric approved route uses original TLS hostname and strips proxy credentials',async t=>{
|
||||
let release,entered;const wait=new Promise(resolve=>release=resolve),started=new Promise(resolve=>entered=resolve);
|
||||
const f=await fixture(t,undefined,{resolveRoute:async destination=>{entered();await wait;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
||||
const body=Buffer.from('public original');
|
||||
f.request.headers['proxy-authorization']='Bearer PRIVATE_PROXY_KEY';f.request.headers.connection='x-hop';f.request.headers['x-hop']='PRIVATE_HOP_VALUE';
|
||||
const pending=f.forward({request:f.request,body});await started;body.fill(120);release();
|
||||
const result=await pending;assert.equal(result.body.toString(),'public response');
|
||||
assert.equal(f.received[0].body,'public original');assert.equal(f.received[0].headers['proxy-authorization'],undefined);assert.equal(f.received[0].headers['x-hop'],undefined);
|
||||
assert.equal(f.connections,1);
|
||||
});
|
||||
test('revocation during route resolution prevents any upstream TCP dial',async t=>{
|
||||
let valid=true;const f=await fixture(t,undefined,{current:()=>valid,resolveRoute:async destination=>{valid=false;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.from('public')}),{code:'authority-expired'});
|
||||
assert.equal(f.connections,0);
|
||||
});
|
||||
test('unapproved and nonnumeric routes cannot invoke system DNS or dial',async t=>{
|
||||
const f=await fixture(t,undefined,{resolveRoute:async destination=>({approved:true,address:'localhost',port:destination.port})});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'route-unapproved'});assert.equal(f.connections,0);
|
||||
});
|
||||
test('revocation after TLS establishment prevents HTTP headers and payload release',async t=>{
|
||||
let checks=0;const f=await fixture(t,undefined,{current:()=>++checks<4});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.from('must never transmit')}),{code:'authority-expired'});
|
||||
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
||||
});
|
||||
test('untrusted server certificate is rejected with fixed error and no retry',async t=>{
|
||||
const f=await fixture(t,undefined,{ca:undefined});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.from('public')}),{code:'upstream-failed'});
|
||||
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
||||
});
|
||||
test('approved numeric route does not substitute certificate hostname',async t=>{
|
||||
const f=await fixture(t);
|
||||
await assert.rejects(f.forward({request:{...f.request,url:f.request.url.replace('localhost','wrong-host.example')},body:Buffer.alloc(0)}),{code:'upstream-failed'});
|
||||
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
||||
});
|
||||
test('oversized streamed response is destroyed at configured limit',async t=>{
|
||||
const f=await fixture(t,(_req,res)=>{res.write('1234');res.end('56789');},{maxResponseBytes:8});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-response-too-large'});
|
||||
assert.equal(f.connections,1);
|
||||
});
|
||||
test('redirect response is returned without following location',async t=>{
|
||||
const f=await fixture(t,(_req,res)=>{res.writeHead(302,{location:'/private'});res.end('redirect');});
|
||||
const result=await f.forward({request:f.request,body:Buffer.alloc(0)});
|
||||
assert.equal(result.statusCode,302);assert.equal(result.headers.location,'/private');assert.equal(f.received.length,1);assert.equal(f.received[0].path,'/submit');
|
||||
});
|
||||
test('abort during route resolution prevents dial and does not retry',async t=>{
|
||||
let entered,release;const started=new Promise(resolve=>entered=resolve),wait=new Promise(resolve=>release=resolve);
|
||||
const f=await fixture(t,undefined,{resolveRoute:async destination=>{entered();await wait;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
||||
const controller=new AbortController(),pending=f.forward({request:f.request,body:Buffer.alloc(0),signal:controller.signal});
|
||||
await started;controller.abort();await assert.rejects(pending,{code:'upstream-cancelled'});release();assert.equal(f.connections,0);
|
||||
});
|
||||
test('wall clock timeout terminates a stalled upstream connection',async t=>{
|
||||
const f=await fixture(t,()=>{}, {deadlineMs:30});
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
||||
assert.ok(f.connections<=1);
|
||||
});
|
||||
test('timed out unresolved routes retain bounded underlying resolution admission',async t=>{
|
||||
let calls=0;const pending=[];
|
||||
const f=await fixture(t,undefined,{deadlineMs:15,maxPendingResolutions:2,resolveRoute:destination=>{
|
||||
calls++;return new Promise(resolve=>pending.push(()=>resolve({approved:true,address:'127.0.0.1',port:destination.port})));
|
||||
}});
|
||||
for(let index=0;index<2;index++)await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
||||
for(let index=0;index<3;index++)await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'route-capacity-exhausted'});
|
||||
assert.equal(calls,2);assert.equal(f.connections,0);
|
||||
pending[0]();await new Promise(resolve=>setImmediate(resolve));
|
||||
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
||||
assert.equal(calls,3);assert.equal(f.connections,0);
|
||||
for(const release of pending)release();
|
||||
});
|
||||
test('invalid destinations and methods reject before resolver invocation',async t=>{
|
||||
let resolutions=0;const f=await fixture(t,undefined,{resolveRoute:async()=>{resolutions++;}});
|
||||
for(const change of [{url:'http://localhost/'},{url:'https://user:password@localhost/'},{url:'https://localhost/#fragment'},{url:'https://127.1/'},{method:'CONNECT'}])await assert.rejects(f.forward({request:{...f.request,...change},body:Buffer.alloc(0)}),{code:'invalid-forward-request'});
|
||||
assert.equal(resolutions,0);assert.equal(f.connections,0);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import https from 'node:https';
|
||||
import fs from 'node:fs';
|
||||
import {X509Certificate} from 'node:crypto';
|
||||
import {once} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {bindConnectAuthority}=require('../apps/network-broker/tls-authority.cjs');
|
||||
const {createInspectionServer}=require('../apps/network-broker/https-inspection-server.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url));
|
||||
const key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
|
||||
async function fixture(t,inspect,current=()=>true,customForward,config={}) {
|
||||
const received=[];let connections=0;
|
||||
const receiver=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.writeHead(201,{'content-type':'text/plain','set-cookie':['session=synthetic; Secure; HttpOnly','locale=en; Secure']});res.end('accepted');});
|
||||
receiver.on('connection',()=>connections++);
|
||||
receiver.listen(0,'127.0.0.1');await once(receiver,'listening');
|
||||
const destination=`localhost:${receiver.address().port}`;
|
||||
const broker=createInspectionServer({key,cert,binding:bindConnectAuthority(destination),authority:'signed-policy-1',current,inspect,deadlineMs:1000,...config,forward:customForward||(({request,body,signal})=>new Promise((resolve,reject)=>{
|
||||
assert.equal(new URL(request.url).protocol,'https:');
|
||||
const outgoing=https.request({host:'127.0.0.1',port:receiver.address().port,path:new URL(request.url).pathname,method:request.method,servername:'localhost',ca:cert,rejectUnauthorized:true,signal,headers:{host:destination,'content-length':body.length}},res=>{const chunks=[];res.on('data',chunk=>chunks.push(chunk));res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));});outgoing.on('error',reject);outgoing.end(body);
|
||||
}))});
|
||||
const port=await broker.listen();
|
||||
t.after(async()=>{await broker.close();receiver.closeAllConnections();await new Promise(r=>receiver.close(r));});
|
||||
const send=(body,{host=destination,servername='localhost',signal}={})=>new Promise((resolve,reject)=>{
|
||||
const req=https.request({host:'127.0.0.1',port,method:'POST',path:'/submit',servername,ca:cert,rejectUnauthorized:true,agent:false,signal,headers:{host,'content-length':Buffer.byteLength(body)}},res=>{let result='';res.on('data',c=>result+=c);res.on('end',()=>resolve({status:res.statusCode,result}));});req.setTimeout(2000,()=>req.destroy(new Error('test-deadline')));req.on('error',reject);req.end(body);
|
||||
});
|
||||
return {send,received,destination,get connections(){return connections;}};
|
||||
}
|
||||
|
||||
test('synthetic TLS certificate explicitly covers localhost and loopback',()=>{
|
||||
const certificate=new X509Certificate(cert);
|
||||
assert.equal(certificate.checkHost('localhost'),'localhost');assert.equal(certificate.checkIP('127.0.0.1'),'127.0.0.1');
|
||||
});
|
||||
test('verified TLS delivers benign original and blocks secret before upstream TCP dial',async t=>{
|
||||
const f=await fixture(t,async({body,request})=>{assert.equal(new URL(request.url).protocol,'https:');const decision=body.toString().includes('SECRET')?'block':'allow';body.fill(120);return {decision};});
|
||||
assert.equal((await f.send('SECRET')).status,403);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
assert.deepEqual(await f.send('public'),{status:201,result:'accepted'});assert.equal(f.connections,1);assert.deepEqual(f.received,['public']);
|
||||
});
|
||||
test('HTTPS inspection delay releases no TCP connection until complete approval',async t=>{
|
||||
let entered,release;const waiting=new Promise(r=>entered=r),hold=new Promise(r=>release=r);
|
||||
const f=await fixture(t,async()=>{entered();await hold;return {decision:'allow'};});
|
||||
const pending=f.send('public');await waiting;assert.equal(f.connections,0);release();assert.equal((await pending).status,201);assert.equal(f.connections,1);
|
||||
});
|
||||
test('revocation during HTTPS inspection blocks without dialing',async t=>{
|
||||
let valid=true;
|
||||
const f=await fixture(t,async()=>{valid=false;return {decision:'allow'};},()=>valid);
|
||||
assert.deepEqual(await f.send('public'),{status:403,result:'authority-expired'});assert.equal(f.connections,0);
|
||||
});
|
||||
test('inner Host mismatch and missing DNS SNI reject before inspection or dial',async t=>{
|
||||
let inspections=0;
|
||||
const f=await fixture(t,async()=>{inspections++;return {decision:'allow'};});
|
||||
assert.deepEqual(await f.send('public',{host:'evil.example:443'}),{status:403,result:'authority-mismatch'});
|
||||
assert.deepEqual(await f.send('public',{servername:''}),{status:403,result:'missing-sni'});
|
||||
assert.equal(inspections,0);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
});
|
||||
test('upstream failure returns fixed 502 without retry or private errors',async t=>{
|
||||
let attempts=0;
|
||||
const f=await fixture(t,async()=>({decision:'allow'}),()=>true,async()=>{attempts++;throw new Error('private upstream credentials');});
|
||||
assert.deepEqual(await f.send('public'),{status:502,result:'upstream-failed'});assert.equal(attempts,1);assert.equal(f.connections,0);
|
||||
});
|
||||
test('client disconnect aborts in-flight forward once without retry',async t=>{
|
||||
let entered,aborted,attempts=0;
|
||||
const started=new Promise(r=>entered=r),cancelled=new Promise(r=>aborted=r);
|
||||
const f=await fixture(t,async()=>({decision:'allow'}),()=>true,({signal})=>new Promise((_,reject)=>{attempts++;signal.addEventListener('abort',()=>{aborted();reject(new Error('cancelled'));},{once:true});entered();}));
|
||||
const controller=new AbortController();
|
||||
const pending=f.send('public',{signal:controller.signal});const rejection=assert.rejects(pending);
|
||||
await started;controller.abort();await rejection;
|
||||
await Promise.race([cancelled,new Promise((_,reject)=>{const timer=setTimeout(()=>reject(new Error('abort-deadline')),1000);timer.unref();})]);
|
||||
assert.equal(attempts,1);
|
||||
});
|
||||
test('forwarding wall clock deadline cancels a continuously active upstream once',async t=>{
|
||||
let attempts=0,aborts=0;
|
||||
const f=await fixture(t,async()=>({decision:'allow'}),()=>true,({signal})=>new Promise((_,reject)=>{
|
||||
attempts++;
|
||||
// No completion, just work continuing independently of socket idle.
|
||||
const activity=setInterval(()=>{},2);
|
||||
signal.addEventListener('abort',()=>{aborts++;clearInterval(activity);reject(new Error('private deadline detail'));},{once:true});
|
||||
}),{deadlineMs:100});
|
||||
const result=await f.send('public');
|
||||
assert.deepEqual(result,{status:502,result:'upstream-failed'});assert.equal(attempts,1);assert.equal(aborts,1);
|
||||
});
|
||||
test('response bounds reject invalid construction immediately',()=>{
|
||||
for(const maxResponseBytes of [-1,1.5,16777217])assert.throws(()=>createInspectionServer({key,cert,authority:'p',current:()=>true,inspect:async()=>({decision:'allow'}),forward:async()=>{},maxResponseBytes}),{code:'invalid-configuration'});
|
||||
});
|
||||
test('standalone HTTPS job budget survives timed-out requests on new sockets',async t=>{
|
||||
let calls=0;
|
||||
const f=await fixture(t,()=>{calls++;return new Promise(()=>{});},()=>true,undefined,{maxClients:1,deadlineMs:100});
|
||||
for(let index=0;index<4;index++)assert.equal((await f.send('public')).status,403);
|
||||
assert.equal(calls,1);assert.equal(f.connections,0);
|
||||
});
|
||||
|
||||
test('one absolute deadline spans inspection and forwarding without resetting the budget',async t=>{
|
||||
let inspectEntered,releaseInspect,forwardEntered,releaseForward;const inspected=new Promise(resolve=>inspectEntered=resolve),first=new Promise(resolve=>releaseInspect=resolve),forwarded=new Promise(resolve=>forwardEntered=resolve),second=new Promise(resolve=>releaseForward=resolve);let signal;
|
||||
const f=await fixture(t,async()=>{inspectEntered();await first;return {decision:'allow'};},()=>true,async value=>{signal=value.signal;forwardEntered();await second;return {statusCode:201,headers:{},body:Buffer.from('must not release')};},{deadlineMs:100});
|
||||
const pending=f.send('public');await inspected;await new Promise(resolve=>setTimeout(resolve,55));releaseInspect();await forwarded;
|
||||
const result=await pending;assert.equal(result.status,502);assert.equal(result.result,'upstream-failed');assert.equal(signal.aborted,true);releaseForward();assert.equal(f.connections,0);
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {generateKeyPairSync} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {once} from 'node:events';
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import tls from 'node:tls';
|
||||
import {defaultPolicy,signPolicy,inspect} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createManagedProxy}=require('../apps/network-broker/managed-proxy.cjs'),{createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs');
|
||||
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'});
|
||||
async function fixture(t,configure=()=>{},scanner,enterpriseInspector,options={}) {
|
||||
const received=[];let connections=0;
|
||||
const target=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const c of req)chunks.push(c);received.push(Buffer.concat(chunks).toString());res.writeHead(201);res.end('accepted');});target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
|
||||
const destination=`localhost:${target.address().port}`,p=defaultPolicy();p.assignment={deviceId:'device-1',groups:[]};p.dlp.rules=[{id:'company-secret',label:'Confidential',kind:'literal',value:'PROJECT SECRET',action:'block'}];configure(p);
|
||||
const policyAuthority=await createPolicyAuthority({publicKey,deviceId:'device-1',minimumVersion:1});policyAuthority.accept(signPolicy(p,privateKey));
|
||||
const audit=createSyntheticAudit(t);
|
||||
const proxy=await createManagedProxy({policyAuthority,persistAudit:audit.persist,localInspector:scanner,enterpriseInspector,resolveCertificate:binding=>binding.authority===destination?{authority:destination,key,cert}:undefined,forward:({body,signal,maxResponseBytes})=>new Promise((resolve,reject)=>{
|
||||
const request=https.request({host:'127.0.0.1',port:target.address().port,method:'POST',servername:'localhost',ca:cert,rejectUnauthorized:true,signal,headers:{'content-length':body.length}},res=>{const chunks=[];let bytes=0;res.on('data',chunk=>{bytes+=chunk.length;if(bytes>maxResponseBytes){request.destroy();reject(Error('bound'));}else chunks.push(chunk);});res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));});request.on('error',reject);request.end(body);
|
||||
}),deadlineMs:1000,authMode:'bearer',...options});const port=await proxy.listen();
|
||||
t.after(async()=>{await proxy.close();target.closeAllConnections();await new Promise(r=>target.close(r));});
|
||||
const send=(body,{path='/submit',headers={}}={})=>new Promise((resolve,reject)=>{
|
||||
const req=http.request({host:'127.0.0.1',port,method:'CONNECT',path:destination,headers:{host:destination,'proxy-authorization':`Bearer ${proxy.capability}`}});req.setTimeout(2000,()=>req.destroy(Error('deadline')));req.on('error',reject);req.on('connect',(response,socket)=>{if(response.statusCode!==200){socket.destroy();resolve(response.statusCode);return;}const secure=tls.connect({socket,servername:'localhost',ca:cert,rejectUnauthorized:true});let text='';secure.setTimeout(2000,()=>secure.destroy(Error('deadline')));secure.on('error',reject);secure.on('secureConnect',()=>{const extra=Object.entries(headers).map(([name,value])=>`${name}: ${value}\r\n`).join('');secure.write(`POST ${path} HTTP/1.1\r\nHost: ${destination}\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${extra}Connection: close\r\n\r\n`);secure.write(body);});secure.on('data',chunk=>text+=chunk);secure.on('end',()=>{secure.destroy();resolve(Number(/^HTTP\/1.1 (\d+)/.exec(text)?.[1]));});});req.end();
|
||||
});return {send,received,policyAuthority,p,audit,get connections(){return connections;}};
|
||||
}
|
||||
test('signed company literal and credentials block before dial; public actualTLS delivery',async t=>{
|
||||
const f=await fixture(t);
|
||||
assert.equal(await f.send('PROJECT SECRET'),403);assert.equal(await f.send('sk-abcdefghijklmnopqrstuvwxyz123456'),403);assert.equal(f.connections,0);
|
||||
assert.equal(await f.send('public document'),201);assert.deepEqual(f.received,['public document']);
|
||||
});
|
||||
test('signed semantic or YARA requirements cannot omit local scanner',async t=>{
|
||||
const vector=Array(768).fill(0);vector[0]=1;
|
||||
for(const feature of ['semantic','yara']) {
|
||||
const f=await fixture(t,p=>{if(feature==='semantic'){p.dlp.semantic.enabled=true;p.dlp.semantic.revision='a'.repeat(40);p.dlp.semantic.categories=[{id:'confidential',label:'Confidential',threshold:.9,prototype:vector}];}else p.security.yara.enabled=true;});
|
||||
assert.equal(await f.send('public'),403);assert.equal(f.connections,0);
|
||||
}
|
||||
});
|
||||
test('binary upload requires complete local coverage and magic-byte policy applies first',async t=>{
|
||||
const f=await fixture(t);assert.equal(await f.send(Buffer.from([0,1,2,255])),403);assert.equal(f.connections,0);
|
||||
const blocked=await fixture(t,p=>p.fileTransfers.upload.blockedTypes=['pe'],async()=>assert.fail('scanner must not run'));assert.equal(await blocked.send(Buffer.from('MZPUBLIC')),403);assert.equal(blocked.connections,0);
|
||||
});
|
||||
test('genuine signed policy update during local scan invalidates release',async t=>{
|
||||
let entered,release;const started=new Promise(r=>entered=r),hold=new Promise(r=>release=r);
|
||||
const f=await fixture(t,p=>p.security.yara.enabled=true,async()=>{entered();await hold;return {decision:'allow',coverage:{complete:true}};});
|
||||
const pending=f.send('public');await started;const next=structuredClone(f.p);next.version=2;f.policyAuthority.accept(signPolicy(next,privateKey));release();assert.equal(await pending,403);assert.equal(f.connections,0);
|
||||
});
|
||||
test('wrong device and tampered signature cannot authorize listener creation',async()=>{
|
||||
const authority=await createPolicyAuthority({publicKey,deviceId:'device-1'}),p=defaultPolicy();p.assignment={deviceId:'device-2',groups:[]};
|
||||
assert.throws(()=>authority.accept(signPolicy(p,privateKey)));await assert.rejects(createManagedProxy({policyAuthority:authority}),{code:'managed-authority-invalid'});
|
||||
const envelope=signPolicy(defaultPolicy(),privateKey);envelope.signature='AAAA';assert.throws(()=>authority.accept(envelope));await assert.rejects(createManagedProxy({policyAuthority:authority}),{code:'managed-authority-invalid'});
|
||||
});
|
||||
test('mandatory ICAP cannot omit scanner or accept incomplete unbound evidence',async t=>{
|
||||
const configure=p=>{p.enterprise.icap.enabled=true;p.enterprise.icap.url='icaps://scanner.example/scan';};
|
||||
const missing=await fixture(t,configure);assert.equal(await missing.send('public'),403);assert.equal(missing.connections,0);
|
||||
const incomplete=await fixture(t,configure,undefined,async()=>({decision:'allow',coverage:{complete:true},sha256:'0'.repeat(64),bytes:6}));assert.equal(await incomplete.send('public'),403);assert.equal(incomplete.connections,0);
|
||||
});
|
||||
test('partial local coverage and scanner mutation fail closed; destination blacklist applies',async t=>{
|
||||
for(const inspector of [async()=>({decision:'allow',coverage:{complete:false}}),async bytes=>{bytes.fill(0);return {decision:'allow',coverage:{complete:true}};}]) {
|
||||
const f=await fixture(t,p=>p.security.yara.enabled=true,inspector);assert.equal(await f.send('public'),403);assert.equal(f.connections,0);
|
||||
}
|
||||
const f=await fixture(t,p=>p.browser.blockedHosts=['localhost']);assert.equal(await f.send('public'),403);assert.equal(f.connections,0);
|
||||
});
|
||||
test('decoded text representations receive independent local scans before release',async t=>{
|
||||
const seen=[];
|
||||
const f=await fixture(t,p=>p.security.yara.enabled=true,async bytes=>{
|
||||
const text=bytes.toString();seen.push(text);return {decision:text==='hidden private meaning'?'block':'allow',coverage:{complete:true}};
|
||||
});
|
||||
assert.equal(await f.send('hidden%20private%20meaning'),403);assert.ok(seen.includes('hidden%20private%20meaning'));assert.ok(seen.includes('hidden private meaning'));assert.equal(f.connections,0);
|
||||
});
|
||||
test('ASCII PDF magic requires local parsed coverage even when raw UTF8 inspection allows',async t=>{
|
||||
const document=Buffer.from('%PDF-1.4\n1 0 obj\n<< /Filter /ASCIIHexDecode >>\nstream\n50524F4A45435420534543524554>\nendstream\nendobj\n%%EOF');
|
||||
const missing=await fixture(t);assert.equal(inspect(document,missing.p).decision,'allow');assert.equal(await missing.send(document),403);assert.equal(missing.connections,0);
|
||||
let scanned=0;
|
||||
const rejected=await fixture(t,()=>{},async bytes=>{scanned++;assert.deepEqual(bytes,document);return {decision:'block',coverage:{complete:true}};});assert.equal(await rejected.send(document),403);assert.equal(scanned,1);assert.equal(rejected.connections,0);
|
||||
// Controlled scanner contract proof, not evidence of real PDF parsing.
|
||||
const publicDocument=Buffer.from('%PDF-1.4\npublic test document\n%%EOF');
|
||||
const allowed=await fixture(t,()=>{},async bytes=>{assert.deepEqual(bytes,publicDocument);return {decision:'allow',coverage:{complete:true}};});assert.equal(await allowed.send(publicDocument),201);assert.deepEqual(allowed.received,[publicDocument.toString()]);
|
||||
});
|
||||
test('custom header and Cookie corporate secrets block despite benign body',async t=>{
|
||||
const f=await fixture(t);
|
||||
assert.equal(await f.send('public',{headers:{'X-Note':'PROJECT SECRET'}}),403);
|
||||
assert.equal(await f.send('public',{headers:{Cookie:'note=PROJECT SECRET'}}),403);
|
||||
assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
});
|
||||
test('signed semantic query requires independent decoded URL inspection',async t=>{
|
||||
const seen=[];const vector=Array(768).fill(0);vector[0]=1;
|
||||
const f=await fixture(t,p=>{p.dlp.semantic.enabled=true;p.dlp.semantic.revision='a'.repeat(40);p.dlp.semantic.categories=[{id:'sensitive',label:'Sensitive',threshold:.9,prototype:vector}];},async bytes=>{const value=bytes.toString();seen.push(value);return {decision:value.includes('semantic query')?'block':'allow',coverage:{complete:true}};});
|
||||
assert.equal(await f.send('public',{path:'/submit?note=semantic%20query'}),403);assert.ok(seen.some(value=>value.includes('semantic query')));assert.equal(f.connections,0);
|
||||
});
|
||||
test('multipart files receive independent magic policy and complete parsed document gate',async t=>{
|
||||
const multipart=document=>Buffer.from(`--test-boundary\r\nContent-Disposition: form-data; name="note"\r\n\r\npublic text\r\n--test-boundary\r\nContent-Disposition: form-data; name="document"; filename="report.pdf"\r\nContent-Type: application/pdf\r\n\r\n${document}\r\n--test-boundary--\r\n`);
|
||||
const headers={'Content-Type':'multipart/form-data; boundary=test-boundary'};
|
||||
const document='%PDF-1.4\npublic sample\n%%EOF',body=multipart(document);
|
||||
const blocked=await fixture(t,p=>p.fileTransfers.upload.blockedTypes=['pdf']);assert.equal(await blocked.send(body,{headers}),403);assert.equal(blocked.connections,0);
|
||||
const missing=await fixture(t);assert.equal(await missing.send(body,{headers}),403);assert.equal(missing.connections,0);
|
||||
let parsed=0;
|
||||
const allowed=await fixture(t,()=>{},async bytes=>{parsed++;assert.equal(bytes.toString(),document);return {decision:'allow',coverage:{complete:true}};});assert.equal(await allowed.send(body,{headers}),201);assert.equal(parsed,1);assert.deepEqual(allowed.received,[body.toString()]);
|
||||
const rejected=await fixture(t,()=>{},async()=>({decision:'block',coverage:{complete:true}}));assert.equal(await rejected.send(multipart('%PDF-1.4\n50524F4A45435420534543524554>\n%%EOF'),{headers}),403);assert.equal(rejected.connections,0);
|
||||
});
|
||||
test('malformed multipart body cannot be released as ordinary printable text',async t=>{
|
||||
const f=await fixture(t);assert.equal(await f.send('--boundary\r\ntruncated',{headers:{'Content-Type':'multipart/form-data; boundary=boundary'}}),403);assert.equal(f.connections,0);
|
||||
});
|
||||
test('durable allow receipt is required before any upstream dial',async t=>{
|
||||
let entered,release,f;
|
||||
const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve);
|
||||
f=await fixture(t,()=>{},undefined,undefined,{persistAudit:async event=>{entered();await hold;return f.audit.persist(event);}});
|
||||
const pending=f.send('public');await started;assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
release();assert.equal(await pending,201);assert.equal(f.audit.events().length,1);assert.equal(f.audit.events()[0].kind,'upload.authorized');
|
||||
});
|
||||
test('failed audit persistence blocks without replay and never logs protected input',async t=>{
|
||||
let calls=0;
|
||||
const f=await fixture(t,()=>{},undefined,undefined,{persistAudit:async()=>{calls++;throw Error('PRIVATE_AUDIT_FAILURE_CONTENT');}});
|
||||
assert.equal(await f.send('public'),403);assert.equal(calls,1);assert.equal(f.connections,0);
|
||||
const blocked=await fixture(t);assert.equal(await blocked.send('PROJECT SECRET'),403);
|
||||
const events=blocked.audit.events();assert.equal(events.length,1);assert.equal(events[0].kind,'upload.block');assert.equal(events[0].ruleId,'company-secret');assert.equal(events[0].request.detectedType,'text');assert.equal(events[0].request.bytes,14);assert.ok(!JSON.stringify(events).includes('PROJECT SECRET'));assert.ok(!JSON.stringify(events).includes('sha256'));
|
||||
});
|
||||
test('signed update during durable commit blocks release after receipt, without retry',async t=>{
|
||||
let entered,release,f,calls=0;
|
||||
const started=new Promise(resolve=>entered=resolve),hold=new Promise(resolve=>release=resolve);
|
||||
f=await fixture(t,()=>{},undefined,undefined,{persistAudit:async event=>{calls++;entered();await hold;return f.audit.persist(event);}});
|
||||
const pending=f.send('public');await started;const next=structuredClone(f.p);next.version=2;f.policyAuthority.accept(signPolicy(next,privateKey));release();assert.equal(await pending,403);assert.equal(f.connections,0);assert.equal(calls,2);
|
||||
const events=f.audit.events();assert.equal(events.length,2);assert.equal(events[1].broker.authorizedEventId,events[0].eventId);assert.equal(events[1].broker.requestId,events[0].broker.requestId);assert.notEqual(events[0].eventId,events[1].eventId);assert.equal(events[1].broker.reason,'broker-authority-expired');
|
||||
});
|
||||
test('nonsettling audit consumes shared capacity across timed-out CONNECT requests',async t=>{
|
||||
let calls=0;
|
||||
const f=await fixture(t,()=>{},undefined,undefined,{maxClients:1,deadlineMs:100,persistAudit:()=>{calls++;return new Promise(()=>{});}});
|
||||
for(let index=0;index<4;index++)assert.equal(await f.send('public'),403);
|
||||
assert.equal(calls,1);assert.equal(f.connections,0);
|
||||
});
|
||||
test('valid policy cannot create a managed listener without mandatory audit adapter',async()=>{
|
||||
const authority=await createPolicyAuthority({publicKey,deviceId:'device-1'});authority.accept(signPolicy(defaultPolicy(),privateKey));
|
||||
await assert.rejects(createManagedProxy({policyAuthority:authority,resolveCertificate:()=>{},forward:()=>{}}),{code:'managed-audit-unavailable'});
|
||||
});
|
||||
test('genuine local block retains signed detector ID without allowing incomplete coverage',async t=>{
|
||||
const document=Buffer.from('%PDF-1.4\npublic encoded fixture\n%%EOF');
|
||||
const f=await fixture(t,()=>{},async()=>({decision:'block',ruleId:'company-secret',coverage:{complete:false},privateDetails:'NEVER_SERIALIZE'}));
|
||||
assert.equal(await f.send(document),403);assert.equal(f.connections,0);
|
||||
const event=f.audit.events()[0];assert.equal(event.ruleId,'company-secret');assert.equal(event.kind,'upload.block');assert.ok(!JSON.stringify(event).includes('NEVER_SERIALIZE'));
|
||||
const unknown=await fixture(t,()=>{},async()=>({decision:'block',ruleId:'ARBITRARY_PRIVATE_RULE_TEXT',coverage:{complete:true}}));assert.equal(await unknown.send(document),403);assert.equal(unknown.audit.events()[0].ruleId,'inspection-incomplete');
|
||||
const mutated=await fixture(t,()=>{},async bytes=>{bytes.fill(0);return {decision:'block',ruleId:'company-secret'};});assert.equal(await mutated.send(document),403);assert.equal(mutated.audit.events()[0].ruleId,'inspection-incomplete');
|
||||
});
|
||||
|
||||
test('lifecycle upstream failure has one causal durable terminal event without retry',async t=>{
|
||||
let attempts=0;const f=await fixture(t,undefined,undefined,undefined,{forward:async()=>{attempts++;throw Error('private upstream details');}});
|
||||
assert.equal(await f.send('public'),502);assert.equal(attempts,1);assert.equal(f.connections,0);
|
||||
const events=f.audit.events();assert.equal(events.length,2);assert.equal(events[0].broker.phase,'request.authorized');assert.equal(events[1].broker.phase,'forward.fault');assert.equal(events[1].broker.reason,'broker-upstream-failed');assert.equal(events[1].broker.requestId,events[0].broker.requestId);assert.equal(events[1].broker.authorizedEventId,events[0].eventId);assert.equal(JSON.stringify(events).includes('private upstream'),false);
|
||||
});
|
||||
test('lifecycle CONNECT destination denial and ambiguous framing record before any target TCP',async t=>{
|
||||
const denied=await fixture(t,p=>p.browser.blockedHosts=['localhost']);assert.equal(await denied.send('public'),403);assert.equal(denied.connections,0);const event=denied.audit.events()[0];assert.equal(event.broker.phase,'connect.block');assert.equal(event.broker.reason,'broker-connect-denied');
|
||||
const ambiguous=await fixture(t);await ambiguous.send('public',{headers:{'Transfer-Encoding':'chunked'}}).catch(()=>{});assert.equal(ambiguous.connections,0);const records=ambiguous.audit.events();assert.equal(records.length,1);assert.equal(records[0].broker.reason,'broker-framing-block');assert.equal(records[0].broker.phase,'request.block');
|
||||
});
|
||||
|
||||
test('encoded document body/header/URL carriers hit actual signed fingerprint before target TCP',async t=>{
|
||||
const {createHash}=await import('node:crypto'),document=Buffer.from('%PDF-1.4 Registered document'),fingerprint=createHash('sha256').update(document).digest('hex');
|
||||
const f=await fixture(t,p=>p.dlp.rules=[{id:'encoded-fingerprint',label:'Synthetic',kind:'sha256',value:fingerprint,action:'block'}],async()=>assert.fail('registered fingerprint must block before local parser'));
|
||||
const encoded=document.toString('base64'),uri='data:application/pdf;base64,'+encoded;
|
||||
for(const body of [JSON.stringify({file:encoded}),JSON.stringify({file:encodeURIComponent(uri)}),JSON.stringify({file:Buffer.from(JSON.stringify({file:encoded})).toString('base64')})])assert.equal(await f.send(body,{headers:{'Content-Type':'application/json'}}),403);
|
||||
assert.equal(await f.send('public',{headers:{'X-Document':uri}}),403);assert.equal(await f.send('public',{path:'/upload?file='+encodeURIComponent(uri)}),403);assert.equal(f.connections,0);assert.equal(f.audit.events().length,5);assert.ok(f.audit.events().every(event=>event.ruleId==='encoded-fingerprint'));
|
||||
});
|
||||
test('public encoded document must receive full local coverage while original outer JSON remains exact',async t=>{
|
||||
const document=Buffer.from('%PDF-1.4 Public registered form'),seen=[];
|
||||
const f=await fixture(t,undefined,async bytes=>{seen.push(Buffer.from(bytes));return {decision:'allow',coverage:{complete:true}};});
|
||||
const outer=JSON.stringify({file:document.toString('base64')});assert.equal(await f.send(outer,{headers:{'Content-Type':'application/json'}}),201);assert.deepEqual(f.received,[outer]);assert.ok(seen.some(bytes=>bytes.equals(document)));
|
||||
});
|
||||
test('encoded vector SVG and nested carriers beyond the global depth budget are unscannable',async t=>{
|
||||
const f=await fixture(t,undefined,async()=>assert.fail('unsupported SVG must not be counted as plain text coverage'));
|
||||
assert.equal(await f.send(JSON.stringify({file:'data:image/svg+xml;base64,'+Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0"/></svg>').toString('base64')}),{headers:{'Content-Type':'application/json'}}),403);
|
||||
let value='data:application/pdf;base64,'+Buffer.from('%PDF-1.4 public').toString('base64');for(let count=0;count<6;count++)value='data:application/json;base64,'+Buffer.from(JSON.stringify({file:value})).toString('base64');
|
||||
assert.equal(await f.send(JSON.stringify({file:value}),{headers:{'Content-Type':'application/json'}}),403);assert.equal(f.connections,0);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createNativeManagementCredentialLoader}=require('../apps/network-broker/management-credential.cjs');
|
||||
const current={deviceId:'device',origin:'https://management.example',policyVersion:8,policyKeySha256:'a'.repeat(64),policyDigest:'b'.repeat(64)};
|
||||
// Synthetic stdio fixture exercises the real JS consumer only. Native selector,
|
||||
// registry and DPAPI execution are separate gates; this is not their proof.
|
||||
function client(stale=false){return createNativeManagementCredentialLoader({expectedBinding:current,current:()=>true,launch:()=>spawn(process.execPath,['-e',`let bytes=Buffer.alloc(0);process.stdin.on('data',part=>bytes=Buffer.concat([bytes,part]));process.stdin.on('end',()=>{const request=JSON.parse(bytes.subarray(4));const reply={...request,token:'synthetic_token_0123456789'};${stale?"reply.policyVersion=7;reply.policyDigest='c'.repeat(64);":""}const body=Buffer.from(JSON.stringify(reply)),header=Buffer.alloc(4);header.writeUInt32LE(body.length);process.stdout.write(Buffer.concat([header,body]));});`],{windowsHide:true,stdio:['pipe','pipe','ignore']})});}
|
||||
test('current request echo accepts a reused private credential without claiming stored old metadata is current',async()=>{
|
||||
const previous=process.env.SAFETYNET_SERVICE_CLIENT_SID;process.env.SAFETYNET_SERVICE_CLIENT_SID='S-1-5-21-1-2-3-1001';
|
||||
const loader=client();try{const value=await loader.load(current);assert.equal(value.token.toString(),'synthetic_token_0123456789');assert.equal(value.deviceId,current.deviceId);value.token.fill(0);}finally{await loader.close();if(previous===undefined)delete process.env.SAFETYNET_SERVICE_CLIENT_SID;else process.env.SAFETYNET_SERVICE_CLIENT_SID=previous;}
|
||||
});
|
||||
test('echoing the credential record older policy binding instead of current request is rejected',async()=>{
|
||||
const previous=process.env.SAFETYNET_SERVICE_CLIENT_SID;process.env.SAFETYNET_SERVICE_CLIENT_SID='S-1-5-21-1-2-3-1001';
|
||||
const loader=client(true);try{await assert.rejects(loader.load(current),{message:'broker-management-refused'});}finally{await loader.close();if(previous===undefined)delete process.env.SAFETYNET_SERVICE_CLIENT_SID;else process.env.SAFETYNET_SERVICE_CLIENT_SID=previous;}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{createNativeManagementCredentialLoader}=require('../apps/network-broker/management-credential.cjs');
|
||||
const binding={deviceId:'device',origin:'https://management.example',policyVersion:7,policyKeySha256:'a'.repeat(64),policyDigest:'b'.repeat(64)};
|
||||
const emit="let bytes=Buffer.alloc(0);process.stdin.on('data',part=>{bytes=Buffer.concat([bytes,part]);});process.stdin.on('end',()=>{const input=JSON.parse(bytes.subarray(4).toString());const value={...input,token:require('node:crypto').randomBytes(32).toString('base64url')};RESPONSE;});";
|
||||
const response="const b=Buffer.from(JSON.stringify(value)),h=Buffer.alloc(4);h.writeUInt32LE(b.length);process.stdout.write(Buffer.concat([h,b]));";
|
||||
function loader(code,options={}){return createNativeManagementCredentialLoader({expectedBinding:binding,current:()=>true,deadlineMs:1000,...options,launch:(file,args,config)=>{assert.ok(file.endsWith('safetynet-service-host.exe'));assert.deepEqual(args,['--management-credential']);assert.deepEqual(Object.keys(config.env).sort(),['SAFETYNET_SERVICE_CLIENT_SID','SystemRoot','WINDIR']);assert.equal(config.shell,false);return spawn(process.execPath,['-e',code],{stdio:['pipe','pipe','ignore'],env:config.env});}});}
|
||||
test.before(()=>{process.env.SAFETYNET_SERVICE_CLIENT_SID='S-1-12-1-1-2-3-4';});
|
||||
test.after(()=>{delete process.env.SAFETYNET_SERVICE_CLIENT_SID;});
|
||||
test('fixed native-helper contract yields private credential only after exact bound reply and close',async()=>{const client=loader(emit.replace('RESPONSE',response+'setTimeout(()=>{},100);'));let finished=false;const work=client.load(binding);work.then(()=>{finished=true;});await new Promise(r=>setTimeout(r,50));assert.equal(finished,false);const value=await work;assert.equal(value.deviceId,binding.deviceId);assert.ok(Buffer.isBuffer(value.token));assert.equal(value.token.length,43);value.token.fill(0);client.close();});
|
||||
test('wrong policy/device/origin or unsafe native reply cannot yield credential',async()=>{const client=loader(emit.replace('RESPONSE',"value.policyDigest='c'.repeat(64);"+response));await assert.rejects(client.load(binding),{message:'broker-management-refused'});for(const key of ['deviceId','origin','policyVersion','policyDigest','policyKeySha256'])await assert.rejects(client.load({...binding,[key]:key==='policyVersion'?8:'other'}),{message:'broker-management-refused'});client.close();});
|
||||
test('nonzero or duplicate native reply is refused without retries',async()=>{for(const suffix of ['process.exitCode=5;','process.stdout.write(Buffer.from([2,0,0,0,123,125]));']){const client=loader(emit.replace('RESPONSE',response+suffix));await assert.rejects(client.load(binding),{message:'broker-management-refused'});client.close();}});
|
||||
test('signal/revocation and native timeout wait actual owned helper termination',async()=>{const client=loader('setInterval(()=>{},1000);',{deadlineMs:40});await assert.rejects(client.load(binding),{message:'broker-management-refused'});client.close();let active=true;const revoked=loader(emit.replace('RESPONSE',response+'setTimeout(()=>{},150);'),{current:()=>active});const work=revoked.load(binding);active=false;await assert.rejects(work,{message:'broker-management-refused'});revoked.close();});
|
||||
test('actual Windows LocalMachine DPAPI synthetic roundtrip keeps plaintext out of files', {skip:process.platform!=='win32'},async()=>{
|
||||
const script=`$ErrorActionPreference='Stop'
|
||||
Add-Type -AssemblyName System.Security
|
||||
$stream=[Console]::OpenStandardInput()
|
||||
$header=New-Object byte[] 4
|
||||
$offset=0;while($offset -lt 4){$n=$stream.Read($header,$offset,4-$offset);if($n -eq 0){throw 'invalid'};$offset+=$n}
|
||||
$length=[BitConverter]::ToUInt32($header,0);if($length -gt 4096){throw 'invalid'}
|
||||
$bytes=New-Object byte[] $length;$offset=0;while($offset -lt $length){$n=$stream.Read($bytes,$offset,$length-$offset);if($n -eq 0){throw 'invalid'};$offset+=$n}
|
||||
$request=[Text.Encoding]::UTF8.GetString($bytes)|ConvertFrom-Json
|
||||
$entropy=[Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
|
||||
$random=New-Object byte[] 32;[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($random)
|
||||
$token=[Convert]::ToBase64String($random).TrimEnd('=').Replace('+','-').Replace('/','_')
|
||||
$plain=[Text.Encoding]::UTF8.GetBytes($token)
|
||||
$file=[IO.Path]::GetTempFileName()
|
||||
try{
|
||||
$encrypted=[Security.Cryptography.ProtectedData]::Protect($plain,$entropy,[Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
[IO.File]::WriteAllBytes($file,$encrypted)
|
||||
$decoded=[Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($file),$entropy,[Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
if([Convert]::ToBase64String($plain) -ne [Convert]::ToBase64String($decoded)){throw 'invalid'}
|
||||
$value=[ordered]@{deviceId=$request.deviceId;origin=$request.origin;policyVersion=$request.policyVersion;policyKeySha256=$request.policyKeySha256;policyDigest=$request.policyDigest;token=[Text.Encoding]::UTF8.GetString($decoded)}
|
||||
$out=[Text.Encoding]::UTF8.GetBytes(($value|ConvertTo-Json -Compress));$stdout=[Console]::OpenStandardOutput();$h=[BitConverter]::GetBytes([uint32]$out.Length);$stdout.Write($h,0,4);$stdout.Write($out,0,$out.Length);$stdout.Flush()
|
||||
}finally{[Array]::Clear($plain,0,$plain.Length);if($decoded){[Array]::Clear($decoded,0,$decoded.Length)};[Array]::Clear($random,0,$random.Length);[IO.File]::Delete($file)}
|
||||
`;
|
||||
const client=createNativeManagementCredentialLoader({expectedBinding:binding,current:()=>true,launch:()=>spawn('powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['pipe','pipe','ignore']})});const value=await client.load(binding);assert.equal(value.token.length,43);value.token.fill(0);client.close();
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import https from 'node:https';
|
||||
import {once} from 'node:events';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {generateKeyPairSync} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {defaultPolicy,signPolicy,verifyPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createManagementIdentityOperation}=require('../apps/network-broker/management-identity-operation.cjs');
|
||||
const channel=require('../packages/core/policy-channel.cjs'),{verifyServiceAcceptance}=require('../apps/network-broker/service-acceptance.cjs');
|
||||
const {signManifest,verifyReleaseEnvelope,verifyDeploymentEnvelope}=require('../packages/core/windows-trust.cjs');
|
||||
const pub=k=>k.publicKey.export({type:'spki',format:'pem'}),priv=k=>k.privateKey.export({type:'pkcs8',format:'pem'});
|
||||
async function fixture(t){
|
||||
const cert=await readFile(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=await readFile(new URL('./fixtures/icap/key.pem',import.meta.url)),ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87'),vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'});let requests=0,tcp=0;
|
||||
let result=channel.identity(ed.privateKey,pq.privateKey,pub(pq)),status=200,handler;
|
||||
const server=https.createServer({key,cert},async(req,res)=>{requests++;assert.equal(req.headers.authorization,'Bearer synthetic-protected-token');if(handler){await handler(req,res);return;}assert.equal(req.method,'GET');assert.equal(req.url,'/api/device/channel/identity');res.writeHead(status,{'content-type':'application/json'});res.end(JSON.stringify(result));});server.on('connection',()=>tcp++);server.listen(0,'::');await once(server,'listening');t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));});const origin=`https://localhost:${server.address().port}`,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-v2',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:1,policyPublicKey:pub(ed),expiresAt:Date.now()+60000,users:[{sid,deviceId:'device',minimumPolicyVersion:7}],management:{origin,signatureKey:pub(pq),protocol:channel.VERSION}},deploymentEnvelope=signManifest(deployment,priv(customer),'deployment'),d=verifyDeploymentEnvelope(deploymentEnvelope,{release:r,authenticatedSid:sid});
|
||||
const policy=defaultPolicy();policy.version=7;policy.assignment={deviceId:'device',groups:[]};policy.proxy={mode:'direct'};const policyEnvelope=signPolicy(policy,priv(ed));
|
||||
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:[]}},files={releaseEnvelope,deploymentEnvelope};const plan=await verifyServiceAcceptance({...files,releaseAnchor:snapshot.releaseAnchor,authenticatedSid:sid,state:snapshot.acceptanceState,policyEnvelope});snapshot.acceptanceState=plan.nextState;
|
||||
const store={async snapshot(){return snapshot;},async envelopes(){return files;}};const config={store,policyEnvelope,network:{mode:'direct',dns:'system',ca:cert.toString()},current:()=>true,loadCredential:async binding=>{assert.equal(binding.deviceId,'device');assert.equal(binding.origin,origin);return {deviceId:'device',origin,token:'synthetic-protected-token'};}};
|
||||
return {config,origin,pq,ed,snapshot,get requests(){return requests;},get tcp(){return tcp;},set result(v){result=v;},set status(v){status=v;},set handler(v){handler=v;}};
|
||||
}
|
||||
test('actual HTTPS identity verifies genuine Ed25519 plus deployment-pinned MLDSA87',async t=>{const f=await fixture(t),op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());const value=await op.identity();assert.equal(value.signatureKey,pub(f.pq));assert.equal(f.requests,1);await assert.rejects(op.identity({url:'https://attacker.example',token:'override'}),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||||
test('protected credentials missing or mismatched never start network',async t=>{let storeCalls=0;await assert.rejects(createManagementIdentityOperation({store:{snapshot(){storeCalls++;}}}),{message:'broker-management-refused'});assert.equal(storeCalls,0);const f=await fixture(t),op=await createManagementIdentityOperation({...f.config,loadCredential:async()=>({deviceId:'other',origin:f.origin,token:'synthetic-protected-token'})});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.tcp,0);});
|
||||
test('identity tampering and redirect fail without retry',async t=>{const f=await fixture(t),op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());f.result={version:channel.VERSION,signatureKey:pub(f.pq),edSignature:'a',pqSignature:'b'};await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,1);f.status=302;await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,2);});
|
||||
test('timed-out protected credential retains admission until original settlement',async t=>{const f=await fixture(t);let settle,calls=0;const pending=new Promise(r=>{settle=r;}),op=await createManagementIdentityOperation({...f.config,deadlineMs:20,loadCredential:()=>{calls++;return pending;}});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(calls,1);assert.equal(f.tcp,0);let closed=false;const closing=op.close().then(()=>{closed=true;});await new Promise(r=>setTimeout(r,10));assert.equal(closed,false);const lateToken=Buffer.from('synthetic-protected-token');settle({deviceId:'device',origin:f.origin,token:lateToken});await closing;assert.ok(lateToken.every(v=>v===0));assert.equal(f.tcp,0);});
|
||||
test('validly dual-signed identity with another PQ key is refused by deployed pin',async t=>{const f=await fixture(t),other=generateKeyPairSync('ml-dsa-87');f.result=channel.identity(f.ed.privateKey,other.privateKey,pub(other));const op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||||
test('pre-enrollment floors and revocation during credential load cannot dial',async t=>{const f=await fixture(t),saved=f.snapshot.acceptanceState;f.snapshot.acceptanceState={...saved,policies:[]};await assert.rejects(createManagementIdentityOperation(f.config),{message:'broker-management-refused'});f.snapshot.acceptanceState=saved;let active=true;const op=await createManagementIdentityOperation({...f.config,current:()=>active,loadCredential:async()=>{active=false;return {deviceId:'device',origin:f.origin,token:'synthetic-protected-token'};}});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.tcp,0);});
|
||||
test('private protected token lease is erased after actual HTTPS use',async t=>{const f=await fixture(t),token=Buffer.from('synthetic-protected-token');const op=await createManagementIdentityOperation({...f.config,loadCredential:async binding=>{assert.match(binding.policyDigest,/^[a-f0-9]{64}$/);return {deviceId:'device',origin:f.origin,token};}});t.after(()=>op.close());await op.identity();assert.ok(token.every(byte=>byte===0));assert.equal(f.requests,1);});
|
||||
function policyHandler(f,{version=8,deviceId='device',challengeDevice='device',tamper=false,onChallenge=()=>{}}={}){
|
||||
let pair,used=false;return async(req,res)=>{
|
||||
assert.equal(req.method,'POST');const chunks=[];for await(const part of req)chunks.push(part);const body=JSON.parse(Buffer.concat(chunks).toString());
|
||||
if(req.url==='/api/device/channel/challenge'){assert.deepEqual(body,{});pair=channel.challenge(challengeDevice,f.ed.privateKey,f.pq.privateKey);onChallenge();res.end(JSON.stringify(pair.bundle));return;}
|
||||
assert.equal(req.url,'/api/device/channel/policy');assert.equal(used,false);used=true;const context=channel.receive(body,pair.bundle,pair.privateKey);try{const p=defaultPolicy();p.version=version;p.assignment={deviceId,groups:[]};p.proxy={mode:'direct'};const envelope=signPolicy(p,priv(f.ed)),response=channel.response(envelope,pair.bundle,context);if(tamper)response.encrypted.tag='a'.repeat(22);res.end(JSON.stringify(response));}finally{context.responseKey.fill(0);}
|
||||
};
|
||||
}
|
||||
test('actual paired MLKEM1024/MLDSA87 HTTPS policy operation verifies new device-bound policy',async t=>{const f=await fixture(t);f.handler=policyHandler(f);const op=await createManagementIdentityOperation({...f.config,loadCredential:undefined,credentialLoader:{load:f.config.loadCredential}});t.after(()=>op.close());const result=await op.policy();assert.equal(verifyPolicy(result,pub(f.ed),7).version,8);assert.equal(f.requests,2);assert.equal(f.snapshot.acceptanceState.policies[0].minimumVersion,7);await assert.rejects(op.policy({url:'https://other.example'}),{message:'broker-management-refused'});assert.equal(f.requests,2);});
|
||||
test('policy channel wrong device, rollback, same-version equivocation and encrypted tampering fail',async t=>{for(const options of [{challengeDevice:'other'},{deviceId:'other'},{version:6},{version:7},{tamper:true}]){const f=await fixture(t);f.handler=policyHandler(f,options);const op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.requests,options.challengeDevice?1:2);}});
|
||||
test('revocation after signed challenge prevents policy POST and uses one total job budget',async t=>{const f=await fixture(t);let active=true;f.handler=policyHandler(f,{onChallenge:()=>{active=false;}});const op=await createManagementIdentityOperation({...f.config,current:()=>active});t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||||
test('challenge and policy share one deadline rather than renewing per request',async t=>{const f=await fixture(t),handle=policyHandler(f);f.handler=async(req,res)=>{const end=res.end.bind(res);res.end=(...args)=>{setTimeout(()=>end(...args),180);return res;};await handle(req,res);};const token=Buffer.from('synthetic-protected-token'),op=await createManagementIdentityOperation({...f.config,deadlineMs:300,loadCredential:async()=>({deviceId:'device',origin:f.origin,token})});t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});await new Promise(r=>setTimeout(r,250));assert.equal(f.requests,2);assert.ok(token.every(v=>v===0));});
|
||||
test('default fixed native loader refuses unavailable authenticated service context before network',async t=>{const f=await fixture(t),previous=process.env.SAFETYNET_SERVICE_CLIENT_SID;delete process.env.SAFETYNET_SERVICE_CLIENT_SID;try{const op=await createManagementIdentityOperation({...f.config,loadCredential:undefined});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.tcp,0);}finally{if(previous!==undefined)process.env.SAFETYNET_SERVICE_CLIENT_SID=previous;}});
|
||||
@@ -0,0 +1,50 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{parseMultipart}=require('../apps/network-broker/multipart.cjs');
|
||||
const boundary='----WebKitFormBoundarySynthetic42',type=`multipart/form-data; boundary=${boundary}`;
|
||||
function encode(parts,ending=true){
|
||||
const output=[];
|
||||
for(const part of parts){output.push(Buffer.from(`--${boundary}\r\n${part.headers}\r\n\r\n`));output.push(Buffer.from(part.body));output.push(Buffer.from('\r\n'));}
|
||||
if(ending)output.push(Buffer.from(`--${boundary}--\r\n`));return Buffer.concat(output);
|
||||
}
|
||||
const text={headers:'Content-Disposition: form-data; name="note"',body:'public note'};
|
||||
const pdf={headers:'Content-Disposition: form-data; name="files"; filename="report.pdf"\r\nContent-Type: application/pdf',body:Buffer.from('%PDF-1.4\n\0\xffbinary','latin1')};
|
||||
const zip={headers:'Content-Disposition: form-data; name="files"; filename="archive.zip"\r\nContent-Type: application/zip',body:Buffer.from([0x50,0x4b,3,4,0,255,1])};
|
||||
test('Chromium form fields and PDF ZIP parts are preserved completely as independent bytes',()=>{
|
||||
const body=encode([text,pdf,zip]),parsed=parseMultipart(body,type);
|
||||
assert.equal(parsed.coverage.complete,true);assert.equal(parsed.parts.length,3);
|
||||
assert.equal(parsed.parts[0].body.toString(),'public note');
|
||||
assert.deepEqual(parsed.parts[1].body,pdf.body);assert.deepEqual(parsed.parts[2].body,zip.body);
|
||||
assert.equal(parsed.parts[1].filename,'report.pdf');assert.equal(parsed.parts[1].contentType,'application/pdf');
|
||||
body.fill(0);assert.deepEqual(parsed.parts[1].body,pdf.body);
|
||||
parsed.dispose();assert.ok(parsed.parts.every(part=>part.body.every(value=>value===0)));
|
||||
});
|
||||
test('boundary-like bytes without complete delimiter suffix remain inside file',()=>{
|
||||
const body=Buffer.from(`prefix\r\n--${boundary}XYZ\r\n--${boundary}-\r\nsuffix`);
|
||||
const parsed=parseMultipart(encode([{...pdf,body}]),type);assert.deepEqual(parsed.parts[0].body,body);parsed.dispose();
|
||||
});
|
||||
test('quoted boundary and escaped filename handle actual MIME syntax',()=>{
|
||||
const parsed=parseMultipart(encode([{...text,headers:'Content-Disposition: form-data; name="upload"; filename="quoted\\"name.txt"'}]),`multipart/form-data; boundary="${boundary}"`);
|
||||
assert.equal(parsed.parts[0].filename,'quoted"name.txt');parsed.dispose();
|
||||
});
|
||||
test('empty multipart form has complete zero-part coverage',()=>{
|
||||
assert.equal(parseMultipart(encode([]),type).parts.length,0);
|
||||
});
|
||||
test('truncation malformed CRLF preamble and epilogue never yield partial coverage',()=>{
|
||||
const valid=encode([text,pdf]);
|
||||
for(const body of [valid.subarray(0,-5),encode([text],false),Buffer.concat([Buffer.from('preamble'),valid]),Buffer.concat([valid,Buffer.from('epilogue')]),Buffer.from(valid.toString().replace('\r\n','\n'))])assert.throws(()=>parseMultipart(body,type),{code:'invalid-multipart'});
|
||||
});
|
||||
test('ambiguous disposition and duplicate headers are rejected',()=>{
|
||||
for(const headers of ['Content-Disposition: form-data; name="a"; name="b"','Content-Disposition: form-data; name="a"\r\ncontent-disposition: form-data; name="b"','Content-Disposition: form-data; name="a"; filename="one"; filename="two"','Content-Disposition: form-data; name="a"\r\n folded: value'])assert.throws(()=>parseMultipart(encode([{headers,body:'public'}]),type),{code:'invalid-multipart'});
|
||||
});
|
||||
test('unsupported extended filenames nested multipart and transfer encoding fail closed',()=>{
|
||||
for(const headers of ["Content-Disposition: form-data; name=upload; filename*=UTF-8''file.pdf",'Content-Disposition: form-data; name="a"\r\nContent-Type: multipart/mixed; boundary=inner','Content-Disposition: form-data; name="a"\r\nContent-Transfer-Encoding: base64'])assert.throws(()=>parseMultipart(encode([{headers,body:'public'}]),type),{code:'unsupported-multipart'});
|
||||
});
|
||||
test('whole payload part count and per-part headers are strictly bounded',()=>{
|
||||
const body=encode([text,pdf]);
|
||||
for(const options of [{maxBytes:body.length-1},{maxParts:1},{maxHeaderBytes:5}])assert.throws(()=>parseMultipart(body,type,options),{code:'multipart-limit'});
|
||||
});
|
||||
test('invalid boundary parameters and duplicate boundary are rejected',()=>{
|
||||
for(const value of ['multipart/form-data','multipart/form-data; boundary=""','multipart/form-data; boundary="bad\r\nvalue"',`multipart/form-data; boundary=${boundary}; boundary=other`,'multipart/form-data; boundary='+ 'x'.repeat(71)])assert.throws(()=>parseMultipart(encode([text]),value),{code:'invalid-multipart'});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {defaultPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createFixtureDiagnostics}=require('./fixtures/broker-native-diagnostics.cjs'),{createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
|
||||
test('authorized private synthetic OTLP fixture retrieves real durable trace with audit child timings',async t=>{
|
||||
const audit=createSyntheticAudit(t),policy=defaultPolicy(),diagnostics=await createFixtureDiagnostics({enabled:true,policy,audit});
|
||||
await diagnostics.run({bytes:0},async()=>diagnostics.persist({eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:1,request:{bytes:0,detectedType:'text'}}));
|
||||
const proof=await diagnostics.finish();assert.equal(proof.enabled,true);assert.ok(proof.spanCount>=2);assert.equal(proof.invalid,0);assert.equal(proof.failedExports,0);assert.equal(proof.lifetimeFailedExports,0);
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
// Explicit Windows integration: run with the verified bundled Node executable
|
||||
// and SAFETYNET_NATIVE_BROKER_INSPECTION=1. Default unit suites skip this gate.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {createRequire} from 'node:module';
|
||||
import {createHash,generateKeyPairSync} from 'node:crypto';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import tls from 'node:tls';
|
||||
import {once} from 'node:events';
|
||||
import {defaultPolicy,signPolicy,inspect} from '../packages/core/policy.mjs';
|
||||
import {verifyBrokerRuntime} from '../scripts/provision-broker-runtime.mjs';
|
||||
const require=createRequire(import.meta.url),{startInspector}=require('../apps/browser/inspection.cjs');
|
||||
const {createManagedProxy}=require('../apps/network-broker/managed-proxy.cjs');
|
||||
const {createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs');
|
||||
const {createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs');
|
||||
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
|
||||
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
function pdf(text){
|
||||
const stream=`BT /F1 12 Tf 72 720 Td <${Buffer.from(text).toString('hex')}> Tj ET`;
|
||||
const objects=['<< /Type /Catalog /Pages 2 0 R >>','<< /Type /Pages /Kids [3 0 R] /Count 1 >>','<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>','<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`];
|
||||
let output='%PDF-1.4\n';const offsets=[0];
|
||||
objects.forEach((object,index)=>{offsets.push(Buffer.byteLength(output));output+=`${index+1} 0 obj\n${object}\nendobj\n`;});
|
||||
const xref=Buffer.byteLength(output);output+=`xref\n0 6\n0000000000 65535 f \n${offsets.slice(1).map(offset=>String(offset).padStart(10,'0')+' 00000 n \n').join('')}trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
|
||||
return Buffer.from(output);
|
||||
}
|
||||
function inspectorChildren(){
|
||||
const script=`@(Get-CimInstance Win32_Process -Filter "ParentProcessId = ${process.pid}" | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -like '*apps*inspector*server.py*' } | Select-Object -ExpandProperty ProcessId) | ConvertTo-Json -Compress`;
|
||||
const output=execFileSync('powershell.exe',['-NoProfile','-NonInteractive','-Command',script],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();
|
||||
return output?[].concat(JSON.parse(output)):[];
|
||||
}
|
||||
test('bundled Windows Node managed TLS broker uses real contained document inspection before dial',{
|
||||
skip:process.platform!=='win32'||process.env.SAFETYNET_NATIVE_BROKER_INSPECTION!=='1',timeout:120000
|
||||
},async t=>{
|
||||
const root=path.resolve('.'),proofDirectory=path.join(root,'artifacts/broker-native-inspection');await fs.mkdir(proofDirectory,{recursive:true});
|
||||
const proof={startedAt:new Date().toISOString(),outcome:'running',diagnosticsEnabled:false,scans:[],office:[]};
|
||||
let inspector,proxy,target;const started=performance.now();
|
||||
try{
|
||||
const runtime=await verifyBrokerRuntime(path.join(root,'.runtime/windows/broker'));
|
||||
assert.equal(path.resolve(process.execPath).toLowerCase(),path.join(root,'.runtime/windows/broker/node.exe').toLowerCase(),'Run this gate with the bundled broker Node executable');
|
||||
proof.runtime={version:runtime.version,executableSha256:runtime.files.find(file=>file.path==='node.exe').sha256};
|
||||
inspector=startInspector(root,false);assert.ok(inspector,'Contained inspector runtime unavailable');await inspector.waitReady();
|
||||
const children=inspectorChildren();assert.equal(children.length,1,'Expected one owned contained inspector parent');proof.inspectorOwnedPids=children;
|
||||
// startInspector only resolves readiness after validating Job Object plus
|
||||
// zero-network-capability AppContainer/private-pipe/read-only containment.
|
||||
proof.containmentStartupValidated=true;
|
||||
const key=await fs.readFile(new URL('./fixtures/icap/key.pem',import.meta.url)),cert=await fs.readFile(new URL('./fixtures/icap/cert.pem',import.meta.url));
|
||||
const received=[];let connections=0;
|
||||
target=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);const body=Buffer.concat(chunks);received.push({sha256:hash(body),bytes:body.length});res.end('public receiver acknowledgment');});
|
||||
target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
|
||||
const port=target.address().port,destination=`localhost:${port}`;
|
||||
const keys=generateKeyPairSync('ed25519'),policy=defaultPolicy();policy.assignment={deviceId:'native-broker-device',groups:[]};
|
||||
policy.dlp.rules=[{id:'synthetic-confidential-document',label:'Synthetic confidential',kind:'literal',value:'SYNTHETIC INTERNAL BLUEPRINT',action:'block'}];
|
||||
assert.equal(policy.diagnostics.enabled,false);
|
||||
const authority=await createPolicyAuthority({deviceId:'native-broker-device',publicKey:keys.publicKey.export({type:'spki',format:'pem'})});
|
||||
authority.accept(signPolicy(policy,keys.privateKey.export({type:'pkcs8',format:'pem'})));
|
||||
const snapshot=authority.snapshot();
|
||||
const forward=createHttpsForwarder({authority:snapshot.token,current:authority.current,resolveRoute:async value=>({approved:value.hostname==='localhost'&&value.port===port,address:'127.0.0.1',port:value.port}),ca:cert,deadlineMs:45000,maxResponseBytes:Math.min(16777216,policy.fileTransfers.download.maxBytes)});
|
||||
const durableAudit=createSyntheticAudit(t);
|
||||
proxy=await createManagedProxy({policyAuthority:authority,authMode:'bearer',persistAudit:durableAudit.persist,resolveCertificate:binding=>binding.authority===destination?{authority:destination,key,cert}:undefined,
|
||||
localInspector:async(bytes,signedPolicy)=>{const before=performance.now(),result=await inspector.inspect(bytes,signedPolicy);proof.scans.push({durationMs:performance.now()-before,decision:result.decision,complete:result.coverage.complete,contentType:result.contentType,matchedSyntheticRule:result.ruleId==='synthetic-confidential-document'});return result;},forward,deadlineMs:45000});
|
||||
const proxyPort=await proxy.listen();
|
||||
const send=(body,mime='application/pdf')=>new Promise((resolve,reject)=>{
|
||||
const request=http.request({host:'127.0.0.1',port:proxyPort,method:'CONNECT',path:destination,headers:{host:destination,'proxy-authorization':`Bearer ${proxy.capability}`}});
|
||||
request.setTimeout(55000,()=>request.destroy(new Error('native-test-deadline')));request.once('error',reject);
|
||||
request.once('connect',(response,socket)=>{
|
||||
if(response.statusCode!==200){socket.destroy();resolve(response.statusCode);return;}
|
||||
const secure=tls.connect({socket,servername:'localhost',ca:cert,rejectUnauthorized:true});let reply='';
|
||||
secure.setTimeout(55000,()=>secure.destroy(new Error('native-test-deadline')));secure.once('error',reject);
|
||||
secure.once('secureConnect',()=>secure.write(Buffer.concat([Buffer.from(`POST /upload HTTP/1.1\r\nHost: ${destination}\r\nContent-Type: ${mime}\r\nContent-Length: ${body.length}\r\nConnection: close\r\n\r\n`),body])));
|
||||
secure.on('data',chunk=>reply+=chunk.toString());secure.once('end',()=>{secure.destroy();resolve(Number(/^HTTP\/1.1 (\d+)/.exec(reply)?.[1]));});
|
||||
});request.end();
|
||||
});
|
||||
const secret=pdf(policy.dlp.rules[0].value),publicDocument=pdf('Public example report');
|
||||
assert.equal(inspect(secret,policy).decision,'allow','Encoded PDF must require genuine parsing, not raw literal detection');
|
||||
assert.equal(await send(secret),403);assert.equal(connections,0);assert.equal(received.length,0);
|
||||
proof.secret={status:403,upstreamConnections:connections,receivedBytes:0};
|
||||
assert.equal(await send(publicDocument),200);assert.deepEqual(received,[{sha256:hash(publicDocument),bytes:publicDocument.length}]);
|
||||
assert.ok(proof.scans.some(scan=>scan.decision==='block'&&scan.contentType==='pdf'&&scan.matchedSyntheticRule));
|
||||
assert.ok(proof.scans.some(scan=>scan.decision==='allow'&&scan.complete&&scan.contentType==='pdf'));
|
||||
proof.public={status:200,upstreamConnections:connections,...received[0]};
|
||||
const officeRoot=path.join(root,'artifacts/office-coverage-fixtures');
|
||||
// Only synthetic fixtures are modified by stdlib ZIP/XML; no classifier,
|
||||
// document parser or OCR result is substituted in the actual broker gate.
|
||||
const mutate=[
|
||||
'import sys,zipfile,pathlib,xml.etree.ElementTree as ET',
|
||||
'root=pathlib.Path(sys.argv[1]); marker=sys.argv[2]',
|
||||
"for source,destination,member in [('public-generated.docx','secret-metadata.docx','docProps/core.xml'),('public-generated.xlsx','secret-hidden.xlsx','xl/worksheets/sheet2.xml')]:",
|
||||
' with zipfile.ZipFile(root/source) as archive:',
|
||||
' records=[(entry,archive.read(entry)) for entry in archive.infolist()]',
|
||||
" if source.endswith('.xlsx'):",
|
||||
" workbook=ET.fromstring(archive.read('xl/workbook.xml'))",
|
||||
" sheets=[node for node in workbook.iter() if node.tag.endswith('}sheet')]",
|
||||
" assert len(sheets)==2 and sheets[1].attrib.get('state')=='hidden'",
|
||||
" with zipfile.ZipFile(root/destination,'w') as output:",
|
||||
' changed=False',
|
||||
' for entry,data in records:',
|
||||
' if entry.filename==member:',
|
||||
' document=ET.fromstring(data)',
|
||||
" if source.endswith('.docx'):",
|
||||
" title=document.find('{http://purl.org/dc/elements/1.1/}title')",
|
||||
" if title is None: title=ET.SubElement(document,'{http://purl.org/dc/elements/1.1/}title')",
|
||||
' title.text=marker',
|
||||
' else:',
|
||||
" namespace='{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'",
|
||||
" sheet=document.find(namespace+'sheetData')",
|
||||
" if sheet is None: raise RuntimeError('Synthetic sheet data absent')",
|
||||
" row=ET.SubElement(sheet,namespace+'row',{'r':'1'})",
|
||||
" cell=ET.SubElement(row,namespace+'c',{'r':'A1','t':'inlineStr'})",
|
||||
" value=ET.SubElement(ET.SubElement(cell,namespace+'is'),namespace+'t'); value.text=marker",
|
||||
" data=ET.tostring(document,encoding='utf-8',xml_declaration=True);changed=True",
|
||||
' output.writestr(entry,data)',
|
||||
" if not changed: raise RuntimeError('Synthetic fixture target part absent')",
|
||||
''
|
||||
].join('\n');
|
||||
execFileSync(path.join(root,'.runtime/windows/python/python.exe'),['-c',mutate,officeRoot,policy.dlp.rules[0].value],{windowsHide:true,stdio:'pipe',timeout:10000});
|
||||
const expectedPublic=[received[0]];
|
||||
for(const [label,name,mime] of [
|
||||
['docx','public-generated.docx','application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
||||
['xlsx','public-generated.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']]){
|
||||
const bytes=await fs.readFile(path.join(officeRoot,name)),firstScan=proof.scans.length;
|
||||
assert.equal(await send(bytes,mime),200);
|
||||
const observed=proof.scans.slice(firstScan);assert.ok(observed.some(scan=>scan.contentType==='office'&&scan.decision==='allow'&&scan.complete));
|
||||
expectedPublic.push({sha256:hash(bytes),bytes:bytes.length});proof.office.push({case:label+'-public',status:200,complete:true,sha256:hash(bytes),bytes:bytes.length});
|
||||
}
|
||||
for(const [label,name,mime] of [
|
||||
['docx-metadata','secret-metadata.docx','application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
||||
['xlsx-hidden-sheet','secret-hidden.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']]){
|
||||
const before=connections,firstScan=proof.scans.length,bytes=await fs.readFile(path.join(officeRoot,name));
|
||||
assert.equal(await send(bytes,mime),403);assert.equal(connections,before);
|
||||
assert.ok(proof.scans.slice(firstScan).some(scan=>scan.contentType==='office'&&scan.decision==='block'&&scan.matchedSyntheticRule));
|
||||
proof.office.push({case:label,status:403,matchedSyntheticRule:true,newUpstreamConnections:0});
|
||||
}
|
||||
const complex=await fs.readFile(path.join(officeRoot,'public-generated.pptx')),beforeComplex=connections,firstComplexScan=proof.scans.length;
|
||||
assert.equal(await send(complex,'application/vnd.openxmlformats-officedocument.presentationml.presentation'),403);assert.equal(connections,beforeComplex);
|
||||
assert.ok(proof.scans.slice(firstComplexScan).some(scan=>scan.contentType==='office'&&scan.decision==='block'&&!scan.complete));
|
||||
proof.office.push({case:'unsupported-complex-pptx',status:403,complete:false,newUpstreamConnections:0});
|
||||
assert.deepEqual(received,expectedPublic);
|
||||
const auditEvents=durableAudit.events();
|
||||
assert.equal(auditEvents.filter(event=>event.kind==='upload.authorized').length,3);
|
||||
assert.equal(auditEvents.filter(event=>event.kind==='upload.block'&&event.ruleId==='synthetic-confidential-document').length,3);
|
||||
assert.equal(auditEvents.length,7);
|
||||
proof.audit={persistedEvents:auditEvents.length,authorizedEvents:3,matchedSecretBlocks:3,syntheticMetadataFsynced:true};
|
||||
proof.outcome='passed';
|
||||
}catch(error){proof.outcome='failed';proof.errorType=error.name;throw error;}
|
||||
finally{
|
||||
if(proxy)await proxy.close();if(target){target.closeAllConnections();await new Promise(resolve=>target.close(resolve));}
|
||||
if(inspector){inspector.stop();const deadline=Date.now()+10000;while(inspectorChildren().length&&Date.now()<deadline)await new Promise(resolve=>setTimeout(resolve,100));proof.ownedInspectorParentsRemaining=inspectorChildren().length;}
|
||||
proof.durationMs=performance.now()-started;await fs.writeFile(path.join(proofDirectory,`proof-${proof.startedAt.replaceAll(':','-')}.json`),JSON.stringify(proof,null,2)+'\n');
|
||||
await fs.writeFile(path.join(proofDirectory,`office-proof-${proof.startedAt.replaceAll(':','-')}.json`),JSON.stringify({startedAt:proof.startedAt,outcome:proof.outcome,diagnosticsEnabled:false,runtime:proof.runtime,office:proof.office,scans:proof.scans.filter(scan=>scan.contentType==='office'),audit:proof.audit,ownedInspectorParentsRemaining:proof.ownedInspectorParentsRemaining},null,2)+'\n');
|
||||
assert.equal(proof.ownedInspectorParentsRemaining??0,0,'Owned inspector parent did not terminate');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,mkdir,writeFile,rm,symlink,open} from 'node:fs/promises';
|
||||
import {writeFileSync,utimesSync} from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {buildBrokerPackageManifest,verifyBrokerPackage} from '../scripts/broker-package-contract.mjs';
|
||||
|
||||
const release={version:'0.1.0',protocol:1,entrypoint:'broker.exe'};
|
||||
async function fixture(t){
|
||||
const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-broker-contract-'));
|
||||
t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
await mkdir(path.join(root,'runtime'));
|
||||
// Synthetic bytes deliberately are not a broker executable or launch proof.
|
||||
await writeFile(path.join(root,'broker.exe'),'synthetic packaged executable');
|
||||
await writeFile(path.join(root,'runtime','scanner.dll'),'synthetic scanner');
|
||||
const manifest=await buildBrokerPackageManifest(root,release);
|
||||
return {root,manifest};
|
||||
}
|
||||
test('complete bundled broker inventory hashes and verifies without external runtime',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
assert.equal(manifest.files.length,2);
|
||||
assert.equal((await verifyBrokerPackage(root,manifest,release)).files,2);
|
||||
});
|
||||
for(const operation of ['missing','tampered','extra'])test(`rejects ${operation} broker payload`,async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
if(operation==='missing')await rm(path.join(root,'broker.exe'));
|
||||
if(operation==='tampered')await writeFile(path.join(root,'runtime','scanner.dll'),'replacement');
|
||||
if(operation==='extra')await writeFile(path.join(root,'runtime','injected.dll'),'injection');
|
||||
await assert.rejects(verifyBrokerPackage(root,manifest,release),{code:operation==='tampered'?'broker_package_integrity_mismatch':'broker_package_inventory_mismatch'});
|
||||
});
|
||||
test('runtime protocol and version must match integrity-protected expectations',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
for(const expected of [{version:'0.2.0',protocol:1},{version:'0.1.0',protocol:2}])
|
||||
await assert.rejects(verifyBrokerPackage(root,manifest,expected),{code:'broker_package_incompatible'});
|
||||
});
|
||||
test('rejects ambiguous paths and manifests before package traversal',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
for(const unsafe of ['../broker.exe','C:/broker.exe','runtime\\scanner.dll','CON.exe','broker.exe:stream','./broker.exe']){
|
||||
const changed=structuredClone(manifest);changed.files[0].path=unsafe;
|
||||
await assert.rejects(verifyBrokerPackage(root,changed,release),{code:'broker_manifest_invalid'});
|
||||
}
|
||||
const duplicate=structuredClone(manifest);duplicate.files.push({...duplicate.files[0],path:duplicate.files[0].path.toUpperCase()});
|
||||
await assert.rejects(verifyBrokerPackage(root,duplicate,release),{code:'broker_manifest_invalid'});
|
||||
});
|
||||
test('rejects absent entrypoint and malformed hashes',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
await assert.rejects(verifyBrokerPackage(root,{...manifest,entrypoint:'missing.exe'},release),{code:'broker_manifest_invalid'});
|
||||
const changed=structuredClone(manifest);changed.files[0].sha256='invalid';
|
||||
await assert.rejects(verifyBrokerPackage(root,changed,release),{code:'broker_manifest_invalid'});
|
||||
});
|
||||
test('directory junctions cannot redirect broker payload outside installation',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
const target=path.join(root,'runtime'),alias=path.join(root,'linked');
|
||||
await symlink(target,alias,process.platform==='win32'?'junction':'dir');
|
||||
await assert.rejects(verifyBrokerPackage(root,manifest,release),{code:'broker_path_unsafe'});
|
||||
});
|
||||
test('unavailable package errors contain no filesystem or raw exception content',async()=>{
|
||||
await assert.rejects(verifyBrokerPackage(path.join(os.tmpdir(),'absent-sensitive-company-path'),{...release,format:'safetynet-broker-package-v1',files:[{path:'broker.exe',bytes:0,sha256:'0'.repeat(64)}]},release),error=>error.code==='broker_package_unavailable'&&error.message==='broker_package_unavailable');
|
||||
});
|
||||
|
||||
test('same-size mutation during actual read rejects with bounded fixed invariant facts',async t=>{
|
||||
const {root,manifest}=await fixture(t),target=path.join(root,'broker.exe');
|
||||
const probe=await open(target,'r'),prototype=Object.getPrototypeOf(probe);
|
||||
const original=prototype.createReadStream;await probe.close();
|
||||
let mutated=false;
|
||||
t.mock.method(prototype,'createReadStream',function(options){
|
||||
const stream=original.call(this,options);
|
||||
stream.once('end',()=>{
|
||||
if(mutated)return;mutated=true;
|
||||
writeFileSync(target,'X'.repeat(manifest.files.find(file=>file.path==='broker.exe').bytes));
|
||||
const future=new Date(Date.now()+5000);utimesSync(target,future,future);
|
||||
});
|
||||
return stream;
|
||||
});
|
||||
await assert.rejects(verifyBrokerPackage(root,manifest,release),error=>{
|
||||
assert.equal(error.code,'broker_package_changed');assert.equal(error.message,error.code);
|
||||
assert.ok(error.failureFacts.changed.includes('modification-time'));
|
||||
assert.ok(error.failureFacts.changed.length<=5);
|
||||
assert.ok(error.failureFacts.changed.every(value=>['size-before-read','size-after-read','modification-time','change-time','file-identity'].includes(value)));
|
||||
assert.ok(Object.isFrozen(error.failureFacts)&&Object.isFrozen(error.failureFacts.changed));
|
||||
assert.ok(!JSON.stringify(error.failureFacts).includes(root));return true;
|
||||
});
|
||||
assert.equal(mutated,true);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {mkdtemp,mkdir,writeFile,readFile,cp,rm} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {brokerResourceConfiguration,prepareBrokerResources,verifyPackagedBroker,embeddedBrokerManifestReader,snapshotScannerResources,verifyScannerResources} from '../scripts/package-broker-resources.mjs';
|
||||
import {buildBrokerPackageManifest} from '../scripts/broker-package-contract.mjs';
|
||||
import {BROKER_PAYLOAD} from '../scripts/stage-broker-payload.mjs';
|
||||
const require=createRequire(import.meta.url),asar=require('@electron/asar');
|
||||
const {getFileMatchers,copyFiles}=require('app-builder-lib/out/fileMatcher.js');
|
||||
const build={extraResources:[{from:'apps/inspector',to:'inspector'},{from:'.runtime/windows/broker-payload',to:'broker'}],files:['apps/browser/**/*',{from:'.runtime/windows/broker-payload.manifest.json',to:'apps/browser/broker-package-manifest.json'}]};
|
||||
async function fixture(t){const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-package-resources-'));t.after(()=>rm(root,{recursive:true,force:true}));return root;}
|
||||
test('declared single broker and protected ASAR manifest mapping is mandatory',()=>{
|
||||
const payloadRoot=path.resolve('.runtime/windows/broker-payload-fixture'),manifestPath=payloadRoot+'.manifest.json';
|
||||
const configured=brokerResourceConfiguration(build,{payloadRoot,manifestPath});
|
||||
assert.equal(configured.extraResources[1].from,payloadRoot);assert.equal(configured.files[1].from,manifestPath);assert.equal(configured.files[1].to,'apps/browser/broker-package-manifest.json');
|
||||
for(const bad of [{...build,extraResources:build.extraResources.slice(0,1)},{...build,extraResources:[...build.extraResources,build.extraResources[1]]},{...build,extraResources:[...build.extraResources,{from:'unexpected',to:'BROKER'}]},{...build,extraResources:[...build.extraResources,{from:'unexpected',to:'broker/injected.dll'}]},{...build,files:[...build.files,build.files[1]]},{...build,files:build.files.slice(0,1)}])assert.throws(()=>brokerResourceConfiguration(bad,{payloadRoot,manifestPath}),{code:'broker_package_configuration_missing'});
|
||||
assert.equal(build.extraResources[1].from,'.runtime/windows/broker-payload');
|
||||
});
|
||||
test('actual ASAR reader verifies embedded manifest and rejects tampered external broker bytes',async t=>{
|
||||
const root=await fixture(t),resources=path.join(root,'resources'),broker=path.join(resources,'broker'),app=path.join(root,'app');
|
||||
await mkdir(broker,{recursive:true});await mkdir(path.join(app,'apps/browser'),{recursive:true});
|
||||
await writeFile(path.join(broker,'node.exe'),'synthetic payload, not executable proof');
|
||||
const manifest=await buildBrokerPackageManifest(broker,BROKER_PAYLOAD);
|
||||
const manifestPath=path.join(root,'broker.manifest.json');await writeFile(manifestPath,JSON.stringify(manifest));
|
||||
const configured=brokerResourceConfiguration(build,{payloadRoot:broker,manifestPath});
|
||||
const matchers=getFileMatchers(configured,'files',app,{defaultSrc:root,globalOutDir:path.join(root,'output'),macroExpander:value=>value,customBuildOptions:{}});
|
||||
const manifestMatcher=matchers.find(matcher=>matcher.from===manifestPath);
|
||||
assert.ok(manifestMatcher);assert.equal(manifestMatcher.to,path.join(app,'apps/browser/broker-package-manifest.json'));
|
||||
await copyFiles([manifestMatcher],null,false);
|
||||
const archive=path.join(resources,'app.asar');await asar.createPackage(app,archive);
|
||||
await verifyPackagedBroker({resources,expectedManifest:manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)});
|
||||
await writeFile(path.join(broker,'node.exe'),'tampered');
|
||||
await assert.rejects(verifyPackagedBroker({resources,expectedManifest:manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)}),{code:'broker_package_integrity_mismatch'});
|
||||
});
|
||||
test('adjacent modified metadata cannot replace expected protected manifest',async()=>{
|
||||
const expected={format:'reviewed'},changed={format:'attacker-adjacent'};
|
||||
await assert.rejects(verifyPackagedBroker({resources:path.resolve('resources'),expectedManifest:expected,readEmbeddedManifest:async()=>Buffer.from(JSON.stringify(changed))}),{code:'broker_package_manifest_mismatch'});
|
||||
await assert.rejects(verifyPackagedBroker({resources:path.resolve('resources'),expectedManifest:expected}),{code:'broker_package_manifest_reader_required'});
|
||||
});
|
||||
test('scanner resource inventory verifies copied bytes and rejects missing altered and extra files',async t=>{
|
||||
const root=await fixture(t),resources=path.join(root,'output');
|
||||
for(const directory of ['apps/inspector','.runtime/windows/python','.runtime/windows/semantic']){await mkdir(path.join(root,directory),{recursive:true});await writeFile(path.join(root,directory,'payload.dat'),'synthetic '+directory);}
|
||||
await mkdir(path.join(root,'apps/inspector/__pycache__'));await writeFile(path.join(root,'apps/inspector/__pycache__/ignored.pyc'),'not shipped');
|
||||
const expected=await snapshotScannerResources(root);assert.equal(expected.length,3);
|
||||
for(const [from,to] of [['apps/inspector','inspector'],['.runtime/windows/python','python'],['.runtime/windows/semantic','semantic']]){await mkdir(path.join(resources,to),{recursive:true});await cp(path.join(root,from,'payload.dat'),path.join(resources,to,'payload.dat'));}
|
||||
await verifyScannerResources(resources,expected);
|
||||
await writeFile(path.join(resources,'python/payload.dat'),'changed');await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'});
|
||||
await cp(path.join(root,'.runtime/windows/python/payload.dat'),path.join(resources,'python/payload.dat'));
|
||||
await writeFile(path.join(resources,'inspector/injected.dll'),'extra');await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'});
|
||||
await rm(path.join(resources,'inspector/injected.dll'));await rm(path.join(resources,'semantic/payload.dat'));await assert.rejects(verifyScannerResources(resources,expected),{code:'scanner_package_integrity_mismatch'});
|
||||
assert.equal((await readFile(path.join(root,'apps/inspector/payload.dat'))).toString(),'synthetic apps/inspector');
|
||||
});
|
||||
test('real prepared standalone payload keeps source identities pinned through subsequent packaging',{
|
||||
skip:process.platform!=='win32'||process.env.SAFETYNET_BROKER_PACKAGE_PREPARE!=='1',timeout:60000
|
||||
},async t=>{
|
||||
const root=await fixture(t),repository=path.resolve('.');
|
||||
// Explicit integration gate copies reviewed real sources and the pinned
|
||||
// official runtime. Zod is fetched and integrity checked against the lock.
|
||||
for(const directory of ['apps/network-broker','packages/core'])await cp(path.join(repository,directory),path.join(root,directory),{recursive:true});
|
||||
await mkdir(path.join(root,'apps/browser'),{recursive:true});
|
||||
for(const name of ['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs'])await cp(path.join(repository,'apps/browser',name),path.join(root,'apps/browser',name));
|
||||
await cp(path.join(repository,'package-lock.json'),path.join(root,'package-lock.json'));
|
||||
await cp(path.join(repository,'.runtime/windows/broker'),path.join(root,'.runtime/windows/broker'),{recursive:true});
|
||||
const prepared=await prepareBrokerResources({root,build});
|
||||
assert.ok(prepared.manifest.files.some(file=>file.path==='apps/network-broker/audit-gate.cjs'));
|
||||
assert.ok(prepared.manifest.files.some(file=>file.path==='node_modules/zod/LICENSE'));
|
||||
assert.ok(prepared.sourceSnapshot.length>20);await prepared.verifySources();
|
||||
const changed=path.join(root,'apps/network-broker/policy-authority.cjs');await writeFile(changed,(await readFile(changed))+'\n// synthetic post-stage change\n');
|
||||
await assert.rejects(prepared.verifySources(),{code:'broker_stage_source_changed'});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,writeFile,readFile,cp,rm} from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {gzipSync} from 'node:zlib';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {BROKER_PAYLOAD,extractVerifiedZod,snapshotSources,verifySourceSnapshot,stageBrokerPayload} from '../scripts/stage-broker-payload.mjs';
|
||||
import {verifyBrokerPackage} from '../scripts/broker-package-contract.mjs';
|
||||
import {BROKER_RUNTIME} from '../scripts/provision-broker-runtime.mjs';
|
||||
const repo=fileURLToPath(new URL('../',import.meta.url));
|
||||
// Explicit native proof uses a prepared local stage. Production expected
|
||||
// state comes from the protected ASAR, never this adjacent test manifest.
|
||||
const nativeStage=process.env.SAFETYNET_TEST_BROKER_ROOT;
|
||||
async function currentStage(){
|
||||
assert.equal(typeof nativeStage,'string','explicit native stage required');
|
||||
const root=path.resolve(nativeStage),parent=path.resolve(repo,'.runtime/windows');
|
||||
assert.equal(path.dirname(root),parent,'stage must stay in the reviewed runtime directory');
|
||||
assert.match(path.basename(root),/^broker-payload-package-[a-z0-9-]+$/,'a unique current stage is required');
|
||||
const manifest=JSON.parse(await readFile(root+'.manifest.json','utf8'));
|
||||
await verifyBrokerPackage(root,manifest,BROKER_PAYLOAD);
|
||||
const executable=manifest.files.find(file=>file.path==='node.exe');
|
||||
assert.equal(executable?.sha256,BROKER_RUNTIME.executableSha256,'exact official runtime pin required');
|
||||
return {root,manifest};
|
||||
}
|
||||
async function temporary(fn){const root=await mkdtemp(path.join(tmpdir(),'safetynet-broker-stage-test-'));try{return await fn(root);}finally{const absolute=path.resolve(root);assert.equal(path.dirname(absolute),path.resolve(tmpdir()));assert.ok(path.basename(absolute).startsWith('safetynet-broker-stage-test-'));await rm(absolute,{recursive:true,force:true});}}
|
||||
function tar(entries){const chunks=[];for(const [name,content,type='0'] of entries){const data=Buffer.from(content),header=Buffer.alloc(512);header.write(name);header.write('0000644\0',100);header.write('0000000\0',108);header.write('0000000\0',116);header.write(data.length.toString(8).padStart(11,'0')+'\0',124);header.fill(32,148,156);header.write(type,156);header.write('ustar\0',257);header.write([...header].reduce((a,b)=>a+b,0).toString(8).padStart(6,'0')+'\0 ',148);chunks.push(header,data,Buffer.alloc((512-data.length%512)%512));}chunks.push(Buffer.alloc(1024));return gzipSync(Buffer.concat(chunks));}
|
||||
function lock(bytes){return {version:'4.6.7',resolved:'https://registry.npmjs.org/zod/-/zod-4.6.7.tgz',integrity:'sha512-'+createHash('sha512').update(bytes).digest('base64')};}
|
||||
const minimum=[['package/package.json',JSON.stringify({name:'zod',version:'4.6.7'})],['package/LICENSE','MIT synthetic fixture'],['package/index.js','export const z=1;']];
|
||||
test('verified dependency archive copies runtime and license, excludes development and secrets',()=>{const bytes=tar([...minimum,['package/src/dev.ts','dev'],['package/test/fixture.js','dev'],['package/.env','SECRET'],['package/key.pem','KEY']]);const files=extractVerifiedZod(bytes,lock(bytes));assert.deepEqual(files.map(f=>f.path),['node_modules/zod/package.json','node_modules/zod/LICENSE','node_modules/zod/index.js']);});
|
||||
test('archive rejects tampering, traversal, symlinks, duplicate-case paths and ADS before staging',()=>{const valid=tar(minimum);assert.throws(()=>extractVerifiedZod(Buffer.concat([valid,Buffer.from('tamper')]),lock(valid)),/integrity/);for(const entry of [['package/../../escape.js','x'],['package/link.js','x','2'],['package/INDEX.js','x'],['package/a.js:stream.js','x'],['package/CON.js','x']]){const bytes=tar([...minimum,entry]);assert.throws(()=>extractVerifiedZod(bytes,lock(bytes)),/archive/);}});
|
||||
test('source snapshot detects changes, including a same-size mutation',()=>temporary(async root=>{await writeFile(path.join(root,'source.cjs'),'AAA');const snapshot=await snapshotSources(root,['source.cjs']);await writeFile(path.join(root,'source.cjs'),'BBB');await assert.rejects(verifySourceSnapshot(root,snapshot),/source_changed/);await assert.rejects(snapshotSources(root,['../outside']),/path_unsafe/);}));
|
||||
test('stager refuses destination outside reviewed ignored root before copying',async()=>{await assert.rejects(stageBrokerPayload({root:repo,destination:path.join(repo,'dist','broker')}),/destination_unsafe/);});
|
||||
test('actual staged package excludes activation scripts and has complete trusted inventory',{skip:!nativeStage?'requires explicit SAFETYNET_TEST_BROKER_ROOT native stage':false},async()=>{const {root,manifest}=await currentStage();assert.ok(root);for(const file of manifest.files){assert.ok(!/(^|\/)(tests?|artifacts?|models?|__pycache__)(\/|$)|\.env$|\.(?:pem|py|pyc|exe)$/.test(file.path)||file.path==='node.exe');assert.ok(!file.path.startsWith('apps/browser/')||['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs'].includes(file.path.slice(13)));}assert.ok(manifest.files.some(f=>f.path==='node_modules/zod/LICENSE'));});
|
||||
test('bundled official node loads real authority/policy outside repository with empty dependency environment',{skip:!nativeStage?'requires explicit SAFETYNET_TEST_BROKER_ROOT native stage':false},()=>temporary(async root=>{const {root:staged,manifest}=await currentStage();await cp(staged,root,{recursive:true});const script=`import {createRequire} from 'node:module';import {generateKeyPairSync} from 'node:crypto';const require=createRequire(process.cwd()+'/probe.cjs');const {createPolicyAuthority}=require('./apps/network-broker/policy-authority.cjs');const {defaultPolicy}=await import('./packages/core/policy.mjs');const service=require('./apps/network-broker/service-entry.cjs');const store=require('./apps/network-broker/service-store-client.cjs');const trust=require('./packages/core/windows-trust.cjs');const management=require('./apps/network-broker/management-identity-operation.cjs');const topology=require('./apps/network-broker/wfp-topology.cjs');const revalidation=require('./apps/network-broker/wfp-revalidation.cjs');const keys=generateKeyPairSync('ed25519');const authority=await createPolicyAuthority({publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'synthetic-staged-proof'});console.log(JSON.stringify({node:process.version,policyVersion:defaultPolicy().version,authorityActive:authority.status().active,dependency:require.resolve('zod'),nodePathPresent:!!process.env.NODE_PATH,serviceExportsLoaded:typeof service.runServiceProcess==='function'&&typeof store.createServiceStoreClient==='function'&&typeof trust.verifyDeploymentEnvelope==='function'&&typeof management.createManagementIdentityOperation==='function'&&typeof topology.validateTopology==='function'&&typeof revalidation.createWfpRevalidation==='function'}));`;const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP'])if(process.env[key])env[key]=process.env[key];const output=await new Promise((resolve,reject)=>{const child=spawn(path.join(root,'node.exe'),['--input-type=module','-e',script],{cwd:root,env,windowsHide:true,timeout:15000});let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);child.on('error',reject);child.on('close',code=>code===0?resolve(stdout):reject(Error('bundled probe failed '+code+' '+stderr)));});const proof=JSON.parse(output);assert.equal(proof.node,'v'+BROKER_RUNTIME.version);assert.equal(proof.policyVersion,1);assert.equal(proof.authorityActive,false);assert.equal(proof.nodePathPresent,false);assert.equal(proof.serviceExportsLoaded,true);assert.ok(path.resolve(proof.dependency).startsWith(path.join(root,'node_modules','zod')+path.sep));await writeFile(path.join(root,'apps/network-broker/policy-authority.cjs'),'TAMPER');await assert.rejects(verifyBrokerPackage(root,manifest,BROKER_PAYLOAD),/integrity_mismatch/);}));
|
||||
@@ -0,0 +1,53 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {generateKeyPairSync,sign} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {PassThrough} from 'node:stream';
|
||||
import {defaultPolicy,signPolicy,verifyPolicy} from '../packages/core/policy.mjs';
|
||||
const {createPolicyAuthority}=createRequire(import.meta.url)('../apps/network-broker/policy-authority.cjs');
|
||||
const {gateRequest}=createRequire(import.meta.url)('../apps/network-broker/http-request-gate.cjs');
|
||||
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'});
|
||||
const policy=(version=1)=>({...defaultPolicy(),version,assignment:{deviceId:'device-1',groups:[]}});
|
||||
const create=()=>createPolicyAuthority({publicKey,deviceId:'device-1',minimumVersion:1});
|
||||
test('actual core signature/schema verification produces deeply immutable device authority',async()=>{
|
||||
const authority=await create(),envelope=signPolicy(policy(),privateKey),snapshot=authority.accept(envelope);
|
||||
assert.deepEqual(snapshot.policy,verifyPolicy(envelope,publicKey,1));assert.equal(authority.current(snapshot.token),true);
|
||||
assert.ok(Object.isFrozen(snapshot.policy.dlp.rules));assert.throws(()=>snapshot.policy.browser.allowedHosts.push('evil.example'));
|
||||
envelope.payload='mutated';assert.equal(authority.current(snapshot.token),true);
|
||||
});
|
||||
test('expired, wrong-device, modified, malformed schema and rollback invalidate old authority',async()=>{
|
||||
for(const invalid of ['expired','wrong-device','signature','schema','rollback']) {
|
||||
const authority=await create(),valid=authority.accept(signPolicy(policy(2),privateKey));let envelope;
|
||||
if(invalid==='expired'){const p=policy(3);p.expiresAt=new Date(0).toISOString();envelope=signPolicy(p,privateKey);}
|
||||
if(invalid==='wrong-device'){const p=policy(3);p.assignment.deviceId='device-2';envelope=signPolicy(p,privateKey);}
|
||||
if(invalid==='signature'){envelope=signPolicy(policy(3),privateKey);envelope.signature='AAAA';}
|
||||
if(invalid==='schema'){const p=policy(3);p.dlp.maxBytes=0;const payload=Buffer.from(JSON.stringify(p)).toString('base64url');envelope={payload,signature:sign(null,Buffer.from(payload),keys.privateKey).toString('base64url')};}
|
||||
if(invalid==='rollback')envelope=signPolicy(policy(1),privateKey);
|
||||
assert.throws(()=>authority.accept(envelope),{code:'policy-rejected'});assert.equal(authority.current(valid.token),false);assert.equal(authority.snapshot(),null);assert.equal(authority.status().minimumVersion,2);
|
||||
}
|
||||
});
|
||||
test('same-version changed policy rejected and revocation preserves rollback floor',async()=>{
|
||||
const authority=await create();const p=policy(2),first=authority.accept(signPolicy(p,privateKey));
|
||||
p.browser.blockedHosts=['example.com'];assert.throws(()=>authority.accept(signPolicy(p,privateKey)),{code:'policy-rejected'});assert.equal(authority.current(first.token),false);
|
||||
authority.revoke();assert.throws(()=>authority.accept(signPolicy(policy(1),privateKey)),{code:'policy-rejected'});
|
||||
});
|
||||
test('policy update during asynchronous scan prevents release using old token',async()=>{
|
||||
const authority=await create(),old=authority.accept(signPolicy(policy(),privateKey));let release,entered,dials=0;
|
||||
const scan=new Promise(r=>release=r),started=new Promise(r=>entered=r);
|
||||
const request=new PassThrough();Object.assign(request,{rawHeaders:['Host','example.com','Content-Length','6'],url:'http://example.com/',method:'POST',complete:true});
|
||||
const result=gateRequest(request,{authority:old.token,current:authority.current,inspect:async()=>{entered();await scan;return {decision:'allow'};},forward:()=>{dials++;}});
|
||||
const denied=assert.rejects(result,{code:'authority-expired'});request.end('public');await started;
|
||||
const next=authority.accept(signPolicy(policy(2),privateKey));release();await denied;assert.equal(dials,0);assert.equal(authority.current(next.token),true);assert.ok(next.generation>old.generation);
|
||||
authority.revoke();assert.equal(authority.current(next.token),false);
|
||||
});
|
||||
test('trusted bootstrap pins key and version; invalid bootstrap rejected',async()=>{
|
||||
for(const changed of [{publicKey:'invalid'},{deviceId:'bad/id'},{minimumVersion:-1},{minimumVersion:1.5}])await assert.rejects(createPolicyAuthority({publicKey,deviceId:'device-1',minimumVersion:1,...changed}),{code:'policy-bootstrap-invalid'});
|
||||
const authority=await createPolicyAuthority({publicKey,deviceId:'device-1',minimumVersion:5});assert.throws(()=>authority.accept(signPolicy(policy(4),privateKey)),{code:'policy-rejected'});
|
||||
});
|
||||
test('current authority expires without a policy refresh and rejects a different signing key',async()=>{
|
||||
const authority=await create(),p=policy();p.expiresAt=new Date(Date.now()+150).toISOString();
|
||||
const active=authority.accept(signPolicy(p,privateKey));assert.equal(authority.current(active.token),true);
|
||||
await new Promise(resolve=>setTimeout(resolve,170));assert.equal(authority.current(active.token),false);assert.equal(authority.snapshot(),null);
|
||||
const wrong=generateKeyPairSync('ed25519').privateKey.export({type:'pkcs8',format:'pem'});
|
||||
assert.throws(()=>authority.accept(signPolicy(policy(2),wrong)),{code:'policy-rejected'});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {fork} from 'node:child_process';
|
||||
import {once} from 'node:events';
|
||||
import fs from 'node:fs';
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
import tls from 'node:tls';
|
||||
import net from 'node:net';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
const brokerNode=fileURLToPath(new URL('../.runtime/windows/broker/node.exe',import.meta.url));
|
||||
function child(t) {
|
||||
assert.equal(fs.existsSync(brokerNode),true,'Verified bundled broker Node prerequisite');
|
||||
const process=fork(fileURLToPath(new URL('./fixtures/broker-process-child.cjs',import.meta.url)),[],{execPath:brokerNode,execArgv:[],stdio:['ignore','ignore','ignore','ipc'],env:{SystemRoot:globalThis.process.env.SystemRoot}});
|
||||
t.after(()=>{if(process.exitCode===null)process.kill();});return process;
|
||||
}
|
||||
function bounded(event,ms=3000){let timer;return Promise.race([event,new Promise((_,reject)=>timer=setTimeout(()=>reject(Error('test-deadline')),ms))]).finally(()=>clearTimeout(timer));}
|
||||
async function request(ready,destination,body) {
|
||||
return await new Promise((resolve,reject)=>{
|
||||
const req=http.request({host:'127.0.0.1',port:ready.port,method:'CONNECT',path:destination,headers:{host:destination,'proxy-authorization':`Bearer ${ready.capability}`}});
|
||||
req.setTimeout(2000,()=>req.destroy(Error('test-deadline')));req.on('error',reject);
|
||||
req.on('connect',(res,socket)=>{
|
||||
if(res.statusCode!==200){socket.destroy();reject(Error('connect-rejected'));return;}
|
||||
const secure=tls.connect({socket,servername:'localhost',ca:cert,rejectUnauthorized:true});let text='';
|
||||
secure.setTimeout(2000,()=>secure.destroy(Error('test-deadline')));secure.on('error',reject);secure.on('secureConnect',()=>secure.write(`POST /submit HTTP/1.1\r\nHost: ${destination}\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`));secure.on('data',chunk=>text+=chunk);secure.on('end',()=>{secure.destroy();resolve(text);});
|
||||
});req.end();
|
||||
});
|
||||
}
|
||||
test('separate Node child handles inspected CONNECT and closes on private IPC disconnect',async t=>{
|
||||
const received=[];let connections=0;
|
||||
const target=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.writeHead(201);res.end('accepted');});
|
||||
target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
|
||||
t.after(async()=>{target.closeAllConnections();await new Promise(r=>target.close(r));});
|
||||
const destination=`localhost:${target.address().port}`,worker=child(t);
|
||||
const readyEvent=bounded(once(worker,'message'));worker.send({type:'start',config:{destination}});
|
||||
const [ready]=await readyEvent;assert.equal(ready.type,'ready');assert.notEqual(worker.pid,process.pid);assert.match(ready.capability,/^[a-zA-Z0-9_-]{43}$/);
|
||||
assert.match(await request(ready,destination,'SECRET'),/^HTTP\/1.1 403/);assert.equal(connections,0);
|
||||
assert.match(await request(ready,destination,'public'),/^HTTP\/1.1 201/);assert.deepEqual(received,['public']);
|
||||
const exit=bounded(once(worker,'exit'));worker.disconnect();await exit;
|
||||
const failure=await new Promise(resolve=>{const socket=net.connect(ready.port,'127.0.0.1');socket.setTimeout(1000,()=>{socket.destroy();resolve('timeout');});socket.on('connect',()=>{socket.destroy();resolve('connected');});socket.on('error',error=>resolve(error.code));});
|
||||
assert.equal(failure,'ECONNREFUSED');
|
||||
});
|
||||
test('invalid and oversized private bootstrap reject without spawning another process',async t=>{
|
||||
for(const message of [{type:'command',command:'arbitrary'},{type:'start',config:{destination:'x'.repeat(70000)}}]) {
|
||||
const worker=child(t),reply=bounded(once(worker,'message')),exit=bounded(once(worker,'exit'));
|
||||
worker.send(message);assert.deepEqual((await reply)[0],{type:'error',code:'invalid-message'});await exit;
|
||||
}
|
||||
});
|
||||
test('explicit private stop closes the sole child without restart',async t=>{
|
||||
const worker=child(t),ready=bounded(once(worker,'message'));
|
||||
worker.send({type:'start',config:{destination:'localhost:443'}});assert.equal((await ready)[0].type,'ready');
|
||||
const exit=bounded(once(worker,'exit'));worker.send({type:'stop'});await exit;
|
||||
assert.equal(worker.exitCode,0);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
import {randomUUID,generateKeyPairSync} from 'node:crypto';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{runProductionProcess}=require('../apps/network-broker/production-entry.cjs'),{createProductionBroker}=require('../apps/network-broker/production-factory.cjs'),{createBrokerAudit}=require('../apps/network-broker/audit-gate.cjs');
|
||||
class Channel extends EventEmitter{connected=true;sent=[];send(value,callback){this.sent.push(value);callback?.();this.emit('sent',value);}disconnect(){this.connected=false;this.emit('disconnect');}}
|
||||
const tick=()=>new Promise(resolve=>setImmediate(resolve));
|
||||
const start=()=>({type:'start',version:1,generation:randomUUID(),policyVersion:1,config:{brokerPort:43100}});
|
||||
|
||||
test('close retains late original factory ownership and cleans once before disconnect',async()=>{
|
||||
const channel=new Channel();let releaseFactory,releaseCleanup,closeCalls=0,listens=0;
|
||||
const created={close:()=>{closeCalls++;return new Promise(resolve=>{releaseCleanup=resolve;});},listen:()=>{listens++;return 43100;}};
|
||||
const handle=runProductionProcess(()=>new Promise(resolve=>{releaseFactory=resolve;}),channel);
|
||||
channel.emit('message',start());await tick();const closing=handle.close();assert.equal(handle.close(),closing);
|
||||
await tick();assert.equal(channel.connected,true);releaseFactory(created);await tick();
|
||||
assert.equal(closeCalls,1);assert.equal(listens,0);assert.equal(channel.connected,true);
|
||||
releaseCleanup();await closing;assert.equal(channel.connected,false);assert.equal(channel.sent.length,0);
|
||||
});
|
||||
test('real cleanup failure is sticky and cannot be reported as successful close',async()=>{
|
||||
const channel=new Channel();let calls=0;
|
||||
const handle=runProductionProcess(async()=>({authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)},listen:async()=>43100,close:async()=>{calls++;throw Error('fixed-cleanup-failure');}}),channel);
|
||||
channel.emit('message',start());await tick();const closing=handle.close();await assert.rejects(closing,/fixed-cleanup-failure/);
|
||||
assert.equal(handle.close(),closing);await assert.rejects(handle.close(),/fixed-cleanup-failure/);assert.equal(calls,1);assert.equal(channel.connected,false);
|
||||
});
|
||||
test('close waits original listen settlement and never sends late readiness',async()=>{
|
||||
const channel=new Channel();let releaseListen,closed=0,finished=false;
|
||||
const handle=runProductionProcess(async()=>({listen:()=>new Promise(resolve=>{releaseListen=resolve;}),close:async()=>closed++}),channel);
|
||||
channel.emit('message',start());await tick();const closing=handle.close().then(()=>{finished=true;});await tick();
|
||||
assert.equal(closed,1);assert.equal(finished,false);assert.equal(channel.connected,true);
|
||||
releaseListen(43100);await closing;assert.equal(channel.connected,false);assert.equal(channel.sent.length,0);
|
||||
});
|
||||
test('private v1 IPC requires matching durable receipt before factory continues',async()=>{
|
||||
const channel=new Channel(),message=start();let callbacks,closed=0;
|
||||
runProductionProcess(async(_config,value)=>{callbacks=value;return {authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)},listen:async()=>43100,close:async()=>closed++};},channel);
|
||||
channel.emit('message',message);await tick();assert.equal(channel.sent[0].type,'ready');
|
||||
const event={eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:1,request:{bytes:3,detectedType:'text'}};
|
||||
let completed=false;const pending=callbacks.persistAudit(event).then(()=>completed=true);await tick();assert.equal(completed,false);const request=channel.sent[1];assert.equal(request.type,'audit');
|
||||
channel.emit('message',{type:'audit-receipt',version:1,generation:message.generation,policyVersion:1,requestId:request.requestId,sequence:request.sequence,receipt:{persisted:true,eventId:event.eventId}});await pending;
|
||||
channel.emit('message',{type:'stop',version:1,generation:message.generation,policyVersion:1});await tick();assert.equal(closed,1);
|
||||
});
|
||||
test('wrong generation or uncertain receipt closes child with no automatic retry',async()=>{
|
||||
const channel=new Channel(),message=start();let callbacks;
|
||||
runProductionProcess(async(_config,value)=>{callbacks=value;return {authMode:'basic',credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)},listen:async()=>43100,close:async()=>{}};},channel);
|
||||
channel.emit('message',message);await tick();const pending=callbacks.acknowledgeCertificate({authority:'example.com:443',cert:'public certificate'});const rejected=assert.rejects(pending,/broker-parent-unavailable/);const request=channel.sent[1];
|
||||
channel.emit('message',{type:'certificate-receipt',version:1,generation:randomUUID(),policyVersion:1,requestId:request.requestId,sequence:request.sequence,accepted:true});await rejected;await tick();assert.equal(channel.connected,false);assert.equal(channel.sent.length,2);
|
||||
});
|
||||
test('real factory rejects untrusted paths and unsupported signed proxy before inspector launch',async()=>{
|
||||
const keys=generateKeyPairSync('ed25519'),p=defaultPolicy();p.assignment={deviceId:'device',groups:[]};
|
||||
const config={publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'device',minimumVersion:1,envelope:signPolicy(p,keys.privateKey.export({type:'pkcs8',format:'pem'})),brokerPort:43100,network:{mode:'direct',dns:'system'}};
|
||||
const callbacks={persistAudit:async()=>{},acknowledgeCertificate:async()=>true,expectedPolicyVersion:1};
|
||||
await assert.rejects(createProductionBroker({...config,resourceRoot:'C:/arbitrary'},callbacks),/broker-bootstrap-invalid/);
|
||||
await assert.rejects(createProductionBroker(config,callbacks),/broker-route-unsupported/);
|
||||
await assert.rejects(createProductionBroker({...config,network:{mode:'system',dns:'system'}},callbacks),/broker-bootstrap-invalid/);
|
||||
});
|
||||
test('audit callback cannot mutate frozen request causation',async()=>{
|
||||
const eventId=randomUUID();let event;
|
||||
const audit=createBrokerAudit({current:()=>true,policyVersion:1,persist:async value=>{event=value;assert.throws(()=>value.broker.requestId=eventId,TypeError);return {persisted:true,eventId:value.eventId};}});
|
||||
const requestId=randomUUID();await audit.record('allow',{bytes:3,detectedType:'text',broker:{requestId,phase:'request.authorized',reason:'broker-approved'}});assert.equal(event.broker.requestId,requestId);
|
||||
});
|
||||
|
||||
test('protected deployment CA bundle rejects noncertificate payload and preserves default roots',()=>{
|
||||
const {deploymentRoots}=require('../apps/network-broker/production-factory.cjs'),fs=require('node:fs'),tls=require('node:tls');const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url),'utf8');
|
||||
assert.equal(deploymentRoots(undefined),undefined);assert.equal(deploymentRoots(cert).length,tls.rootCertificates.length+1);
|
||||
for(const value of [cert+'\nPRIVATE PAYLOAD',cert+'\n-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----',Array(33).fill(cert).join('\n'),'-----BEGIN CERTIFICATE-----\nbroken\n-----END CERTIFICATE-----'])assert.throws(()=>deploymentRoots(value),/broker-bootstrap-invalid/);
|
||||
});
|
||||
|
||||
test('startup diagnosis is one bounded fixed stage without original exception detail',async()=>{
|
||||
const channel=new Channel();runProductionProcess(async()=>{throw Error('broker-startup-transport');},channel);channel.emit('message',start());await tick();assert.equal(channel.sent.length,1);assert.equal(channel.sent[0].type,'startup-fault');assert.equal(channel.sent[0].code,'broker-startup-transport');assert.equal(channel.connected,false);
|
||||
const unsafe=new Channel();runProductionProcess(async()=>{throw Error('private certificate secret');},unsafe);unsafe.emit('message',start());await tick();assert.equal(unsafe.sent[0].code,'broker-startup-failed');assert.equal(JSON.stringify(unsafe.sent).includes('private certificate'),false);
|
||||
});
|
||||
|
||||
test('additive deployment roots satisfy actual strict HTTPS transport construction',()=>{
|
||||
const {deploymentRoots}=require('../apps/network-broker/production-factory.cjs'),{createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs'),fs=require('node:fs');
|
||||
const roots=deploymentRoots(fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url),'utf8'));
|
||||
assert.equal(typeof createHttpsForwarder({authority:'current',current:()=>true,resolveRoute:async()=>({approved:true,address:'127.0.0.1',port:443}),ca:roots.join('\n')}),'function');
|
||||
});
|
||||
|
||||
test('signed proxy and protected network mode cannot silently diverge or bypass',async()=>{
|
||||
const keys=generateKeyPairSync('ed25519'),p=defaultPolicy();p.assignment={deviceId:'device',groups:[]};p.proxy={mode:'fixed_servers',rules:'http://proxy.example',bypass:'*'};
|
||||
const config={publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'device',minimumVersion:1,envelope:signPolicy(p,keys.privateKey.export({type:'pkcs8',format:'pem'})),brokerPort:43100,network:{mode:'fixed_servers',dns:'system',credentials:{username:'synthetic',password:'synthetic-password'}}},callbacks={persistAudit:async()=>{},acknowledgeCertificate:async()=>true,expectedPolicyVersion:1};
|
||||
await assert.rejects(createProductionBroker(config,callbacks),/broker-route-unsupported/);await assert.rejects(createProductionBroker({...config,network:{mode:'direct',dns:'system'}},callbacks),/broker-route-unsupported/);
|
||||
await assert.rejects(createProductionBroker({...config,network:{...config.network,credentials:{username:'bad:username',password:'synthetic'}}},callbacks),/broker-bootstrap-invalid/);
|
||||
});
|
||||
|
||||
test('protected additive corporate CA bundles satisfy both strict transport constructors',()=>{
|
||||
const {deploymentRoots}=require('../apps/network-broker/production-factory.cjs'),{createProxyForwarder}=require('../apps/network-broker/proxy-forwarder.cjs'),fs=require('node:fs');const ca=deploymentRoots(fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url),'utf8')).join('\n');
|
||||
assert.equal(typeof createProxyForwarder({authority:'current',current:()=>true,proxyPolicy:{mode:'fixed_servers',rules:'https://proxy.example:443',bypass:''},resolveProxyRoute:async()=>({approved:true,address:'127.0.0.1',port:443}),ca,proxyCa:ca}),'function');
|
||||
});
|
||||
test('unsupported secure DNS fails before inspector startup',async()=>{
|
||||
const keys=generateKeyPairSync('ed25519');
|
||||
for(const proxy of [false]){const p=defaultPolicy();p.assignment={deviceId:'device',groups:[]};p.enterprise.dns={mode:'secure',templates:['https://resolver.example/dns-query']};
|
||||
const config={publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'device',minimumVersion:1,envelope:signPolicy(p,keys.privateKey.export({type:'pkcs8',format:'pem'})),brokerPort:43100,network:{mode:proxy?'fixed_servers':'direct',dns:'system'}};
|
||||
await assert.rejects(createProductionBroker(config,{persistAudit:async()=>{},acknowledgeCertificate:async()=>true,expectedPolicyVersion:1}),/broker-route-unsupported/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import tls from 'node:tls';
|
||||
import {once} from 'node:events';
|
||||
import {generateKeyPairSync,createHash} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
import {verifyBrokerPackage} from '../scripts/broker-package-contract.mjs';
|
||||
const require=createRequire(import.meta.url),{createBrokerSupervisor}=require('../apps/browser/broker-supervisor.cjs');
|
||||
const {createBrokerCertificatePins}=require('../apps/browser/broker-certificate-pins.cjs');
|
||||
const {createFixtureDiagnostics}=require('./fixtures/broker-native-diagnostics.cjs');
|
||||
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
|
||||
function pdf(text){
|
||||
const stream=`BT /F1 12 Tf 72 720 Td <${Buffer.from(text).toString('hex')}> Tj ET`;
|
||||
const objects=['<< /Type /Catalog /Pages 2 0 R >>','<< /Type /Pages /Kids [3 0 R] /Count 1 >>','<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>','<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`];
|
||||
let output='%PDF-1.4\n';const offsets=[0];objects.forEach((object,index)=>{offsets.push(Buffer.byteLength(output));output+=`${index+1} 0 obj\n${object}\nendobj\n`;});
|
||||
const xref=Buffer.byteLength(output);output+=`xref\n0 6\n0000000000 65535 f \n${offsets.slice(1).map(offset=>String(offset).padStart(10,'0')+' 00000 n \n').join('')}trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;return Buffer.from(output);
|
||||
}
|
||||
test('real verified production child private IPC delivers public bytes and blocks signed secret before TCP',{skip:process.env.SAFETYNET_NATIVE_PRODUCTION_BROKER!=='1'},async t=>{
|
||||
const root=path.resolve(process.env.SAFETYNET_TEST_BROKER_ROOT||'.runtime/windows/broker-payload');
|
||||
const manifest=JSON.parse(fs.readFileSync(root+'.manifest.json','utf8'));
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
const proxyMode=process.env.SAFETYNET_NATIVE_PRODUCTION_PROXY==='1',png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAfElEQVR4nNXOQREAIADDsFL/nocIHlyjIGcbZRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncRIncf4OvLpyqgN9ZSiDcwAAAABJRU5ErkJggg==','base64');
|
||||
const received=[],clientResponses=[],clientBodies=[];const proxyTargets=[];let proxyConnects=0;const proxySockets=new Set();let connections=0,quarantines=0,pins;const leaves=new Map();
|
||||
const receiver=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);assert.equal(req.socket.servername,'localhost');assert.equal(req.headers['proxy-authorization'],undefined);assert.equal(req.headers['accept-encoding'],'identity');received.push(Buffer.concat(chunks));if(req.url==='/asset.png'){res.writeHead(200,{'content-type':'image/png'});res.end(png);}else if(req.url.startsWith('/download-')){res.writeHead(200,{'content-type':'application/pdf','content-disposition':(req.url==='/download-attachment'?'attachment':'inline')+'; filename=report.pdf'});res.end(pdf(req.url==='/download-secret'?'SYNTHETIC_PRIVATE':'Public document'));}else{res.writeHead(201);res.end('accepted');}});receiver.on('connection',()=>connections++);receiver.listen(0,'::');await once(receiver,'listening');
|
||||
const corporate=https.createServer({key,cert});corporate.on('connection',socket=>{proxySockets.add(socket);socket.once('close',()=>proxySockets.delete(socket));});corporate.on('connect',(request,socket,head)=>{proxyConnects++;proxyTargets.push(request.url);const target=require('../apps/network-broker/tls-authority.cjs').parseAuthority(request.url);if(!['127.0.0.1','::1'].includes(target.hostname)||target.port!==receiver.address().port||request.headers['proxy-authorization']!=='Basic '+Buffer.from('proxy-user:proxy-password').toString('base64')){socket.end('HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n');return;}const origin=net.connect(target.port,target.hostname);proxySockets.add(origin);origin.once('close',()=>proxySockets.delete(origin));origin.on('error',()=>socket.destroy());socket.on('error',()=>origin.destroy());socket.once('close',()=>origin.destroy());origin.once('connect',()=>{socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)origin.write(head);socket.pipe(origin);origin.pipe(socket);});});if(proxyMode){corporate.listen(0,'::');await once(corporate,'listening');}
|
||||
const reserved=net.createServer();reserved.listen(0,'127.0.0.1');await once(reserved,'listening');const port=reserved.address().port;await new Promise(resolve=>reserved.close(resolve));
|
||||
const destination=`localhost:${receiver.address().port}`,audit=createSyntheticAudit(t),keys=generateKeyPairSync('ed25519'),policy=defaultPolicy();policy.assignment={deviceId:'native-production',groups:[]};policy.proxy=proxyMode?{mode:'fixed_servers',rules:`https://localhost:${corporate.address().port}`,bypass:''}:{mode:'direct'};policy.security.dns.blockedIPs=['192.0.2.254'];policy.browser.allowDownloads=false;policy.fileTransfers.download.allowWithoutMalwareScanner=true;policy.dlp.rules=[{id:'production-secret',label:'Synthetic',kind:'literal',value:'SYNTHETIC_PRIVATE',action:'block'}];
|
||||
const diagnostics=await createFixtureDiagnostics({enabled:process.env.SAFETYNET_NATIVE_FIXTURE_DIAGNOSTICS==='1',policy,audit});let diagnosticProof;
|
||||
const supervisor=createBrokerSupervisor({resourceRoot:root,verifyPackage:async value=>{await verifyBrokerPackage(value,manifest,{version:manifest.version,protocol:manifest.protocol});return true;},persistAudit:diagnostics.persist,acceptCertificate:async value=>{assert.equal(value.authority,destination);assert.equal(pins.accept(value),true);leaves.set(value.authority,value.cert);return true;},onQuarantine:()=>{quarantines++;t.diagnostic('fixed-startup-code:'+String(supervisor.status().startupCode||'unclassified'));},deadlineMs:45000});
|
||||
t.after(async()=>{try{await supervisor.close();}finally{receiver.closeAllConnections();for(const socket of proxySockets)socket.destroy();if(proxyMode)await new Promise(resolve=>corporate.close(resolve));await new Promise(resolve=>receiver.close(resolve));diagnosticProof=await diagnostics.finish();const directory=path.resolve('artifacts/broker-production-native');fs.mkdirSync(directory,{recursive:true});fs.writeFileSync(path.join(directory,'proof-'+new Date().toISOString().replace(/[:]/g,'-')+'.json'),JSON.stringify({kind:'actual-production-child',stage:path.basename(root),networkMode:policy.proxy.mode,proxyConnects,proxyTargets,diagnostics:diagnosticProof,status:supervisor.status(),connections,receivedBytes:received.map(value=>value.length),quarantines,events:audit.events().map(value=>({eventId:value.eventId,kind:value.kind,ruleId:value.ruleId,broker:value.broker,...(value.traceId?{traceId:value.traceId,spanId:value.spanId}:{}),...(value.file?{file:value.file}:{})}))},null,2));}});
|
||||
const ready=await supervisor.start({policyVersion:policy.version,port,config:{publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'native-production',minimumVersion:1,envelope:signPolicy(policy,keys.privateKey.export({type:'pkcs8',format:'pem'})),brokerPort:port,network:{mode:policy.proxy.mode,dns:'system',ca:cert.toString(),...(proxyMode?{proxyCa:cert.toString(),credentials:{username:'proxy-user',password:'proxy-password'}}:{})}}});
|
||||
pins=createBrokerCertificatePins({generation:ready.generation,policyVersion:ready.policyVersion,current:()=>supervisor.status().ready&&supervisor.status().generation===ready.generation,authorizedAuthority:value=>value===destination});
|
||||
// Production uses one30s total request budget (inspector worker29s);35s
|
||||
// client limit leaves only a bounded transport/audit margin. Prior5s failures
|
||||
// remain preserved rather than being reported as scanner verdicts.
|
||||
const sendRaw=(body,route='/upload',headers={})=>new Promise((resolve,reject)=>{
|
||||
const auth=Buffer.from(`${ready.credentials.username}:${ready.credentials.password}`).toString('base64');
|
||||
const request=http.request({host:'127.0.0.1',port,method:'CONNECT',path:destination,headers:{host:destination,'proxy-authorization':'Basic '+auth}});request.setTimeout(35000,()=>request.destroy(Error('deadline')));request.on('error',reject);
|
||||
request.on('connect',(response,socket)=>{if(response.statusCode!==200){socket.destroy();resolve(response.statusCode);return;}assert.ok(leaves.has(destination));const secure=tls.connect({socket,servername:'localhost',ca:leaves.get(destination),rejectUnauthorized:true});let output='';const chunks=[];secure.setTimeout(35000,()=>secure.destroy(Error('deadline')));secure.on('error',reject);secure.on('secureConnect',()=>secure.write(`POST ${route} HTTP/1.1\r\nHost: ${destination}\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${Object.entries(headers).map(([name,value])=>name+': '+value+'\r\n').join('')}Connection: close\r\n\r\n${body}`));secure.on('data',chunk=>{output+=chunk;chunks.push(chunk);});secure.on('end',()=>{clientResponses.push(output);const bytes=Buffer.concat(chunks);clientBodies.push(bytes.subarray(bytes.indexOf('\r\n\r\n')+4));resolve(Number(/^HTTP\/1.1 (\d+)/.exec(output)?.[1]));});});request.end();
|
||||
});
|
||||
const send=(body,route='/upload',headers={})=>diagnostics.run({bytes:Buffer.byteLength(body),policyVersion:policy.version},()=>sendRaw(body,route,headers));
|
||||
assert.equal(await send('public synthetic'),201);assert.equal(connections,1);assert.deepEqual(received.map(value=>value.toString()),['public synthetic']);
|
||||
assert.equal(await send('SYNTHETIC_PRIVATE'),403);assert.equal(connections,1);assert.equal(received.length,1);assert.equal(quarantines,0);
|
||||
const secretDocument=pdf('SYNTHETIC_PRIVATE'),encodedSecret=JSON.stringify({file:secretDocument.toString('base64')}),secretURI=JSON.stringify({file:encodeURIComponent('data:application/pdf;base64,'+secretDocument.toString('base64'))});
|
||||
assert.equal(await send(encodedSecret,'/upload',{'Content-Type':'application/json'}),403);assert.equal(await send(secretURI,'/upload',{'Content-Type':'application/json'}),403);assert.equal(connections,1);
|
||||
const publicOuter=JSON.stringify({file:pdf('Public encoded document').toString('base64')});assert.equal(await send(publicOuter,'/upload',{'Content-Type':'application/json'}),201);assert.equal(received[1].toString(),publicOuter);
|
||||
assert.equal(await send('','/asset.png',{'Accept-Encoding':'gzip, br'}),200);assert.deepEqual(clientBodies.at(-1),png);
|
||||
assert.equal(await send('','/download-attachment'),502);assert.equal(clientBodies.at(-1).includes(Buffer.from('%PDF-')),false);
|
||||
assert.equal(await send('','/download-public'),200);assert.equal(clientResponses.at(-1).split('\r\n\r\n')[1],pdf('Public document').toString());
|
||||
assert.equal(await send('','/download-secret'),502);assert.equal(clientResponses.at(-1).includes('%PDF-'),false);assert.equal(clientResponses.at(-1).includes(Buffer.from('SYNTHETIC_PRIVATE').toString('hex')),false);
|
||||
const events=audit.events();assert.equal(events.length,13);assert.equal(events[10].kind,'download.scan');assert.equal(events[12].kind,'download.block');assert.equal(events[12].ruleId,'production-secret');assert.equal(events[12].file.bytes,pdf('SYNTHETIC_PRIVATE').length);assert.equal(events[12].file.sha256,createHash('sha256').update(pdf('SYNTHETIC_PRIVATE')).digest('hex'));assert.equal(events[10].file.sha256,createHash('sha256').update(pdf('Public document')).digest('hex'));assert.equal(events[12].broker.authorizedEventId,events[11].eventId);assert.equal(events[8].broker.reason,'broker-download-policy-block');assert.equal(events[6].file.sha256,createHash('sha256').update(png).digest('hex'));assert.equal(events[2].ruleId,'production-secret');assert.equal(events[3].ruleId,'production-secret');assert.equal(events[1].ruleId,'production-secret');assert.equal(events[1].broker.phase,'request.block');assert.equal(JSON.stringify(events).includes('SYNTHETIC_PRIVATE'),false);if(proxyMode)assert.equal(proxyConnects,connections);
|
||||
await supervisor.close();assert.equal(supervisor.status().closed,true);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';import https from 'node:https';import net from 'node:net';import fs from 'node:fs';import {once} from 'node:events';import {createRequire} from 'node:module';
|
||||
const {createProxyForwarder,parseFixedProxy}=createRequire(import.meta.url)('../apps/network-broker/proxy-forwarder.cjs');
|
||||
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
|
||||
async function fixture(t,{rejectAuth=false,ca=cert,current=()=>true,tlsProxy=false,resolveOriginRoute}={}){
|
||||
let originRequests=0,connects=0,proxyAuth,originAuth,connectTarget,originSni,proxyTcp=0;const sockets=new Set();
|
||||
const origin=https.createServer({cert,key},(req,res)=>{originRequests++;originAuth=req.headers['proxy-authorization'];res.end('public');});origin.on('tlsClientError',()=>{});origin.listen(0,'127.0.0.1');await once(origin,'listening');
|
||||
origin.on('secureConnection',s=>{originSni=s.servername;});
|
||||
const proxy=tlsProxy?https.createServer({cert,key}):http.createServer();proxy.on('connect',(req,socket,head)=>{connects++;connectTarget=req.url;proxyAuth=req.headers['proxy-authorization'];if(rejectAuth){socket.end('HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n');return;}const upstream=net.connect(origin.address().port,'127.0.0.1',()=>{socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)upstream.write(head);socket.pipe(upstream);upstream.pipe(socket);});sockets.add(upstream);upstream.on('error',()=>socket.destroy());});proxy.on('connection',s=>{proxyTcp++;sockets.add(s);});proxy.listen(0,'127.0.0.1');await once(proxy,'listening');
|
||||
t.after(async()=>{for(const socket of sockets)socket.destroy();origin.closeAllConnections();await Promise.all([new Promise(r=>proxy.close(r)),new Promise(r=>origin.close(r))]);});
|
||||
const forward=createProxyForwarder({authority:'p',current,proxyPolicy:{mode:'fixed_servers',rules:`${tlsProxy?"https":"http"}://localhost:${proxy.address().port}`,bypass:''},resolveProxyRoute:async d=>({approved:true,address:'127.0.0.1',port:d.port}),resolveOriginRoute,credentials:{username:'corp',password:'synthetic'},ca:ca===null?undefined:ca,proxyCa:cert,deadlineMs:1000});
|
||||
return {forward,request:{method:'POST',url:`https://localhost:${origin.address().port}/`,headers:{'proxy-authorization':'must-not-reach-origin'}},get originRequests(){return originRequests;},get connects(){return connects;},get proxyAuth(){return proxyAuth;},get originAuth(){return originAuth;},get connectTarget(){return connectTarget;},get originSni(){return originSni;},get proxyTcp(){return proxyTcp;}};
|
||||
}
|
||||
test('real CONNECT proxy carries verified HTTPS and credentials stay proxy-only',async t=>{const f=await fixture(t);const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.originRequests,1);assert.equal(f.connects,1);assert.equal(f.proxyAuth,'Basic '+Buffer.from('corp:synthetic').toString('base64'));assert.equal(f.originAuth,undefined);});
|
||||
test('proxy authentication rejection never falls back direct',async t=>{const f=await fixture(t,{rejectAuth:true});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.originRequests,0);assert.equal(f.connects,1);});
|
||||
test('untrusted origin TLS prevents HTTP release',async t=>{const f=await fixture(t,{ca:null});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.originRequests,0);});
|
||||
test('revoked authority never dials proxy or origin',async t=>{const f=await fixture(t,{current:()=>false});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.connects,0);assert.equal(f.originRequests,0);});
|
||||
test('unsupported PAC fallback and bypass fail closed',()=>{for(const rules of ['socks://a:1080','http://a:80;http://b:80','a:80'])assert.throws(()=>parseFixedProxy({mode:'fixed_servers',rules,bypass:''}));assert.throws(()=>parseFixedProxy({mode:'fixed_servers',rules:'http://a:80',bypass:'*'}));});
|
||||
|
||||
|
||||
test('real TLS proxy verifies outer endpoint and origin independently',async t=>{const f=await fixture(t,{tlsProxy:true});const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.originRequests,1);});
|
||||
test('timed out resolver retains admission until actual settlement',async()=>{
|
||||
let settle;const pending=new Promise(resolve=>{settle=resolve;});
|
||||
const forward=createProxyForwarder({authority:'p',current:()=>true,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveProxyRoute:()=>pending,deadlineMs:5});
|
||||
const arguments_={request:{method:'POST',url:'https://localhost:443/',headers:{}},body:Buffer.from('public')};
|
||||
const requests=Array.from({length:8},()=>assert.rejects(forward(arguments_),{code:'upstream-cancelled'}));await Promise.all(requests);
|
||||
await assert.rejects(forward(arguments_),{code:'proxy-capacity-exhausted'});settle({approved:false});await new Promise(resolve=>setImmediate(resolve));
|
||||
await assert.rejects(forward(arguments_),{code:'proxy-route-unapproved'});
|
||||
});
|
||||
|
||||
test('proxy URI defaults follow scheme and CA inputs are typed',()=>{
|
||||
assert.equal(parseFixedProxy({mode:'fixed_servers',rules:'http://proxy.example',bypass:''}).port,80);
|
||||
assert.equal(parseFixedProxy({mode:'fixed_servers',rules:'https://proxy.example',bypass:''}).port,443);
|
||||
assert.throws(()=>createProxyForwarder({authority:'p',current:()=>true,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveProxyRoute:()=>{},ca:{untrusted:true}}));
|
||||
});
|
||||
test('approved numeric origin CONNECT preserves original TLS hostname and exact port',async t=>{const f=await fixture(t,{tlsProxy:true,resolveOriginRoute:async r=>({approved:true,address:'127.0.0.1',port:r.port})});const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.connectTarget,'127.0.0.1:'+new URL(f.request.url).port);assert.equal(f.originSni,'localhost');});
|
||||
test('blocked resolved origin prevents even proxy TCP and never falls back to hostname',async t=>{const {createDnsPolicyRoute}=createRequire(import.meta.url)('../apps/network-broker/dns-policy-route.cjs');const f=await fixture(t,{resolveOriginRoute:createDnsPolicyRoute({current:()=>true,blocked:a=>a==='127.0.0.1',lookup:async()=>[{address:'127.0.0.1',family:4}]})});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.proxyTcp,0);assert.equal(f.connects,0);assert.equal(f.originRequests,0);});
|
||||
test('origin resolver abort retains original slot and revocation prevents proxy dial',async()=>{let settle,active=true;const pending=new Promise(r=>{settle=r;});let proxyLookups=0;const forward=createProxyForwarder({authority:'p',current:()=>active,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveOriginRoute:()=>pending,resolveProxyRoute:async()=>{proxyLookups++;return {approved:false};},deadlineMs:5});const args={request:{method:'POST',url:'https://localhost:443/',headers:{}},body:Buffer.from('public')};await Promise.all(Array.from({length:8},()=>assert.rejects(forward(args),{code:'upstream-cancelled'})));active=false;await assert.rejects(forward(args),{code:'proxy-capacity-exhausted'});settle({approved:true,address:'127.0.0.1',port:443});await new Promise(r=>setImmediate(r));assert.equal(proxyLookups,0);await assert.rejects(forward(args),{code:'authority-expired'});});
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,writeFile,rm,symlink} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {BROKER_RUNTIME,verifyOfficialChecksums,verifyBrokerRuntime} from '../scripts/provision-broker-runtime.mjs';
|
||||
const checksums=`${BROKER_RUNTIME.archiveSha256} ${BROKER_RUNTIME.archive}\n${BROKER_RUNTIME.executableSha256} win-x64/node.exe\n`;
|
||||
async function fixture(t){
|
||||
const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-node-runtime-'));t.after(()=>rm(root,{force:true,recursive:true}));
|
||||
const manifest={format:'safetynet-node-broker-runtime-v1',version:BROKER_RUNTIME.version,platform:'win32',arch:'x64',archiveSha256:BROKER_RUNTIME.archiveSha256,files:[{path:'node.exe',bytes:9,sha256:BROKER_RUNTIME.executableSha256},{path:'LICENSE',bytes:7,sha256:BROKER_RUNTIME.licenseSha256}]};
|
||||
await writeFile(path.join(root,'runtime-manifest.json'),JSON.stringify(manifest));
|
||||
await writeFile(path.join(root,'node.exe'),'synthetic');await writeFile(path.join(root,'LICENSE'),'license');
|
||||
return {root,manifest};
|
||||
}
|
||||
test('official checksums must match reviewed archive and standalone executable pins',()=>{
|
||||
assert.doesNotThrow(()=>verifyOfficialChecksums(checksums));
|
||||
for(const text of [checksums.replace(BROKER_RUNTIME.archiveSha256,'0'.repeat(64)),checksums+checksums,checksums.replace('win-x64/node.exe','win-arm64/node.exe'),'x'.repeat(65537)])assert.throws(()=>verifyOfficialChecksums(text));
|
||||
});
|
||||
test('edited manifest cannot relabel wrong runtime version architecture or archive',async t=>{
|
||||
const {root,manifest}=await fixture(t);
|
||||
for(const change of [{version:'24.22.0'},{arch:'arm64'},{archiveSha256:'0'.repeat(64)},{files:[manifest.files[0],manifest.files[0]]}]){
|
||||
await writeFile(path.join(root,'runtime-manifest.json'),JSON.stringify({...manifest,...change}));
|
||||
await assert.rejects(verifyBrokerRuntime(root),{code:'broker_runtime_manifest_invalid'});
|
||||
}
|
||||
});
|
||||
test('synthetic executable cannot be accepted as official runtime even with shaped manifest',async t=>{
|
||||
const {root}=await fixture(t);await assert.rejects(verifyBrokerRuntime(root),{code:'broker_runtime_integrity_mismatch'});
|
||||
});
|
||||
test('unlisted runtime injection is rejected before payload verification',async t=>{
|
||||
const {root}=await fixture(t);await writeFile(path.join(root,'injection.dll'),'private');
|
||||
await assert.rejects(verifyBrokerRuntime(root),{code:'broker_runtime_inventory_mismatch'});
|
||||
});
|
||||
test('runtime directory junction is not trusted as an installation path',async t=>{
|
||||
const {root}=await fixture(t),alias=root+'-alias';t.after(()=>rm(alias,{force:true,recursive:true}));
|
||||
await symlink(root,alias,process.platform==='win32'?'junction':'dir');
|
||||
await assert.rejects(verifyBrokerRuntime(alias),{code:'broker_runtime_path_unsafe'});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {generateKeyPairSync} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{verifyServiceAcceptance}=require('../apps/network-broker/service-acceptance.cjs');
|
||||
const {acceptanceStateDigest,StateSchema}=require('../apps/network-broker/service-acceptance.cjs'),{createServiceCommitGate}=require('../apps/network-broker/service-commit-gate.cjs');
|
||||
const {verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest}=require('../packages/core/windows-trust.cjs');
|
||||
const pub=key=>key.publicKey.export({type:'spki',format:'pem'}),priv=key=>key.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'),verified=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:verified,authenticatedSid:sid});
|
||||
const policy=defaultPolicy();policy.version=7;policy.assignment={deviceId:'device',groups:[]};
|
||||
const input={state:{format:'safetynet-service-floors-v1',release:{minimumSequence:1,acceptedDigest:verified.digest},deployment:{tenant:release.deploymentTenant,minimumSequence:1,acceptedDigest:d.digest},policies:[]},releaseEnvelope,deploymentEnvelope,releaseAnchor:pub(vendor),authenticatedSid:sid,policyEnvelope:signPolicy(policy,priv(key))};
|
||||
return {input,deployment,customer,key,policy};
|
||||
}
|
||||
test('genuine signed acceptance raises all in-memory floors and preserves the protected input',async()=>{
|
||||
const f=fixture(),result=await verifyServiceAcceptance(f.input);assert.equal(result.bootstrap.minimumVersion,7);assert.equal(result.nextState.policies.length,1);assert.equal(result.nextState.policies[0].minimumVersion,7);assert.equal(f.input.state.policies.length,0);assert.ok(Object.isFrozen(result.nextState.policies));
|
||||
const replay=await verifyServiceAcceptance({...f.input,state:result.nextState});assert.deepEqual(replay.nextState,result.nextState);
|
||||
});
|
||||
test('newer deployment cannot lower an accepted policy floor; same policy version cannot equivocate',async()=>{
|
||||
const f=fixture(),accepted=await verifyServiceAcceptance(f.input);f.deployment.deploymentSequence=2;f.deployment.users[0].minimumPolicyVersion=1;
|
||||
const input={...f.input,state:accepted.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment')};f.policy.version=6;
|
||||
await assert.rejects(verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
|
||||
f.policy.version=7;f.policy.brand.name='Changed same version';await assert.rejects(verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
|
||||
f.policy.version=8;const next=await verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))});assert.equal(next.bootstrap.minimumVersion,8);assert.equal(next.nextState.deployment.minimumSequence,2);
|
||||
});
|
||||
test('missing state, duplicate floor identities, unknown peer and absent device assignment fail closed',async()=>{
|
||||
const f=fixture(),accepted=await verifyServiceAcceptance(f.input);
|
||||
for(const input of [{...f.input,state:undefined},{...f.input,state:{...accepted.nextState,policies:[...accepted.nextState.policies,...accepted.nextState.policies]}},{...f.input,authenticatedSid:'S-1-12-1-5-6-7-8'}])await assert.rejects(verifyServiceAcceptance(input),{message:'broker-bootstrap-invalid'});
|
||||
delete f.policy.assignment;await assert.rejects(verifyServiceAcceptance({...f.input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
|
||||
});
|
||||
test('approved key rotation retains old-key floors when that key is selected again',async()=>{
|
||||
const f=fixture(),first=await verifyServiceAcceptance(f.input),replacement=generateKeyPairSync('ed25519');
|
||||
f.deployment.deploymentSequence=2;f.deployment.policyPublicKey=pub(replacement);f.deployment.users[0].minimumPolicyVersion=1;f.policy.version=1;
|
||||
const rotated=await verifyServiceAcceptance({...f.input,state:first.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment'),policyEnvelope:signPolicy(f.policy,priv(replacement))});assert.equal(rotated.nextState.policies.length,2);
|
||||
f.deployment.deploymentSequence=3;f.deployment.policyPublicKey=pub(f.key);f.policy.version=6;
|
||||
await assert.rejects(verifyServiceAcceptance({...f.input,state:rotated.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment'),policyEnvelope:signPolicy(f.policy,priv(f.key))}),error=>error.message==='broker-bootstrap-invalid'&&error.failureStage==='policy');
|
||||
});
|
||||
test('bootstrap gate releases no plan before exact protected commit receipt',async()=>{
|
||||
const f=fixture();let enter,finish,completed=false;const entered=new Promise(resolve=>enter=resolve),hold=new Promise(resolve=>finish=resolve);
|
||||
const gate=createServiceCommitGate({current:()=>true,commitState:async request=>{assert.equal(request.expectedPreviousSHA,acceptanceStateDigest(f.input.state));assert.ok(Object.isFrozen(request.candidateState));enter();await hold;return {persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)};}});
|
||||
const pending=gate.prepare(f.input).then(value=>{completed=true;return value;});await entered;assert.equal(completed,false);finish();const plan=await pending;assert.equal(plan.policyVersion,7);await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});
|
||||
});
|
||||
test('failed or uncertain native commit is sticky without automatic retry',async()=>{
|
||||
const f=fixture();
|
||||
for(const answer of [undefined,true,{persisted:true,stateSha256:'0'.repeat(64)},{persisted:false,stateSha256:'0'.repeat(64)}]){
|
||||
let commits=0;const gate=createServiceCommitGate({current:()=>true,commitState:async()=>{commits++;return answer;}});
|
||||
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});assert.equal(commits,1);
|
||||
}
|
||||
});
|
||||
test('revocation during original commit blocks even a correct persisted receipt',async()=>{
|
||||
const f=fixture();let active=true;
|
||||
const gate=createServiceCommitGate({current:()=>active,commitState:async request=>{active=false;return {persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)};}});
|
||||
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});
|
||||
});
|
||||
test('commit plan composes with service startup and rejects another signed policy at the same version',async()=>{
|
||||
const {createServiceFactory}=require('../apps/network-broker/service-bootstrap.cjs'),f=fixture();
|
||||
const gate=createServiceCommitGate({current:()=>true,commitState:async request=>({persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)})});
|
||||
const plan=await gate.prepare(f.input);let launches=0;
|
||||
const factory=createServiceFactory({loadTrust:async parsed=>{assert.ok(Object.isFrozen(parsed.envelope));return plan;},factory:async()=>{launches++;return {listen:async()=>43120,close:async()=>{}};}});
|
||||
const config={publicKey:pub(f.key),deviceId:'device',minimumVersion:7,envelope:f.input.policyEnvelope,brokerPort:43120,network:{mode:'direct',dns:'system'}},callbacks={expectedPolicyVersion:7};
|
||||
f.policy.brand.name='Differently signed same version';const other=signPolicy(f.policy,priv(f.key));
|
||||
await assert.rejects(factory({...config,envelope:other},callbacks),{message:'broker-bootstrap-invalid'});assert.equal(launches,0);
|
||||
const accepted=await factory(config,callbacks);assert.equal(await accepted.listen(),43120);await accepted.close();assert.equal(launches,1);
|
||||
});
|
||||
test('valid record count cannot bypass total serialized state and CAS frame budget',async()=>{
|
||||
const f=fixture();f.input.state.policies=Array.from({length:8000},(_,index)=>({sid:'S-1-12-1-4294967295-4294967295-4294967295-4294967295',deviceId:'device'+String(index).padStart(74,'0'),policyKeySha256:'a'.repeat(64),minimumVersion:Number.MAX_SAFE_INTEGER,acceptedDigest:'b'.repeat(64)}));
|
||||
assert.doesNotThrow(()=>StateSchema.parse(f.input.state));
|
||||
let commits=0;const gate=createServiceCommitGate({current:()=>true,commitState:async()=>{commits++;}});
|
||||
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});assert.equal(commits,0);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {generateKeyPairSync,createHash} from 'node:crypto';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
const {createServiceFactory}=createRequire(import.meta.url)('../apps/network-broker/service-bootstrap.cjs');
|
||||
function fixture(){
|
||||
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),policy=defaultPolicy();policy.version=4;policy.assignment={deviceId:'managed-device',groups:[]};policy.proxy={mode:'direct'};
|
||||
const config={publicKey,deviceId:'managed-device',minimumVersion:4,envelope:signPolicy(policy,keys.privateKey.export({type:'pkcs8',format:'pem'})),brokerPort:43120,network:{mode:'direct',dns:'system'}};
|
||||
return {config,binding:{publicKey,deviceId:'managed-device',minimumVersion:4}};
|
||||
}
|
||||
const trusted=f=>({bootstrap:f.binding,deployment:{manifest:{expiresAt:Date.now()+60000}},policyVersion:4,policyDigest:createHash('sha256').update(f.config.envelope.payload).digest('hex')});
|
||||
const broker=()=>({listen:async()=>43120,close:async()=>{}});
|
||||
|
||||
test('caller-selected receiver port fails before trust loading or construction',async()=>{
|
||||
const f=fixture();let loads=0,launches=0;
|
||||
const factory=createServiceFactory({loadTrust:async()=>{loads++;return trusted(f);},factory:async()=>{launches++;return broker();}});
|
||||
for(const port of [1,43100,43121,65535])await assert.rejects(factory({...f.config,brokerPort:port},{expectedPolicyVersion:4}),{message:'broker-bootstrap-invalid'});
|
||||
assert.equal(loads,0);assert.equal(launches,0);
|
||||
});
|
||||
test('service bootstrap rejects caller-selected key/device or lower protected floor before factory',async()=>{
|
||||
const f=fixture();let launches=0;const factory=createServiceFactory({loadTrust:async()=>trusted(f),factory:async()=>{launches++;return broker();}});
|
||||
const forged=generateKeyPairSync('ed25519').publicKey.export({type:'spki',format:'pem'});
|
||||
for(const changed of [{publicKey:forged},{deviceId:'other-user-device'},{minimumVersion:3},{minimumVersion:0},{resourceRoot:'C:/caller-selected'}])await assert.rejects(factory({...f.config,...changed},{expectedPolicyVersion:4}),{message:'broker-bootstrap-invalid'});
|
||||
assert.equal(launches,0);
|
||||
});
|
||||
test('trusted SID-derived mapping keeps stricter caller floor and exact callbacks without mutation',async()=>{
|
||||
const f=fixture(),callbacks={expectedPolicyVersion:4};let observed;
|
||||
const factory=createServiceFactory({loadTrust:async()=>trusted(f),factory:async(config,value)=>{observed={config,value};return broker();}});
|
||||
const result=await factory({...f.config,minimumVersion:5},callbacks);assert.equal(await result.listen(),43120);await result.close();assert.equal(observed.config.minimumVersion,5);assert.equal(observed.config.publicKey,f.binding.publicKey);assert.equal(observed.value,callbacks);assert.equal(f.config.minimumVersion,4);
|
||||
});
|
||||
test('missing assignment, invalid trusted floor and failed trust load expose only fixed error',async()=>{
|
||||
const f=fixture();let launches=0;
|
||||
for(const loadTrust of [async()=>({}),async()=>({...trusted(f),bootstrap:{...f.binding,minimumVersion:0}}),async()=>{throw Error('PRIVATE_PATH_OR_KEY');}]){
|
||||
const factory=createServiceFactory({loadTrust,factory:async()=>{launches++;}});
|
||||
await assert.rejects(factory(f.config,{}),error=>error.message==='broker-bootstrap-invalid'&&!String(error).includes('PRIVATE'));
|
||||
}
|
||||
assert.equal(launches,0);
|
||||
});
|
||||
test('real vendor/customer signature chain binds bootstrap to actual supplied peer SID',async()=>{
|
||||
const {verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest}=createRequire(import.meta.url)('../packages/core/windows-trust.cjs');
|
||||
const f=fixture(),vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'});
|
||||
const pub=key=>key.publicKey.export({type:'spki',format:'pem'}),priv=key=>key.privateKey.export({type:'pkcs8',format:'pem'});
|
||||
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 signedRelease=signManifest(release,priv(vendor),'release'),verifiedRelease=verifyReleaseEnvelope(signedRelease,{publicKey:pub(vendor),floor:{minimumSequence:1}});
|
||||
const sid='S-1-12-1-1-2-3-4',deployment={format:'safetynet-windows-deployment-v1',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:1,policyPublicKey:f.binding.publicKey,expiresAt:Date.now()+60000,users:[{sid,deviceId:f.binding.deviceId,minimumPolicyVersion:4}]};
|
||||
let envelope=signManifest(deployment,priv(customer),'deployment'),peer=sid,launches=0;
|
||||
const factory=createServiceFactory({loadTrust:async()=>{const deployment=verifyDeploymentEnvelope(envelope,{release:verifiedRelease,authenticatedSid:peer,floor:{minimumSequence:1}});return {...trusted(f),bootstrap:deployment.bootstrap,deployment};},factory:async()=>{launches++;return broker();}});
|
||||
const result=await factory(f.config,{expectedPolicyVersion:4});assert.equal(await result.listen(),43120);await result.close();
|
||||
peer='S-1-12-1-5-6-7-8';await assert.rejects(factory(f.config,{}),{message:'broker-bootstrap-invalid'});
|
||||
peer=sid;envelope=signManifest(deployment,priv(vendor),'deployment');await assert.rejects(factory(f.config,{}),{message:'broker-bootstrap-invalid'});
|
||||
assert.equal(launches,1);
|
||||
});
|
||||
test('deployment expiration rejects startup and closes an otherwise valid running broker',async()=>{
|
||||
const f=fixture();let closed=0,expired;
|
||||
const stopped=new Promise(resolve=>expired=resolve);
|
||||
const factory=createServiceFactory({loadTrust:async()=>({...trusted(f),deployment:{manifest:{expiresAt:Date.now()+60}}}),factory:async()=>({listen:async()=>43120,close:async()=>{closed++;expired();}})});
|
||||
const result=await factory(f.config,{expectedPolicyVersion:4});assert.equal(await result.listen(),43120);
|
||||
await Promise.race([stopped,new Promise((_,reject)=>{const limit=setTimeout(()=>reject(Error('expiry not enforced')),1000);limit.unref();})]);
|
||||
assert.equal(closed,1);await assert.rejects(result.listen(),{message:'broker-bootstrap-invalid'});await result.close();assert.equal(closed,1);
|
||||
let launches=0;const rejected=createServiceFactory({loadTrust:async()=>({...trusted(f),deployment:{manifest:{expiresAt:Date.now()-1}}}),factory:async()=>{launches++;return broker();}});
|
||||
await assert.rejects(rejected(f.config,{}),{message:'broker-bootstrap-invalid'});assert.equal(launches,0);
|
||||
});
|
||||
test('repeated close waits the original cleanup and preserves its failure',async()=>{
|
||||
const f=fixture();let finish,closes=0;
|
||||
const cleanup=new Promise((_,reject)=>finish=reject);
|
||||
const factory=createServiceFactory({loadTrust:async()=>trusted(f),factory:async()=>({listen:async()=>43120,close:()=>{closes++;return cleanup;}})});
|
||||
const result=await factory(f.config,{expectedPolicyVersion:4}),first=result.close(),second=result.close();assert.equal(first,second);
|
||||
const failure=Error('synthetic cleanup failure'),rejected=assert.rejects(first,error=>error===failure);await new Promise(resolve=>setImmediate(resolve));assert.equal(closes,1);finish(failure);await rejected;await assert.rejects(result.close(),error=>error===failure);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {readFile,writeFile,mkdir,mkdtemp,rm} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {spawn} from 'node:child_process';
|
||||
const require=createRequire(import.meta.url),asar=require('@electron/asar');
|
||||
const {getFileMatchers,copyFiles}=require('app-builder-lib/out/fileMatcher.js');
|
||||
test('actual builder file matching puts only fixed framing dependency in ASAR and isolated adapter loads',async t=>{
|
||||
const repo=process.cwd(),root=await mkdtemp(path.join(os.tmpdir(),'safetynet-service-package-'));t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
const source=path.join(root,'source'),app=path.join(root,'app'),extracted=path.join(root,'isolated');
|
||||
for(const file of ['apps/browser/broker-service-supervisor.cjs','apps/browser/broker-supervisor.cjs','apps/network-broker/framed-channel.cjs']){const target=path.join(source,file);await mkdir(path.dirname(target),{recursive:true});await writeFile(target,await readFile(path.join(repo,file)));}
|
||||
await writeFile(path.join(source,'apps/network-broker/unreviewed-extra.cjs'),'throw Error("must not ship inside browser");');
|
||||
const pkg=JSON.parse(await readFile(path.join(repo,'package.json'),'utf8'));
|
||||
assert.equal(pkg.build.files.filter(value=>value==='apps/network-broker/framed-channel.cjs').length,1);
|
||||
assert.ok(!pkg.build.files.some(value=>typeof value==='string'&&value.startsWith('apps/network-broker/')&&value.includes('*')));
|
||||
const matchers=getFileMatchers(pkg.build,'files',app,{defaultSrc:source,globalOutDir:path.join(root,'out'),macroExpander:value=>value,customBuildOptions:{}});
|
||||
const normal=matchers.filter(matcher=>matcher.from===source);assert.equal(normal.length,1);await copyFiles(normal,null,false);
|
||||
const archive=path.join(root,'app.asar');await asar.createPackage(app,archive);
|
||||
assert.deepEqual(asar.extractFile(archive,path.normalize('apps/network-broker/framed-channel.cjs')),await readFile(path.join(repo,'apps/network-broker/framed-channel.cjs')));
|
||||
assert.ok(!asar.listPackage(archive).some(value=>value.endsWith('unreviewed-extra.cjs')));
|
||||
asar.extractAll(archive,extracted);
|
||||
const script="const value=require('./apps/browser/broker-service-supervisor.cjs');process.stdout.write(JSON.stringify({loaded:typeof value.createServiceBrokerSupervisor==='function'}));";
|
||||
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP'])if(process.env[key])env[key]=process.env[key];
|
||||
const output=await new Promise((resolve,reject)=>{const child=spawn(process.execPath,['-e',script],{cwd:extracted,env,windowsHide:true,timeout:5000});let bytes=0,stdout='';child.stdout.on('data',chunk=>{bytes+=chunk.length;if(bytes>4096){child.kill();reject(Error('package-probe-output-limit'));}else stdout+=chunk;});child.stderr.on('data',()=>{});child.once('error',reject);child.once('close',code=>code===0?resolve(stdout):reject(Error('package-probe-failed')));});
|
||||
assert.deepEqual(JSON.parse(output),{loaded:true});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
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});}
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {createRequire} from 'node:module';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {defaultPolicy} from '../packages/core/policy.mjs';
|
||||
const {createServiceBrokerSupervisor}=createRequire(import.meta.url)('../apps/browser/broker-service-supervisor.cjs');
|
||||
const {createFixtureDiagnostics}=createRequire(import.meta.url)('./fixtures/broker-native-diagnostics.cjs');
|
||||
const {createSyntheticAudit}=createRequire(import.meta.url)('./fixtures/broker-durable-audit.cjs');
|
||||
const root=path.resolve(fileURLToPath(new URL('../',import.meta.url)));
|
||||
function setup(extra={}){
|
||||
let launched,quarantines=0;const events=[];
|
||||
const supervisor=createServiceBrokerSupervisor({installationRoot:root,verifyInstallation:()=>true,verifyPackage:()=>true,persistAudit:event=>{events.push(event);return {persisted:true,eventId:event.eventId};},acceptCertificate:()=>true,onQuarantine:()=>quarantines++,deadlineMs:5000,shutdownMs:5000,
|
||||
launch:(executable,args,options)=>{launched={executable,args,options};return spawn(process.execPath,[fileURLToPath(new URL('./fixtures/broker-service-stdio-child.cjs',import.meta.url))],options);},...extra});
|
||||
return {supervisor,events,get launched(){return launched;},get quarantines(){return quarantines;}};
|
||||
}
|
||||
test('service transport uses fixed native command and real bounded stdio protocol',async()=>{
|
||||
const f=setup();try{
|
||||
const ready=await f.supervisor.start({policyVersion:3,port:43871,config:{port:43871}});
|
||||
assert.equal(ready.port,43871);assert.equal(f.supervisor.status().ready,true);
|
||||
assert.equal(f.launched.executable,path.join(root,'safetynet-service-host.exe'));assert.deepEqual(f.launched.args,['--client-stdio']);
|
||||
assert.equal(f.launched.options.shell,false);assert.equal(f.launched.options.windowsHide,true);assert.deepEqual(f.launched.options.stdio,['pipe','pipe','ignore']);
|
||||
assert.ok(Object.keys(f.launched.options.env).every(key=>['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'].includes(key)));
|
||||
assert.equal(f.launched.options.env.SAFETYNET_SERVICE_CLIENT_SID,undefined);assert.equal(f.launched.options.env.NODE_OPTIONS,undefined);
|
||||
}finally{await f.supervisor.close();}
|
||||
assert.equal(f.quarantines,0);assert.equal(f.supervisor.status().closed,true);
|
||||
});
|
||||
test('real stdio audit roundtrip retains event identity and durable receipt',async()=>{
|
||||
const f=setup();try{
|
||||
await f.supervisor.start({policyVersion:3,port:43871,config:{port:43871,audit:true}});
|
||||
const until=Date.now()+3000;while(!f.events.length&&Date.now()<until)await new Promise(resolve=>setTimeout(resolve,10));
|
||||
assert.equal(f.events.length,1);assert.equal(f.events[0].eventId,'12345678-1234-4234-8234-123456789abc');
|
||||
assert.equal(f.supervisor.status().ready,true);
|
||||
}finally{await f.supervisor.close();}
|
||||
});
|
||||
test('malformed native stream quarantines and cannot restart',async()=>{
|
||||
const f=setup();await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{malformed:true}}),/unavailable/);
|
||||
assert.equal(f.quarantines,1);assert.equal(f.supervisor.status().ready,false);
|
||||
await assert.rejects(f.supervisor.start({policyVersion:3,port:43871}),/invalid/);await f.supervisor.close();
|
||||
});
|
||||
test('unverified installed native client or broker payload never launches',async()=>{
|
||||
for(const extra of [{verifyInstallation:()=>false},{verifyPackage:()=>false}]){
|
||||
const f=setup(extra);await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/unavailable/);assert.equal(f.launched,undefined);await f.supervisor.close();
|
||||
}
|
||||
});
|
||||
test('real service-adapter subprocess exports audit-correlated OTLP timings',async t=>{
|
||||
const audit=createSyntheticAudit(t),diagnostics=await createFixtureDiagnostics({enabled:true,policy:defaultPolicy(),audit});
|
||||
const f=setup({persistAudit:diagnostics.persist,traceOperation:diagnostics.stage});let proof;
|
||||
try{
|
||||
await diagnostics.run({bytes:0},async()=>{
|
||||
await f.supervisor.start({policyVersion:3,port:43871,config:{port:43871,audit:true}});
|
||||
const until=Date.now()+3000;while(!audit.events().length&&Date.now()<until)await new Promise(resolve=>setTimeout(resolve,10));
|
||||
assert.equal(audit.events().length,1);assert.equal(f.supervisor.status().ready,true);
|
||||
await f.supervisor.close();
|
||||
});
|
||||
}finally{try{await f.supervisor.close();}finally{proof=await diagnostics.finish();}}
|
||||
assert.ok(proof.spanCount>=2);assert.equal(proof.invalid,0);assert.equal(proof.failedExports,0);assert.equal(proof.lifetimeFailedExports,0);
|
||||
assert.deepEqual(proof.stages.map(value=>value.name).sort(),['browser.broker.shutdown','browser.broker.start','browser.broker.verify']);
|
||||
assert.ok(proof.stages.every(value=>value.parentSpanId===proof.parentSpanId&&value.durationMs>0));
|
||||
t.diagnostic(JSON.stringify({kind:'actual-service-adapter-test-child',...proof}));
|
||||
});
|
||||
@@ -22,6 +22,17 @@ test('ECS retains actual causal UUIDs and marks terminal forwarding failure as e
|
||||
test('a durable authorization is not reported as completed delivery',()=>{
|
||||
const output=toECS(event('request.authorized'));assert.equal(output.event.outcome,'unknown');assert.deepEqual(output.event.type,['allowed']);
|
||||
});
|
||||
test('typed download records retain actual file evidence and causal authorization',()=>{
|
||||
for(const blocked of [false,true]){
|
||||
const input=event();input.kind=blocked?'download.block':'download.scan';delete input.metadata.request;
|
||||
input.metadata.file={bytes:617,detectedType:'pdf',sha256:'a'.repeat(64)};
|
||||
input.metadata.broker.phase=blocked?'response.block':'response.scanned';input.metadata.broker.reason=blocked?'broker-download-inspection-block':'broker-download-approved';
|
||||
const output=toECS(input),cef=toCEF(input);
|
||||
assert.equal(output.file.size,617);assert.equal(output.file.hash.sha256,'a'.repeat(64));assert.equal(output.event.outcome,blocked?'failure':'success');
|
||||
assert.equal(output.safetynet.broker.authorized_event_id,input.metadata.broker.authorizedEventId);
|
||||
assert.ok(cef.includes('fileHash='+'a'.repeat(64)));assert.ok(cef.includes('outcome='+(blocked?'failure':'success')));
|
||||
}
|
||||
});
|
||||
test('CEF standard custom string preserves all causation without overwriting integrity or type',()=>{
|
||||
const input=event();input.audit={hash:'synthetic-hash',sequence:3};
|
||||
const output=toCEF(input);assert.ok(output.includes('cs2Label=BrokerLifecycle'));
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,mkdir,copyFile,readFile,writeFile,rm} from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {fork} from 'node:child_process';
|
||||
import {createRequire} from 'node:module';
|
||||
import {BROKER_RUNTIME} from '../scripts/provision-broker-runtime.mjs';
|
||||
const {createBrokerSupervisor}=createRequire(import.meta.url)('../apps/browser/broker-supervisor.cjs');
|
||||
test('actual pinned bundled Node private IPC startup and confirmed terminal shutdown',{skip:process.platform!=='win32'||process.env.SAFETYNET_BROKER_SUPERVISOR_NATIVE!=='1'},async()=>{
|
||||
const runtime=path.resolve('.runtime/windows/broker/node.exe');
|
||||
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
assert.equal(hash(await readFile(runtime)),BROKER_RUNTIME.executableSha256);
|
||||
const root=await mkdtemp(path.join(tmpdir(),'safetynet-supervisor-'));
|
||||
let child,exited=false,quarantines=0; const savedOptions=process.env.NODE_OPTIONS; process.env.NODE_OPTIONS="--require C:/safetynet-nonexistent-injection.cjs";
|
||||
try{
|
||||
await mkdir(path.join(root,'apps/network-broker'),{recursive:true});await copyFile(runtime,path.join(root,'node.exe'));
|
||||
const fixture=`'use strict';if(process.version!=='v${BROKER_RUNTIME.version}'||process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.ELECTRON_RUN_AS_NODE)process.exit(91);process.on('message',m=>{if(m.type==='start')process.send({type:'ready',version:1,generation:m.generation,policyVersion:m.policyVersion,authMode:'basic',sequence:0,port:m.config.brokerPort,credentials:{username:'safetynet-'+ 'a'.repeat(32),password:'b'.repeat(43)}});});setInterval(()=>{},1000);`;
|
||||
await writeFile(path.join(root,'apps/network-broker/production-entry.cjs'),fixture);
|
||||
const supervisor=createBrokerSupervisor({resourceRoot:root,verifyPackage:async target=>target===root&&hash(await readFile(path.join(target,'node.exe')))===BROKER_RUNTIME.executableSha256&&await readFile(path.join(target,'apps/network-broker/production-entry.cjs'),'utf8')===fixture,persistAudit:()=>{throw Error();},acceptCertificate:()=>false,onQuarantine:()=>quarantines++,spawn:(...args)=>{assert.equal(args[2].env.NODE_OPTIONS,undefined);child=fork(...args);child.once('exit',()=>{exited=true;});return child;}});
|
||||
const ready=await supervisor.start({policyVersion:1,port:43871,config:{brokerPort:43871}});assert.equal(ready.port,43871);
|
||||
await supervisor.close();assert.equal(exited,true);assert.notEqual(child.exitCode===null&&child.signalCode===null,true);assert.equal(quarantines,0);
|
||||
}finally{
|
||||
if(savedOptions===undefined)delete process.env.NODE_OPTIONS;else process.env.NODE_OPTIONS=savedOptions;
|
||||
if(child&&!exited){child.kill('SIGKILL');await new Promise(resolve=>child.once('exit',resolve));}
|
||||
await rm(root,{recursive:true,force:true});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {createRequire} from 'node:module';
|
||||
const {createBrokerSupervisor}=createRequire(import.meta.url)('../apps/browser/broker-supervisor.cjs');
|
||||
function fixture(extra={}){const child=new EventEmitter();child.sent=[];child.send=(m,cb)=>{child.sent.push(m);cb();};child.kill=()=>{child.killed=true;child.exitCode=0;child.emit('exit',0);};let options,quarantines=0;const supervisor=createBrokerSupervisor({resourceRoot:'C:/trusted/broker',verifyPackage:()=>true,persistAudit:event=>({persisted:true,eventId:event.eventId}),acceptCertificate:()=>true,onQuarantine:()=>quarantines++,spawn:(entry,args,opts)=>{options={entry,args,opts};return child;},deadlineMs:100,...extra});return {child,supervisor,get options(){return options;},get quarantines(){return quarantines;}};}
|
||||
async function starting(f){const pending=f.supervisor.start({policyVersion:3,port:43871,config:{synthetic:true}});await new Promise(resolve=>setImmediate(resolve));return {pending,context:(({version,generation,policyVersion})=>({version,generation,policyVersion}))(f.child.sent[0])};}
|
||||
function ready(f,context){f.child.emit('message',{type:'ready',...context,authMode:'basic',sequence:0,port:43871,credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)}});}
|
||||
test('fixed verified launch, sanitized environment and private authenticated generation',async()=>{const f=fixture();const {pending,context}=await starting(f);ready(f,context);assert.equal((await pending).port,43871);assert.deepEqual(f.options.opts.execArgv,[]);assert.equal(f.options.opts.env.NODE_OPTIONS,undefined);assert.equal(f.options.opts.env.NODE_PATH,undefined);assert.ok(Object.keys(f.options.opts.env).every(key=>['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'].includes(key)));for(const key of ['USERPROFILE','LOCALAPPDATA','APPDATA'])assert.equal(f.options.opts.env[key],process.env[key]);assert.ok(f.options.entry.endsWith('production-entry.cjs'));assert.equal(f.supervisor.status().ready,true);await f.supervisor.close();assert.equal(f.child.killed,true);});
|
||||
test('audit receipt preserves authenticated event ID',async()=>{const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;const eventId='12345678-1234-1234-1234-123456789abc';f.child.emit('message',{type:'audit',...context,requestId:eventId,sequence:1,event:{eventId}});await new Promise(resolve=>setImmediate(resolve));assert.equal(f.child.sent[1].receipt.eventId,eventId);await f.supervisor.close();});
|
||||
test('wrong generation quarantines with no retry',async()=>{const f=fixture();const {pending,context}=await starting(f);const rejected=assert.rejects(pending,/unavailable/);ready(f,{...context,generation:'wrong'});await rejected;assert.equal(f.quarantines,1);assert.equal(f.child.killed,true);await assert.rejects(f.supervisor.start({policyVersion:3,port:43871}),/invalid/);});
|
||||
test('deadline and child crash fail closed',async()=>{const f=fixture({deadlineMs:5});const {pending}=await starting(f);await assert.rejects(pending,/unavailable/);assert.equal(f.quarantines,1);const g=fixture();const {pending:p,context}=await starting(g);ready(g,context);await p;g.child.emit('exit',1);assert.equal(g.supervisor.status().ready,false);assert.equal(g.quarantines,1);});
|
||||
test('unverified package never spawns',async()=>{const f=fixture({verifyPackage:()=>false});await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/unavailable/);assert.equal(f.options,undefined);});
|
||||
|
||||
test('close waits terminal exit and rejects unconfirmed shutdown',async()=>{
|
||||
const f=fixture({shutdownMs:10});const {pending,context}=await starting(f);ready(f,context);await pending;
|
||||
f.child.kill=()=>{f.child.killed=true;};
|
||||
await assert.rejects(f.supervisor.close(),/shutdown-unconfirmed/);
|
||||
assert.equal(f.supervisor.status().failed,true);assert.equal(f.quarantines,1);
|
||||
});
|
||||
test('concurrent start reserves before asynchronous package verification',async()=>{
|
||||
let release;const f=fixture({verifyPackage:()=>new Promise(resolve=>{release=resolve;})});
|
||||
const first=f.supervisor.start({policyVersion:3,port:43871,config:{}});
|
||||
await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/invalid/);
|
||||
release(true);await new Promise(resolve=>setImmediate(resolve));
|
||||
const context=(({version,generation,policyVersion})=>({version,generation,policyVersion}))(f.child.sent[0]);ready(f,context);await first;await f.supervisor.close();
|
||||
});
|
||||
|
||||
test('monotonic stream rejects replay, gaps and out-of-order requests',async()=>{
|
||||
for(const sequences of [[1,1],[2],[1,3],[1,0]]){
|
||||
const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;
|
||||
for(const sequence of sequences)f.child.emit('message',{type:'audit',...context,sequence,requestId:'12345678-1234-1234-1234-123456789abc',event:{eventId:'12345678-1234-1234-1234-123456789abc'}});
|
||||
assert.equal(f.supervisor.status().failed,true);assert.equal(f.quarantines,1);await f.supervisor.close();
|
||||
}
|
||||
});
|
||||
test('more than 10000 legitimate requests retain constant replay state',async()=>{
|
||||
const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;
|
||||
for(let sequence=1;sequence<=10001;sequence++){
|
||||
const requestId=`12345678-1234-1234-1234-${sequence.toString(16).padStart(12,'0')}`;
|
||||
f.child.emit('message',{type:'audit',...context,sequence,requestId,event:{eventId:requestId}});
|
||||
assert.equal(f.child.sent.at(-1).sequence,sequence);
|
||||
}
|
||||
assert.equal(f.supervisor.status().ready,true);assert.equal(f.quarantines,0);await f.supervisor.close();
|
||||
});
|
||||
test('fixed startup diagnostic is local, bounded and never grants readiness',async()=>{
|
||||
const f=fixture();const {pending,context}=await starting(f);const rejection=assert.rejects(pending,/unavailable/);
|
||||
f.child.emit('message',{type:'startup-fault',...context,code:'broker-startup-inspector'});await rejection;
|
||||
assert.equal(f.supervisor.status().startupCode,'broker-startup-inspector');assert.equal(f.supervisor.status().ready,false);await f.supervisor.close();
|
||||
const g=fixture();const started=await starting(g);const rejected=assert.rejects(started.pending,/unavailable/);
|
||||
g.child.emit('message',{type:'startup-fault',...started.context,code:'raw arbitrary exception'});await rejected;assert.equal(g.supervisor.status().startupCode,undefined);await g.supervisor.close();
|
||||
});
|
||||
|
||||
test('diagnostics cannot skip or replace verification and cannot repeat protected work',async()=>{
|
||||
for(const tracer of [()=>true,()=>{throw Error('fixed-diagnostics-failure');},async(_name,_attributes,run)=>{await run();try{await run();}catch{}return true;}]){
|
||||
let calls=0;const f=fixture({verifyPackage:()=>{calls++;return false;},traceOperation:tracer});
|
||||
await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/unavailable/);
|
||||
assert.equal(calls,1);assert.equal(f.options,undefined);await f.supervisor.close();
|
||||
}
|
||||
});
|
||||
test('diagnostic stages retain fixed names and original ready and shutdown outcomes',async()=>{
|
||||
const stages=[];const f=fixture({traceOperation:async(name,attributes,run)=>{stages.push({name,attributes});await run();return {ready:false};}});
|
||||
const {pending,context}=await starting(f);ready(f,context);
|
||||
assert.equal((await pending).port,43871);await f.supervisor.close();
|
||||
assert.deepEqual(stages.map(value=>value.name),['browser.broker.verify','browser.broker.start','browser.broker.shutdown']);
|
||||
assert.ok(stages.every(value=>JSON.stringify(value.attributes)==='{"component":"network"}'));
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const {parseAuthority,bindConnectAuthority,verifyInnerAuthority}=createRequire(import.meta.url)('../apps/network-broker/tls-authority.cjs');
|
||||
|
||||
test('canonical DNS, IPv4 and bracketed IPv6 ports and default 443',()=>{
|
||||
assert.equal(parseAuthority('Example.COM').authority,'example.com:443');
|
||||
assert.equal(parseAuthority('example.com:443').authority,'example.com:443');
|
||||
assert.equal(parseAuthority('127.0.0.1:8443').authority,'127.0.0.1:8443');
|
||||
assert.equal(parseAuthority('[2001:0DB8:0:0::1]').authority,'[2001:db8::1]:443');
|
||||
assert.equal(parseAuthority('xn--bcher-kva.example:65535').port,65535);
|
||||
assert.throws(()=>bindConnectAuthority('example.com'),{code:'missing-port'});
|
||||
});
|
||||
test('reject legacy IP aliases, controls, userinfo, paths and ambiguous ports',()=>{
|
||||
for(const value of ['127.1:443','0177.0.0.1:443','2130706433:443','0x7f000001:443','0x7f.0.0.1:443','example.com:0','example.com:0443','example.com:65536','example.com:+443','example.com:','example.com:443:4','[email protected]:443','example.com/path','example.com?x',' example.com','example.com\r\n','example.com.','-bad.example','bad_.example','[fe80::1%25eth0]:443','::1:443','https://example.com:443']) {
|
||||
assert.throws(()=>parseAuthority(value),error=>error.name==='AuthorityError'&&!error.message.includes(value));
|
||||
}
|
||||
});
|
||||
test('inner Host, absolute URL and SNI bind to authenticated CONNECT',()=>{
|
||||
const binding=bindConnectAuthority('EXAMPLE.com:443');
|
||||
assert.equal(verifyInnerAuthority(binding,{rawHeaders:['Host','example.com'],url:'/submit?x=1',servername:'example.com'}),binding);
|
||||
assert.equal(verifyInnerAuthority(binding,{rawHeaders:['host','EXAMPLE.com:443'],url:'https://example.com/submit',servername:'EXAMPLE.COM'}),binding);
|
||||
assert.ok(Object.isFrozen(binding));
|
||||
});
|
||||
test('cross authority reuse and missing/duplicate Host or mismatched SNI fail closed',()=>{
|
||||
const binding=bindConnectAuthority('example.com:443');
|
||||
const valid={rawHeaders:['Host','example.com'],url:'/',servername:'example.com'};
|
||||
for(const changed of [
|
||||
{rawHeaders:[]},{rawHeaders:['Host','example.com','host','example.com']},
|
||||
{rawHeaders:['Host','evil.com']},{rawHeaders:['Host','example.com:8443']},
|
||||
{url:'https://evil.com/'},{url:'https://example.com:8443/'},
|
||||
{url:'https://[email protected]/'},{url:'http://example.com/'},
|
||||
{url:'//evil.com/'},{url:'/x#fragment'},{url:'/x\r\n'},
|
||||
{servername:'evil.com'},{servername:'example.com:443'},{servername:undefined}
|
||||
])assert.throws(()=>verifyInnerAuthority(binding,{...valid,...changed}));
|
||||
assert.throws(()=>verifyInnerAuthority({...binding},valid),{code:'invalid-binding'});
|
||||
});
|
||||
test('IP CONNECT binding accepts absent SNI but does not accept a different IP',()=>{
|
||||
const binding=bindConnectAuthority('[::1]:443');
|
||||
assert.equal(verifyInnerAuthority(binding,{rawHeaders:['Host','[0:0:0:0:0:0:0:1]'],url:'https://[::1]/'}),binding);
|
||||
assert.throws(()=>verifyInnerAuthority(binding,{rawHeaders:['Host','[::1]'],url:'/',servername:'::2'}),{code:'sni-mismatch'});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {X509Certificate} from 'node:crypto';
|
||||
import https from 'node:https';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createSessionCertificates}=require('../apps/network-broker/tls-certificates.cjs');
|
||||
test('session leaf has exact SAN, bounded lifetime, valid signature and no CA authority',async()=>{
|
||||
const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
try{for(const authority of ['example.com:443','127.0.0.1:443','[::1]:443','[::ffff:c000:201]:443']){
|
||||
const leaf=issuer.resolve({authority}),certificate=new X509Certificate(leaf.cert);
|
||||
assert.equal(certificate.ca,false);assert.equal(certificate.verify(certificate.publicKey),true);
|
||||
assert.equal(certificate.checkHost('attacker.test',{subject:'never'}),undefined);
|
||||
assert.ok(Date.parse(certificate.validTo)<=Date.now()+3600000);
|
||||
assert.equal(issuer.resolve({authority}),leaf);
|
||||
const metadata=issuer.publicMetadata({authority});
|
||||
assert.deepEqual(Object.keys(metadata).sort(),['authority','cert','expiresAt','fingerprint256']);
|
||||
assert.equal(metadata.fingerprint256,certificate.fingerprint256);
|
||||
}}finally{issuer.close();}
|
||||
assert.throws(()=>issuer.resolve({authority:'example.com:443'}),/broker-certificate-unavailable/);
|
||||
});
|
||||
test('deny, revocation and cache exhaustion do not issue or reuse leaves',async()=>{
|
||||
let active=true,allowed=true;
|
||||
const issuer=await createSessionCertificates({maxAuthorities:1,current:()=>active,authorized:()=>allowed});
|
||||
try{issuer.resolve({authority:'example.com:443'});
|
||||
assert.throws(()=>issuer.resolve({authority:'other.test:443'}),/broker-certificate-unavailable/);
|
||||
allowed=false;assert.throws(()=>issuer.resolve({authority:'example.com:443'}),/broker-certificate-unavailable/);
|
||||
allowed=true;active=false;assert.throws(()=>issuer.resolve({authority:'example.com:443'}),/broker-certificate-unavailable/);
|
||||
}finally{issuer.close();}
|
||||
});
|
||||
test('generated certificate works with verified physical HTTPS and rejects a different hostname',async()=>{
|
||||
const issuer=await createSessionCertificates({current:()=>true,authorized:()=>true});
|
||||
const leaf=issuer.resolve({authority:'localhost:443'});
|
||||
const server=https.createServer({key:leaf.key,cert:leaf.cert},(_request,response)=>response.end('public'));
|
||||
try{await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
||||
const request=servername=>new Promise((resolve,reject)=>{https.get({host:'127.0.0.1',port:server.address().port,servername,ca:leaf.cert,agent:false},response=>{let body='';response.on('data',part=>body+=part);response.on('end',()=>resolve(body));}).on('error',reject);});
|
||||
assert.equal(await request('localhost'),'public');
|
||||
await assert.rejects(request('attacker.test'),{code:'ERR_TLS_CERT_ALTNAME_INVALID'});
|
||||
}finally{await new Promise(resolve=>server.close(resolve));issuer.close();}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const {GUIDS,planBrokerListenerProtection}=createRequire(import.meta.url)('../apps/network-broker/windows-wfp-plan.cjs');
|
||||
const appId=Buffer.from('\\device\\harddiskvolume3\\safetynet\\node.exe\0','utf16le');
|
||||
test('supplemental receiver deny protects exact tuple and preserves same-exe limitation',()=>{
|
||||
const p=planBrokerListenerProtection({appId,brokerPort:43871});
|
||||
assert.equal(p.completeBoundary,false);assert.equal(p.activation,'not-installed');
|
||||
const matches=(packet)=>p.filters[0].conditions.every(c=>c.match==='equal'?packet[c.field]===c.value:packet[c.field]!==c.value);
|
||||
const tuple={[GUIDS.protocol]:6,[GUIDS.localAddress]:0x7f000001,[GUIDS.localPort]:43871,[GUIDS.appId]:'other'};
|
||||
assert.equal(matches(tuple),true);
|
||||
for(const [field,value] of [[GUIDS.protocol,17],[GUIDS.localAddress,0x7f000002],[GUIDS.localPort,43872],[GUIDS.appId,p.applicationId]])assert.equal(matches({...tuple,[field]:value}),false);
|
||||
assert.equal(p.filters[0].action,'FWP_ACTION_BLOCK');assert.equal(p.filters[0].clearActionRight,false);
|
||||
assert.ok(p.prerequisites.includes('same-executable-instance-protection'));
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {GUIDS,planBrowserNetworkBoundary}=require('../apps/network-broker/windows-wfp-plan.cjs');
|
||||
const identity=Buffer.from('\\device\\harddiskvolume3\\program files\\safetynet\\safetynet.exe\0'.replaceAll('\\\\','\\').replace('\\0','\0'),'utf16le');
|
||||
function plan(port=43871){return planBrowserNetworkBoundary({appId:identity,brokerPort:port});}
|
||||
// Independent interpreter of a serialized WFP conjunction: no production matcher.
|
||||
function matchingFilters(p,packet){return p.filters.filter(rule=>rule.layer===(packet.family===4?GUIDS.connectV4:GUIDS.connectV6)&&rule.conditions.every(c=>{
|
||||
const values={[GUIDS.appId]:packet.appId,[GUIDS.protocol]:packet.protocol,[GUIDS.remoteAddress]:packet.address,[GUIDS.remotePort]:packet.port};
|
||||
if(c.match==='equal')return values[c.field]===c.value;
|
||||
if(c.match==='not-equal')return values[c.field]!==c.value;
|
||||
throw Error('Unsupported match');
|
||||
}));}
|
||||
test('only approved IPv4 TCP broker tuple survives exhaustive port/protocol edges',()=>{
|
||||
const p=plan();
|
||||
for(const address of [0,0x7f000001,0x7f000002,0x7fffffff,0x0a000001,0xffffffff]){
|
||||
for(const protocol of [0,1,6,17,58,255]){
|
||||
for(const port of [0,1,53,80,443,3478,43870,43871,43872,65535]){
|
||||
const matches=matchingFilters(p,{family:4,address,protocol,port,appId:p.applicationId});
|
||||
const approved=address===0x7f000001&&protocol===6&&port===43871;
|
||||
assert.equal(matches.length,approved?0:1,JSON.stringify({address,protocol,port}));
|
||||
}
|
||||
}
|
||||
}
|
||||
for(let port=0;port<=65535;port++)assert.equal(matchingFilters(p,{family:4,address:0x7f000001,protocol:6,port,appId:p.applicationId}).length,port===43871?0:1);
|
||||
});
|
||||
test('all IPv6 including mapped loopback is denied, unrelated applications untouched',()=>{
|
||||
const p=plan();
|
||||
for(const address of ['::1','::ffff:127.0.0.1','fe80::1','2001:db8::1'])for(const protocol of [6,17,58]){
|
||||
assert.equal(matchingFilters(p,{family:6,address,protocol,port:43871,appId:p.applicationId}).length,1);
|
||||
assert.equal(matchingFilters(p,{family:6,address,protocol,port:43871,appId:'other-app'}).length,0);
|
||||
}
|
||||
assert.equal(matchingFilters(p,{family:4,address:0x08080808,protocol:17,port:53,appId:'other-app'}).length,0);
|
||||
});
|
||||
test('plan contains only deny rules and never overrides enterprise veto or activates BFE',()=>{
|
||||
const p=plan();assert.equal(p.activation,'not-installed');assert.ok(p.filters.every(f=>f.action==='FWP_ACTION_BLOCK'&&f.clearActionRight===false));
|
||||
assert.ok(Object.isFrozen(p)&&Object.isFrozen(p.filters)&&p.filters.every(f=>Object.isFrozen(f.conditions)));
|
||||
const copy=Buffer.from(identity);const q=planBrowserNetworkBoundary({appId:copy,brokerPort:43871});copy.fill(0);assert.equal(q.applicationId,identity.toString('hex'));
|
||||
});
|
||||
test('rejects invalid ports and noncanonical or arbitrary executable identities',()=>{
|
||||
for(const port of [0,65536,1.5,'443',NaN])assert.throws(()=>plan(port));
|
||||
for(const path of ['C:\\safetynet.exe\0','\\device\\harddiskvolume3\\..\\a.exe\0','\\device\\harddiskvolume3\\A.exe\0','\\device\\harddiskvolume3\\a.exe','\\device\\harddiskvolume3\\a.exe:stream\0'])assert.throws(()=>planBrowserNetworkBoundary({appId:Buffer.from(path,'utf16le'),brokerPort:123}));
|
||||
});
|
||||
test('inbound-first flows are denied for the browser in both families',()=>{
|
||||
const p=plan();assert.equal(p.version,2);assert.equal(p.filters.length,6);
|
||||
for(const layer of [GUIDS.recvV4,GUIDS.recvV6])for(const protocol of [1,6,17,58,255])for(const port of [0,53,43871,65535]){
|
||||
const packet={[GUIDS.appId]:p.applicationId,[GUIDS.protocol]:protocol,[GUIDS.localPort]:port};
|
||||
const matches=p.filters.filter(rule=>rule.layer===layer&&rule.conditions.every(c=>c.match==='equal'?packet[c.field]===c.value:packet[c.field]!==c.value));
|
||||
assert.equal(matches.length,1);assert.equal(matches[0].action,'FWP_ACTION_BLOCK');
|
||||
assert.equal(matches[0].conditions.length,1);
|
||||
}
|
||||
// Model the documented ALE stateful direction: an approved outbound CONNECT
|
||||
// creates its flow; its reply is not a NEW incoming accept authorization.
|
||||
assert.equal(matchingFilters(p,{family:4,address:0x7f000001,protocol:6,port:43871,appId:p.applicationId}).length,0);
|
||||
assert.ok(p.prerequisites.includes('physical-inbound-first-tcp-udp-tests'));
|
||||
assert.ok(p.prerequisites.includes('browser-management-inspector-telemetry-and-agent-ipc-migration'));
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {createWfpRevalidation}=require('../apps/network-broker/wfp-revalidation.cjs');
|
||||
const valid=()=>({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('unreadable native snapshot never admits',async()=>{const reasons=[];const m=createWfpRevalidation({readTopology:async()=>({...valid(),allObjectsReadable:false}),quarantine:r=>reasons.push(r)});assert.equal(await m.start(),false);assert.equal(m.current(),false);assert.deepEqual(reasons,['topology-incomplete']);m.close();});
|
||||
test('slow initial original callback remains admitted only once and stale success cannot recover',async()=>{let resolve,calls=0;const reasons=[];const m=createWfpRevalidation({readTopology:()=>{calls++;return new Promise(r=>{resolve=r;});},quarantine:r=>reasons.push(r),deadlineMs:10});const starting=m.start();await new Promise(r=>setTimeout(r,30));assert.equal(m.status().pending,true);assert.equal(m.current(),false);resolve(valid());assert.equal(await starting,false);assert.equal(calls,1);assert.deepEqual(reasons,['wfp-revalidation-timeout']);m.close();});
|
||||
test('periodic failed BFE status quarantines and never auto recovers',async()=>{let calls=0;const reasons=[];const m=createWfpRevalidation({readTopology:async()=>({...valid(),bfeRunning:++calls===1}),quarantine:r=>reasons.push(r),intervalMs:100});assert.equal(await m.start(),true);await new Promise(r=>setTimeout(r,150));assert.equal(m.current(),false);assert.deepEqual(reasons,['wfp-bfe-unavailable']);m.close();});
|
||||
@@ -0,0 +1,13 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {GUIDS}=require('../apps/network-broker/windows-wfp-plan.cjs');
|
||||
const {validateTopology,createTopologyGuard,planServiceReceiver}=require('../apps/network-broker/wfp-topology.cjs');
|
||||
const snapshot=()=>({ownedSublayer:'ours',enumerationComplete:true,allObjectsReadable:true,sublayers:[{key:'ours',weight:65535},{key:'corp',weight:100}],filters:[...Array.from({length:8},(_,i)=>({key:String(i),sublayer:'ours',layer:GUIDS.connectV4,ownedVerified:true})),{sublayer:'corp',layer:GUIDS.recvV4,action:'BLOCK'}]});
|
||||
test('complete highest owned topology admits while corporate block stays present',()=>{assert.equal(validateTopology(snapshot()).safe,true);assert.equal(validateTopology(snapshot()).zeroChangeWindowProven,false);});
|
||||
test('equal priority hard permit/callout ambiguity cannot be admitted',()=>{for(const action of ['PERMIT','CALLOUT','BLOCK']){const s=snapshot();s.sublayers[1].weight=65535;s.filters.at(-1).action=action;assert.equal(validateTopology(s).reason,'priority-collision');}});
|
||||
test('partial visibility and unverified own filters fail closed',()=>{const s=snapshot();s.allObjectsReadable=false;assert.equal(validateTopology(s).safe,false);s.allObjectsReadable=true;s.filters[0].ownedVerified=false;assert.equal(validateTopology(s).safe,false);});
|
||||
test('malformed filter rows cannot silently vanish from a snapshot',()=>{for(const row of [null,{}, {layer:3,sublayer:'corp'}, {layer:GUIDS.recvV4}]){const s=snapshot();s.filters.push(row);assert.equal(validateTopology(s).reason,'snapshot-invalid');}});
|
||||
test('change during initial enumeration cannot be undone by later successful snapshot',()=>{const reasons=[];const g=createTopologyGuard({quarantine:r=>reasons.push(r)});g.changed();assert.equal(g.admit(snapshot()),false);assert.equal(g.current(),false);g.changed();assert.deepEqual(reasons,['wfp-topology-changed']);});
|
||||
test('service receiver soft permit and lower block match only exact service tuple',()=>{const p=planServiceReceiver({appId:Buffer.from('\\device\\harddiskvolume3\\safetynet\\node.exe\0','utf16le'),brokerPort:43120,serviceSid:'S-1-5-80-1-2-3-4-5'});assert.equal(p.filters[0].action,'FWP_ACTION_PERMIT');assert.equal(p.filters[0].clearActionRight,false);assert.ok(p.filters[0].weight>p.filters[1].weight);assert.equal(p.filters[0].conditions.at(-1).type,'FWP_SECURITY_DESCRIPTOR_TYPE');assert.equal(p.completeBoundary,false);assert.throws(()=>planServiceReceiver({appId:Buffer.alloc(8),brokerPort:3,serviceSid:'S-1-5-19'}));});
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{createWorkAdmission}=require('../apps/network-broker/work-admission.cjs');
|
||||
function deferred(){let resolve,reject;const promise=new Promise((yes,no)=>{resolve=yes;reject=no;});return {promise,resolve,reject};}
|
||||
test('outer deadline and abort races cannot free never-settled worker admission',async()=>{
|
||||
const admission=createWorkAdmission({maxJobs:2}),first=deferred(),second=deferred();
|
||||
const bytes=Buffer.from('synthetic private body');
|
||||
const work=admission.run(()=>first.promise,{payload:bytes});
|
||||
assert.equal(await Promise.race([work,Promise.resolve('outer aborted')]),'outer aborted');
|
||||
assert.equal(bytes.toString(),'synthetic private body');
|
||||
const other=admission.run(()=>second.promise);
|
||||
assert.equal(await Promise.race([other,Promise.resolve('outer deadline')]),'outer deadline');
|
||||
assert.throws(()=>admission.run(()=>{throw Error('must not execute');}),{code:'work_capacity_exhausted'});
|
||||
assert.equal(admission.status().active,2);
|
||||
first.resolve(42);assert.equal(await work,42);assert.ok(bytes.every(value=>value===0));
|
||||
assert.equal(admission.status().active,1);
|
||||
second.resolve(17);assert.equal(await other,17);assert.equal(admission.status().active,0);
|
||||
});
|
||||
test('reservation is synchronous and shared across concurrent callbacks',async()=>{
|
||||
const admission=createWorkAdmission({maxJobs:1}),pending=deferred();let calls=0;
|
||||
const work=admission.run(()=>{calls++;return pending.promise;});
|
||||
assert.throws(()=>admission.run(()=>calls++),{code:'work_capacity_exhausted'});assert.equal(calls,1);
|
||||
pending.resolve();await work;
|
||||
assert.equal(await admission.run(()=>++calls),2);
|
||||
});
|
||||
test('sync and async failure preserve identity release slot and wipe private payload',async()=>{
|
||||
const admission=createWorkAdmission({maxJobs:1}),failure=new Error('synthetic worker failure'),bytes=Buffer.from('private');
|
||||
assert.throws(()=>admission.run(()=>{throw failure;},{payload:bytes}),error=>error===failure);
|
||||
assert.ok(bytes.every(value=>value===0));assert.equal(admission.status().active,0);
|
||||
const other=Buffer.from('private');
|
||||
await assert.rejects(admission.run(()=>Promise.reject(failure),{payload:other}),error=>error===failure);
|
||||
assert.ok(other.every(value=>value===0));assert.equal(admission.status().active,0);
|
||||
});
|
||||
test('close rejects new work without claiming pending workers terminated',async()=>{
|
||||
const admission=createWorkAdmission({maxJobs:1}),pending=deferred(),bytes=Buffer.from('still used');
|
||||
const work=admission.run(()=>pending.promise,{payload:bytes});admission.close();
|
||||
assert.deepEqual(admission.status(),{active:1,maxJobs:1,closed:true,available:0});
|
||||
assert.equal(bytes.toString(),'still used');assert.throws(()=>admission.run(()=>true),{code:'work_admission_closed'});
|
||||
pending.resolve();await work;assert.equal(admission.status().active,0);assert.ok(bytes.every(value=>value===0));
|
||||
});
|
||||
test('configuration and admission failures use fixed public errors and immutable status',()=>{
|
||||
assert.throws(()=>createWorkAdmission({maxJobs:0}),{message:'work_configuration_invalid'});
|
||||
const admission=createWorkAdmission();assert.equal(Object.isFrozen(admission.status()),true);
|
||||
assert.throws(()=>admission.run(()=>{}, {payload:'private content'}),{message:'work_configuration_invalid'});
|
||||
admission.close();const bytes=Buffer.from('not admitted');
|
||||
assert.throws(()=>admission.run(()=>{}, {payload:bytes}),{code:'work_admission_closed'});
|
||||
assert.ok(bytes.every(value=>value===0));
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {mkdtemp,writeFile,access,rm,mkdir} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {defaultPolicy} from '../packages/core/policy.mjs';
|
||||
const {createCapabilityCrypto}=createRequire(import.meta.url)('../apps/browser/dpapi.cjs');
|
||||
const {createFixtureDiagnostics}=createRequire(import.meta.url)('./fixtures/broker-native-diagnostics.cjs');
|
||||
const {createSyntheticAudit}=createRequire(import.meta.url)('./fixtures/broker-durable-audit.cjs');
|
||||
const enabled=process.platform==='win32'&&process.env.SAFETYNET_NATIVE_DPAPI_HELPER==='1';
|
||||
test('actual isolated private Python performs Windows DPAPI despite poisoned Python environment',{skip:!enabled},async t=>{
|
||||
const root=process.cwd(),temporary=await mkdtemp(path.join(os.tmpdir(),'safetynet-dpapi-injection-'));
|
||||
t.after(()=>rm(temporary,{recursive:true,force:true}));const marker=path.join(temporary,'startup-hook-ran');
|
||||
await writeFile(path.join(temporary,'sitecustomize.py'),`from pathlib import Path\nPath(${JSON.stringify(marker)}).write_text('synthetic startup hook')\n`);
|
||||
const convert=createCapabilityCrypto({root,environment:{...process.env,PYTHONPATH:temporary,PYTHONHOME:path.join(temporary,'missing-runtime'),PYTHONINSPECT:'1',UNLISTED_SECRET:'synthetic-inherited-secret'}});
|
||||
const payload=Buffer.from('synthetic private capability');
|
||||
const audit=createSyntheticAudit(t),diagnostics=await createFixtureDiagnostics({enabled:true,policy:defaultPolicy(),audit});let diagnosticProof,timings;
|
||||
t.after(async()=>{if(!diagnosticProof)await diagnostics.finish();});
|
||||
const started=performance.now();let encrypted,decrypted;
|
||||
await diagnostics.run({bytes:0},async()=>{
|
||||
encrypted=await diagnostics.stage('browser.credential',{operation:'credential.issue',component:'agent'},()=>convert('protect',payload));
|
||||
decrypted=await diagnostics.stage('browser.credential',{operation:'credential.claim',component:'agent'},()=>convert('unprotect',encrypted));
|
||||
// Synthetic fsync event supplies correlation only; no browser upload occurs.
|
||||
diagnostics.persist({eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:1,request:{bytes:0,detectedType:'text'}});
|
||||
});
|
||||
const traceId=audit.events()[0].traceId,actual=await diagnostics.trace(traceId),parent=actual.spans.find(value=>value.name==='browser.request');
|
||||
timings=actual.spans.filter(value=>value.name==='browser.credential').map(value=>({name:value.name,parentSpanId:value.parentSpanId,durationMs:Number(BigInt(value.endTimeUnixNano)-BigInt(value.startTimeUnixNano))/1e6}));
|
||||
assert.equal(timings.length,2);assert.ok(timings.every(value=>value.parentSpanId===parent.spanId&&value.durationMs>0));
|
||||
diagnosticProof=await diagnostics.finish();assert.equal(diagnosticProof.failedExports,0);assert.equal(diagnosticProof.lifetimeFailedExports,0);
|
||||
assert.equal(decrypted,payload.toString());assert.notEqual(encrypted,decrypted);assert.equal(Buffer.from(encrypted,'base64').includes(payload),false);
|
||||
await assert.rejects(access(marker),{code:'ENOENT'});payload.fill(0);
|
||||
const maximum=Buffer.alloc(32768,97),maximumEncrypted=await convert('protect',maximum);
|
||||
assert.ok(Buffer.byteLength(maximumEncrypted)>32768);assert.equal(await convert('unprotect',maximumEncrypted),maximum.toString());maximum.fill(0);
|
||||
const directory=path.join(root,'artifacts/dpapi-helper');await mkdir(directory,{recursive:true});
|
||||
await writeFile(path.join(directory,'proof.json'),JSON.stringify({kind:'actual-windows-current-user-dpapi',passed:true,isolatedPrivatePython:true,poisonedPythonEnvironmentIgnored:true,startupHookAbsent:true,plaintextStored:false,maximumInputBytes:32768,maximumCiphertextBytes:Buffer.byteLength(maximumEncrypted),maximumRoundtripPassed:true,roundtripMs:performance.now()-started,diagnostics:{...diagnosticProof,timings,syntheticCorrelationEvent:true},serviceCredentialStoreProven:false},null,2));
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {EventEmitter} from 'node:events';
|
||||
const {createCapabilityCrypto}=createRequire(import.meta.url)('../apps/browser/dpapi.cjs');
|
||||
test('capability helper launches fixed isolated Python without startup injection or inherited secrets',async()=>{
|
||||
let observed;const child=new EventEmitter();child.stdout=new EventEmitter();child.stderr=new EventEmitter();child.stdin=new EventEmitter();child.stdin.end=()=>{queueMicrotask(()=>{child.stdout.emit('data',Buffer.from('synthetic-ciphertext'));child.emit('close',0);});};child.kill=()=>{};
|
||||
const convert=createCapabilityCrypto({root:'C:/trusted/safetynet',environment:{SystemRoot:'C:/Windows',TEMP:'C:/temporary',PYTHONPATH:'injected',PYTHONHOME:'injected',PYTHONINSPECT:'1',NODE_OPTIONS:'injected',UNLISTED_SECRET:'synthetic-secret'},launch:(file,args,options)=>{observed={file,args,options};return child;}});
|
||||
assert.equal(await convert('protect','synthetic-input'),'synthetic-ciphertext');
|
||||
assert.ok(observed.file.endsWith('python.exe'));assert.deepEqual(observed.args.slice(0,2),['-I','-B']);assert.equal(observed.args.at(-1),'protect');assert.ok(observed.args[2].endsWith('dpapi.py'));
|
||||
assert.deepEqual(observed.options.env,{SystemRoot:'C:/Windows',TEMP:'C:/temporary'});assert.equal(observed.options.shell,false);assert.equal(observed.options.windowsHide,true);
|
||||
});
|
||||
test('invalid capability input never spawns a helper',async()=>{
|
||||
let calls=0;const convert=createCapabilityCrypto({root:'C:/trusted',launch:()=>{calls++;throw Error('must not launch');}});
|
||||
for(const [operation,input] of [['arbitrary','x'],['protect',''],['protect',{}],['protect',Buffer.alloc(32769)],['unprotect',Buffer.alloc(65537)]])await assert.rejects(convert(operation,input),/Capability rejected/);
|
||||
assert.equal(calls,0);
|
||||
});
|
||||
function fakeChild(){const child=new EventEmitter();child.stdout=new EventEmitter();child.stderr=new EventEmitter();child.stdin=new EventEmitter();child.stdin.end=()=>{};child.kills=0;child.kill=()=>{child.kills++;};return child;}
|
||||
test('failed output stays bounded and original unclosed helpers retain eight slots',async()=>{
|
||||
const children=[];const convert=createCapabilityCrypto({root:'C:/trusted',launch:()=>{const child=fakeChild();children.push(child);return child;}});
|
||||
for(let index=0;index<8;index++){
|
||||
const pending=assert.rejects(convert('protect','synthetic'),/Windows capability protection failed/),child=children[index];
|
||||
child.stdout.emit('data',Buffer.alloc(65537));for(let late=0;late<10;late++)child.stdout.emit('data',Buffer.alloc(65536));await pending;assert.equal(child.kills,1);
|
||||
}
|
||||
await assert.rejects(convert('protect','synthetic'),/Windows capability protection failed/);assert.equal(children.length,8);
|
||||
children[0].emit('close',1);const next=convert('protect','synthetic');assert.equal(children.length,9);
|
||||
children[8].stdout.emit('data',Buffer.from('ciphertext'));children[8].emit('close',0);assert.equal(await next,'ciphertext');
|
||||
for(const child of children.slice(1,8))child.emit('close',1);
|
||||
});
|
||||
test('synchronous launch and pipe faults expose fixed errors with owned cleanup',async()=>{
|
||||
const convert=createCapabilityCrypto({root:'C:/trusted',launch:()=>{throw Error('PRIVATE_PATH_TOKEN');}});
|
||||
for(let attempt=0;attempt<2;attempt++)await assert.rejects(convert('protect','synthetic'),{message:'Windows capability protection failed'});
|
||||
const child=fakeChild(),pipe=createCapabilityCrypto({root:'C:/trusted',launch:()=>child});
|
||||
const pending=assert.rejects(pipe('protect','synthetic'),{message:'Windows capability protection failed'});
|
||||
child.stdout.emit('error',Error('PRIVATE_PATH_TOKEN'));await pending;assert.equal(child.kills,1);child.emit('close',1);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {extractEncodedDocuments}=require('../packages/core/encoded-documents.cjs');
|
||||
const pdf=Buffer.from('%PDF-1.4\nsynthetic bounded document');
|
||||
test('JSON nested values and keys yield private original PDF bytes including base64url',()=>{
|
||||
for(const value of [{nested:[{file:pdf.toString('base64')}]},{[pdf.toString('base64url')]:true}]){
|
||||
const result=extractEncodedDocuments(Buffer.from(JSON.stringify(value)));assert.equal(result.complete,true);assert.equal(result.parts.length,1);assert.deepEqual(result.parts[0].bytes,pdf);
|
||||
result.dispose();assert.ok(result.parts[0].bytes.every(byte=>byte===0));assert.notEqual(pdf[0],0);
|
||||
}
|
||||
});
|
||||
test('data URLs preserve declared MIME, percent decoding and exact decoded content',()=>{
|
||||
const result=extractEncodedDocuments(Buffer.from('data:application/pdf;base64,'+encodeURIComponent(pdf.toString('base64'))));
|
||||
assert.equal(result.complete,true);assert.equal(result.parts[0].mime,'application/pdf');assert.deepEqual(result.parts[0].bytes,pdf);result.dispose();
|
||||
});
|
||||
test('invalid carriers and decoding budgets cannot become complete coverage',()=>{
|
||||
for(const value of ['data:application/pdf;base64,%%%','data:application/pdf;base64,abcde','{"file":'])assert.equal(extractEncodedDocuments(Buffer.from(value),{jsonRequired:true}).complete,false);
|
||||
assert.equal(extractEncodedDocuments(Buffer.from(pdf.toString('base64')),{maxDecodedBytes:1}).complete,false);
|
||||
const result=extractEncodedDocuments(Buffer.from(JSON.stringify([pdf.toString('base64'),Buffer.from('%PDF-1.4\nother').toString('base64')])) ,{maxParts:1});assert.equal(result.complete,false);result.dispose();
|
||||
});
|
||||
test('ordinary text is unchanged and duplicate encoded documents use one private part',()=>{
|
||||
assert.equal(extractEncodedDocuments(Buffer.from('ordinary public text')).parts.length,0);
|
||||
const result=extractEncodedDocuments(Buffer.from(JSON.stringify([pdf.toString('base64'),pdf.toString('base64')])));assert.equal(result.complete,true);assert.equal(result.parts.length,1);result.dispose();
|
||||
});
|
||||
test('embedded and URL-encoded data URLs receive typed binary coverage without rejecting plain source code',()=>{
|
||||
const data='data:application/pdf;base64,'+pdf.toString('base64');
|
||||
for(const value of ['file='+encodeURIComponent(data),'<input value="'+data+'">','document='+pdf.toString('base64')]){
|
||||
const result=extractEncodedDocuments(Buffer.from(value));assert.equal(result.complete,true);assert.ok(result.parts.some(part=>part.bytes.equals(pdf)));result.dispose();
|
||||
}
|
||||
assert.equal(extractEncodedDocuments(Buffer.from('{ let publicValue = 1; }')).complete,true);
|
||||
});
|
||||
test('duplicate encoding does not consume part capacity; different declared MIME is independently checked',()=>{
|
||||
const value=pdf.toString('base64'),duplicate=extractEncodedDocuments(Buffer.from(JSON.stringify([value,value])),{maxParts:1});assert.equal(duplicate.complete,true);duplicate.dispose();
|
||||
const distinct=extractEncodedDocuments(Buffer.from(JSON.stringify(['data:application/pdf;base64,'+value,'data:image/jpeg;base64,'+value])));assert.equal(distinct.parts.length,2);distinct.dispose();
|
||||
const one=extractEncodedDocuments(Buffer.from('data:application/pdf;base64,'+value),{maxParts:1});assert.equal(one.complete,true);assert.equal(one.parts.length,1);one.dispose();
|
||||
});
|
||||
test('large collections and deep JSON exceed bounded traversal without decoded release',()=>{
|
||||
const array=extractEncodedDocuments(Buffer.from(JSON.stringify(Array.from({length:100},()=>0))),{maxNodes:10});assert.equal(array.complete,false);assert.equal(array.parts.length,0);
|
||||
let value=pdf.toString('base64');for(let index=0;index<10;index++)value={next:value};const deep=extractEncodedDocuments(Buffer.from(JSON.stringify(value)),{maxDepth:4});assert.equal(deep.complete,false);deep.dispose();
|
||||
});
|
||||
test('review regressions cover JSON percent carriers, textual envelopes and strict scalar JSON',()=>{
|
||||
const uri='data:application/pdf;base64,'+pdf.toString('base64');
|
||||
for(const body of [{file:encodeURIComponent(uri)},{[encodeURIComponent(uri)]:true}]){
|
||||
const result=extractEncodedDocuments(Buffer.from(JSON.stringify(body)),{jsonRequired:true});assert.equal(result.complete,true);assert.ok(result.parts.some(part=>part.bytes.equals(pdf)));result.dispose();
|
||||
}
|
||||
const inner=JSON.stringify({file:pdf.toString('base64')}),outer=extractEncodedDocuments(Buffer.from(JSON.stringify({file:Buffer.from(inner).toString('base64')})));
|
||||
assert.equal(outer.complete,true);assert.ok(outer.parts.some(part=>part.bytes.toString()===inner));assert.ok(outer.decodedBytes>=inner.length);
|
||||
const nested=extractEncodedDocuments(outer.parts.find(part=>part.bytes.toString()===inner).bytes,{maxDecodedBytes:16777216-outer.decodedBytes});assert.ok(nested.parts.some(part=>part.bytes.equals(pdf)));nested.dispose();outer.dispose();
|
||||
assert.equal(extractEncodedDocuments(Buffer.from('not JSON'),{jsonRequired:true}).complete,false);
|
||||
const scalar=extractEncodedDocuments(Buffer.from(JSON.stringify(pdf.toString('base64'))),{jsonRequired:true});assert.equal(scalar.complete,true);assert.deepEqual(scalar.parts[0].bytes,pdf);scalar.dispose();
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
const {app,session}=require('electron');
|
||||
const {createBrokerCertificatePins}=require('../../apps/browser/broker-certificate-pins.cjs');
|
||||
app.disableHardwareAcceleration();
|
||||
let initialized=false;
|
||||
process.once('disconnect',()=>app.exit(0));
|
||||
process.on('message',async input=>{
|
||||
if(input?.type==='stop'){app.exit(0);return;}
|
||||
if(initialized||input?.type!=='start')return;initialized=true;
|
||||
try{
|
||||
app.setPath('userData',input.profile);await app.whenReady();
|
||||
const partition=session.fromPartition('certificate-pin-native',{cache:false});
|
||||
const pins=createBrokerCertificatePins({generation:'synthetic-generation',policyVersion:4,current:()=>true,authorizedAuthority:authority=>authority===input.leaf.authority});
|
||||
pins.accept({...input.leaf,generation:'synthetic-generation',policyVersion:4});
|
||||
partition.setCertificateVerifyProc((request,callback)=>pins.verify(request,callback));
|
||||
await partition.setProxy({mode:'direct'});
|
||||
const allowed=await partition.fetch(input.publicURL,{signal:AbortSignal.timeout(10000)});
|
||||
const publicBody=await allowed.text();let denied=false;
|
||||
try{await partition.fetch(input.untrustedURL,{signal:AbortSignal.timeout(10000)});}catch{denied=true;}
|
||||
await partition.closeAllConnections();pins.revoke();
|
||||
process.send({type:'proof',publicStatus:allowed.status,publicBody,untrustedRejected:denied,electron:process.versions.electron,chromium:process.versions.chrome});
|
||||
}catch{process.send({type:'fault',reason:'certificate-native-proof-failed'});}
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
// Test-only, actual fsync of synthetic metadata. Production uses the protected
|
||||
// encrypted audit store; this fixture never stores request contents or tokens.
|
||||
const fs=require('node:fs');
|
||||
const path=require('node:path');
|
||||
const {randomUUID}=require('node:crypto');
|
||||
const {BrokerAuditEventSchema}=require('../../apps/network-broker/audit-gate.cjs');
|
||||
function createSyntheticAudit(test){
|
||||
const root=path.resolve(__dirname,'../../artifacts/broker-audit-fixtures');fs.mkdirSync(root,{recursive:true});
|
||||
const filename=path.join(root,`${randomUUID()}.jsonl`),fd=fs.openSync(filename,'wx');
|
||||
test.after(()=>fs.closeSync(fd));
|
||||
return Object.freeze({
|
||||
persist:event=>{BrokerAuditEventSchema.parse(event);fs.writeFileSync(fd,JSON.stringify(event)+'\n');fs.fsyncSync(fd);return {persisted:true,eventId:event.eventId};},
|
||||
events:()=>fs.readFileSync(filename,'utf8').trim().split('\n').filter(Boolean).map(line=>JSON.parse(line))
|
||||
});
|
||||
}
|
||||
module.exports={createSyntheticAudit};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
// Test-only stock Electron client. No production browser APIs or policy edits.
|
||||
const {app,BrowserWindow,session}=require('electron');
|
||||
const {X509Certificate}=require('node:crypto');
|
||||
app.disableHardwareAcceleration();
|
||||
let window,config,loginCount=0,initialized=false;
|
||||
const send=value=>{if(process.connected)process.send(value);};
|
||||
process.on('disconnect',()=>app.exit(0));
|
||||
app.on('login',(event,contents,_details,info,callback)=>{
|
||||
event.preventDefault();
|
||||
if(config&&contents===window?.webContents&&info.isProxy&&info.host==='127.0.0.1'&&info.port===config.proxyPort&&info.scheme.toLowerCase()==='basic'&&info.realm==='SafetyNet private broker'){
|
||||
loginCount++;callback(config.credentials.username,config.credentials.password);
|
||||
}else callback();
|
||||
});
|
||||
process.on('message',async message=>{
|
||||
try {
|
||||
if(message?.type==='initialize'&&!initialized){
|
||||
initialized=true;config=message.config;
|
||||
app.setPath('userData',config.userData);
|
||||
await app.whenReady();
|
||||
const partition=session.fromPartition('broker-interop',{cache:false});
|
||||
partition.setCertificateVerifyProc((request,callback)=>{
|
||||
try {
|
||||
const certificate=new X509Certificate(request.certificate.data);
|
||||
const now=Date.now();
|
||||
if(request.hostname==='localhost'&&certificate.checkHost('localhost')==='localhost'&&certificate.fingerprint256===config.fingerprint&&Date.parse(certificate.validFrom)<=now&&now<Date.parse(certificate.validTo))return callback(0);
|
||||
}catch{}
|
||||
callback(-3);
|
||||
});
|
||||
await partition.setProxy({mode:'fixed_servers',proxyRules:`http=127.0.0.1:${config.proxyPort};https=127.0.0.1:${config.proxyPort}`,proxyBypassRules:'<-loopback>'});
|
||||
window=new BrowserWindow({show:false,webPreferences:{session:partition,nodeIntegration:false,contextIsolation:true,sandbox:true,webSecurity:true}});
|
||||
window.webContents.on('render-process-gone',()=>send({type:'error',code:'renderer-gone'}));
|
||||
await window.loadURL(config.origin+'/');
|
||||
send({type:'ready',loginCount,electron:process.versions.electron,chromium:process.versions.chrome});return;
|
||||
}
|
||||
if(message?.type==='stop'){window?.destroy();app.exit(0);return;}
|
||||
const bodies={public:'public browser content',secret:'PROJECT SECRET',revoked:'public revoked content'};
|
||||
if(!window||!Object.hasOwn(bodies,message?.type))throw Error('invalid-message');
|
||||
const result=await window.webContents.executeJavaScript(`(async()=>{try{const response=await fetch('/submit',{method:'POST',body:${JSON.stringify(bodies[message.type])}});return {status:response.status,body:await response.text()};}catch{return {blocked:true};}})()`);
|
||||
send({type:'result',operation:message.type,result,loginCount});
|
||||
} catch {send({type:'error',code:'interop-failed'});}
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
// Diagnostic fixture only: compare explicitly allowlisted environment profiles
|
||||
// against a real packaged contained inspector. No private content or values.
|
||||
const path=require('node:path');
|
||||
const {startInspector}=require('../../apps/browser/inspection.cjs');
|
||||
const {execFileSync}=require('node:child_process');
|
||||
function children(){const text=execFileSync('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoProfile','-NonInteractive','-Command',`@(Get-CimInstance Win32_Process -Filter "ParentProcessId = ${process.pid}" | Where-Object Name -eq 'python.exe' | Select-Object -ExpandProperty ProcessId) | ConvertTo-Json -Compress`],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();return text?[].concat(JSON.parse(text)):[];}
|
||||
async function main(){
|
||||
const {defaultPolicy}=await import('../../packages/core/policy.mjs');const baseline={...process.env},root=path.resolve(__dirname,'../../.runtime/windows');
|
||||
for(const variant of ['stripped','profile']){
|
||||
const allowed=['SystemRoot','WINDIR','TEMP','TMP',...(variant==='profile'?['USERPROFILE','LOCALAPPDATA','APPDATA']:[])];for(const key of Object.keys(process.env))delete process.env[key];for(const key of allowed)if(baseline[key])process.env[key]=baseline[key];
|
||||
let inspector,stage='startup',result;
|
||||
try{inspector=startInspector(root,true);if(!inspector)throw Error('fixed-path-fault');await inspector.waitReady();stage='scan';const answer=await inspector.inspect(Buffer.from('public synthetic text'),defaultPolicy());result={variant,stage,decision:answer.decision,complete:answer.coverage.complete};}
|
||||
catch{result={variant,stage,failed:true};}
|
||||
finally{inspector?.stop();for(let count=0;count<20&&children().length;count++)await new Promise(resolve=>setTimeout(resolve,100));result.ownedParents=children().length;}
|
||||
process.stdout.write(JSON.stringify(result)+'\n');if(result.ownedParents)throw Error('fixed-cleanup-fault');
|
||||
}
|
||||
}
|
||||
main().catch(()=>{process.stderr.write('fixed-environment-proof-fault\n');process.exitCode=1;});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
// Explicitly authorized synthetic fixture only. Private bounded in-memory OTLP
|
||||
// receiver; no real management policy, content, token or production exporter.
|
||||
const http=require('node:http');
|
||||
const {createDiagnostics}=require('../../apps/browser/diagnostics.cjs');
|
||||
async function createFixtureDiagnostics({enabled,policy,audit}){
|
||||
if(!enabled)return {persist:audit.persist,run:(_attributes,fn)=>fn(),stage:(_name,_attributes,fn)=>fn(),finish:async()=>({enabled:false})};
|
||||
const batches=[];let storedBytes=0,invalid=0;const spans=()=>batches.flatMap(batch=>(batch.resourceSpans||[]).flatMap(resource=>(resource.scopeSpans||[]).flatMap(scope=>scope.spans||[])));
|
||||
const server=http.createServer({maxHeaderSize:8192},async(request,response)=>{
|
||||
if(request.method==='GET'&&/^\/api\/trace\/[a-f0-9]{32}$/.test(request.url)){const id=request.url.split('/').pop();response.setHeader('content-type','application/json');response.end(JSON.stringify({spans:spans().filter(span=>span.traceId===id)}));return;}
|
||||
if(request.method!=='POST'||!['/v1/traces','/v1/logs'].includes(request.url)){response.writeHead(404);response.end();return;}
|
||||
const chunks=[];let size=0;
|
||||
try{for await(const chunk of request){size+=chunk.length;if(size>1048576||storedBytes+size>4194304)throw Error('fixed-budget');chunks.push(chunk);}const text=Buffer.concat(chunks).toString();if(text.includes('SYNTHETIC_PRIVATE')||text.includes('proxy-password')||text.includes('%PDF-'))throw Error('fixed-content');const value=JSON.parse(text);if(request.url==='/v1/traces')batches.push(value);storedBytes+=size;response.setHeader('content-type','application/json');response.end('{}');}catch{invalid++;response.writeHead(400);response.end('{}');}
|
||||
});server.maxConnections=8;server.listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));
|
||||
const endpoint=`http://127.0.0.1:${server.address().port}`,diagnostics=createDiagnostics();let active;
|
||||
policy.diagnostics={...policy.diagnostics,enabled:true,mode:'debug',endpoint,sampleRate:1,debugSampleRate:1,maxRecordAgeMs:120000};
|
||||
if(!diagnostics.configure(policy.diagnostics,{policyVersion:policy.version,deviceId:'native-production-fixture'}))throw Error('fixture-diagnostics-unavailable');
|
||||
return {
|
||||
stage:(name,attributes,fn)=>diagnostics.span(name,attributes,fn),
|
||||
async trace(traceId){if(!/^[a-f0-9]{32}$/.test(traceId))throw Error('fixture-trace-invalid');await diagnostics.flush();const response=await fetch(endpoint+'/api/trace/'+traceId);if(!response.ok)throw Error('fixture-trace-unavailable');return response.json();},
|
||||
persist:event=>diagnostics.withCorrelation(active,()=>diagnostics.spanSync('browser.management',{operation:'audit.deliver',component:'management'},()=>audit.persist({...event,...diagnostics.correlation()}))),
|
||||
run:(attributes,fn)=>diagnostics.span('browser.request',{operation:'broker.request',component:'network',...attributes},async()=>{active=diagnostics.correlation();try{return await fn();}finally{active=undefined;}}),
|
||||
async finish(){
|
||||
try{await diagnostics.flush();const status=diagnostics.status();if(status.failedExports||status.lifetimeFailedExports||invalid)throw Error('fixture-diagnostics-export-failed');const event=audit.events().find(value=>value.traceId);if(!event)throw Error('fixture-diagnostics-correlation-missing');const response=await fetch(endpoint+'/api/trace/'+event.traceId),trace=await response.json();const child=trace.spans.find(value=>value.spanId===event.spanId),parent=trace.spans.find(value=>value.spanId===child?.parentSpanId);if(!child||!parent||parent.name!=='browser.request'||BigInt(parent.endTimeUnixNano)<=BigInt(parent.startTimeUnixNano))throw Error('fixture-diagnostics-parent-missing');const stages=trace.spans.filter(value=>value.name.startsWith('browser.broker.')).map(value=>({name:value.name,parentSpanId:value.parentSpanId,durationMs:Number(BigInt(value.endTimeUnixNano)-BigInt(value.startTimeUnixNano))/1e6}));return {enabled:true,traceId:event.traceId,auditSpanId:event.spanId,parentSpanId:parent.spanId,spanCount:spans().length,storedBytes,stages,invalid,failedExports:status.failedExports,lifetimeFailedExports:status.lifetimeFailedExports};}
|
||||
finally{await diagnostics.shutdown();server.closeAllConnections();await new Promise(resolve=>server.close(resolve));}
|
||||
}
|
||||
};
|
||||
}
|
||||
module.exports={createFixtureDiagnostics};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
// Synthetic classification belongs only in this explicitly test-only factory.
|
||||
const fs=require('node:fs');
|
||||
const path=require('node:path');
|
||||
const https=require('node:https');
|
||||
const {runBrokerProcess}=require('../../apps/network-broker/process-entry.cjs');
|
||||
const {createConnectProxy}=require('../../apps/network-broker/connect-inspection-proxy.cjs');
|
||||
const cert=fs.readFileSync(path.join(__dirname,'icap/cert.pem'));
|
||||
const key=fs.readFileSync(path.join(__dirname,'icap/key.pem'));
|
||||
runBrokerProcess(config=>{
|
||||
if(Object.keys(config).join(',')!=='destination'||typeof config.destination!=='string'||!/^localhost:[1-9][0-9]{0,4}$/.test(config.destination))throw Error('invalid-test-config');
|
||||
const port=Number(config.destination.split(':')[1]);if(port>65535)throw Error('invalid-test-config');
|
||||
return createConnectProxy({authority:'synthetic-test-policy',current:()=>true,resolveCertificate:binding=>binding.authority===config.destination?{authority:config.destination,key,cert}:undefined,inspect:({body})=>({decision:body.toString().includes('SECRET')?'block':'allow'}),forward:({request,body,signal,maxResponseBytes})=>new Promise((resolve,reject)=>{
|
||||
const outgoing=https.request({host:'127.0.0.1',port,path:new URL(request.url).pathname,method:request.method,servername:'localhost',ca:cert,rejectUnauthorized:true,signal,headers:{host:config.destination,'content-length':body.length}},res=>{
|
||||
const chunks=[];let bytes=0;
|
||||
res.on('data',chunk=>{bytes+=chunk.length;if(bytes>maxResponseBytes){outgoing.destroy();reject(Error('response-bound'));return;}chunks.push(Buffer.from(chunk));});res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));res.on('error',reject);
|
||||
});outgoing.on('error',reject);outgoing.end(body);
|
||||
}),deadlineMs:1000,authMode:'bearer'});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
const {createFramedChannel}=require('../../apps/network-broker/framed-channel.cjs');
|
||||
const channel=createFramedChannel({readable:process.stdin,writable:process.stdout,inboundFirstLimit:2097152});
|
||||
channel.once('disconnect',()=>process.exit(0));
|
||||
let context;
|
||||
channel.on('message',message=>{
|
||||
if(message.type==='start'){
|
||||
context={version:1,generation:message.generation,policyVersion:message.policyVersion};
|
||||
if(message.config.malformed){process.stdout.write(Buffer.from([1,0,0,0,0xff]));return;}
|
||||
channel.send({type:'ready',...context,authMode:'basic',sequence:0,port:message.config.port,credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)}},error=>{
|
||||
if(error)process.exit(2);
|
||||
if(message.config.audit){const id='12345678-1234-4234-8234-123456789abc';channel.send({type:'audit',...context,sequence:1,requestId:id,event:{eventId:id,kind:'upload.authorized',ruleId:'broker-approved',policyVersion:context.policyVersion,request:{bytes:0,detectedType:'text'}}});}
|
||||
});
|
||||
}else if(message.type==='audit-receipt'){
|
||||
if(message.receipt?.persisted!==true||message.receipt.eventId!==message.requestId)process.exit(3);
|
||||
}else process.exit(4);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
// Explicit native installer dispatch proof; does not claim NSIS execution or
|
||||
// an elevated-to-different-standard-user Program Files installation proof.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFile,mkdir,readdir,copyFile,mkdtemp,rm,writeFile} from 'node:fs/promises';
|
||||
import {spawn} from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
async function run(executable,args){
|
||||
const child=spawn(executable,args,{windowsHide:true,stdio:['ignore','pipe','pipe']});let output='',bytes=0;
|
||||
for(const stream of [child.stdout,child.stderr])stream.on('data',chunk=>{bytes+=chunk.length;if(bytes>4096)child.kill();else output+=chunk.toString();});
|
||||
const timer=setTimeout(()=>child.kill(),60000);
|
||||
try{return await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',(code,signal)=>resolve({code,signal,output}));});}finally{clearTimeout(timer);}
|
||||
}
|
||||
test('actual fixed installer hook dispatch provisions copied runtime then verifies deterministic native package SID',{
|
||||
skip:process.platform!=='win32'||process.env.SAFETYNET_NATIVE_INSTALLER_ACL!=='1',timeout:90000
|
||||
},async()=>{
|
||||
const repository=path.resolve('.'),artifacts=path.join(repository,'artifacts');
|
||||
await mkdir(artifacts,{recursive:true});const root=await mkdtemp(path.join(artifacts,'installer ACL native-'));
|
||||
const resources=path.join(root,'resources'),python=path.join(resources,'python'),inspector=path.join(resources,'inspector');
|
||||
const proof={startedAt:new Date().toISOString(),outcome:'running',nsisPluginExecuted:false,crossUserProgramFilesProven:false};
|
||||
try{
|
||||
await Promise.all([mkdir(python,{recursive:true}),mkdir(inspector,{recursive:true}),mkdir(path.join(resources,'semantic'),{recursive:true})]);
|
||||
for(const item of await readdir(path.join(repository,'.runtime/windows/python'),{withFileTypes:true}))if(item.isFile())await copyFile(path.join(repository,'.runtime/windows/python',item.name),path.join(python,item.name));
|
||||
for(const name of ['provision_runtime_acl.py','windows_appcontainer.py','windows_containment.py'])await copyFile(path.join(repository,'apps/inspector',name),path.join(inspector,name));
|
||||
await writeFile(path.join(resources,'semantic/public-marker.txt'),'Synthetic installer ACL role marker');
|
||||
const hook=await readFile(path.join(repository,'scripts/install/windows-security.nsh'),'utf8');
|
||||
const command=/nsExec::ExecToStack \/TIMEOUT=245000 '(.*)'/.exec(hook)?.[1];assert.ok(command);
|
||||
const match=/^"\$INSTDIR\\resources\\python\\python.exe" -I -B "\$INSTDIR\\resources\\inspector\\provision_runtime_acl.py" --provision$/.exec(command);assert.ok(match,'Installer command must use the exact copied private interpreter and helper');
|
||||
const executable=path.join(python,'python.exe'),helper=path.join(inspector,'provision_runtime_acl.py');
|
||||
const provision=await run(executable,['-I','-B',helper,'--provision']);assert.equal(provision.code,0,provision.output);assert.equal(provision.signal,null);
|
||||
const verified=JSON.parse(provision.output.trim());assert.equal(verified.verified,true);assert.equal(verified.rootCount,3);proof.profile=verified.profile;
|
||||
const readonly=await run(executable,['-I','-B',helper]);assert.equal(readonly.code,0,readonly.output);assert.equal(JSON.parse(readonly.output).profile,verified.profile);
|
||||
const script=`import sys,ctypes,json\nfrom pathlib import Path\nsys.path.insert(0,sys.argv[1])\nfrom windows_appcontainer import AppContainer,_api\nfrom ctypes import wintypes as W\ncontainer=AppContainer([sys.argv[1],sys.argv[2],sys.argv[3]])\nk,u,a=_api(); sid=ctypes.c_void_p(); assert u.DeriveAppContainerSidFromAppContainerName(container.name,ctypes.byref(sid))==0\ntext=W.LPWSTR(); assert a.ConvertSidToStringSidW(sid,ctypes.byref(text)); derived=text.value; k.LocalFree(text); a.FreeSid(sid)\nassert derived==container.sid_string\nprint(json.dumps({'matches':True,'profile':container.name,'packageAuthority':derived.startswith('S-1-15-2-')}))\n`;
|
||||
const sid=await run(executable,['-I','-B','-c',script,inspector,python,path.join(resources,'semantic')]);assert.equal(sid.code,0,sid.output);
|
||||
const identity=JSON.parse(sid.output);assert.equal(identity.matches,true);assert.equal(identity.packageAuthority,true);assert.equal(identity.profile,verified.profile);
|
||||
proof.deterministicNativePackageSid=true;proof.provisionExit=0;proof.readOnlyVerifyExit=0;
|
||||
const invalid=await run(executable,['-I','-B',helper,'--provision','--root',repository]);assert.notEqual(invalid.code,0);proof.callerChosenRootRejected=true;
|
||||
proof.outcome='passed';
|
||||
}catch(error){proof.outcome='failed';proof.errorType=error.name;throw error;}
|
||||
finally{
|
||||
await writeFile(path.join(artifacts,'installer-acl-native-proof-'+proof.startedAt.replaceAll(':','-')+'.json'),JSON.stringify(proof,null,2)+'\n');
|
||||
const absolute=path.resolve(root);assert.equal(path.dirname(absolute),artifacts);assert.ok(path.basename(absolute).startsWith('installer ACL native-'));await rm(absolute,{recursive:true,force:true});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// Explicit native proof: private synthetic signed-policy OTLP receiver only.
|
||||
// Never publishes/enables diagnostics on the user's browser or production tenant.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {createServer} from 'node:http';
|
||||
import {generateKeyPairSync,createHash} from 'node:crypto';
|
||||
import {readFile,writeFile,mkdir} from 'node:fs/promises';
|
||||
import {once} from 'node:events';
|
||||
import path from 'node:path';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
|
||||
const require=createRequire(import.meta.url),{createDiagnostics}=require('../apps/browser/diagnostics.cjs'),{startInspector}=require('../apps/browser/inspection.cjs'),{createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs');
|
||||
function children(){const text=execFileSync('powershell.exe',['-NoProfile','-NonInteractive','-Command',`@(Get-CimInstance Win32_Process -Filter "ParentProcessId = ${process.pid}" | Where-Object {$_.Name -eq 'python.exe' -and $_.CommandLine -like '*apps*inspector*server.py*'} | Select-Object -ExpandProperty ProcessId) | ConvertTo-Json -Compress`],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();return text?[].concat(JSON.parse(text)):[];}
|
||||
test('real contained Office stages export parent-correlated OTLP with sanitized native errors',{
|
||||
skip:process.platform!=='win32'||process.env.SAFETYNET_OFFICE_OTLP_NATIVE!=='1',timeout:90000
|
||||
},async()=>{
|
||||
const root=path.resolve('.'),payloads=[],diagnostics=createDiagnostics();let inspector;
|
||||
const server=createServer(async(req,res)=>{const chunks=[];let size=0;for await(const chunk of req){size+=chunk.length;assert.ok(size<1048576);chunks.push(chunk);}payloads.push({path:req.url,body:JSON.parse(Buffer.concat(chunks).toString())});res.setHeader('content-type','application/json');res.end('{}');});server.listen(0,'127.0.0.1');await once(server,'listening');
|
||||
try{
|
||||
const keys=generateKeyPairSync('ed25519'),policy=defaultPolicy();policy.assignment={deviceId:'synthetic-office-otel',groups:[]};policy.diagnostics={...policy.diagnostics,enabled:true,mode:'debug',endpoint:`http://127.0.0.1:${server.address().port}`,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000};
|
||||
const authority=await createPolicyAuthority({deviceId:'synthetic-office-otel',publicKey:keys.publicKey.export({type:'spki',format:'pem'})});authority.accept(signPolicy(policy,keys.privateKey.export({type:'pkcs8',format:'pem'})));const trusted=authority.snapshot();diagnostics.configure(trusted.policy.diagnostics,{deviceId:'synthetic-office-otel',policyVersion:trusted.version});
|
||||
inspector=startInspector(root,false,{onStages:envelope=>diagnostics.importStages(envelope)});assert.ok(inspector);await inspector.waitReady();assert.equal(children().length,1);
|
||||
const bytes=await readFile(path.join(root,'artifacts/office-coverage-fixtures/public-generated.xlsx'));const verdict=await diagnostics.span('browser.inspect.local',{operation:'office.native-proof'},()=>inspector.inspect(bytes,trusted.policy));assert.equal(verdict.decision,'allow');assert.equal(verdict.coverage.complete,true);
|
||||
const malformed=await diagnostics.span('browser.inspect.local',{operation:'office.native-error-proof'},()=>inspector.inspect(Buffer.from('PK\x03\x04SYNTHETIC PRIVATE ERROR BODY'),trusted.policy));assert.equal(malformed.decision,'block');
|
||||
await diagnostics.flush();assert.equal(diagnostics.status().failedExports,0);assert.equal(diagnostics.status().lifetimeFailedExports,0);
|
||||
const spans=payloads.flatMap(item=>item.body.resourceSpans||[]).flatMap(resource=>resource.scopeSpans||[]).flatMap(scope=>scope.spans||[]),names=['inspector.office.archive','inspector.office.xml','inspector.office.relationships','inspector.office.ocr'];
|
||||
const all=spans.find(item=>item.name==='inspector.office.relationships'),parse=spans.find(item=>item.name==='inspector.parse'&&item.traceId===all?.traceId);assert.ok(all&&parse);
|
||||
const stages=names.map(name=>{const span=spans.find(item=>item.name===name&&item.traceId===all.traceId);assert.ok(span,name);assert.equal(span.parentSpanId,parse.spanId);assert.ok(BigInt(span.endTimeUnixNano)>=BigInt(span.startTimeUnixNano));return {stage:name,spanId:span.spanId,parentSpanId:span.parentSpanId,durationMs:Number(BigInt(span.endTimeUnixNano)-BigInt(span.startTimeUnixNano))/1e6};});
|
||||
assert.ok(spans.some(item=>item.name==='inspector.office.archive'&&item.status?.code===2));assert.equal(JSON.stringify(payloads).includes('SYNTHETIC PRIVATE ERROR BODY'),false);assert.equal(JSON.stringify(payloads).includes('Public revenue'),false);
|
||||
const directory=path.join(root,'artifacts/office-native-observability');await mkdir(directory,{recursive:true});await writeFile(path.join(directory,'private-otlp-proof-'+new Date().toISOString().replaceAll(':','-')+'.json'),JSON.stringify({receiver:'private-synthetic-fixture',productionCollector:false,decision:'allow',coverageComplete:true,inputSha256:createHash('sha256').update(bytes).digest('hex'),traceId:all.traceId,parseSpanId:parse.spanId,stages,sanitizedErrorStage:true,failedExports:diagnostics.status().failedExports,lifetimeFailedExports:diagnostics.status().lifetimeFailedExports},null,2));
|
||||
}finally{
|
||||
inspector?.stop();await diagnostics.shutdown();server.closeAllConnections();await new Promise(resolve=>server.close(resolve));for(let attempt=0;attempt<20&&children().length;attempt++)await new Promise(resolve=>setTimeout(resolve,100));assert.equal(children().length,0);
|
||||
}
|
||||
});
|
||||
@@ -41,3 +41,12 @@ test('central API accepts typed broker causation and rejects invalid lifecycle s
|
||||
for(const broker of [{...fault.broker,authorizedEventId:undefined},{...fault.broker,authorizedEventId:fault.eventId},{...allow.broker,phase:'connect.block'},{...allow.broker,reason:'arbitrary secret details'},{...allow.broker,body:'private'}])assert.equal((await send({...fault,eventId:broker.authorizedEventId===fault.eventId?fault.eventId:randomUUID(),broker})).status,400);
|
||||
const rows=store.db.prepare('SELECT metadata FROM events ORDER BY id').all();assert.equal(rows.length,2);assert.deepEqual(JSON.parse(rows[1].metadata).broker,fault.broker);assert.equal(store.auditIntegrity.verify().valid,true);
|
||||
});
|
||||
|
||||
test('central typed download audit retains genuine file hash and requires causal request authorization',async t=>{
|
||||
const dir=mkdtempSync(join(tmpdir(),'safetynet-request-audit-')),store=openStore(dir),credential='synthetic-device-download-broker-token';store.db.prepare('INSERT INTO devices(id,name,token_hash) VALUES(?,?,?)').run('download-broker','Synthetic',digest(credential));
|
||||
const server=createApp(store,{publicUrl:'http://127.0.0.1',adminToken:'synthetic-administrator-token-123456789',secure:false}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});});
|
||||
const send=event=>fetch(`http://127.0.0.1:${server.address().port}/api/device/events`,{method:'POST',headers:{authorization:`Bearer ${credential}`,'content-type':'application/json'},body:JSON.stringify(event)});
|
||||
const event={eventId:randomUUID(),policyVersion:1,kind:'download.block',ruleId:'registered-document',file:{bytes:613,detectedType:'pdf',sha256:digest('synthetic real PDF bytes')},broker:{requestId:randomUUID(),authorizedEventId:randomUUID(),phase:'response.block',reason:'broker-download-inspection-block'}};assert.equal((await send(event)).status,200);
|
||||
for(const value of [{...event,eventId:randomUUID(),request:{bytes:613,detectedType:'pdf'}},{...event,eventId:randomUUID(),file:undefined},{...event,eventId:randomUUID(),broker:{...event.broker,authorizedEventId:undefined}},{...event,eventId:randomUUID(),broker:{...event.broker,phase:'request.block'}}])assert.equal((await send(value)).status,400);
|
||||
const stored=JSON.parse(store.db.prepare('SELECT metadata FROM events').get().metadata);assert.deepEqual(stored.file,event.file);assert.deepEqual(stored.broker,event.broker);assert.equal(store.auditIntegrity.verify().valid,true);
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Installer library tests use an in-memory BFE model, never Windows mutation."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent.parent/'apps/network-broker'))
|
||||
from windows_wfp_install import BoundaryInstaller,InstallError,PROVIDER,SUBLAYER,MARKER,SERVICE,_key
|
||||
from windows_wfp_probe import verify_observed
|
||||
IDENTITY='\\device\\harddiskvolume3\\program files\\safetynet\\safetynet.exe\0'.encode('utf-16le').hex()
|
||||
class Backend:
|
||||
def __init__(self):self.objects={};self.calls=[];self.failure=None;self.post_failure=False;self.snapshot=None;self.tamper=False
|
||||
def event(self,event):
|
||||
self.calls.append(event)
|
||||
if self.failure==event:raise InstallError('injected-native-failure',5)
|
||||
def app_id(self,_path):self.event('app-id');return IDENTITY
|
||||
def open(self):self.event('open')
|
||||
def close(self):self.event('close')
|
||||
def prepare_topology(self):return True
|
||||
def begin(self):self.event('begin');self.snapshot=copy.deepcopy(self.objects)
|
||||
def commit(self):self.event('commit');self.snapshot=None
|
||||
def abort(self):self.event('abort');self.objects=self.snapshot;self.snapshot=None
|
||||
def lookup(self,kind,key):self.event('lookup-'+kind);return copy.deepcopy(self.objects.get((kind,key)))
|
||||
def add_provider(self):self.event('add-provider');self.objects[('provider',PROVIDER)]={'flags':1,'data':MARKER.hex(),'service':SERVICE}
|
||||
def add_sublayer(self):self.event('add-sublayer');self.objects[('sublayer',SUBLAYER)]={'flags':1,'data':MARKER.hex(),'provider':PROVIDER,'weight':65535}
|
||||
def add_filter(self,rule):
|
||||
self.event('add-'+rule['id']);value={'provider':PROVIDER,'sublayer':SUBLAYER,'layer':rule['layer'],'flags':1,'action':0x1002 if rule['action']=='FWP_ACTION_PERMIT' else 0x1001,'conditions':copy.deepcopy(rule['conditions']),'weight':rule.get('weight')}
|
||||
for condition in value['conditions']:
|
||||
if condition['type']=='FWP_SECURITY_DESCRIPTOR_TYPE':
|
||||
from windows_wfp_receiver import service_descriptor
|
||||
import ctypes as C
|
||||
with service_descriptor(condition['value'][len('D:(A;;CC;;;'):-1]) as blob:condition['value']=C.string_at(blob.data,blob.size).hex()
|
||||
if self.tamper:value['action']=0x1002
|
||||
self.objects[('filter',_key(rule['id']))]=value
|
||||
def verify(self,plan):
|
||||
self.event('verify')
|
||||
if self.post_failure:return {'verified':False}
|
||||
return verify_observed(plan,[v for (kind,_key_),v in self.objects.items() if kind=='filter'],PROVIDER,SUBLAYER)
|
||||
def delete(self,kind,key):self.event('delete-'+kind);del self.objects[(kind,key)]
|
||||
class InstallTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
root=Path(__file__).resolve().parent.parent
|
||||
script="const {planBrowserNetworkBoundary}=require('./apps/network-broker/windows-wfp-plan.cjs');console.log(JSON.stringify(planBrowserNetworkBoundary({appId:Buffer.from(process.argv[1],'hex'),brokerPort:43871})));"
|
||||
result=subprocess.run(['node','-e',script,IDENTITY],cwd=root,capture_output=True,text=True,timeout=10,check=True);cls.plan=json.loads(result.stdout)
|
||||
def setUp(self):
|
||||
self.temp=tempfile.TemporaryDirectory();self.root=Path(self.temp.name);self.exe=self.root/'safetynet.exe';self.exe.write_bytes(b'SYNTHETIC EXE FIXTURE NEVER EXECUTED');self.sha=hashlib.sha256(self.exe.read_bytes()).hexdigest();self.backend=Backend();self.installer=BoundaryInstaller(self.backend,is_elevated=lambda:True)
|
||||
def tearDown(self):self.temp.cleanup()
|
||||
def apply(self,plan=None):return self.installer.apply(self.root,self.exe,self.sha,43871,plan or self.plan)
|
||||
def rollback(self):return self.installer.rollback(self.root,self.exe,self.sha,43871,self.plan)
|
||||
def test_atomic_order_and_exact_six_deny_filters(self):
|
||||
result=self.apply();self.assertTrue(result['nativeVerified']);self.assertFalse(result['receiverTestsVerified']);self.assertEqual(len(self.backend.objects),8)
|
||||
calls=self.backend.calls;self.assertLess(calls.index('begin'),calls.index('add-provider'));self.assertLess(calls.index('add-sublayer'),calls.index('add-v4-non-tcp'));self.assertLess(calls.index('add-v6-all'),calls.index('commit'));self.assertEqual(calls[-2:],['verify','close'])
|
||||
def test_incomplete_topology_prevents_transaction_or_mutation(self):
|
||||
self.backend.prepare_topology=lambda:False
|
||||
with self.assertRaisesRegex(InstallError,'topology-admission-unavailable'):self.apply()
|
||||
self.assertNotIn('begin',self.backend.calls)
|
||||
self.assertFalse(any(call.startswith('add-') for call in self.backend.calls))
|
||||
self.assertEqual(self.backend.calls[-1],'close')
|
||||
def test_exact_eight_filter_atomic_receiver_install_and_rollback(self):
|
||||
script="const {planServiceReceiver}=require('./apps/network-broker/wfp-topology.cjs');console.log(JSON.stringify(planServiceReceiver({appId:Buffer.from(process.argv[1],'hex'),brokerPort:43871,serviceSid:'S-1-5-80-1-2-3-4-5'})));"
|
||||
result=subprocess.run(['node','-e',script,IDENTITY],cwd=Path(__file__).resolve().parents[1],capture_output=True,text=True,check=True,timeout=10)
|
||||
receiver=json.loads(result.stdout)
|
||||
plan=copy.deepcopy(self.plan);plan.update(version=3,brokerApplicationId=IDENTITY,serviceSid=receiver['serviceSid']);plan['filters']+=receiver['filters']
|
||||
with self.assertRaisesRegex(InstallError,'receiver-install-requires-verified-broker'):self.apply(plan)
|
||||
result=self.installer.apply_managed(self.root,self.exe,self.sha,self.exe,self.sha,43871,plan)
|
||||
self.assertTrue(result['nativeVerified']);self.assertEqual(len(self.backend.objects),10)
|
||||
permit=self.backend.objects[('filter',_key('broker-service-receiver'))]
|
||||
self.assertEqual(permit['action'],0x1002);self.assertEqual(permit['weight'],2)
|
||||
self.installer.rollback(self.root,self.exe,self.sha,43871,plan)
|
||||
self.assertEqual(self.backend.objects,{})
|
||||
def test_repeat_install_does_not_recreate_or_delete_existing_filters(self):
|
||||
self.apply();self.backend.calls=[];self.assertTrue(self.apply()['existing']);self.assertFalse(any(c.startswith(('add-','delete-')) for c in self.backend.calls))
|
||||
def test_administrator_required_before_any_backend_operation(self):
|
||||
self.installer.is_elevated=lambda:False
|
||||
with self.assertRaisesRegex(InstallError,'administrator-required'):self.apply()
|
||||
self.assertEqual(self.backend.calls,[])
|
||||
def test_manifest_hash_and_containment_enforced(self):
|
||||
with self.assertRaisesRegex(InstallError,'installation-hash-mismatch'):self.installer.apply(self.root,self.exe,'0'*64,43871,self.plan)
|
||||
with self.assertRaisesRegex(InstallError,'installation-path-invalid'):self.installer.apply(self.root/'missing',self.exe,self.sha,43871,self.plan)
|
||||
def test_changed_app_id_or_permit_filter_refused_before_open(self):
|
||||
for mutate in ['identity','permit','port','extra']:
|
||||
plan=copy.deepcopy(self.plan)
|
||||
if mutate=='identity':plan['applicationId']='00'
|
||||
if mutate=='permit':plan['filters'][0]['action']='FWP_ACTION_PERMIT'
|
||||
if mutate=='port':plan['filters'][2]['conditions'][-1]['value']=443
|
||||
if mutate=='extra':plan['filters'].append(plan['filters'][0])
|
||||
with self.assertRaises(InstallError):self.apply(plan)
|
||||
self.assertNotIn('open',self.backend.calls)
|
||||
def test_existing_provider_collision_does_not_add_delete(self):
|
||||
self.backend.objects[('provider',PROVIDER)]={'flags':1,'data':'foreign','service':SERVICE}
|
||||
with self.assertRaisesRegex(InstallError,'existing-boundary-collision'):self.apply()
|
||||
self.assertEqual(self.backend.calls[-2:],['abort','close']);self.assertFalse(any(c.startswith(('add-','delete-')) for c in self.backend.calls))
|
||||
def test_every_mutation_or_commit_error_aborts_and_closes(self):
|
||||
for failure in ['add-provider','add-sublayer','add-v4-non-tcp','add-v4-tcp-non-broker-address','add-v4-tcp-other-local-port','add-v6-all','commit']:
|
||||
self.backend=Backend();self.installer=BoundaryInstaller(self.backend,is_elevated=lambda:True);self.backend.failure=failure
|
||||
with self.assertRaises(InstallError):self.apply()
|
||||
self.assertEqual(self.backend.calls[-2:],['abort','close']);self.assertEqual(self.backend.objects,{})
|
||||
def test_readback_tampering_aborts_before_commit(self):
|
||||
self.backend.tamper=True
|
||||
with self.assertRaisesRegex(InstallError,'added-filter-mismatch'):self.apply()
|
||||
self.assertNotIn('commit',self.backend.calls);self.assertEqual(self.backend.objects,{})
|
||||
def test_postcommit_verification_failure_keeps_denies_and_reports_failure(self):
|
||||
self.backend.post_failure=True
|
||||
with self.assertRaisesRegex(InstallError,'postcommit-verification-failed'):self.apply()
|
||||
self.assertEqual(len(self.backend.objects),8);self.assertNotIn('abort',self.backend.calls);self.assertEqual(self.backend.calls[-1],'close')
|
||||
def test_rollback_removes_only_exact_owned_six_filters_and_metadata(self):
|
||||
self.apply();self.backend.calls=[];self.assertTrue(self.rollback()['removedOwnedBoundary']);self.assertEqual(self.backend.objects,{})
|
||||
self.assertEqual([c for c in self.backend.calls if c.startswith('delete-')],['delete-filter']*6+['delete-sublayer','delete-provider'])
|
||||
def test_rollback_metadata_collision_prevents_any_delete(self):
|
||||
self.apply();self.backend.objects[('sublayer',SUBLAYER)]['data']='foreign';self.backend.calls=[]
|
||||
with self.assertRaises(InstallError):self.rollback()
|
||||
self.assertFalse(any(c.startswith('delete-') for c in self.backend.calls));self.assertEqual(len(self.backend.objects),8)
|
||||
def test_rollback_error_restores_ownership_objects(self):
|
||||
self.apply();self.backend.failure='delete-sublayer'
|
||||
with self.assertRaises(InstallError):self.rollback()
|
||||
self.assertEqual(len(self.backend.objects),8);self.assertEqual(self.backend.calls[-2:],['abort','close'])
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Read-only WFP contract and cleanup tests; no filter mutations."""
|
||||
import copy
|
||||
import ctypes as C
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent.parent/'apps/network-broker'))
|
||||
from windows_wfp_probe import ReadOnlyWfp,verify_observed,Filter
|
||||
PROVIDER='2a7bcbf2-e4f5-4e94-bd33-d5442a3bef50'
|
||||
SUBLAYER='403a69b8-fc7c-460a-a247-72e0d72e6bf8'
|
||||
def contract():
|
||||
expected=[];observed=[]
|
||||
for index in range(6):
|
||||
conditions=[{'field':'d78e1e87-8644-4ea5-9437-d809ecefc971','match':'equal','type':'FWP_BYTE_BLOB_TYPE','value':'01020304'}, {'field':'3971ef2b-623e-4f9a-8cb1-6e79b806b9a7','match':'equal','type':'FWP_UINT8','value':index}]
|
||||
expected.append({'layer':'layer-'+str(index),'action':'FWP_ACTION_BLOCK','clearActionRight':False,'conditions':conditions})
|
||||
observed.append({'provider':PROVIDER,'sublayer':SUBLAYER,'layer':'layer-'+str(index),'flags':1,'action':0x1001,'conditions':copy.deepcopy(conditions)})
|
||||
return {'filters':expected},observed
|
||||
class WfpProbeTests(unittest.TestCase):
|
||||
def test_exact_contract_and_reordered_conditions(self):
|
||||
plan,items=contract();items.reverse()
|
||||
for item in items:item['conditions'].reverse()
|
||||
self.assertTrue(verify_observed(plan,items,PROVIDER,SUBLAYER)['verified'])
|
||||
def test_missing_duplicate_or_extra_filters(self):
|
||||
plan,items=contract()
|
||||
for bad in [items[:-1],items+items[:1],items[:3]+items[:1]]:
|
||||
self.assertFalse(verify_observed(plan,bad,PROVIDER,SUBLAYER)['verified'])
|
||||
def test_blob_type_match_layer_and_numeric_mismatch(self):
|
||||
plan,items=contract()
|
||||
for key,value in [('type','FWP_UINT32'),('value','deadbeef'),('match','not-equal')]:
|
||||
bad=copy.deepcopy(items);bad[0]['conditions'][0][key]=value
|
||||
self.assertEqual(verify_observed(plan,bad,PROVIDER,SUBLAYER)['reason'],'filter-condition-mismatch')
|
||||
bad=copy.deepcopy(items);bad[0]['layer']='other-layer';self.assertFalse(verify_observed(plan,bad,PROVIDER,SUBLAYER)['verified'])
|
||||
def test_disabled_nonpersistent_permit_softblock_or_wrong_owner(self):
|
||||
plan,items=contract()
|
||||
for key,value in [('flags',0),('flags',33),('flags',9),('action',0x1002),('provider','11111111-1111-1111-1111-111111111111'),('sublayer','22222222-2222-2222-2222-222222222222')]:
|
||||
bad=copy.deepcopy(items);bad[0][key]=value
|
||||
self.assertEqual(verify_observed(plan,bad,PROVIDER,SUBLAYER)['reason'],'filter-metadata-mismatch')
|
||||
def test_engine_denial_is_unverified_and_not_closed_or_enumerated(self):
|
||||
probe=ReadOnlyWfp.__new__(ReadOnlyWfp);probe.api=Mock();probe.api.FwpmEngineOpen0.return_value=5
|
||||
self.assertEqual(probe.inspect(contract()[0],PROVIDER,SUBLAYER),{'verified':False,'reason':'engine-unavailable','code':5})
|
||||
probe.api.FwpmFilterCreateEnumHandle0.assert_not_called();probe.api.FwpmEngineClose0.assert_not_called()
|
||||
def test_enum_handle_denial_closes_opened_engine(self):
|
||||
probe=ReadOnlyWfp.__new__(ReadOnlyWfp);probe.api=Mock()
|
||||
def opened(_name,_auth,_identity,_session,handle):handle._obj.value=123;return 0
|
||||
probe.api.FwpmEngineOpen0.side_effect=opened;probe.api.FwpmFilterCreateEnumHandle0.return_value=5
|
||||
self.assertEqual(probe.inspect(contract()[0],PROVIDER,SUBLAYER)['code'],5)
|
||||
probe.api.FwpmEngineClose0.assert_called_once();probe.api.FwpmFilterDestroyEnumHandle0.assert_not_called()
|
||||
def test_empty_enumeration_cleans_handle_and_engine(self):
|
||||
probe=ReadOnlyWfp.__new__(ReadOnlyWfp);probe.api=Mock()
|
||||
def opened(_name,_auth,_identity,_session,handle):handle._obj.value=123;return 0
|
||||
def enumeration(_engine,_template,handle):handle._obj.value=456;return 0
|
||||
def empty(_engine,_handle,_limit,_entries,count):count._obj.value=0;return 0
|
||||
probe.api.FwpmEngineOpen0.side_effect=opened;probe.api.FwpmFilterCreateEnumHandle0.side_effect=enumeration;probe.api.FwpmFilterEnum0.side_effect=empty
|
||||
self.assertEqual(probe.inspect(contract()[0],PROVIDER,SUBLAYER)['reason'],'filter-count-mismatch')
|
||||
probe.api.FwpmFilterDestroyEnumHandle0.assert_called_once();probe.api.FwpmEngineClose0.assert_called_once()
|
||||
def test_oversized_native_page_frees_memory_and_closes_handles(self):
|
||||
probe=ReadOnlyWfp.__new__(ReadOnlyWfp);probe.api=Mock()
|
||||
probe.api.FwpmEngineOpen0.side_effect=lambda *args:(setattr(args[-1]._obj,'value',123) or 0)
|
||||
probe.api.FwpmFilterCreateEnumHandle0.side_effect=lambda *args:(setattr(args[-1]._obj,'value',456) or 0)
|
||||
storage=(C.POINTER(Filter)*129)()
|
||||
def oversized(_engine,_handle,_limit,entries,count):
|
||||
C.cast(entries,C.POINTER(C.POINTER(C.POINTER(Filter))))[0]=C.cast(storage,C.POINTER(C.POINTER(Filter)))
|
||||
count._obj.value=129;return 0
|
||||
probe.api.FwpmFilterEnum0.side_effect=oversized
|
||||
self.assertEqual(probe.inspect(contract()[0],PROVIDER,SUBLAYER)['reason'],'enumeration-limit')
|
||||
probe.api.FwpmFreeMemory0.assert_called_once();probe.api.FwpmFilterDestroyEnumHandle0.assert_called_once();probe.api.FwpmEngineClose0.assert_called_once()
|
||||
def test_enumeration_error_cleans_handles(self):
|
||||
probe=ReadOnlyWfp.__new__(ReadOnlyWfp);probe.api=Mock()
|
||||
probe.api.FwpmEngineOpen0.side_effect=lambda *args:(setattr(args[-1]._obj,'value',123) or 0)
|
||||
probe.api.FwpmFilterCreateEnumHandle0.side_effect=lambda *args:(setattr(args[-1]._obj,'value',456) or 0)
|
||||
probe.api.FwpmFilterEnum0.return_value=5
|
||||
self.assertEqual(probe.inspect(contract()[0],PROVIDER,SUBLAYER)['reason'],'enumeration-failed')
|
||||
probe.api.FwpmFilterDestroyEnumHandle0.assert_called_once();probe.api.FwpmEngineClose0.assert_called_once()
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,59 @@
|
||||
import ctypes as C
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps/network-broker'))
|
||||
from windows_wfp_receiver import service_sddl, service_descriptor
|
||||
from windows_wfp_install import NativeBackend
|
||||
from windows_wfp_probe import Filter, Blob
|
||||
|
||||
class ReceiverTests(unittest.TestCase):
|
||||
def test_service_sid_only_and_minimum_match_right(self):
|
||||
self.assertEqual(service_sddl('S-1-5-80-1-2-3-4-5'), 'D:(A;;CC;;;S-1-5-80-1-2-3-4-5)')
|
||||
for sid in ['S-1-5-19', 'S-1-5-80-01-2-3-4-5', 'S-1-5-80-4294967296-2-3-4-5']:
|
||||
with self.assertRaises(Exception): service_sddl(sid)
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'Native Windows SD validation')
|
||||
def test_actual_native_self_relative_sd_and_exact_ace_mask(self):
|
||||
api = C.WinDLL('advapi32')
|
||||
api.IsValidSecurityDescriptor.argtypes = [C.c_void_p]
|
||||
api.IsValidSecurityDescriptor.restype = C.c_int
|
||||
with service_descriptor('S-1-5-80-1-2-3-4-5') as blob:
|
||||
self.assertTrue(api.IsValidSecurityDescriptor(blob.data))
|
||||
raw = C.string_at(blob.data, blob.size)
|
||||
self.assertTrue(int.from_bytes(raw[2:4], 'little') & 0x8000)
|
||||
dacl = int.from_bytes(raw[16:20], 'little')
|
||||
self.assertEqual(int.from_bytes(raw[dacl+4:dacl+6], 'little'), 1)
|
||||
self.assertEqual(int.from_bytes(raw[dacl+12:dacl+16], 'little'), 1)
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'Native Windows SD marshalling')
|
||||
def test_actual_sd_lifetime_and_uint64_soft_permit_marshalling_without_add(self):
|
||||
backend = NativeBackend.__new__(NativeBackend)
|
||||
backend.engine = C.c_void_p(1)
|
||||
captured = []
|
||||
def capture(name, _engine, pointer, _security, _id):
|
||||
self.assertEqual(name, 'FwpmFilterAdd0')
|
||||
value = C.cast(pointer, C.POINTER(Filter)).contents
|
||||
self.assertEqual(value.flags, 1) # PERSISTENT only; no clear-action.
|
||||
self.assertEqual(value.action.type, 0x1002)
|
||||
self.assertEqual(value.weight.type, 4)
|
||||
self.assertEqual(C.cast(value.weight.pointer, C.POINTER(C.c_uint64)).contents.value, 2)
|
||||
self.assertEqual(value.count, 5)
|
||||
condition = value.conditions[4]
|
||||
self.assertEqual(condition.value.type, 14)
|
||||
blob = C.cast(condition.value.pointer, C.POINTER(Blob)).contents
|
||||
captured.append(C.string_at(blob.data, blob.size))
|
||||
backend._call = capture
|
||||
script = "const {planServiceReceiver}=require('./apps/network-broker/wfp-topology.cjs');console.log(JSON.stringify(planServiceReceiver({appId:Buffer.from(process.argv[1],'hex'),brokerPort:43120,serviceSid:'S-1-5-80-1-2-3-4-5'}).filters[0]));"
|
||||
identity = '\\device\\harddiskvolume3\\safetynet\\node.exe\0'.encode('utf-16le').hex()
|
||||
result = subprocess.run(['node','-e',script,identity],cwd=Path(__file__).resolve().parents[1],capture_output=True,text=True,check=True,timeout=10)
|
||||
rule = json.loads(result.stdout)
|
||||
backend.add_filter(rule)
|
||||
rule['conditions'] = rule['conditions'][-1:]
|
||||
with self.assertRaises(Exception): backend.add_filter(rule)
|
||||
self.assertEqual(len(captured), 1)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
import ctypes as C
|
||||
import sys
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps/network-broker'))
|
||||
from windows_wfp_topology import TopologyObserver, CALLBACK
|
||||
|
||||
class ObserverTests(unittest.TestCase):
|
||||
def observer(self, denied=False):
|
||||
calls = []
|
||||
class Api:
|
||||
def FwpmEngineOpen0(self, *_args):
|
||||
_args[-1]._obj.value = 9
|
||||
return 0
|
||||
def FwpmFilterSubscribeChanges0(self, _engine, _subscription, _callback, _context, handle):
|
||||
calls.append('filter-subscribe')
|
||||
handle._obj.value = 10
|
||||
return 0
|
||||
def FwpmSubLayerSubscribeChanges0(self, _engine, _subscription, _callback, _context, handle):
|
||||
calls.append('sublayer-subscribe')
|
||||
if denied:
|
||||
return 5
|
||||
handle._obj.value = 11
|
||||
return 0
|
||||
def FwpmFilterUnsubscribeChanges0(self, *_args): calls.append('filter-unsubscribe')
|
||||
def FwpmSubLayerUnsubscribeChanges0(self, *_args): calls.append('sublayer-unsubscribe')
|
||||
def FwpmEngineClose0(self, *_args): calls.append('close')
|
||||
observer = TopologyObserver.__new__(TopologyObserver)
|
||||
observer.api = Api()
|
||||
observer.engine = C.c_void_p()
|
||||
observer.handles = []
|
||||
observer.changed = threading.Event()
|
||||
observer.callback = CALLBACK(lambda *_args: observer.changed.set())
|
||||
return observer, calls
|
||||
|
||||
def test_second_subscription_failure_releases_first_and_engine(self):
|
||||
observer, calls = self.observer(True)
|
||||
self.assertEqual(observer.start()['code'], 5)
|
||||
self.assertTrue(observer.changed.is_set())
|
||||
self.assertEqual(calls, ['filter-subscribe', 'sublayer-subscribe', 'filter-unsubscribe', 'close'])
|
||||
self.assertEqual(observer.handles, [])
|
||||
|
||||
def test_actual_callback_invalidates_and_never_claims_full_visibility(self):
|
||||
observer, calls = self.observer()
|
||||
result = observer.start()
|
||||
self.assertFalse(result['allObjectsReadable'])
|
||||
observer.callback(None, None)
|
||||
self.assertEqual(observer.snapshot([])['reason'], 'observer-unavailable')
|
||||
observer.close()
|
||||
self.assertEqual(calls[-3:], ['sublayer-unsubscribe', 'filter-unsubscribe', 'close'])
|
||||
observer.close()
|
||||
self.assertEqual(calls.count('close'), 1)
|
||||
|
||||
def test_authorized_readback_cannot_invent_visibility_and_change_is_sticky(self):
|
||||
from types import SimpleNamespace
|
||||
observer, _calls = self.observer()
|
||||
rows=[{'key':str(index),'sublayer':'owned','provider':'provider'} for index in range(8)]
|
||||
observer.revalidate=lambda _layers:{'complete':True,'allObjectsReadable':False,'filters':rows}
|
||||
observer.reader=SimpleNamespace(inspect=lambda *_args:{'verified':True})
|
||||
result=observer.authorized_snapshot({},'provider','owned',[])
|
||||
self.assertFalse(result['allObjectsReadable'])
|
||||
self.assertTrue(all(row['ownedVerified'] for row in result['filters']))
|
||||
def raced(*_args):
|
||||
observer.changed.set()
|
||||
return {'verified':True}
|
||||
observer.reader.inspect=raced
|
||||
self.assertEqual(observer.authorized_snapshot({},'provider','owned',[])['reason'],'owned-readback-unavailable')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,mkdir,writeFile,rm} from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {createWindowsPackagePlan} from '../scripts/windows-package-plan.mjs';
|
||||
import {buildVerifiedInstaller} from '../scripts/verified-installer.mjs';
|
||||
const identity={sourceSha256:'a'.repeat(64),runtimeSha256:'b'.repeat(64),packageSha256:'c'.repeat(64)};
|
||||
async function fixture(t){
|
||||
const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-installer-verifier-'));t.after(()=>rm(root,{force:true,recursive:true}));
|
||||
const output=path.join(root,'dist');await mkdir(output);const plan=createWindowsPackagePlan({workspace:root,outputDirectory:output,electronDirectory:path.join(root,'electron'),platform:'win32'});
|
||||
return {plan,expectedIdentity:identity,readIdentity:async()=>identity,verify:async()=>({identity,fuses:true,resources:true,broker:true,runtimeAcl:true}),installerPath:path.join(output,'fixture.exe')};
|
||||
}
|
||||
test('actual invoked installer arguments require before and after package verification',async t=>{
|
||||
const f=await fixture(t);let calls=0,verifications=0;
|
||||
const result=await buildVerifiedInstaller({...f,verify:async()=>{verifications++;return f.verify();},run:async args=>{calls++;assert.ok(args.includes('--prepackaged'));const fake=Buffer.alloc(1024);fake.write('MZ');await writeFile(f.installerPath,fake);return {status:0};}});
|
||||
assert.equal(calls,1);assert.equal(verifications,2);assert.equal(result.bytes,1024);assert.match(result.sha256,/^[a-f0-9]{64}$/);
|
||||
// Synthetic PE prefix exercises orchestration only, never installer validity.
|
||||
});
|
||||
test('source mutation during successful CLI run invalidates the installer result',async t=>{
|
||||
const f=await fixture(t);let changed=false;
|
||||
await assert.rejects(buildVerifiedInstaller({...f,readIdentity:async()=>changed?{...identity,sourceSha256:'d'.repeat(64)}:identity,run:async()=>{changed=true;return {status:0};}}),{code:'installer_identity_changed'});
|
||||
});
|
||||
test('failed CLI and missing or stale artifact cannot pass based on a good plan',async t=>{
|
||||
const f=await fixture(t);
|
||||
await assert.rejects(buildVerifiedInstaller({...f,run:async()=>({status:1})}),{code:'installer_build_failed'});
|
||||
await assert.rejects(buildVerifiedInstaller({...f,run:async()=>({status:0})}),{code:'installer_artifact_invalid'});
|
||||
const fake=Buffer.alloc(1024);fake.write('MZ');await writeFile(f.installerPath,fake);
|
||||
await assert.rejects(buildVerifiedInstaller({...f,run:async()=>({status:0})}),{code:'installer_artifact_invalid'});
|
||||
});
|
||||
test('post-invocation mandatory security verification cannot be skipped',async t=>{
|
||||
const f=await fixture(t);let calls=0;
|
||||
await assert.rejects(buildVerifiedInstaller({...f,verify:async()=>({...await f.verify(),broker:++calls===1}),run:async()=>({status:0})}),{code:'installer_verification_failed'});
|
||||
});
|
||||
test('identity stays pinned through final artifact hashing and result delivery',async t=>{
|
||||
const f=await fixture(t);let reads=0;
|
||||
await assert.rejects(buildVerifiedInstaller({...f,readIdentity:async()=>++reads>=6?{...identity,sourceSha256:'d'.repeat(64)}:identity,run:async()=>{const fake=Buffer.alloc(1024);fake.write('MZ');await writeFile(f.installerPath,fake);return {status:0};}}),{code:'installer_identity_changed'});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import {createWindowsPackagePlan} from '../scripts/windows-package-plan.mjs';
|
||||
|
||||
const root=path.resolve('.'),options={workspace:root,outputDirectory:path.join(root,'dist'),electronDirectory:path.join(root,'node_modules/electron/dist'),platform:'win32'};
|
||||
const expected={sourceSha256:'a'.repeat(64),runtimeSha256:'b'.repeat(64),packageSha256:'c'.repeat(64)};
|
||||
const receipt={identity:expected,fuses:true,resources:true,broker:true,runtimeAcl:true};
|
||||
test('unpacked and verified NSIS stages use distinct official CLI commands',async()=>{
|
||||
const plan=createWindowsPackagePlan(options);
|
||||
assert.ok(plan.unpacked.args.includes('--dir'));
|
||||
assert.deepEqual(plan.verification.required,['fuses','resources','broker','runtimeAcl']);
|
||||
const installer=await plan.installer({expectedIdentity:expected,readIdentity:async()=>expected,verify:async()=>receipt});
|
||||
assert.deepEqual(installer.args.slice(0,4),['--win','nsis','--prepackaged',plan.unpacked.directory]);
|
||||
assert.equal(installer.args.includes('--dir'),false);
|
||||
assert.deepEqual(installer.args.slice(-2),['--publish','never']);
|
||||
assert.equal(Object.isFrozen(installer.args),true);
|
||||
});
|
||||
test('every security verification gate is mandatory before installer planning',async()=>{
|
||||
const plan=createWindowsPackagePlan(options);
|
||||
for(const key of ['fuses','resources','broker','runtimeAcl'])await assert.rejects(plan.installer({expectedIdentity:expected,readIdentity:async()=>expected,verify:async()=>({...receipt,[key]:false})}),{code:'package_verification_incomplete'});
|
||||
});
|
||||
test('missing verification callbacks cannot authorize an installer',async()=>{
|
||||
await assert.rejects(createWindowsPackagePlan(options).installer({expectedIdentity:expected}),{code:'package_verifier_required'});
|
||||
});
|
||||
test('source runtime and package identity changes each prevent reuse',async()=>{
|
||||
const plan=createWindowsPackagePlan(options);
|
||||
for(const key of Object.keys(expected)){
|
||||
let reads=0;
|
||||
await assert.rejects(plan.installer({expectedIdentity:expected,readIdentity:async()=>++reads===1?expected:{...expected,[key]:'d'.repeat(64)},verify:async()=>receipt}),{code:'package_identity_changed'});
|
||||
await assert.rejects(plan.installer({expectedIdentity:expected,readIdentity:async()=>expected,verify:async()=>({...receipt,identity:{...expected,[key]:'d'.repeat(64)}})}),{code:'package_verification_incomplete'});
|
||||
}
|
||||
});
|
||||
test('outside output malformed branding and unsupported platform are rejected',()=>{
|
||||
for(const override of [{outputDirectory:path.dirname(root)},{outputDirectory:root},{productName:'bad\nname'},{appId:'bad'},{platform:'linux'}]){
|
||||
assert.throws(()=>createWindowsPackagePlan({...options,...override}));
|
||||
}
|
||||
});
|
||||
test('failed verifier remains a failure rather than a distributable result',async()=>{
|
||||
const failure=new Error('synthetic verifier failure');
|
||||
await assert.rejects(createWindowsPackagePlan(options).installer({expectedIdentity:expected,readIdentity:async()=>expected,verify:async()=>{throw failure;}}),error=>error===failure);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,mkdir,writeFile,rm,symlink} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {generateKeyPairSync} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
const {signManifest,verifyReleaseEnvelope}=createRequire(import.meta.url)('../packages/core/windows-trust.cjs');
|
||||
import {buildReleaseInventory,verifyReleaseInventory} from '../scripts/windows-release-inventory.mjs';
|
||||
async function fixture(t){const root=await mkdtemp(path.join(os.tmpdir(),'safetynet-release-inventory-'));t.after(()=>rm(root,{recursive:true,force:true}));for(const file of ['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py','resources/semantic/model.bin']){const name=path.join(root,file);await mkdir(path.dirname(name),{recursive:true});await writeFile(name,'synthetic-public-'+file);}return root;}
|
||||
test('complete actual staged inventory binds all components and excludes only external envelopes',async t=>{
|
||||
const root=await fixture(t);await writeFile(path.join(root,'release-envelope.json'),'synthetic-public-signature');await writeFile(path.join(root,'deployment-envelope.json'),'synthetic-public-deployment');const files=await buildReleaseInventory(root);assert.equal(files.length,7);assert.deepEqual(files.map(file=>file.path),files.map(file=>file.path).sort());assert.ok(files.every(file=>/^[a-f0-9]{64}$/.test(file.sha256)));assert.equal(await verifyReleaseInventory(root,{manifest:{files}}),true);
|
||||
});
|
||||
test('missing scanner, same-size tampering and extra broker module cannot pass trusted inventory',async t=>{
|
||||
const root=await fixture(t),files=await buildReleaseInventory(root);const target=path.join(root,'resources/broker/node.exe');const original=Buffer.from('synthetic-public-resources/broker/node.exe');await writeFile(target,Buffer.alloc(original.length,65));await assert.rejects(verifyReleaseInventory(root,{manifest:{files}}),{code:'windows-release-inventory-mismatch'});await writeFile(target,original);await writeFile(path.join(root,'resources/broker/extra.cjs'),'synthetic-public-extra');await assert.rejects(verifyReleaseInventory(root,{manifest:{files}}),{code:'windows-release-inventory-mismatch'});await rm(path.join(root,'resources/inspector/server.py'));await assert.rejects(buildReleaseInventory(root),{code:'windows-release-component-missing'});
|
||||
});
|
||||
test('actual junction cannot smuggle unsigned external files into release inventory',async t=>{
|
||||
const root=await fixture(t);await symlink(path.join(root,'resources/python'),path.join(root,'resources/linked'),process.platform==='win32'?'junction':'dir');await assert.rejects(buildReleaseInventory(root),{code:'windows-release-path-unsafe'});
|
||||
});
|
||||
|
||||
test('genuine canonical signed release verifies actual inventory despite JSON field order, then rejects tamper',async t=>{
|
||||
const root=await fixture(t),vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'});
|
||||
const manifest={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:1,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:customer.publicKey.export({type:'spki',format:'pem'}),files:await buildReleaseInventory(root)};
|
||||
const signed=signManifest(manifest,vendor.privateKey.export({type:'pkcs8',format:'pem'}),'release'),verified=verifyReleaseEnvelope(signed,{publicKey:vendor.publicKey.export({type:'spki',format:'pem'})});
|
||||
assert.notDeepEqual(Object.keys(manifest.files[0]),Object.keys(verified.manifest.files[0]));
|
||||
assert.equal(await verifyReleaseInventory(root,verified),true);
|
||||
const browser=verified.manifest.files.find(file=>file.path==='SafetyNet.exe');await writeFile(path.join(root,'SafetyNet.exe'),Buffer.alloc(browser.bytes,66));
|
||||
await assert.rejects(verifyReleaseInventory(root,verified),{code:'windows-release-inventory-mismatch'});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {generateKeyPairSync,sign} from 'node:crypto';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest,publicKeyCoordinates,verifiedManagementBinding,DOMAINS}=require('../packages/core/windows-trust.cjs');
|
||||
function key(type='ec'){const value=generateKeyPairSync(type,type==='ec'?{namedCurve:'prime256v1'}:{});return {publicKey:value.publicKey.export({type:'spki',format:'pem'}),privateKey:value.privateKey.export({type:'pkcs8',format:'pem'})};}
|
||||
function fixture(){
|
||||
const vendor=key(),customer=key(),policy=key('ed25519');
|
||||
const release={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:2,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:customer.publicKey,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:4,sha256:'a'.repeat(64)}))};
|
||||
const deployment={format:'safetynet-windows-deployment-v1',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:3,policyPublicKey:policy.publicKey,expiresAt:Date.now()+60000,users:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-workstation',minimumPolicyVersion:7}]};
|
||||
const verified=verifyReleaseEnvelope(signManifest(release,vendor.privateKey,'release'),{publicKey:vendor.publicKey});
|
||||
return {vendor,customer,policy,release,deployment,verified};
|
||||
}
|
||||
function deploy(f,value=f.deployment,options={}){return verifyDeploymentEnvelope(signManifest(value,f.customer.privateKey,'deployment'),{release:f.verified,authenticatedSid:f.deployment.users[0].sid,...options});}
|
||||
test('vendor release delegates independent customer signer and actual SID produces exact pinned bootstrap',()=>{const f=fixture(),verified=deploy(f);assert.deepEqual(verified.bootstrap,{publicKey:f.policy.publicKey,deviceId:'synthetic-workstation',minimumVersion:7});assert.ok(Object.isFrozen(verified)&&Object.isFrozen(verified.bootstrap)&&Object.isFrozen(f.verified.manifest.files));assert.equal(publicKeyCoordinates(f.vendor.publicKey).length,64);});
|
||||
test('vendor key cannot sign delegated customer deployment and fake verified releases are rejected',()=>{const f=fixture();assert.throws(()=>verifyDeploymentEnvelope(signManifest(f.deployment,f.vendor.privateKey,'deployment'),{release:f.verified,authenticatedSid:f.deployment.users[0].sid}),{code:'windows-trust-signature-invalid'});assert.throws(()=>verifyDeploymentEnvelope(signManifest(f.deployment,f.customer.privateKey,'deployment'),{release:structuredClone(f.verified),authenticatedSid:f.deployment.users[0].sid}),{code:'windows-deployment-invalid'});});
|
||||
test('release signature tamper, unpinned signer and invalid public key types fail with fixed errors',()=>{const f=fixture(),envelope=signManifest(f.release,f.vendor.privateKey,'release');const tampered={...envelope,payload:Buffer.from(JSON.stringify({...f.release,releaseSequence:99})).toString('base64url')};assert.throws(()=>verifyReleaseEnvelope(tampered,{publicKey:f.vendor.publicKey}),{code:'windows-trust-signature-invalid'});assert.throws(()=>verifyReleaseEnvelope(envelope,{publicKey:f.customer.publicKey}),{code:'windows-trust-signature-invalid'});for(const pem of [f.vendor.privateKey,f.policy.publicKey,'secret invalid key'])assert.throws(()=>verifyReleaseEnvelope(envelope,{publicKey:pem}),error=>error.message===error.code&&!error.message.includes('secret'));});
|
||||
test('strict canonical signed bytes reject duplicate fields, envelope extras and cross-domain signatures',()=>{
|
||||
const f=fixture(),text=JSON.stringify({...f.release,extra:'secret'}),payload=Buffer.from(text).toString('base64url');
|
||||
const signed=text=>sign('sha256',Buffer.concat([Buffer.from(DOMAINS.release),Buffer.from(text)]),{key:f.vendor.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64url');
|
||||
assert.throws(()=>verifyReleaseEnvelope({payload,signature:signed(text)},{publicKey:f.vendor.publicKey}),{code:'windows-trust-canonical-invalid'});
|
||||
const good=signManifest(f.release,f.vendor.privateKey,'release'),canonical=Buffer.from(good.payload,'base64url').toString(),duplicate=canonical.replace('"releaseSequence":2','"releaseSequence":2,"releaseSequence":2');assert.throws(()=>verifyReleaseEnvelope({payload:Buffer.from(duplicate).toString('base64url'),signature:signed(duplicate)},{publicKey:f.vendor.publicKey}),{code:'windows-trust-canonical-invalid'});
|
||||
assert.throws(()=>verifyReleaseEnvelope({...good,key:f.vendor.publicKey},{publicKey:f.vendor.publicKey}),{code:'windows-trust-envelope-invalid'});
|
||||
const deploymentSignature=sign('sha256',Buffer.concat([Buffer.from(DOMAINS.deployment),Buffer.from(good.payload,'base64url')]),{key:f.vendor.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64url');assert.throws(()=>verifyReleaseEnvelope({...good,signature:deploymentSignature},{publicKey:f.vendor.publicKey}),{code:'windows-trust-signature-invalid'});
|
||||
});
|
||||
test('protected release/deployment floors reject rollback and same sequence equivocation',()=>{const f=fixture(),envelope=signManifest(f.release,f.vendor.privateKey,'release');assert.throws(()=>verifyReleaseEnvelope(envelope,{publicKey:f.vendor.publicKey,floor:{minimumSequence:3}}),{code:'windows-trust-rollback'});assert.throws(()=>verifyReleaseEnvelope(envelope,{publicKey:f.vendor.publicKey,floor:{minimumSequence:2,acceptedDigest:'0'.repeat(64)}}),{code:'windows-trust-rollback'});assert.ok(verifyReleaseEnvelope(envelope,{publicKey:f.vendor.publicKey,floor:{minimumSequence:2,acceptedDigest:f.verified.digest}}));const d=deploy(f);assert.throws(()=>deploy(f,f.deployment,{floor:{minimumSequence:4}}),{code:'windows-trust-rollback'});assert.throws(()=>deploy(f,{...f.deployment,expiresAt:f.deployment.expiresAt+1},{floor:{minimumSequence:3,acceptedDigest:d.digest}}),{code:'windows-trust-rollback'});});
|
||||
test('release/tenant/expiry/SID binding rejects caller-selected authority or unknown peer',()=>{const f=fixture();for(const change of [{releaseId:'32345678-1234-1234-1234-123456789abc'},{deploymentTenant:'32345678-1234-1234-1234-123456789abc'},{expiresAt:1}])assert.throws(()=>deploy(f,{...f.deployment,...change}),{code:'windows-deployment-binding-invalid'});assert.throws(()=>deploy(f,f.deployment,{authenticatedSid:'S-1-5-21-1-2-3-1002'}),{code:'windows-deployment-user-denied'});assert.throws(()=>signManifest({...f.deployment,publicKey:f.policy.publicKey},f.customer.privateKey,'deployment'),{code:'windows-deployment-invalid'});});
|
||||
test('strict inventory and user schemas reject traversal, case duplicates, unsafe bounds and ambiguous accounts',()=>{const f=fixture();for(const files of [[{path:'../SafetyNet.exe',bytes:1,sha256:'a'.repeat(64)}],[...f.release.files,{path:'safetynet.exe',bytes:4,sha256:'a'.repeat(64)}]])assert.throws(()=>signManifest({...f.release,files},f.vendor.privateKey,'release'),{code:'windows-release-invalid'});for(const sid of ['S-1-5-18','S-1-5-21-1-2-3-01001','S-1-5-21-1-2-3-4294967296'])assert.throws(()=>signManifest({...f.deployment,users:[{...f.deployment.users[0],sid}]},f.customer.privateKey,'deployment'),{code:'windows-deployment-invalid'});assert.throws(()=>deploy(f,f.deployment,{floor:{minimumSequence:-1}}),{code:'windows-trust-floor-invalid'});});
|
||||
test('canonical Entra user SID binds actual peer while service aliases remain rejected',()=>{const f=fixture(),sid='S-1-12-1-123-456-789-1000',deployment={...f.deployment,users:[{...f.deployment.users[0],sid}]};assert.equal(deploy(f,deployment,{authenticatedSid:sid}).bootstrap.deviceId,'synthetic-workstation');for(const invalid of ['S-1-12-1-0123-456-789-1000','S-1-12-1-4294967296-456-789-1000','S-1-5-80-1-2-3-4-5','S-1-5-32-544'])assert.throws(()=>signManifest({...deployment,users:[{...deployment.users[0],sid:invalid}]},f.customer.privateKey,'deployment'),{code:'windows-deployment-invalid'});});
|
||||
function v2(f){const pq=key('ml-dsa-87'),channel=require('../packages/core/policy-channel.cjs');return {pq,channel,deployment:{...f.deployment,format:'safetynet-windows-deployment-v2',management:{origin:'https://management.example',signatureKey:pq.publicKey,protocol:channel.VERSION}}};}
|
||||
test('signed v2 returns only provenance-verified frozen management binding compatible with actual PQ channel',()=>{const f=fixture(),{pq,channel,deployment}=v2(f),verified=deploy(f,deployment),binding=verifiedManagementBinding(verified);assert.deepEqual(binding,deployment.management);assert.ok(Object.isFrozen(binding));const identity=channel.identity(f.policy.privateKey,pq.privateKey,pq.publicKey);assert.equal(channel.verifyIdentity(identity,verified.bootstrap.publicKey,binding.signatureKey),pq.publicKey);assert.throws(()=>verifiedManagementBinding(structuredClone(verified)),{code:'windows-management-denied'});assert.throws(()=>verifiedManagementBinding(deploy(f)),{code:'windows-management-denied'});});
|
||||
test('v2 management rejects noncanonical origins, credential fields and channel algorithm downgrade',()=>{const f=fixture(),{pq,deployment}=v2(f),management=deployment.management;for(const origin of ['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://\u7ba1\u7406.example'])assert.throws(()=>signManifest({...deployment,management:{...management,origin}},f.customer.privateKey,'deployment'));for(const changed of [{...management,token:'synthetic-disallowed'},{...management,protocol:'safetynet-policy-mlkem768-mldsa65-v1'},{...management,signatureKey:key('ml-dsa-65').publicKey},{...management,signatureKey:f.policy.publicKey},{...management,signatureKey:pq.privateKey},{...management,signatureKey:pq.publicKey.trim()}])assert.throws(()=>signManifest({...deployment,management:changed},f.customer.privateKey,'deployment'));});
|
||||
test('management format/tampering cannot bypass delegated signatures or v1 historical capability boundary',()=>{const f=fixture(),{deployment}=v2(f),envelope=signManifest(deployment,f.customer.privateKey,'deployment'),tampered={...envelope,payload:Buffer.from(JSON.stringify({...deployment,management:{...deployment.management,origin:'https://attacker.example'}})).toString('base64url')};assert.throws(()=>verifyDeploymentEnvelope(tampered,{release:f.verified,authenticatedSid:f.deployment.users[0].sid}),{code:'windows-trust-signature-invalid'});for(const value of [{...deployment,format:'safetynet-windows-deployment-v3'},{...deployment,format:'safetynet-windows-deployment-v1'},{...deployment,extra:'unknown'},Object.fromEntries(Object.entries(deployment).filter(([name])=>name!=='management'))])assert.throws(()=>signManifest(value,f.customer.privateKey,'deployment'),{code:'windows-deployment-invalid'});assert.throws(()=>verifyDeploymentEnvelope(signManifest(deployment,f.vendor.privateKey,'deployment'),{release:f.verified,authenticatedSid:f.deployment.users[0].sid}),{code:'windows-trust-signature-invalid'});assert.equal(verifiedManagementBinding(deploy(f,deployment)).origin,'https://management.example');});
|
||||
|
||||
test('verified management projection cannot grant an expired deployment capability',t=>{
|
||||
const f=fixture(),{deployment}=v2(f),verified=deploy(f,deployment);
|
||||
t.mock.method(Date,'now',()=>deployment.expiresAt);
|
||||
assert.throws(()=>verifiedManagementBinding(verified),{code:'windows-management-denied'});
|
||||
});
|
||||
test('management origin host subset matches routed DNS and canonical IPv4/IPv6',()=>{
|
||||
const f=fixture(),{deployment}=v2(f);
|
||||
for(const origin of ['https://localhost:8443','https://127.0.0.1','https://[::1]','https://[::ffff:c0a8:1]'])assert.equal(verifiedManagementBinding(deploy(f,{...deployment,management:{...deployment.management,origin}})).origin,origin);
|
||||
for(const origin of ['https://bad_host.example','https://0xabc.example','https://[0:0:0:0:0:0:0:1]','https://[::ffff:192.168.0.1]','https://127.1','https://management.example:','https://[::1]:'])assert.throws(()=>signManifest({...deployment,management:{...deployment.management,origin}},f.customer.privateKey,'deployment'));
|
||||
});
|
||||
Reference in new issue
Block a user