Files
safetynet/tests/broker-production-native.test.mjs
T

60 lines
12 KiB
JavaScript

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);
});