52 lines
5.8 KiB
JavaScript
52 lines
5.8 KiB
JavaScript
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));
|
|
});
|