Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11213 -58

No files matched your search

+25 -9
View File
@@ -11,20 +11,29 @@ SafetyNet combines an Electron/Chromium desktop browser, an administrator web co
The current milestone is the [Windows DLP MVP](docs/windows-dlp-mvp.md): enforce
confidential-content checks before browser clipboard release and outbound text
or file transmission, with central policy and local semantic inspection.
Native public/blocked transfer and restart fixtures pass. Final acceptance is
still open: a reproducible Chromium WebRTC renderer crash in initial blank
iframes requires a verified native fix. Other platforms and broader features
remain outside this milestone.
S Browser uses official, unchanged Electron/Chromium releases. It does not require
a custom Chromium build or changes to installed Chrome. Browser, DLP broker,
private runtimes, scanners and models are intended to ship in one Windows
installer; the new broker component package is verified separately.
Final acceptance is still open: the main browser must be connected to the
protected Windows service, with verified installation and network enforcement.
A reproducible WebRTC renderer crash remains; the existing restriction stays
until its replacement is tested. See the [current acceptance matrix](docs/core-dlp-current-status.md)
for completed component tests and remaining integration work. Other platforms
and broader features remain outside this milestone.
[Native transport auditing](docs/native-transport-observability.md) records
tested WebTransport lifecycle attempts without exposing raw browser debugging
APIs. Its observation channel preserves clipboard protection; unsupported
transport payloads remain blocked by the native relay boundary.
APIs. Its observation channel preserves clipboard protection. These tests do
not establish the new Windows network boundary or complete transport coverage.
[Bounded diagnostics](docs/diagnostics-backpressure.md) preserve authorized
exports through policy-version updates and cancel them when permissions change.
Current native clipboard, upload, restart, local Gemma and transport fixtures
pass with zero lifetime export failures; historical failures remain documented.
Controlled clipboard, upload, restart, local Gemma and transport fixtures have
export evidence; historical failures remain documented. Production broker
diagnostics, durable audit delivery and final installer acceptance remain separate
requirements.
S Browser uses translucent materials, rounded controls, light/dark themes, and a content-first layout inspired by Safari. Website content is left unchanged.
@@ -194,7 +203,14 @@ npm run package:win
npm run verify:package
```
The packaging workflow produces an unpacked evaluation build in `dist/win-unpacked`. Distribute the complete folder, not the executable alone. Code signing, a production installer and a trusted update service remain unfinished.
The existing evaluation build is an unpacked application in `dist/win-unpacked`.
The required release deliverable is one Windows installer containing the browser,
its separate DLP broker and private runtime, local scanners and semantic models.
It must require no separately installed Node/Python or manual broker startup.
That complete installation and automatic browser-to-broker integration are still
being implemented and verified. An unpacked folder or broker payload alone does
not prove this deliverable. Code signing and a trusted update service remain
unfinished.
## Current boundaries
+40
View File
@@ -0,0 +1,40 @@
'use strict';
const {BrokerAuditEventSchema}=require('../../packages/core/broker-audit.cjs');
// Parent-only adapter. persistQueue must synchronously encrypt, write, fsync and
// commit the complete queue, return undefined on success and throw on failure.
// An OTLP send, boolean result or asynchronous enqueue is insufficient.
// Bind current() to the authenticated child and the accepted policy generation.
function createBrokerAuditPersistence({current,policyVersion,getQueue,persistQueue,onFailure}={}){
if(![current,getQueue,persistQueue,onFailure].every(fn=>typeof fn==='function')||!Number.isSafeInteger(policyVersion)||policyVersion<1)throw Error('broker-audit-parent-invalid');
let failed=false;
const fail=()=>{const first=!failed;failed=true;if(first)try{onFailure();}catch{}throw Error('broker-audit-parent-unavailable');};
const active=()=>{try{return !failed&&current()===true;}catch{return false;}};
return Object.freeze({
persist(input){
if(!active())return fail();
let event,queue;
try{
let nodes=0;
const plain=(value,depth=0)=>{
if(++nodes>128||depth>6)return false;
if(!value||typeof value!=='object')return true;
if(![Object.prototype,null].includes(Object.getPrototypeOf(value)))return false;
return Object.values(Object.getOwnPropertyDescriptors(value)).every(item=>'value'in item&&plain(item.value,depth+1));
};
if(!input||typeof input!=='object'||!plain(input))return fail();
event=BrokerAuditEventSchema.parse(input);queue=getQueue();
if(event.policyVersion!==policyVersion||!Array.isArray(queue)||queue.length>=10000||queue.some(item=>item.eventId===event.eventId))return fail();
Object.freeze(event.request);if(event.file)Object.freeze(event.file);if(event.broker)Object.freeze(event.broker);Object.freeze(event);
queue.push(event);
const result=persistQueue();
// Fail closed for an accidentally asynchronous writer; suppress its rejected
// promise without issuing a receipt or retrying uncertain persistence.
if(result&&typeof result.then==='function'){Promise.resolve(result).catch(()=>{});return fail();}
if(result!==undefined)return fail();
if(!active())return fail();
return Object.freeze({persisted:true,eventId:event.eventId});
}catch{return fail();}
}
});
}
module.exports={createBrokerAuditPersistence};
+55
View File
@@ -0,0 +1,55 @@
'use strict';
const {X509Certificate}=require('node:crypto');
const {isIP}=require('node:net');
const fail=()=>{throw Error('broker-certificate-pin-rejected');};
function authorityFacts(value){
if(typeof value!=='string'||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))fail();
const match=/^(\[[^\]]+\]|[^:]+):([1-9][0-9]{0,4})$/.exec(value);if(!match||Number(match[2])>65535)fail();
let url;try{url=new URL('https://'+value+'/');}catch{fail();}
const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname;
if(!isIP(hostname)&&hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))fail();
if(`${isIP(hostname)===6?'['+hostname+']':hostname}:${Number(match[2])}`!==value)fail();
return Object.freeze({hostname,authority:value});
}
/** For one managed Electron session only. Electron's verifier has NO port and
* caches results: CONNECT/SNI/inner-Host gates must still bind the port. Revoke
* must also close existing connections/stop the old broker before replacement.
* Management sessions retain the platform verifier and receive no such pins.
*/
function createBrokerCertificatePins({generation,policyVersion,current,authorizedAuthority,maxPins=128}={}){
if(typeof generation!=='string'||!generation||!Number.isSafeInteger(policyVersion)||policyVersion<1||typeof current!=='function'||typeof authorizedAuthority!=='function'||!Number.isInteger(maxPins)||maxPins<1||maxPins>1024)fail();
let revoked=false;const pins=new Map();
const active=()=>{try{return !revoked&&current()===true;}catch{return false;}};
const authorized=authority=>{try{return authorizedAuthority(authority)===true;}catch{return false;}};
return Object.freeze({
accept(input){
if(!active()||!input||input.generation!==generation||input.policyVersion!==policyVersion||typeof input.cert!=='string'||Buffer.byteLength(input.cert)>16384||!/^-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+\r?\n-----END CERTIFICATE-----\s*$/.test(input.cert))fail();
const facts=authorityFacts(input.authority);if(!authorized(facts.authority))fail();
let certificate;try{certificate=new X509Certificate(input.cert);}catch{fail();}
const now=Date.now(),until=Date.parse(certificate.validTo),from=Date.parse(certificate.validFrom);
const matches=isIP(facts.hostname)?certificate.checkIP(facts.hostname):certificate.checkHost(facts.hostname,{subject:'never',wildcards:false});
if(certificate.ca||!certificate.subjectAltName||certificate.subjectAltName.includes(',')||!matches||!certificate.verify(certificate.publicKey)||!Number.isFinite(until)||!Number.isFinite(from)||from>now||until<=now||until>now+3601000||input.fingerprint256!==certificate.fingerprint256)fail();
for(const [key,pin]of pins)if(pin.expiresAt<=now)pins.delete(key);
const key=facts.authority;
if(!pins.has(key)&&pins.size>=maxPins)fail();
if(!active()||!authorized(key))fail();
pins.set(key,Object.freeze({...facts,fingerprint:certificate.fingerprint256,expiresAt:until}));return true;
},
verify(request,callback){
let result=-3;
try{
// Electron exposes Certificate.data/fingerprint, not Node's fingerprint256.
// Derive our fingerprint from the actual presented PEM, never a claim.
const data=request?.certificate?.data,host=request?.hostname;
if(typeof data!=='string'||Buffer.byteLength(data)>16384)throw Error('certificate-data-unavailable');
const fingerprint=new X509Certificate(data).fingerprint256;
const match=[...pins.values()].find(pin=>pin.hostname===host&&pin.fingerprint===fingerprint);
if(match)result=active()&&match.expiresAt>Date.now()&&authorized(match.authority)?0:-2;
}catch{result=-3;}
callback(result);
},
revoke(){revoked=true;},
status(){return Object.freeze({active:active(),pins:pins.size});}
});
}
module.exports={createBrokerCertificatePins,authorityFacts};
@@ -0,0 +1,34 @@
'use strict';
const path=require('node:path');
const {spawn}=require('node:child_process');
const {EventEmitter}=require('node:events');
const {createBrokerSupervisor}=require('./broker-supervisor.cjs');
const {createFramedChannel}=require('../network-broker/framed-channel.cjs');
// Privileged browser construction only. Installation verification covers the
// fixed native client as well as the signed broker payload. No renderer-supplied
// command, path, pipe name, environment or identity enters this transport.
function createServiceBrokerSupervisor({installationRoot,verifyInstallation,verifyPackage,persistAudit,acceptCertificate,onQuarantine,launch=spawn,deadlineMs,shutdownMs,traceOperation}={}){
if(!path.isAbsolute(installationRoot||'')||typeof verifyInstallation!=='function'||typeof launch!=='function'||typeof verifyPackage!=='function')throw Error('broker-supervisor-invalid');
const root=path.resolve(installationRoot),resourceRoot=path.join(root,'resources','broker');
function connect(){
const env={};for(const name of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[name])env[name]=process.env[name];
// --client-stdio authenticates the retained actual browser parent and the
// connected service pipe handle in native code, before forwarding frames.
const child=launch(path.join(root,'safetynet-service-host.exe'),['--client-stdio'],{cwd:root,env,windowsHide:true,shell:false,stdio:['pipe','pipe','ignore']});
if(!child||!child.stdin||!child.stdout||typeof child.kill!=='function')throw Error('broker-supervisor-unavailable');
const transport=new EventEmitter();
const channel=createFramedChannel({readable:child.stdout,writable:child.stdin,inboundFirstLimit:65536,outboundFirstLimit:2097152});
Object.defineProperties(transport,{exitCode:{get:()=>child.exitCode},signalCode:{get:()=>child.signalCode}});
channel.on('message',message=>transport.emit('message',message));
channel.once('disconnect',()=>transport.emit('disconnect'));
child.once('error',()=>{channel.disconnect();transport.emit('error',Error('broker-supervisor-unavailable'));});
child.once('exit',(code,signal)=>{channel.disconnect();transport.emit('exit',code,signal);});
transport.send=(message,callback)=>channel.send(message,callback);
// Only the retained child handle is terminated. The service owns the broker
// Job; losing this connection causes its generation to close independently.
transport.kill=signal=>{channel.disconnect();return child.kill(signal);};
return transport;
}
return createBrokerSupervisor({resourceRoot,verifyPackage:async location=>await verifyInstallation(root)===true&&await verifyPackage(location)===true,persistAudit,acceptCertificate,onQuarantine,spawn:connect,deadlineMs,shutdownMs,traceOperation});
}
module.exports={createServiceBrokerSupervisor};
+111
View File
@@ -0,0 +1,111 @@
'use strict';
const {fork}=require('node:child_process');
const path=require('node:path');
const {randomUUID,X509Certificate}=require('node:crypto');
const UUID=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
const ENTRY='apps/network-broker/production-entry.cjs';
const STARTUP_CODES=new Set(['broker-bootstrap-invalid','broker-route-unsupported','broker-startup-inspector','broker-startup-certificate','broker-startup-transport','broker-startup-managed','broker-startup-failed']);
// Parent-only: resourceRoot and verifyPackage come from the trusted embedded
// installer contract. Never accept executable/module paths from renderer IPC.
function createBrokerSupervisor({resourceRoot,verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn=fork,deadlineMs=15000,shutdownMs=5000,traceOperation=(_name,_attributes,run)=>run()}={}){
if(!path.isAbsolute(resourceRoot||'')||![verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn].every(f=>typeof f==='function')||!Number.isInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000)throw Error('broker-supervisor-invalid');
if(!Number.isInteger(shutdownMs)||shutdownMs<1||shutdownMs>30000)throw Error('broker-supervisor-invalid');
if(typeof traceOperation!=='function')throw Error('broker-supervisor-invalid');
// Instrumentation may neither skip a protected operation nor replace its
// result, retry it, or invoke it twice. Only fixed stage names are exported.
async function stage(name,operation){
let original,called=false;
const once=()=>{if(called)throw Error('broker-diagnostics-invalid');called=true;try{original=Promise.resolve(operation());}catch(error){original=Promise.reject(error);}original.catch(()=>{});return original;};
try{await traceOperation(name,{component:'network'},once);}catch(error){if(called)throw error;return once();}
if(!called)once();return original;
}
let child,current,failed=false,closed=false,starting=false,timer,readyResolve,readyReject,lastSequence=0,stopping,pendingCertificates=0,startupCode;
const stopChild=()=>{
clearTimeout(timer);if(stopping)return stopping;
const previous=child;if(!previous)return Promise.resolve();
previous.removeAllListeners('message');
stopping=stage('browser.broker.shutdown',()=>new Promise((resolve,reject)=>{
let done=false;
const finish=()=>{if(done)return;done=true;clearTimeout(limit);child=undefined;resolve();};
const limit=setTimeout(()=>{if(done)return;done=true;failed=true;try{onQuarantine('broker-shutdown-unconfirmed');}catch{}reject(Error('broker-shutdown-unconfirmed'));},shutdownMs);
previous.once('exit',finish);
// ChildProcess retains ownership state; no numeric PID/taskkill operation
// is performed after exit, avoiding a reused-PID tree kill. Native job-based
// descendant containment remains an installer/launcher prerequisite.
if(previous.exitCode!==null&&previous.exitCode!==undefined||previous.signalCode)finish();
else try{previous.kill('SIGKILL');}catch{}
}));
// Fault callers do not await shutdown, but close() receives its real outcome.
stopping.catch(()=>{});return stopping;
};
const fault=()=>{if(failed||closed)return;failed=true;stopChild();try{onQuarantine('broker-supervisor-unavailable');}catch{}readyReject?.(Error('broker-supervisor-unavailable'));};
const send=value=>{if(!child||failed||closed)throw Error('broker-supervisor-unavailable');child.send(value,error=>{if(error)fault();});};
const context=()=>({version:1,generation:current.generation,policyVersion:current.policyVersion});
const exact=(m,keys)=>Object.keys(m).sort().join(',')===keys.sort().join(',');
async function message(m,owner,generation){
if(owner!==child||generation!==current.generation)return;
try{
const bytes=Buffer.byteLength(JSON.stringify(m));
if(bytes>65536||!m||typeof m!=='object'||Array.isArray(m)||m.version!==1||m.generation!==current.generation||m.policyVersion!==current.policyVersion)throw Error();
const common=['type','version','generation','policyVersion'];
if(m.type==='startup-fault'){
if(current.ready||!exact(m,[...common,'code'])||!STARTUP_CODES.has(m.code))throw Error();
// Bounded local pre-enrollment state only; no raw errors or unauthorised
// telemetry exporter. This never grants readiness or permits a retry.
startupCode=m.code;fault();return;
}
if(m.type==='ready'){
if(m.sequence!==0||m.authMode!=='basic'||current.ready||!exact(m,[...common,'port','credentials','authMode','sequence'])||!Number.isInteger(m.port)||m.port!==current.port||!m.credentials||!exact(m.credentials,['username','password'])||!/^safetynet-[a-f0-9]{32}$/.test(m.credentials.username)||!/^[A-Za-z0-9_-]{43}$/.test(m.credentials.password))throw Error();
current.ready=true;clearTimeout(timer);readyResolve(Object.freeze({generation:current.generation,policyVersion:current.policyVersion,port:m.port,credentials:Object.freeze({...m.credentials})}));return;
}
if(!UUID.test(m.requestId)||!Number.isSafeInteger(m.sequence)||m.sequence!==lastSequence+1)throw Error();lastSequence=m.sequence;
if(m.type==='audit'&&exact(m,[...common,'requestId','sequence','event'])){
const receipt=persistAudit(m.event);
if(!receipt||receipt.persisted!==true||receipt.eventId!==m.event.eventId||receipt.then)throw Error();
if(owner!==child||generation!==current.generation)throw Error();
send({type:'audit-receipt',...context(),requestId:m.requestId,sequence:m.sequence,receipt});return;
}
if(m.type==='certificate'&&exact(m,[...common,'requestId','sequence','authority','cert'])){
if(pendingCertificates>=16)throw Error();
if(typeof m.authority!=='string'||m.authority.length>300||typeof m.cert!=='string'||m.cert.length>16384||!/^-----BEGIN CERTIFICATE-----/.test(m.cert))throw Error();
const certificate=new X509Certificate(m.cert);if(certificate.ca)throw Error();
pendingCertificates++;
const certificateDeadline=setTimeout(fault,shutdownMs);
let accepted;
// Admission remains occupied until the ORIGINAL callback settles even if
// the deadline quarantines. A timed-out operation cannot free capacity.
try{accepted=await acceptCertificate(Object.freeze({...context(),authority:m.authority,cert:m.cert,fingerprint256:certificate.fingerprint256}));}
finally{clearTimeout(certificateDeadline);pendingCertificates--;}
if(accepted!==true||owner!==child||generation!==current.generation)throw Error();
send({type:'certificate-receipt',...context(),requestId:m.requestId,sequence:m.sequence,accepted:true});return;
}
throw Error();
}catch{fault();}
}
return Object.freeze({
async start({policyVersion,port,config}={}){
if(failed||closed||child||starting||!Number.isSafeInteger(policyVersion)||policyVersion<1||!Number.isInteger(port)||port<1||port>65535)throw Error('broker-supervisor-invalid');
starting=true;
try{
if(await stage('browser.broker.verify',()=>verifyPackage(resourceRoot))!==true)throw Error();
if(failed||closed)throw Error();
current={generation:randomUUID(),policyVersion,port,ready:false};lastSequence=0;
const envelope={type:'start',...context(),config};if(Buffer.byteLength(JSON.stringify(envelope))>2097152)throw Error();
// Allowlisted Windows runtime essentials only; inherited Node injection,
// proxy credentials and developer environment never reach the child.
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[key])env[key]=process.env[key];
const pending=new Promise((resolve,reject)=>{readyResolve=resolve;readyReject=reject;});
const owner=spawn(path.join(resourceRoot,ENTRY),[],{execPath:path.join(resourceRoot,'node.exe'),cwd:resourceRoot,env,execArgv:[],windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],serialization:'json'});
child=owner;const generation=current.generation;
owner.on('message',m=>{void message(m,owner,generation);});
for(const event of ['error','exit','disconnect'])owner.once(event,()=>{if(owner===child)fault();});
timer=setTimeout(fault,deadlineMs);send(envelope);return await stage('browser.broker.start',()=>pending);
}catch{fault();throw Error('broker-supervisor-unavailable');}
},
async close(){closed=true;readyReject?.(Error('broker-supervisor-closed'));await stopChild();},
// Replacement is deliberately stop-and-new-supervisor: no automatic retry
// and no stale ready state or credentials survive a policy generation.
status(){return Object.freeze({ready:!!current?.ready&&!failed&&!closed,failed,closed,generation:current?.generation,startupCode});}
});
}
module.exports={createBrokerSupervisor};
+2
View File
@@ -10,6 +10,8 @@ const {trace,ROOT_CONTEXT,SpanStatusCode,TraceFlags}=require('@opentelemetry/api
const names=new Set(['browser.window','browser.network','browser.ipc','browser.agent','browser.management','browser.navigation','browser.dns','browser.request','browser.request.headers','browser.inspect.local','browser.inspect.enterprise','browser.download','browser.clipboard','browser.visual','browser.password.fill','browser.ai','browser.policy','browser.startup','browser.parser','browser.semantic','browser.yara','browser.icap','browser.tab','browser.security','browser.evidence']);
const operations=new Set(['audit.deliver','evidence.observer.install','evidence.observer.drain','evidence.observer.dispose','policy.verify','policy.bookmarks','policy.agent-reconcile','policy.page-configure','policy.audit','evidence.capture','evidence.authorize','evidence.challenge','evidence.encrypt','evidence.send','evidence.ack','agent.request','agent.challenge','agent.decrypt','agent.transport','agent.authorize','agent.audit','agent.dispatch','agent.response','agent.project','agent.encrypt','library:organize-plan','library:organize-apply','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','state','authorize-setting','settings-update','window-control','chrome-layout','share-url','bookmarks-mutate','enroll','sso','navigate','back','forward','reload','ai','inspect-file','agent-connection','ai-panel','vault-panel','vault-status','vault-create','vault-unlock','vault-lock','vault-list','vault-save','vault-delete','vault-fill','vault-generate','tab-create','tab-activate','tab-close','tab-group','sessions','read','inspect','scroll','click','type','select','check','screenshot','copy','cut','paste','scan','download','sync','create','activate','close','group','policy','request','dns','parse','embed','yara','icap']);
for(const operation of ['visual.probe','visual.capture','visual.inspect.local','visual.inspect.enterprise','visual.audit','visual.validate','visual.project','visual.release'])operations.add(operation);
for(const operation of ['broker.request','broker.framing','broker.buffer','broker.inspect','broker.authorize','broker.forward'])operations.add(operation);
for(const name of ['browser.broker.verify','browser.broker.start','browser.broker.shutdown'])names.add(name);
const components=new Set(['browser','ipc','agent','management','navigation','network','dns','dlp','inspector','parser','semantic','yara','icap','download','clipboard','visual','vault','ai','policy','tabs']);
for(const operation of ['text.local','text.enterprise'])operations.add(operation);
for(const operation of ['history:list','history:go','window:state','window:control'])operations.add(operation);
+34 -14
View File
@@ -1,20 +1,40 @@
const {app}=require('electron');
const {spawn}=require('node:child_process');
const path=require('node:path');
function capabilityCrypto(operation,input){
const root=path.join(__dirname,'../..');
const python=app.isPackaged?path.join(process.resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
const script=app.isPackaged?path.join(process.resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
if(!['protect','unprotect'].includes(operation)||Buffer.byteLength(input)>32768)return Promise.reject(Error('Capability rejected'));
function createCapabilityCrypto({packaged=false,resourcesPath,root=path.join(__dirname,'../..'),launch=spawn,environment=process.env}={}){
const python=packaged?path.join(resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
const script=packaged?path.join(resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
const active=new Set();
return function convert(operation,input){
const inputLimit=operation==='protect'?32768:65536;
if(!['protect','unprotect'].includes(operation)||!(typeof input==='string'||Buffer.isBuffer(input))||!Buffer.byteLength(input)||Buffer.byteLength(input)>inputLimit)return Promise.reject(Error('Capability rejected'));
if(active.size>=8)return Promise.reject(Error('Windows capability protection failed'));
return new Promise((resolve,reject)=>{
const environment={...process.env};for(const key of ['ADMIN_TOKEN','OIDC_CLIENT_SECRET','AI_API_KEY','INSPECTOR_TOKEN','SIEM_HTTP_TOKEN','SAFETYNET_PROXY_PASSWORD'])delete environment[key];
const child=spawn(python,[script,operation],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:environment});let output='',settled=false;
const timer=setTimeout(()=>{child.kill();fail();},5000);
function fail(){if(!settled){settled=true;clearTimeout(timer);reject(Error('Windows capability protection failed'));}}
child.on('error',fail);child.stdin.on('error',fail);child.stderr.on('data',()=>{});
child.stdout.on('data',data=>{output+=data;if(output.length>65536){child.kill();fail();}});
child.on('close',code=>{if(code!==0||!output)return fail();if(!settled){settled=true;clearTimeout(timer);resolve(output);}});
child.stdin.end(input);
// Python startup hooks and inherited secrets must not enter a capability
// helper. Isolated mode also disables user-site and Python environment paths.
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(environment[key])env[key]=environment[key];
let child,timer,settled=false,terminal=false,bytes=0;
const output=Buffer.alloc(operation==='protect'?65536:32768),slot={};active.add(slot);
function fail(){
if(settled)return;settled=true;clearTimeout(timer);output.fill(0);
// A rejected operation does not free the slot until actual child close.
if(child&&!terminal){try{child.kill();}catch{}child.stdin?.destroy?.();child.stdout?.destroy?.();child.stderr?.destroy?.();}
reject(Error('Windows capability protection failed'));
}
try{
child=launch(python,['-I','-B',script,operation],{windowsHide:true,shell:false,stdio:['pipe','pipe','pipe'],env});
child.once('close',code=>{
terminal=true;active.delete(slot);
if(settled)return;if(code!==0||!bytes){fail();return;}
settled=true;clearTimeout(timer);const result=output.subarray(0,bytes).toString('utf8');output.fill(0);resolve(result);
});
child.on('error',fail);child.stdin.on('error',fail);child.stdout.on('error',fail);child.stderr.on('error',fail);child.stderr.on('data',()=>{});
child.stdout.on('data',data=>{if(settled)return;if(!Buffer.isBuffer(data)||bytes+data.length>output.length){fail();return;}data.copy(output,bytes);bytes+=data.length;});
timer=setTimeout(fail,5000);child.stdin.end(input);
}catch{if(!child)active.delete(slot);fail();}
});
};
}
module.exports={capabilityCrypto};
let sharedConverter;
function capabilityCrypto(operation,input){sharedConverter??=createCapabilityCrypto({packaged:app.isPackaged,resourcesPath:process.resourcesPath});return sharedConverter(operation,input);}
module.exports={capabilityCrypto,createCapabilityCrypto};
+3 -2
View File
@@ -102,6 +102,7 @@ let message='Enroll this browser to receive company policy.',syncTimer,localInsp
const recentRequests=new Map();
const agentFile=()=>path.join(app.getPath('userData'),'agent-connection.json');
const agentGrants=require('./agent-grants.cjs');
function protectAgentCapability(value){return diagnosticSpan('browser.credential',{operation:'credential.issue',component:'agent',bytes:Buffer.byteLength(value)},()=>require('./dpapi.cjs').capabilityCrypto('protect',value));}
const agentActorStorage=new (require('node:async_hooks').AsyncLocalStorage)();
let agentServiceMode;
function effectiveAgentPolicy(){if(!policy)return policy;const {grants}=agentGrants.reconcileGenerationFloor(policy.agents.grants||[],device?.agentGenerationFloor||{});return {...policy,agents:{...policy.agents,grants}};}
@@ -121,7 +122,7 @@ async function pairIndividualAgent(principalId){
const grant=approvedAgentGrants().find(g=>g.id===principalId),initial=policy,service=agentService;
if(!grant||!service||agentServiceMode!=='individual')throw Error('Agent grant unavailable');
const connection=service.issuePrincipal(grant.id,grant.generation,grant.expiresAt);
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
return {address:service.address,credentialPath,principalId:grant.id,generation:grant.generation};
}
@@ -457,7 +458,7 @@ async function ensureAgentAPI(){
if(!checkURL(url))return {allowed:false};
return {allowed:true,role:'agent',scopes:[scope]};
},audit:auditAgent,
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
});emit();
}finally{agentStarting=false;}
}
+8 -3
View File
@@ -24,10 +24,15 @@ def convert(data,protect):
if __name__=='__main__':
try:
if sys.platform!='win32' or len(sys.argv)!=2 or sys.argv[1] not in ['protect','unprotect']: raise ValueError()
data=sys.stdin.buffer.read(32769)
if not data or len(data)>32768:raise ValueError()
limit=32768 if sys.argv[1]=='protect' else 65536
data=sys.stdin.buffer.read(limit+1)
if not data or len(data)>limit:raise ValueError()
if sys.argv[1]=='protect':output=base64.b64encode(convert(data,True))
else:output=convert(base64.b64decode(data,validate=True),False)
else:
decoded=base64.b64decode(data,validate=True)
if base64.b64encode(decoded)!=data:raise ValueError()
output=convert(decoded,False)
if len(output)>(65536 if sys.argv[1]=='protect' else 32768):raise ValueError()
sys.stdout.buffer.write(output)
except Exception:
sys.stderr.write('Windows capability protection failed.\n');sys.exit(1)
+16
View File
@@ -7,10 +7,13 @@ import argparse
import json
from pathlib import Path
import sys
import os
import threading
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.dont_write_bytecode=True
from windows_appcontainer import AppContainer, SandboxError
from windows_containment import contain_inspector
def main():
@@ -26,8 +29,21 @@ def main():
raise SandboxError('Use the private Python belonging to this installation')
roots = [source, runtime]
if semantic.is_dir(): roots.append(semantic)
deadline = None
if args.provision:
# Installer-only: closing/killing this process terminates grant children.
# nsExec's own timeout renews on output, so enforce our own hard deadline.
containment = contain_inspector()
if containment.get('active') is not True or containment.get('killsDescendants') is not True:
raise SandboxError('Installer resource containment unavailable')
deadline = threading.Timer(240, lambda: os._exit(2))
deadline.daemon = True
deadline.start()
try:
sandbox = AppContainer(roots, provision=args.provision)
print(json.dumps({'verified': True, 'provisioned': args.provision, 'rootCount': len(roots), 'profile': sandbox.name}))
finally:
if deadline is not None: deadline.cancel()
if __name__ == '__main__':
+73
View File
@@ -0,0 +1,73 @@
'use strict';
const {randomUUID}=require('node:crypto');
const {isIP}=require('node:net');
const {z}=require('zod');
const {FileFactsSchema}=require('../../packages/core/transfer-facts.cjs');
const {RequestFactsSchema}=require('../../packages/core/request-transfer-facts.cjs');
const {parseAuthority}=require('./tls-authority.cjs');
const {GateError}=require('./http-request-gate.cjs');
const {BrokerAuditEventSchema,BrokerFactsSchema,BROKER_REASONS}=require('../../packages/core/broker-audit.cjs');
const reasons=BROKER_REASONS;
const builtinRuleIds=Object.freeze(['private-key','aws-access-key','api-secret','content-blocklist','size-limit','uninspectable','normalization-limit','file-transfer-size-limit','file-transfer-type-blocked','file-transfer-type-not-allowed','file-transfer-unknown','file-transfer-extension-mismatch','file-transfer-mime-mismatch','inspection-incomplete']);
const FactsSchema=RequestFactsSchema.extend({host:z.string().max(253).optional(),reason:z.enum(reasons).optional(),matchedRuleId:z.string().min(1).max(100).optional(),broker:BrokerFactsSchema.optional()}).strict();
// Existing /api/device/events-compatible subset. Network request digests are
// intentionally absent: bodies can contain passwords or credential envelopes.
const fail=code=>{throw new GateError(code);};
function freeze(event){if(event.request)Object.freeze(event.request);if(event.file)Object.freeze(event.file);if(event.broker)Object.freeze(event.broker);return Object.freeze(event);}
/** persist is a trusted durable store adapter, not a telemetry exporter. Its
* receipt is issued ONLY after persistence succeeds; an asynchronous queue
* enqueue or successful OTLP request is not a durability acknowledgment.
* No retry occurs when persistence outcome is uncertain.
*/
function createBrokerAudit({persist,current,policyVersion,mandatory=true,correlation,allowedRuleIds=[]}={}){
if(typeof current!=='function'||!Number.isSafeInteger(policyVersion)||policyVersion<1||typeof mandatory!=='boolean'||persist!==undefined&&typeof persist!=='function'||correlation!==undefined&&typeof correlation!=='function')fail('audit-configuration-invalid');
if(!Array.isArray(allowedRuleIds)||allowedRuleIds.length>20000||allowedRuleIds.some(id=>typeof id!=='string'||!id||id.length>100||[...id].some(character=>character.charCodeAt(0)<32||character.charCodeAt(0)===127)))fail('audit-configuration-invalid');
// Copy the trusted signed-policy IDs; callers cannot expand authorization
// later by mutating their original list. These are IDs, never rule bodies.
const knownRules=new Set([...builtinRuleIds,...allowedRuleIds]);
const isCurrent=()=>{try{return current()===true;}catch{return false;}};
return Object.freeze({
async recordDownload(decision,input){
const schema=FileFactsSchema.extend({host:z.string().max(253).optional(),matchedRuleId:z.string().min(1).max(100).optional(),broker:BrokerFactsSchema}).strict();
let facts;try{if(!input||Object.values(Object.getOwnPropertyDescriptors(input)).some(value=>!('value'in value)))fail('audit-facts-invalid');facts=schema.parse(input);}catch{fail('audit-facts-invalid');}
if(!['allow','block'].includes(decision)||facts.matchedRuleId&&(decision==='allow'||!knownRules.has(facts.matchedRuleId)))fail('audit-facts-invalid');
if(facts.host!==undefined){try{if(parseAuthority(isIP(facts.host)===6?'['+facts.host+']':facts.host).hostname!==facts.host)fail('audit-facts-invalid');}catch{fail('audit-facts-invalid');}}
if(decision==='allow'&&!isCurrent())fail('authority-expired');if(!persist)fail('audit-unavailable');
let event;try{event=freeze(BrokerAuditEventSchema.parse({eventId:randomUUID(),kind:decision==='allow'?'download.scan':'download.block',ruleId:facts.matchedRuleId||facts.broker.reason,policyVersion,...(facts.host?{host:facts.host}:{}),file:{bytes:facts.bytes,detectedType:facts.detectedType,sha256:facts.sha256},broker:facts.broker}));}catch{fail('audit-facts-invalid');}
let receipt;try{receipt=await persist(event);if(!receipt||Object.values(Object.getOwnPropertyDescriptors(receipt)).some(value=>!('value'in value))||receipt.persisted!==true||receipt.eventId!==event.eventId)fail('audit-persistence-failed');}catch{fail('audit-persistence-failed');}
if(decision==='allow'&&!isCurrent()){const error=new GateError('authority-expired');error.committedEventId=event.eventId;throw error;}return Object.freeze({persisted:true,eventId:event.eventId});
},
async record(decision,input){
if(!['allow','block','fault'].includes(decision))fail('audit-facts-invalid');
let parsed;
try{
if(!input||typeof input!=='object'||![Object.prototype,null].includes(Object.getPrototypeOf(input))||Object.values(Object.getOwnPropertyDescriptors(input)).some(descriptor=>!('value'in descriptor)))fail('audit-facts-invalid');
parsed=FactsSchema.safeParse(input);
}catch{fail('audit-facts-invalid');}
if(!parsed.success)fail('audit-facts-invalid');
const facts=parsed.data;
if(facts.matchedRuleId&&(decision==='allow'||!knownRules.has(facts.matchedRuleId)))fail('audit-facts-invalid');
const permittedReasons=facts.broker?{allow:['broker-approved'],block:['broker-scan-timeout','broker-policy-block','broker-authority-expired','broker-connect-denied','broker-auth-denied','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout'],fault:['broker-scan-timeout','broker-inspection-fault','broker-audit-failed','broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired']}:{allow:['broker-approved'],block:['broker-policy-block','broker-authority-expired'],fault:['broker-inspection-fault','broker-audit-failed']};
if(facts.reason&&!permittedReasons[decision].includes(facts.reason))fail('audit-facts-invalid');
if(facts.host!==undefined){try{if(parseAuthority(isIP(facts.host)===6?'['+facts.host+']':facts.host).hostname!==facts.host)fail('audit-facts-invalid');}catch{fail('audit-facts-invalid');}}
if(decision==='allow'&&!isCurrent())fail('authority-expired');
if(!persist){if(mandatory)fail('audit-unavailable');return Object.freeze({persisted:false});}
const trace={};
try{
const ids=correlation?.();
if(ids&&/^[a-f0-9]{32}$/.test(ids.traceId)&&!/^(0){32}$/.test(ids.traceId)&&/^[a-f0-9]{16}$/.test(ids.spanId)&&!/^(0){16}$/.test(ids.spanId))Object.assign(trace,{traceId:ids.traceId,spanId:ids.spanId});
}catch{}
// Server schema has one ruleId and no separate reason field. Preserve a
// verified matched detector ID there; otherwise use the fixed reason.
// Matched IDs are not supplied to diagnostic traces by this helper.
let event;try{event=freeze(BrokerAuditEventSchema.parse({eventId:randomUUID(),kind:decision==='allow'?'upload.authorized':'upload.block',ruleId:facts.matchedRuleId||facts.reason||(decision==='allow'?'broker-approved':decision==='block'?'broker-policy-block':'broker-inspection-fault'),policyVersion,...(facts.host?{host:facts.host}:{}),request:{bytes:facts.bytes,detectedType:facts.detectedType},...(facts.broker?{broker:Object.freeze({...facts.broker})}:{}),...trace}));}catch{fail('audit-facts-invalid');}
let receipt;try{receipt=await persist(event);}catch{fail('audit-persistence-failed');}
try{
if(!receipt||Object.values(Object.getOwnPropertyDescriptors(receipt)).some(descriptor=>!('value'in descriptor))||receipt.persisted!==true||receipt.eventId!==event.eventId)fail('audit-persistence-failed');
}catch{fail('audit-persistence-failed');}
if(decision==='allow'&&!isCurrent()){const error=new GateError('authority-expired');error.committedEventId=event.eventId;throw error;}
return Object.freeze({persisted:true,eventId:event.eventId});
}
});
}
module.exports={createBrokerAudit,BrokerAuditEventSchema};
+13
View File
@@ -0,0 +1,13 @@
'use strict';
const {GateError}=require('./http-request-gate.cjs');
// A separate, server-global control budget prevents an unresponsive scan from
// starving its terminal audit while still bounding all unfinished commits.
async function recordLifecycle({admission,deadlineMs,context,code,facts,scope}){
if(!context)return;
let timer;
try {
const original=admission.run(()=>context.reject(code,facts,scope));
return await Promise.race([original,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new GateError('audit-deadline')),deadlineMs);})]);
} finally {clearTimeout(timer);}
}
module.exports={recordLifecycle};
@@ -0,0 +1,64 @@
'use strict';
const http=require('node:http');
const {randomBytes,timingSafeEqual}=require('node:crypto');
const {bindConnectAuthority}=require('./tls-authority.cjs');
const {createInspectionServer}=require('./https-inspection-server.cjs');
const {GateError}=require('./http-request-gate.cjs');
const {createWorkAdmission}=require('./work-admission.cjs');
const {recordLifecycle}=require('./bounded-audit.cjs');
/** Isolated authenticated CONNECT-to-inspection chain. Scoped certificates
* are supplied by a trusted resolver, never generated/trusted globally here.
* No opaque tunnel or target connection is opened by this acceptor.
*/
function createConnectProxy({authority,current,resolveCertificate,inspect,forward,deadlineMs=5000,maxClients=8,maxBytes=1048576,maxResponseBytes=1048576,authMode='basic',lifecycle}) {
if(typeof authority!=='string'||!authority||typeof current!=='function'||typeof resolveCertificate!=='function'||typeof inspect!=='function'||typeof forward!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000||!Number.isSafeInteger(maxClients)||maxClients<1||maxClients>64||!Number.isSafeInteger(maxBytes)||maxBytes<0||maxBytes>16777216||!Number.isSafeInteger(maxResponseBytes)||maxResponseBytes<0||maxResponseBytes>16777216)throw new GateError('invalid-configuration');
if(lifecycle!==undefined&&(!lifecycle||typeof lifecycle.begin!=='function'))throw new GateError('invalid-configuration');
if(!['basic','bearer'].includes(authMode))throw new GateError('invalid-configuration');
const capability=randomBytes(32).toString('base64url'),username=`safetynet-${randomBytes(16).toString('hex')}`;
const credentials=Object.freeze({username,password:capability});
const expected=Buffer.from(authMode==='basic'?`Basic ${Buffer.from(`${username}:${capability}`).toString('base64')}`:`Bearer ${capability}`);
const sockets=new Set(),legs=new Set();
const admission=createWorkAdmission({maxJobs:maxClients});
const auditAdmission=createWorkAdmission({maxJobs:maxClients}),contexts=new WeakMap();
const contextFor=socket=>{if(!lifecycle)return undefined;if(!contexts.has(socket))contexts.set(socket,lifecycle.begin());return contexts.get(socket);};
const rejected=(context,code,facts={bytes:0,detectedType:'unknown'})=>recordLifecycle({admission:auditAdmission,deadlineMs,context,code,facts,scope:'connect'});
const server=http.createServer({maxHeaderSize:8192},(req,res)=>{void rejected(contextFor(req.socket),'unsupported-method').catch(()=>{}).finally(()=>{if(!res.destroyed){res.writeHead(403,{'connection':'close'});res.end('connect-required');}});});
const reject=(socket,status,code)=>socket.end(`HTTP/1.1 ${status}\r\n${status.startsWith('407')?`Proxy-Authenticate: ${authMode==='basic'?'Basic realm="SafetyNet private broker", charset="UTF-8"':'Bearer realm="SafetyNet private broker"'}\r\n`:''}Connection: close\r\nContent-Length: ${Buffer.byteLength(code)}\r\n\r\n${code}`);
server.on('connect',async(req,socket,head)=>{
socket.pause();
let context,binding;
try {
context=contextFor(socket);
const headers=Object.create(null);
for(let index=0;index<req.rawHeaders.length;index+=2){const name=req.rawHeaders[index].toLowerCase();if(Object.hasOwn(headers,name))throw new GateError('duplicate-header');headers[name]=req.rawHeaders[index+1];}
const received=Buffer.from(headers['proxy-authorization']||'');
if(received.length!==expected.length||!timingSafeEqual(received,expected)){await rejected(context,'authentication-required');reject(socket,'407 Proxy Authentication Required','authentication-required');return;}
if(head.length||headers['transfer-encoding']!==undefined||headers['content-length']!==undefined||headers.upgrade!==undefined)throw new GateError('unsupported-connect-framing');
binding=bindConnectAuthority(req.url);
if(headers.host!==binding.authority)throw new GateError('authority-mismatch');
if(current(authority)!==true)throw new GateError('authority-expired');
let timer;
const resolverController=new AbortController(),cancelResolver=()=>resolverController.abort();
socket.once('close',cancelResolver);
const resolved=await Promise.race([admission.run(()=>resolveCertificate(binding,{signal:resolverController.signal})),new Promise((_,rejectPromise)=>{timer=setTimeout(()=>{resolverController.abort();rejectPromise(new GateError('certificate-deadline'));},deadlineMs);})]).finally(()=>{clearTimeout(timer);socket.removeListener('close',cancelResolver);resolverController.abort();});
if(socket.destroyed||current(authority)!==true)throw new GateError('authority-expired');
if(!resolved||resolved.authority!==binding.authority)throw new GateError('destination-unapproved');
const leg=createInspectionServer({key:resolved.key,cert:resolved.cert,binding,authority,current,inspect,forward,deadlineMs,maxClients,maxBytes,maxResponseBytes,workAdmission:admission,lifecycle,auditAdmission,connectionContext:context});
legs.add(leg);socket.once('close',()=>{legs.delete(leg);void leg.close().catch(()=>{});});
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
leg.acceptAuthenticatedSocket(socket);socket.resume();
} catch(error) {
try {await rejected(context,error instanceof GateError?error.code:'connect-denied',{bytes:0,detectedType:'unknown',...(binding?{host:binding.hostname}:{})});}catch{}
if(!socket.destroyed)reject(socket,'403 Forbidden','connect-denied');
}
});
server.maxConnections=maxClients;server.maxHeadersCount=32;server.headersTimeout=deadlineMs;server.requestTimeout=deadlineMs;
server.on('connection',socket=>{sockets.add(socket);socket.setTimeout(deadlineMs*3,()=>socket.destroy());socket.once('close',()=>sockets.delete(socket));});
server.on('clientError',(_error,socket)=>{void rejected(contextFor(socket),'invalid-headers').catch(()=>{}).finally(()=>socket.destroy());});
return Object.freeze({capability,authMode,credentials,
async listen(port=0){if(!Number.isInteger(port)||port<0||port>65535)throw new GateError('invalid-configuration');await new Promise((resolve,rejectPromise)=>{server.once('error',rejectPromise);server.listen(port,'127.0.0.1',()=>{server.removeListener('error',rejectPromise);resolve();});});return server.address().port;},
async close(){admission.close();auditAdmission.close();for(const socket of sockets)socket.destroy();await Promise.all([...legs].map(leg=>leg.close()));await new Promise(resolve=>server.close(resolve));}
});
}
module.exports={createConnectProxy};
@@ -0,0 +1,28 @@
'use strict';
// Trusted caller supplies its already policy-authorized diagnostics instance.
// This adapter neither creates an exporter nor configures/enables diagnostics.
const stages=Object.freeze({
request:['browser.network','broker.request'],
framing:['browser.request','broker.framing'],
buffer:['browser.request','broker.buffer'],
inspect:['browser.request','broker.inspect'],
authorize:['browser.network','broker.authorize'],
forward:['browser.network','broker.forward']
});
function createBrokerDiagnostics(diagnostics){
if(!diagnostics||typeof diagnostics.span!=='function')throw new TypeError('broker_diagnostics_invalid');
const span=diagnostics.span.bind(diagnostics);
return Object.freeze({
wrap(stage,fn,input={}){
if(!Object.hasOwn(stages,stage)||typeof fn!=='function')throw new TypeError('broker_diagnostics_stage_invalid');
const [name,operation]=stages[stage],attributes={operation};
// Data descriptors only: never evaluate untrusted telemetry getters.
if(input&&typeof input==='object')for(const key of ['bytes','count','policyVersion']){
const descriptor=Object.getOwnPropertyDescriptor(input,key);
if(descriptor&&'value'in descriptor&&Number.isSafeInteger(descriptor.value)&&descriptor.value>=0)attributes[key]=descriptor.value;
}
return span(name,attributes,fn);
}
});
}
module.exports={createBrokerDiagnostics};
+24
View File
@@ -0,0 +1,24 @@
'use strict';
const {isIP}=require('node:net');
const dns=require('node:dns').promises;
// Resolve once in the broker and bind the approved numeric address to the dial.
// Active IP policy uses this approved tuple for numeric proxy CONNECT; ordinary
// hostname CONNECT remains supported when that origin IP policy is inactive.
function createDnsPolicyRoute({lookup=dns.lookup,blocked,current}={}){
if(typeof lookup!=='function'||typeof blocked!=='function'||typeof current!=='function')throw Error('broker-route-denied');
return async({hostname,port,signal})=>{
const check=()=>{if(signal?.aborted||current()!==true)throw Error('broker-route-denied');};
try{
check();if(typeof hostname!=='string'||!hostname||hostname.length>253||!Number.isSafeInteger(port)||port<1||port>65535)throw Error();
const literal=hostname.replace(/^\[|\]$/g,'');
if(isIP(literal)){if(blocked(literal))throw Error();check();return Object.freeze({approved:true,address:literal,port});}
const records=await lookup(hostname,{all:true,verbatim:true});check();
if(!Array.isArray(records)||!records.length||records.length>64||records.some(r=>!r||typeof r.address!=='string'||!isIP(r.address)||blocked(r.address)))throw Error();
const address=records[0].address;check();return Object.freeze({approved:true,address,port});
}catch{throw Error('broker-route-denied');}
};
}
function assertSupportedDns(policy,network,{numericOriginSupported=false}={}){
if(policy.enterprise?.dns?.mode==='secure'||!numericOriginSupported&&network.mode==='fixed_servers'&&policy.security?.dns?.enabled!==false&&policy.security?.dns?.blockedIPs?.length)throw Error('broker-route-unsupported');
}
module.exports={createDnsPolicyRoute,assertSupportedDns};
@@ -0,0 +1,46 @@
'use strict';
const {createHash}=require('node:crypto');
const {GateError}=require('./http-request-gate.cjs');
const {validateResponse}=require('./http-response-gate.cjs');
const {detectType,inspectFileTransfer}=require('../../packages/core/file-transfer.cjs');
const {scanDownloadEnterprise}=require('../browser/enterprise-inspection.cjs');
const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
function filename(value,url){
if(value===undefined){try{return decodeURIComponent(new URL(url).pathname.split('/').pop());}catch{throw new GateError('invalid-upstream-response');}}
if(typeof value!=='string'||value.length>8192||!/^\s*(attachment|inline)(?:\s*;|\s*$)/i.test(value))throw new GateError('invalid-upstream-response');
const matches=[...value.matchAll(/;\s*filename(\*)?\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^;\s]+))/gi)];
if(matches.length>1)throw new GateError('invalid-upstream-response');if(!matches.length)return '';
const match=matches[0],text=match[2]!==undefined?match[2].replace(/\\(.)/g,'$1'):match[3];
if(match[1]){if(!/^UTF-8''/i.test(text))throw new GateError('invalid-upstream-response');try{return decodeURIComponent(text.slice(7));}catch{throw new GateError('invalid-upstream-response');}}return text;
}
/** A full buffered response is held until document/download analysis succeeds.
* Incoming HTML/API text is not classified as outgoing company data. Partial
* and compressed representations are unsupported, never counted as covered.
*/
function createDownloadResponseGate({forward,policy,current,inspector,enterpriseScanner,maxBytes=16777216,auditRequired=true}={}){
if(typeof auditRequired!=='boolean'||typeof forward!=='function'||typeof current!=='function'||!policy||!Number.isSafeInteger(maxBytes)||maxBytes<0||maxBytes>16777216)throw new GateError('invalid-configuration');
const check=signal=>{if(signal?.aborted)throw new GateError('aborted');if(current()!==true)throw new GateError('authority-expired');};
return async input=>{
check(input.signal);if(auditRequired&&typeof input.auditContext?.response!=='function')throw new GateError('audit-unavailable');const upstream=await forward(input);check(input.signal);
const response=validateResponse(upstream,maxBytes);let scanCopy;
const known=new Set([...(policy.dlp?.rules||[]).map(rule=>rule.id),...(policy.dlp?.semantic?.categories||[]).map(rule=>rule.id),...(policy.security?.yara?.rules||[]).map(rule=>rule.id)]);
const record=async(decision,reason,ruleId)=>{if(input.auditContext?.response)await input.auditContext.response(decision,{bytes:response.body.length,detectedType:detectType(response.body),sha256:digest(response.body),host:new URL(input.request.url).hostname.replace(/^\[|\]$/g,''),reason,...(known.has(ruleId)?{matchedRuleId:ruleId}:{})});else if(auditRequired)throw new GateError('audit-unavailable');check(input.signal);};
try{
if(response.statusCode===206)throw new GateError('invalid-upstream-response');
const disposition=response.headers['content-disposition'],type=detectType(response.body),mime=response.headers['content-type'];
const claimed=filename(disposition,input.request.url);
const document=type!=='text'||disposition!==undefined||/^(?:application\/(?:pdf|zip|octet-stream|msword|vnd\.)|image\/)/i.test(mime||'');
if(!document)return response;
if(/^image\/svg\+xml(?:;|$)/i.test(mime||'')){await record('block','broker-download-inspection-block');throw new GateError('download-inspection-blocked');}
if(/^\s*attachment(?:\s*;|\s*$)/i.test(disposition||'')&&policy.browser.allowDownloads!==true){await record('block','broker-download-policy-block');throw new GateError('download-policy-blocked');}
const verdict=inspectFileTransfer(response.body,policy,'download',{filename:claimed,mime});if(verdict.decision!=='allow'){await record('block','broker-download-policy-block',verdict.ruleId);throw new GateError('download-policy-blocked');}
input.markStage?.('scan');scanCopy=Buffer.from(response.body);const originalDigest=digest(scanCopy);
const result=await scanDownloadEnterprise(scanCopy,policy,inspector,enterpriseScanner);check(input.signal);
if(digest(scanCopy)!==originalDigest||result?.decision!=='allow'||result.coverage?.complete!==true||result.sha256!==originalDigest||result.bytes!==scanCopy.length){await record('block','broker-download-inspection-block',digest(scanCopy)===originalDigest?result?.ruleId:undefined);throw new GateError('download-inspection-blocked');}
input.markStage?.('forward');await record('allow','broker-download-approved');
return response;
}catch(error){response.body.fill(0);if(error instanceof GateError)throw error;throw new GateError('download-inspection-blocked');}
finally{scanCopy?.fill(0);}
};
}
module.exports={createDownloadResponseGate};
+68
View File
@@ -0,0 +1,68 @@
'use strict';
const {EventEmitter}=require('node:events');
const {TextDecoder}=require('node:util');
const SMALL=65536,LARGE=2097152;
// Transport only: the native host and parent must authenticate their pipe peer
// before constructing this channel. Framing does not grant policy authority.
function createFramedChannel({readable,writable,inboundFirstLimit=SMALL,outboundFirstLimit=SMALL}={}){
if(!readable||!writable||typeof readable.on!=='function'||typeof writable.write!=='function'||![SMALL,LARGE].includes(inboundFirstLimit)||![SMALL,LARGE].includes(outboundFirstLimit))throw Error('broker-channel-invalid');
const channel=new EventEmitter(),decoder=new TextDecoder('utf-8',{fatal:true});
let connected=true,firstRead=true,firstWrite=true,header=Buffer.alloc(4),headerBytes=0,payload,payloadBytes=0,pending=0;
const outstanding=new Set();
Object.defineProperty(channel,'connected',{get:()=>connected});
function disconnect(){
if(!connected)return;connected=false;header.fill(0);payload?.fill(0);payload=undefined;
readable.off('data',data);readable.off('end',disconnect);readable.off('close',disconnect);
// Keep bounded error listeners until destroyed streams settle.
readable.destroy?.();if(writable!==readable)writable.destroy?.();
for(const record of outstanding)record.notify(true);
channel.emit('disconnect');
}
function data(chunk){
if(!connected)return;
try{
if(!Buffer.isBuffer(chunk))throw Error();
let offset=0;
while(offset<chunk.length&&connected){
if(!payload){
const take=Math.min(4-headerBytes,chunk.length-offset);chunk.copy(header,headerBytes,offset,offset+take);headerBytes+=take;offset+=take;
if(headerBytes!==4)continue;
const length=header.readUInt32LE(0),limit=firstRead?inboundFirstLimit:SMALL;
if(length<1||length>limit)throw Error();
payload=Buffer.alloc(length);payloadBytes=0;headerBytes=0;header.fill(0);
}
const take=Math.min(payload.length-payloadBytes,chunk.length-offset);chunk.copy(payload,payloadBytes,offset,offset+take);payloadBytes+=take;offset+=take;
if(payloadBytes===payload.length){
const completed=payload;payload=undefined;payloadBytes=0;firstRead=false;
let message;try{message=JSON.parse(decoder.decode(completed));}finally{completed.fill(0);}
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
channel.emit('message',message);
}
}
}catch{disconnect();}
}
channel.send=(message,callback=()=>{})=>{
if(typeof callback!=='function')throw Error('broker-channel-invalid');
const notifyFailure=()=>{disconnect();try{callback(Error('broker-channel-unavailable'));}catch{}};
if(!connected||pending>=8){notifyFailure();return;}
let frame;
try{
if(!message||typeof message!=='object'||Array.isArray(message))throw Error();
const text=JSON.stringify(message),bytes=Buffer.byteLength(text),limit=firstWrite?outboundFirstLimit:SMALL;
if(bytes<1||bytes>limit)throw Error();
frame=Buffer.alloc(bytes+4);frame.writeUInt32LE(bytes);frame.write(text,4);firstWrite=false;pending++;
}catch{notifyFailure();return;}
let settled=false,notified=false;
const record={notify(error){if(notified)return;notified=true;try{callback(error?Error('broker-channel-unavailable'):undefined);}catch{disconnect();}}};
outstanding.add(record);
// Completion of the underlying write owns buffer release. Disconnect notifies
// failure immediately, but cannot claim that an outstanding write has settled.
const done=error=>{if(settled)return;settled=true;pending--;outstanding.delete(record);frame.fill(0);if(error)disconnect();record.notify(error||!connected);};
try{writable.write(frame,done);}catch{done(Error());}
};
channel.disconnect=disconnect;
readable.on('data',data);readable.once('end',disconnect);readable.once('close',disconnect);readable.on('error',disconnect);
if(writable!==readable){writable.once('close',disconnect);writable.on('error',disconnect);}
return channel;
}
module.exports={createFramedChannel};
+109
View File
@@ -0,0 +1,109 @@
'use strict';
const {createHash}=require('node:crypto');
const {performance}=require('node:perf_hooks');
const digest=body=>createHash('sha256').update(body).digest('hex');
class GateError extends Error {
constructor(code) { super(code); this.name='GateError'; this.code=code; }
}
const fail=code=>{throw new GateError(code);};
function parseRequest(request) {
const headers=Object.create(null);
if(!Array.isArray(request.rawHeaders)||request.rawHeaders.length%2) fail('invalid-headers');
for(let index=0;index<request.rawHeaders.length;index+=2) {
const name=request.rawHeaders[index].toLowerCase(),value=request.rawHeaders[index+1];
if(headers[name]!==undefined) fail('duplicate-header');
headers[name]=value;
}
if(headers.upgrade!==undefined||headers['transfer-encoding']!==undefined||headers['content-encoding']!==undefined||headers.expect!==undefined) fail('unsupported-framing');
if(headers.connection&&headers.connection.toLowerCase()!=='close'&&headers.connection.toLowerCase()!=='keep-alive') fail('unsupported-framing');
if(!/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/.test(request.method)) fail('unsupported-method');
let destination;
try {destination=new URL(request.url);} catch {fail('invalid-destination');}
// First gate supports explicit HTTP proxy requests only. TLS CONNECT,
// streaming protocols and OS egress confinement need separate enforcement.
if(destination.protocol!=='http:'||destination.username||destination.password||destination.hash) fail('invalid-destination');
if(!headers.host||headers.host.toLowerCase()!==destination.host.toLowerCase()) fail('authority-mismatch');
const length=headers['content-length'];
if(length!==undefined&&!/^(0|[1-9][0-9]*)$/.test(length)) fail('invalid-length');
const expected=length===undefined?0:Number(length);
if(!Number.isSafeInteger(expected)) fail('invalid-length');
return Object.freeze({method:request.method,url:destination.href,headers:Object.freeze(headers),expected});
}
/** Provisional trusted broker API, not a public renderer/socket API.
* current(authority) MUST synchronously verify enrollment, policy generation,
* expiration and revocation. forward is trusted and must make its first dial
* synchronously when called, without yielding before using the supplied bytes.
* This module never forwards an IncomingMessage or an inspector-owned buffer.
*/
async function gateRequest(request,{inspect,current,forward,authority,maxBytes=1048576,deadlineMs=5000,signal}) {
if(typeof authority!=='string'||!authority||typeof inspect!=='function'||typeof current!=='function'||typeof forward!=='function'||!Number.isSafeInteger(maxBytes)||maxBytes<0||!Number.isSafeInteger(deadlineMs)||deadlineMs<1) fail('invalid-configuration');
const snapshot=parseRequest(request);
if(snapshot.expected>maxBytes) fail('body-too-large');
const isCurrent=()=>{try {return current(authority)===true;} catch {return false;}};
if(!isCurrent()) fail('authority-expired');
const expires=performance.now()+deadlineMs;
let timer,abortListener,requestAbort,requestError,requestClose,data,end;
let settled=false,body;
const chunks=[];
let bytes=0;
const stop=new AbortController();
try {
const buffered=await new Promise((resolve,reject)=>{
const deny=code=>{if(settled)return;settled=true;stop.abort();request.pause();reject(new GateError(code));};
timer=setTimeout(()=>deny('deadline'),deadlineMs);
abortListener=()=>deny('aborted');
requestAbort=()=>deny('aborted');
requestError=()=>deny('request-error');
requestClose=()=>{if(!request.complete)deny('incomplete-body');};
data=chunk=>{
bytes+=chunk.length;
if(bytes>maxBytes||bytes>snapshot.expected){deny('body-too-large');return;}
chunks.push(Buffer.from(chunk));
};
end=()=>{
if(settled)return;
if(!request.complete||bytes!==snapshot.expected){deny('incomplete-body');return;}
resolve(Buffer.concat(chunks));
};
signal?.addEventListener('abort',abortListener,{once:true});
request.on('aborted',requestAbort).on('error',requestError).on('close',requestClose).on('data',data).once('end',end);
if(signal?.aborted||request.aborted)deny('aborted');
else if(request.readableEnded)end();
});
body=buffered;
if(performance.now()>=expires)fail('deadline');
if(!isCurrent())fail('authority-expired');
const originalHash=digest(body);
// Deadline and abort remain active during asynchronous inspection.
const verdict=await Promise.race([
Promise.resolve().then(()=>inspect(Object.freeze({request:snapshot,body:Buffer.from(body),signal:stop.signal}))),
new Promise((_,reject)=>{
if(stop.signal.aborted)reject(new GateError(signal?.aborted?'aborted':'deadline'));
else stop.signal.addEventListener('abort',()=>reject(new GateError(signal?.aborted?'aborted':'deadline')),{once:true});
})
]);
if(stop.signal.aborted)fail('aborted');
if(performance.now()>=expires)fail('deadline');
if(verdict?.decision!=='allow')fail('inspection-denied');
if(!isCurrent())fail('authority-expired');
if(digest(body)!==originalHash)fail('integrity-failed');
settled=true;
clearTimeout(timer);
// Final verification and call have no asynchronous gap.
return await forward(Object.freeze({request:snapshot,body:Buffer.from(body)}));
} catch(error) {
if(error instanceof GateError)throw error;
throw new GateError('inspection-or-forward-failed');
} finally {
clearTimeout(timer);
signal?.removeEventListener('abort',abortListener);
request.removeListener('aborted',requestAbort).removeListener('error',requestError).removeListener('close',requestClose).removeListener('data',data).removeListener('end',end);
for(const chunk of chunks)chunk.fill(0);
body?.fill(0);
}
}
module.exports={gateRequest,GateError};
@@ -0,0 +1,37 @@
'use strict';
const {GateError}=require('./http-request-gate.cjs');
const allowed=new Set(['content-disposition','content-type','content-language','cache-control','expires','last-modified','etag','location','set-cookie','vary','content-security-policy','content-security-policy-report-only','referrer-policy','permissions-policy','x-content-type-options','x-frame-options','strict-transport-security']);
const invalid=value=>typeof value!=='string'||value.length>8192||[...value].some(character=>character.charCodeAt(0)<32&&character!=='\t'||character.charCodeAt(0)===127);
/** Provisional buffered response. No streaming/compression forwarding.
* Content-Length is recomputed from a private body copy. Hop-by-hop and
* unrecognized headers are never forwarded. Set-Cookie may be a bounded
* string array; values are forwarded separately without comma combining.
*/
function validateResponse(record,maxBytes=1048576) {
if(!record||!Number.isSafeInteger(record.statusCode)||record.statusCode<200||record.statusCode>599||!Number.isSafeInteger(maxBytes)||maxBytes<0||!Buffer.isBuffer(record.body)||record.body.length>maxBytes||!record.headers||typeof record.headers!=='object'||Array.isArray(record.headers))throw new GateError('invalid-upstream-response');
const headers=Object.create(null);
const seen=new Set(),hop=new Set();
for(const [key,value] of Object.entries(record.headers))if(key.toLowerCase()==='connection') {
if(invalid(value))throw new GateError('invalid-upstream-response');
for(const token of value.split(',')){if(!/^[a-zA-Z0-9-]+$/.test(token.trim()))throw new GateError('invalid-upstream-response');hop.add(token.trim().toLowerCase());}
}
let size=0;
for(const [key,value] of Object.entries(record.headers)) {
if(!/^[a-zA-Z0-9-]+$/.test(key))throw new GateError('invalid-upstream-response');
const name=key.toLowerCase();
if(seen.has(name))throw new GateError('invalid-upstream-response');seen.add(name);
if(name==='content-encoding'&&value!==undefined&&value!=='identity')throw new GateError('unsupported-upstream-encoding');
if(!allowed.has(name)||hop.has(name))continue;
const values=Array.isArray(value)?value:[value];
if(values.length>32||(Array.isArray(value)&&name!=='set-cookie')||values.some(invalid))throw new GateError('invalid-upstream-response');
size+=key.length+values.reduce((sum,item)=>sum+item.length,0);
if(size>32768)throw new GateError('invalid-upstream-response');
headers[name]=Array.isArray(value)?Object.freeze([...value]):value;
}
if((record.statusCode===204||record.statusCode===304)&&record.body.length)throw new GateError('invalid-upstream-response');
const body=Buffer.from(record.body);
headers['content-length']=String(body.length);headers.connection='close';
return Object.freeze({statusCode:record.statusCode,headers:Object.freeze(headers),body});
}
module.exports={validateResponse};
+102
View File
@@ -0,0 +1,102 @@
'use strict';
const https=require('node:https');
const tls=require('node:tls');
const {isIP}=require('node:net');
const {parseAuthority}=require('./tls-authority.cjs');
const {validateResponse}=require('./http-response-gate.cjs');
const {GateError}=require('./http-request-gate.cjs');
const {createWorkAdmission}=require('./work-admission.cjs');
const methods=new Set(['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS']);
const hop=new Set(['host','content-length','connection','proxy-connection','proxy-authorization','proxy-authenticate','keep-alive','te','trailer','transfer-encoding','upgrade']);
const fail=code=>{throw new GateError(code);};
function requestSnapshot(request,body,maxBytes){
if(!request||!methods.has(request.method)||!Buffer.isBuffer(body)||body.length>maxBytes||typeof request.url!=='string'||!request.headers||typeof request.headers!=='object')fail('invalid-forward-request');
let url,binding;try{
const match=/^https:\/\/([^/?#]+)(?:[/?]|$)/.exec(request.url);if(!match)fail('invalid-forward-request');
binding=parseAuthority(match[1]);url=new URL(request.url);
if(url.username||url.password||url.hash||[...request.url].some(character=>character.charCodeAt(0)<=32||character.charCodeAt(0)===127||character==='\\')||url.protocol!=='https:')fail('invalid-forward-request');
}catch{fail('invalid-forward-request');}
const headers=Object.create(null),seen=new Set();let headerBytes=0;
const connectionNamed=new Set();
for(const [name,value] of Object.entries(request.headers))if(name.toLowerCase()==='connection'){
if(typeof value!=='string')fail('invalid-forward-request');
for(const token of value.split(',')){if(!/^[a-z0-9-]+$/i.test(token.trim()))fail('invalid-forward-request');connectionNamed.add(token.trim().toLowerCase());}
}
for(const [name,value] of Object.entries(request.headers)){
const lower=name.toLowerCase();
if(!/^[!#$%&'*+.^_`|~0-9a-z-]+$/i.test(name)||seen.has(lower)||typeof value!=='string'||[...value].some(character=>{const code=character.charCodeAt(0);return code!==9&&(code<32||code===127||code>255);}))fail('invalid-forward-request');
seen.add(lower);headerBytes+=name.length+value.length;if(headerBytes>32768)fail('invalid-forward-request');
if(!hop.has(lower)&&!connectionNamed.has(lower))headers[lower]=value;
}
headers['accept-encoding']='identity';headers.host=url.host;headers['content-length']=String(body.length);headers.connection='close';
return {binding,path:url.pathname+url.search,method:request.method,headers};
}
/** Trusted route resolver MUST approve the original hostname/port and return
* {approved:true,address:<numeric IP>,port:<same port>}. It must apply corporate
* DNS/proxy policy; this transport never does system lookup or follows redirects.
* ca is an optional trusted configuration/test CA, never renderer-supplied.
*/
function createHttpsForwarder({authority,current,resolveRoute,ca,deadlineMs=5000,maxRequestBytes=16777216,maxResponseBytes=1048576,maxPendingResolutions=8}={}){
if(typeof authority!=='string'||!authority||typeof current!=='function'||typeof resolveRoute!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000||![maxRequestBytes,maxResponseBytes].every(n=>Number.isSafeInteger(n)&&n>=0&&n<=16777216))fail('invalid-forward-configuration');
if(ca!==undefined&&!Buffer.isBuffer(ca)&&typeof ca!=='string')fail('invalid-forward-configuration');
if(!Number.isSafeInteger(maxPendingResolutions)||maxPendingResolutions<1||maxPendingResolutions>64)fail('invalid-forward-configuration');
// Separate budget from outer forward admission: timed-out resolvers retain
// their slot until actual settlement, avoiding nested single-slot deadlock.
const resolutions=createWorkAdmission({maxJobs:maxPendingResolutions});
const trustedCa=Buffer.isBuffer(ca)?Buffer.from(ca):ca;
const isCurrent=()=>{try{return current(authority)===true;}catch{return false;}};
return async function forward({request,body,signal,maxResponseBytes:requestedLimit=maxResponseBytes}){
if(!Number.isSafeInteger(requestedLimit)||requestedLimit<0||requestedLimit>maxResponseBytes)fail('invalid-forward-request');
const snapshot=requestSnapshot(request,body,maxRequestBytes),privateBody=Buffer.from(body),controller=new AbortController();
let outgoing,timer,externalAbort,cancel;
const cancellation=new Promise((_,reject)=>{cancel=()=>{outgoing?.destroy();reject(new GateError('upstream-cancelled'));};controller.signal.addEventListener('abort',cancel,{once:true});});
void cancellation.catch(()=>{});
try{
externalAbort=()=>controller.abort();signal?.addEventListener('abort',externalAbort,{once:true});
timer=setTimeout(()=>controller.abort(),deadlineMs);
if(signal?.aborted)controller.abort();
if(controller.signal.aborted)fail('upstream-cancelled');
if(!isCurrent())fail('authority-expired');
let resolution;
try{resolution=resolutions.run(()=>resolveRoute(Object.freeze({hostname:snapshot.binding.hostname,port:snapshot.binding.port,signal:controller.signal})));}
catch(error){if(error.code==='work_capacity_exhausted')fail('route-capacity-exhausted');throw error;}
const route=await Promise.race([resolution,cancellation]);
if(controller.signal.aborted)fail('upstream-cancelled');
if(!isCurrent())fail('authority-expired');
if(!route||route.approved!==true||!isIP(route.address)||route.port!==snapshot.binding.port)fail('route-unapproved');
const response=await Promise.race([new Promise((resolve,reject)=>{
// Last synchronous authorization check precedes request creation/dial.
if(!isCurrent()){reject(new GateError('authority-expired'));return;}
outgoing=https.request({host:route.address,port:route.port,path:snapshot.path,method:snapshot.method,headers:snapshot.headers,
servername:isIP(snapshot.binding.hostname)?undefined:snapshot.binding.hostname,
ca:trustedCa,rejectUnauthorized:true,checkServerIdentity:(_hostname,certificate)=>tls.checkServerIdentity(snapshot.binding.hostname,certificate),
lookup:(_hostname,_options,callback)=>callback(new Error('unexpected-name-resolution')),agent:false,maxHeaderSize:32768,signal:controller.signal},incoming=>{
const chunks=[];let bytes=0,headerBytes=0;
for(const value of incoming.rawHeaders)headerBytes+=value.length;
const wipe=()=>{for(const chunk of chunks)chunk.fill(0);chunks.length=0;};
incoming.once('close',wipe);
if(headerBytes>32768){reject(new GateError('invalid-upstream-response'));incoming.destroy();return;}
incoming.on('data',chunk=>{bytes+=chunk.length;if(bytes>requestedLimit){reject(new GateError('upstream-response-too-large'));incoming.destroy();return;}chunks.push(Buffer.from(chunk));});
incoming.once('aborted',()=>reject(new GateError('upstream-failed')));
incoming.once('error',()=>reject(new GateError('upstream-failed')));
incoming.once('end',()=>{
try{if(!incoming.complete)fail('upstream-failed');resolve(validateResponse({statusCode:incoming.statusCode,headers:incoming.headers,body:Buffer.concat(chunks)},requestedLimit));}
catch(error){reject(error);}finally{wipe();}
});
});
outgoing.once('error',()=>reject(new GateError('upstream-failed')));
// TLS setup is asynchronous. Reauthorize at the actual secure socket
// before any HTTP headers/body are released, not merely before dial.
outgoing.once('socket',socket=>socket.once('secureConnect',()=>{
if(controller.signal.aborted){reject(new GateError('upstream-cancelled'));outgoing.destroy();return;}
if(!isCurrent()){reject(new GateError('authority-expired'));outgoing.destroy();return;}
outgoing.end(privateBody);
}));
}),cancellation]);
if(!isCurrent())fail('authority-expired');
return response;
}catch(error){outgoing?.destroy();if(error instanceof GateError)throw error;throw new GateError('upstream-failed');}
finally{clearTimeout(timer);signal?.removeEventListener('abort',externalAbort);controller.signal.removeEventListener('abort',cancel);privateBody.fill(0);}
};
}
module.exports={createHttpsForwarder};
@@ -0,0 +1,80 @@
'use strict';
const https=require('node:https');
const {verifyInnerAuthority}=require('./tls-authority.cjs');
const {gateRequest,GateError}=require('./http-request-gate.cjs');
const {validateResponse}=require('./http-response-gate.cjs');
const {createWorkAdmission}=require('./work-admission.cjs');
const {recordLifecycle}=require('./bounded-audit.cjs');
/** Isolated decrypted HTTPS leg for one caller-authenticated CONNECT binding.
* Trusted caller supplies certificate, authorization and HTTPS forwarder.
* This is NOT a CONNECT proxy, certificate issuer or OS network boundary.
*/
function createInspectionServer({key,cert,binding,authority,current,inspect,forward,maxBytes=1048576,maxResponseBytes=1048576,deadlineMs=5000,maxClients=8,workAdmission,lifecycle,auditAdmission,connectionContext}) {
if(typeof authority!=='string'||!authority||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000||!Number.isSafeInteger(maxBytes)||maxBytes<0||maxBytes>16777216||!Number.isSafeInteger(maxResponseBytes)||maxResponseBytes<0||maxResponseBytes>16777216||!Number.isSafeInteger(maxClients)||maxClients<1||maxClients>64||typeof current!=='function'||typeof inspect!=='function'||typeof forward!=='function')throw new GateError('invalid-configuration');
const sockets=new Set();
const admission=workAdmission||createWorkAdmission({maxJobs:maxClients});
const controlAdmission=auditAdmission||createWorkAdmission({maxJobs:maxClients});
const rejectAudit=(context,code,facts,scope='request')=>recordLifecycle({admission:controlAdmission,deadlineMs,context,code,facts,scope});
let server;
try {
server=https.createServer({key,cert,minVersion:'TLSv1.2',handshakeTimeout:deadlineMs},async(req,res)=>{
const controller=new AbortController();let phase='request',totalExpired=false,authorized=false;const expires=performance.now()+deadlineMs;
const totalTimer=setTimeout(()=>{totalExpired=true;controller.abort();},deadlineMs);
const auditContext=lifecycle?.begin(connectionContext?.requestId);
let receivedBytes=0;
const received=chunk=>{receivedBytes=Math.min(20*1024*1024,receivedBytes+chunk.length);};
req.on('data',received);
const abort=()=>controller.abort();
res.once('close',abort);
try {
verifyInnerAuthority(binding,{rawHeaders:req.rawHeaders,url:req.url,servername:req.socket.servername||undefined});
const target=req.url.startsWith('/')?`https://${binding.authority}${req.url}`:req.url;
const parsed=new URL(target);
const rawHeaders=req.rawHeaders.slice();
for(let index=0;index<rawHeaders.length;index+=2)if(rawHeaders[index].toLowerCase()==='host')rawHeaders[index+1]=binding.authority;
// Stream methods remain bound to the actual IncomingMessage. Only
// immutable routing metadata is adapted for the reusable HTTP gate.
const adapted=new Proxy(req,{get(object,name){if(name==='url')return `http://${binding.authority}${parsed.pathname}${parsed.search}`;if(name==='rawHeaders')return rawHeaders;const value=Reflect.get(object,name);return typeof value==='function'?value.bind(object):value;}});
const secureSnapshot=value=>Object.freeze({...value,auditContext,markStage:value=>{if(value==='scan'||value==='forward')phase=value;},request:Object.freeze({...value.request,url:target})});
const response=await gateRequest(adapted,{authority,current,maxBytes,deadlineMs:Math.max(1,Math.ceil(expires-performance.now())),signal:controller.signal,inspect:value=>{phase='scan';return admission.run(()=>inspect(secureSnapshot(value)),{payload:value.body});},forward:async value=>{
phase='forward';authorized=true;
let cancelled;
let forwardExpired=false;
const forwardTimer=setTimeout(()=>{forwardExpired=true;controller.abort();},Math.max(1,Math.ceil(expires-performance.now())));
try {
const cancellation=new Promise((_,reject)=>{cancelled=()=>reject(new GateError('upstream-failed'));controller.signal.addEventListener('abort',cancelled,{once:true});if(controller.signal.aborted)cancelled();});
const result=await Promise.race([admission.run(()=>forward(Object.freeze({...secureSnapshot(value),signal:controller.signal,maxResponseBytes})),{payload:value.body}),cancellation]);
return validateResponse(result,maxResponseBytes);
} catch(error) {throw new GateError(totalExpired?(phase==='scan'?'inspection-deadline':'forward-deadline'):forwardExpired?'forward-deadline':controller.signal.aborted?'aborted':['invalid-upstream-response','unsupported-upstream-encoding','authority-expired','audit-unavailable','audit-persistence-failed'].includes(error?.code)?error.code:'upstream-failed');}
finally {clearTimeout(forwardTimer);controller.signal.removeEventListener('abort',cancelled);}
}});
if(controller.signal.aborted||res.destroyed)return;
res.writeHead(response.statusCode,response.headers);res.end(response.body);
} catch(error) {
const code=totalExpired?(phase==='scan'?'inspection-deadline':phase==='forward'?'forward-deadline':'deadline'):error instanceof GateError||error?.name==='AuthorityError'?error.code:'inspection-failed';
try {await rejectAudit(auditContext,code,{bytes:receivedBytes,detectedType:'unknown',host:binding.hostname});}catch{}
const upstream=code==='inspection-deadline'?authorized:['upstream-failed','forward-deadline','invalid-upstream-response','unsupported-upstream-encoding'].includes(code);
if(!res.destroyed&&!res.headersSent){res.writeHead(upstream?502:403,{'connection':'close','content-type':'text/plain'});res.end(upstream?'upstream-failed':code);}
} finally {clearTimeout(totalTimer);res.removeListener('close',abort);req.removeListener('data',received);}
});
} catch {throw new GateError('invalid-tls-configuration');}
server.maxConnections=maxClients;
server.headersTimeout=deadlineMs;
server.requestTimeout=deadlineMs;
server.keepAliveTimeout=1;
// Idle fallback leaves room for a complete inspection phase followed by
// its separately bounded forward phase. Neither wall clock is renewable.
server.on('connection',socket=>{sockets.add(socket);socket.setTimeout(deadlineMs*3,()=>socket.destroy());socket.once('close',()=>sockets.delete(socket));});
const malformedContexts=new WeakMap();
server.on('clientError',(_error,socket)=>{if(!malformedContexts.has(socket))malformedContexts.set(socket,lifecycle?.begin(connectionContext?.requestId));const context=malformedContexts.get(socket);void rejectAudit(context,'invalid-headers',{bytes:0,detectedType:'unknown',host:binding.hostname}).catch(()=>{}).finally(()=>socket.destroy());});
server.on('tlsClientError',(_error,socket)=>{void rejectAudit(connectionContext,'connect-denied',{bytes:0,detectedType:'unknown',host:binding.hostname},'connect').catch(()=>{}).finally(()=>socket.destroy());});
return Object.freeze({
// Trusted authenticated CONNECT acceptor only; no public IPC exposure.
// HTTPS Server owns TLS negotiation on this accepted raw TCP socket.
acceptAuthenticatedSocket(socket){server.emit('connection',socket);},
async listen(){await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',()=>{server.removeListener('error',reject);resolve();});});return server.address().port;},
async close(){if(!workAdmission)admission.close();if(!auditAdmission)controlAdmission.close();for(const socket of sockets)socket.destroy();await new Promise(resolve=>server.close(resolve));}
});
}
module.exports={createInspectionServer};
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const {randomUUID}=require('node:crypto');
const {GateError}=require('./http-request-gate.cjs');
function reasonFor(code,scope){
if(code==='inspection-deadline')return 'broker-scan-timeout';
if(code==='authority-expired')return 'broker-authority-expired';
if(code==='authentication-required')return scope==='connect'?'broker-auth-denied':'broker-framing-block';
if(['deadline','certificate-deadline','forward-deadline'].includes(code))return 'broker-request-timeout';
if(['aborted','incomplete-body','request-error'].includes(code))return 'broker-request-aborted';
if(code==='body-too-large')return 'broker-size-block';
if(['invalid-upstream-response','unsupported-upstream-encoding'].includes(code))return 'broker-response-invalid';
if(code==='upstream-failed')return 'broker-upstream-failed';
if(['invalid-headers','duplicate-header','unsupported-framing','unsupported-method','invalid-destination','authority-mismatch','invalid-length','unsupported-connect-framing','missing-host','duplicate-host','invalid-target','missing-sni','sni-mismatch'].includes(code))return 'broker-framing-block';
return scope==='connect'?'broker-connect-denied':'broker-inspection-fault';
}
/** One bounded state object per request. UUIDs express audit causation, never
* invented OpenTelemetry traces. A failed/uncertain commit is never retried.
*/
function createBrokerLifecycle(audit){
if(!audit||typeof audit.record!=='function')throw new GateError('audit-configuration-invalid');
return Object.freeze({begin(connectionId){
if(connectionId!==undefined&&!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(connectionId))throw new GateError('audit-configuration-invalid');
const requestId=randomUUID();let decisionWork,decisionType,authorizedEventId,terminalWork,responseWork,responseDecision,responseCommitted=false;
const broker=(phase,reason)=>({requestId,...(connectionId?{connectionId}:{}),phase,reason,...(phase==='forward.fault'||phase.startsWith('response.')?{authorizedEventId}: {})});
return Object.freeze({requestId,
decision(decision,facts){
if(decisionWork)return decisionWork;if(terminalWork)throw new GateError('aborted');
decisionType=decision;
const reason=facts.reason||(decision==='allow'?'broker-approved':decision==='block'?'broker-policy-block':'broker-inspection-fault');
const phase=decision==='allow'?'request.authorized':decision==='block'?'request.block':'request.fault';
decisionWork=audit.record(decision,{...facts,reason,broker:broker(phase,reason)}).then(receipt=>{if(decision==='allow')authorizedEventId=receipt.eventId;return receipt;},error=>{if(decision==='allow'&&error?.committedEventId)authorizedEventId=error.committedEventId;throw error;});
return decisionWork;
},
response(decision,facts){
if(responseWork)return responseWork;if(terminalWork)throw new GateError('aborted');responseDecision=decision;
responseWork=(async()=>{await decisionWork;if(!authorizedEventId)throw new GateError('authority-expired');const reason=decision==='allow'?'broker-download-approved':facts.reason||'broker-download-inspection-block';const safeFacts={...facts};delete safeFacts.reason;return audit.recordDownload(decision,{...safeFacts,broker:broker(decision==='allow'?'response.scanned':'response.block',reason)}).then(receipt=>{responseCommitted=true;return receipt;},error=>{responseCommitted=!!error?.committedEventId;throw error;});})();return responseWork;
},
reject(code,facts,scope='request'){
if(terminalWork)return terminalWork;
terminalWork=(async()=>{
if(decisionWork){
try {await decisionWork;} catch {if(!authorizedEventId)return {persisted:false,uncertain:true};}
if(decisionType!=='allow')return {persisted:true,alreadyRecorded:true};
}
if(responseWork){try{await responseWork;}catch{if(!responseCommitted)return {persisted:false,uncertain:true};}if(responseDecision==='block')return {persisted:true,alreadyRecorded:true};}
let reason=reasonFor(code,scope);
const phase=authorizedEventId?'forward.fault':scope==='connect'?'connect.block':reason==='broker-inspection-fault'?'request.fault':'request.block';
if(phase==='forward.fault'&&!['broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired','broker-scan-timeout'].includes(reason))reason='broker-upstream-failed';
return audit.record(phase==='forward.fault'||phase==='request.fault'?'fault':'block',{...facts,reason,broker:broker(phase,reason)});
})();
return terminalWork;
}
});
}});
}
module.exports={createBrokerLifecycle};
+119
View File
@@ -0,0 +1,119 @@
'use strict';
const {createHash}=require('node:crypto');
const {createConnectProxy}=require('./connect-inspection-proxy.cjs');
const {GateError}=require('./http-request-gate.cjs');
const {inspectFileTransfer,detectType}=require('../../packages/core/file-transfer.cjs');
const {createBrokerAudit}=require('./audit-gate.cjs');
const {createBrokerLifecycle}=require('./lifecycle-audit.cjs');
const {icapRequired}=require('../browser/enterprise-inspection.cjs');
const {inspectTextRepresentations}=require('../browser/inspect-text-representations.cjs');
const {snapshotHeaders,inspectHeaders}=require('../browser/header-inspection.cjs');
const {parseMultipart}=require('./multipart.cjs');
const {extractEncodedDocuments}=require('../../packages/core/encoded-documents.cjs');
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
/** Signed policy foundation for an isolated inspection broker. Trusted
* callbacks supply scoped certificates, real local/enterprise scanners and
* upstream transport. No classifier substitutes, enrollment or main wiring.
* A policy update invalidates this snapshot: integration must replace the
* listener with a newly authorized snapshot after protected publication.
*/
async function createManagedProxy({policyAuthority,snapshot=policyAuthority?.snapshot(),resolveCertificate,localInspector,enterpriseInspector,forward,persistAudit,auditCorrelation,deadlineMs=5000,maxClients=8,authMode='basic'}) {
if(!snapshot||!policyAuthority||typeof policyAuthority.current!=='function'||!policyAuthority.current(snapshot.token)||policyAuthority.snapshot()!==snapshot||typeof resolveCertificate!=='function'||typeof forward!=='function')throw new GateError('managed-authority-invalid');
if(typeof persistAudit!=='function')throw new GateError('managed-audit-unavailable');
const core=await import('../../packages/core/policy.mjs');
const p=snapshot.policy,current=token=>token===snapshot.token&&policyAuthority.current(token);
const check=signal=>{if(signal?.aborted)throw new GateError('aborted');if(!current(snapshot.token))throw new GateError('authority-expired');};
check();
const registeredIds=[...p.dlp.rules.map(rule=>rule.id),...p.dlp.semantic.categories.map(category=>category.id),...p.security.yara.rules.map(rule=>rule.id)];
const knownIds=new Set([...registeredIds,'private-key','aws-access-key','api-secret','content-blocklist','size-limit','uninspectable','normalization-limit','file-transfer-size-limit','file-transfer-type-blocked','file-transfer-type-not-allowed','file-transfer-unknown','file-transfer-extension-mismatch','file-transfer-mime-mismatch','inspection-incomplete']);
const audit=createBrokerAudit({persist:persistAudit,current:()=>current(snapshot.token),policyVersion:p.version,allowedRuleIds:registeredIds,correlation:auditCorrelation});
const lifecycle=createBrokerLifecycle(audit);
const destinationAllowed=url=>core.destinationAllowed(url,p)&&core.inspect(Buffer.from(url),p).decision==='allow';
const blocked=ruleId=>({decision:'block',ruleId});
const scanner=async(fn,bytes,signal,enterprise=false)=>{
const copy=Buffer.from(bytes),digest=hash(copy);
try {
check(signal);const result=await fn(copy,p,{signal});check(signal);
if(hash(copy)!==digest)return blocked('inspection-incomplete');
if(result?.decision==='block')return blocked(knownIds.has(result.ruleId)?result.ruleId:'inspection-incomplete');
if(result?.decision!=='allow'||result.coverage?.complete!==true||enterprise&&(result.sha256!==digest||result.bytes!==copy.length))return blocked('inspection-incomplete');
return result;
} finally {copy.fill(0);}
};
const textScan=(bytes,signal)=>inspectTextRepresentations({bytes,policy:p,isCurrent:()=>current(snapshot.token),
inspectLocal:typeof localInspector==='function'?value=>scanner(localInspector,value,signal):undefined,
inspectEnterprise:icapRequired(p,'upload')&&typeof enterpriseInspector==='function'?value=>scanner(enterpriseInspector,value,signal,true):undefined});
const textValue=async(value,signal,budget)=>{const bytes=Buffer.from(value);try{return await inspectContent(bytes,signal,undefined,budget);}finally{bytes.fill(0);}};
const inspectContent=async(bytes,signal,metadata,budget,depth=0)=>{
check(signal);const transfer=inspectFileTransfer(bytes,p,'upload',metadata);
if(transfer.decision!=='allow')return blocked(transfer.ruleId);
const verdict=core.inspect(bytes,p);
if(verdict.decision!=='allow'&&verdict.ruleId!=='uninspectable')return verdict;
if(transfer.detectedType==='text'){
if(/^image\/svg\+xml(?:;|$)/i.test(metadata?.mime||'')||/<svg(?:\s|>)/i.test(bytes.toString('utf8')))return blocked('inspection-incomplete');
if(!budget||budget.bytes<1||budget.nodes<1||budget.parts<1)return blocked('inspection-incomplete');
const decoded=extractEncodedDocuments(bytes,{jsonRequired:/^(?:application\/(?:json|[a-z0-9.+-]+\+json))(?:;|$)/i.test(metadata?.mime||''),maxDecodedBytes:budget.bytes,maxNodes:budget.nodes,maxParts:budget.parts,maxDepth:32});
try{
budget.bytes-=decoded.decodedBytes;budget.nodes-=decoded.visitedNodes;budget.parts-=decoded.parts.length;
if(decoded.complete!==true||budget.bytes<0||budget.nodes<0||budget.parts<0||depth>=4&&decoded.parts.length)return blocked('inspection-incomplete');
for(const part of decoded.parts){const result=await inspectContent(part.bytes,signal,{mime:part.mime},budget,depth+1);if(result.decision!=='allow')return result;}
check(signal);return textScan(bytes,signal);
}finally{decoded.dispose();}
}
if(typeof localInspector!=='function')return blocked('inspector-unavailable');
const local=await scanner(localInspector,bytes,signal);if(local.decision!=='allow')return local;
if(icapRequired(p,'upload')){const enterprise=await scanner(enterpriseInspector,bytes,signal,true);if(enterprise.decision!=='allow')return enterprise;}
check(signal);return {decision:'allow'};
};
return createConnectProxy({authority:snapshot.token,current,authMode,lifecycle,deadlineMs,maxClients,maxBytes:p.dlp.maxBytes,maxResponseBytes:Math.min(16777216,p.fileTransfers.download.maxBytes),
resolveCertificate:async(binding,options)=>{
check();if(!destinationAllowed(`https://${binding.authority}/`))throw new GateError('destination-denied');
const result=await resolveCertificate(binding,options);check();return result;
},
inspect:async({request,body,signal,auditContext})=>{
const classify=async()=>{
const budget={bytes:Math.min(p.dlp.maxBytes,16777216),nodes:20000,parts:64};
check();if(!destinationAllowed(request.url))return blocked('destination-denied');
if(icapRequired(p,'upload')&&typeof enterpriseInspector!=='function')return blocked('enterprise-unavailable');
const urlVerdict=await textValue(request.url,signal,budget);if(urlVerdict.decision!=='allow')return urlVerdict;
const headers=snapshotHeaders(request.headers,p);
const headerVerdict=await inspectHeaders(headers,{policy:p,inspect:core.inspect,isCurrent:()=>current(snapshot.token),
inspectLocal:typeof localInspector==='function'?bytes=>scanner(localInspector,bytes,signal):undefined,
inspectEnterprise:icapRequired(p,'upload')?bytes=>scanner(enterpriseInspector,bytes,signal,true):undefined});
if(headerVerdict.decision!=='allow')return headerVerdict;
for(const value of headers.values){const verdict=await textValue(value,signal,budget);if(verdict.decision!=='allow')return verdict;}
if(/^multipart\//i.test(request.headers['content-type']||'')){
const raw=core.inspect(body,p);if(raw.decision!=='allow'&&raw.ruleId!=='uninspectable')return raw;
const parsed=parseMultipart(body,request.headers['content-type'],{maxBytes:p.dlp.maxBytes});
try {
if(parsed.coverage?.complete!==true)return blocked('multipart-incomplete');
for(const part of parsed.parts){
for(const value of [part.name,part.filename,part.contentType].filter(value=>value!==undefined)){
const verdict=core.inspect(Buffer.from(value),p);if(verdict.decision!=='allow')return verdict;
const representation=await textValue(value,signal,budget);if(representation.decision!=='allow')return representation;
}
const verdict=await inspectContent(part.body,signal,{filename:part.filename,mime:part.contentType},budget);if(verdict.decision!=='allow')return verdict;
}
check(signal);return {decision:'allow'};
} finally {parsed.dispose();}
}
return inspectContent(body,signal,{mime:request.headers['content-type']},budget);
};
const facts={bytes:body.length,detectedType:detectType(body),host:new URL(request.url).hostname.replace(/^\[|\]$/g,'')};
let verdict;
try {verdict=await classify();}
catch {
if(signal.aborted)throw new GateError('aborted');
await auditContext.decision(current(snapshot.token)?'fault':'block',{...facts,reason:current(snapshot.token)?'broker-inspection-fault':'broker-authority-expired'});
throw new GateError('inspection-failed');
}
// This awaits a durable receipt while the original inspection job still
// owns global admission. Outer cancellation cannot free pending audit.
await auditContext.decision(verdict.decision==='allow'?'allow':'block',{...facts,...(verdict.decision!=='allow'&&knownIds.has(verdict.ruleId)?{matchedRuleId:verdict.ruleId}:{})});
check(signal);return verdict;
},
forward:value=>{check();return forward(value);}
});
}
module.exports={createManagedProxy};
@@ -0,0 +1,49 @@
'use strict';
const {spawn}=require('node:child_process');
const path=require('node:path');
const {TextDecoder}=require('node:util');
const {installationRoot}=require('./service-store-client.cjs');
const {createWorkAdmission}=require('./work-admission.cjs');
const refusal=()=>Error('broker-management-refused');
const exact=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join(',')===keys.sort().join(',');
const keys=['deviceId','origin','policyVersion','policyKeySha256','policyDigest'];
function validBinding(value){
if(!exact(value,keys)||! /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(value.deviceId)||!Number.isSafeInteger(value.policyVersion)||value.policyVersion<1||![value.policyKeySha256,value.policyDigest].every(v=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)))throw refusal();
if(typeof value.origin!=='string'||value.origin.length>2048)throw refusal();const u=new URL(value.origin);if(u.protocol!=='https:'||u.origin!==value.origin||u.username||u.password||u.pathname!=='/'||u.search||u.hash)throw refusal();return Object.freeze({...value});
}
// Read-only contract for the fixed native protected store. Native command must
// authenticate enabled SCM SID + retained signed Node parent and validate its
// OS-minted user SID, signed deployment and exact accepted policy digest.
// This module never creates a store or treats an environment SID as authority.
function createNativeManagementCredentialLoader({expectedBinding,current,launch=spawn,deadlineMs=10000}={}){
let expected;try{expected=validBinding(expectedBinding);if(typeof current!=='function'||typeof launch!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>30000)throw refusal();}catch{throw refusal();}
let closed=false,closing;const stops=new Set(),operations=new Set(),admission=createWorkAdmission({maxJobs:1}),active=()=>{try{return !closed&&current()===true;}catch{return false;}};
const load=async(binding,{signal}={})=>{
try{
const request=validBinding(binding);if(!active()||signal?.aborted||keys.some(k=>request[k]!==expected[k]))throw refusal();
const operation=admission.run(()=>new Promise((resolve,reject)=>{
let child,timer,total=0,failed=false;const output=Buffer.alloc(8196);
const stop=()=>{failed=true;try{child?.kill();}catch{}};stops.add(stop);
try{
const sid=process.env.SAFETYNET_SERVICE_CLIENT_SID;if(typeof sid!=='string'||! /^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/.test(sid)||sid.split('-').slice(4).some(n=>Number(n)>4294967295))throw refusal();
child=launch(path.join(installationRoot,'safetynet-service-host.exe'),['--management-credential'],{cwd:installationRoot,shell:false,windowsHide:true,stdio:['pipe','pipe','ignore'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR,SAFETYNET_SERVICE_CLIENT_SID:sid}});
signal?.addEventListener('abort',stop,{once:true});timer=setTimeout(stop,deadlineMs);child.once('error',stop);child.stdin.on('error',stop);child.stdout.on('error',stop);
child.stdout.on('data',part=>{if(failed)return;if(!Buffer.isBuffer(part)||total+part.length>output.length){stop();return;}part.copy(output,total);total+=part.length;if(total>=4&&(output.readUInt32LE(0)<2||output.readUInt32LE(0)>8192||total>output.readUInt32LE(0)+4))stop();});
child.once('close',(code,endedSignal)=>{
stops.delete(stop);clearTimeout(timer);signal?.removeEventListener('abort',stop);let token;
try{
if(failed||code!==0||endedSignal||!active()||signal?.aborted||total<6||output.readUInt32LE(0)!==total-4)throw refusal();
const value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(output.subarray(4,total)));
if(!exact(value,[...keys,'token'])||keys.some(k=>value[k]!==expected[k])||typeof value.token!=='string'||value.token.length<16||value.token.length>4096||! /^[A-Za-z0-9._~-]+$/.test(value.token))throw refusal();
token=Buffer.from(value.token);resolve(Object.freeze({deviceId:expected.deviceId,origin:expected.origin,token}));
}catch{token?.fill(0);reject(refusal());}finally{output.fill(0);}
});
const payload=Buffer.from(JSON.stringify(request));if(payload.length>4096){payload.fill(0);throw refusal();}const frame=Buffer.alloc(payload.length+4);frame.writeUInt32LE(payload.length);payload.copy(frame,4);payload.fill(0);child.stdin.end(frame,()=>frame.fill(0));
}catch{stop();if(!child){stops.delete(stop);clearTimeout(timer);output.fill(0);reject(refusal());}}
}));
operations.add(operation);try{return await operation;}finally{operations.delete(operation);}
}catch{throw refusal();}
};
return Object.freeze({load,close(){if(closing)return closing;closed=true;admission.close();for(const stop of stops)stop();closing=Promise.allSettled([...operations]).then(()=>undefined);return closing;}});
}
module.exports={createNativeManagementCredentialLoader};
@@ -0,0 +1,69 @@
'use strict';
const {createHash,createPublicKey}=require('node:crypto');
const {TextDecoder}=require('node:util');
const {verifyServiceAcceptance}=require('./service-acceptance.cjs');
const {createServiceStoreClient}=require('./service-store-client.cjs');
const {createDnsPolicyRoute,assertSupportedDns}=require('./dns-policy-route.cjs');
const {createHttpsForwarder}=require('./https-forwarder.cjs');
const {createProxyForwarder}=require('./proxy-forwarder.cjs');
const {deploymentRoots}=require('./production-factory.cjs');
const {createWorkAdmission}=require('./work-admission.cjs');
const channel=require('../../packages/core/policy-channel.cjs');
const refused=()=>Error('broker-management-refused');
// The fixed native loader supplies device credentials from protected storage;
// injected loaders are explicit trusted test/service adapters, never IPC fields.
// There is no URL, token, method, header, file path or general fetch IPC surface.
// Enrollment is deliberately unavailable until its own authenticated bootstrap
// contract exists. Only an already durably accepted policy may use this leg.
async function createManagementIdentityOperation({store=createServiceStoreClient(),policyEnvelope,loadCredential,credentialLoader,current,network,deadlineMs=10000}={}){
let authority,ownedLoader;
try{
if(credentialLoader){if(loadCredential!==undefined||typeof credentialLoader.load!=='function')throw refused();loadCredential=credentialLoader.load.bind(credentialLoader);}
if(loadCredential!==undefined&&typeof loadCredential!=='function'||typeof current!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>30000)throw refused();
const snapshot=await store.snapshot(),files=await store.envelopes();
const plan=await verifyServiceAcceptance({...files,state:snapshot.acceptanceState,releaseAnchor:snapshot.releaseAnchor,authenticatedSid:snapshot.authenticatedSid,policyEnvelope});
if(plan.nextStateDigest!==plan.previousStateDigest)throw refused();
const {verifiedManagementBinding}=require('../../packages/core/windows-trust.cjs'),binding=verifiedManagementBinding(plan.deployment);
const {createPolicyAuthority}=require('./policy-authority.cjs');authority=await createPolicyAuthority(plan.bootstrap);const accepted=authority.accept(policyEnvelope),policy=accepted.policy;
const active=()=>{try{return current()===true&&authority.current(accepted.token)&&Date.now()<plan.deployment.manifest.expiresAt;}catch{return false;}};
if(!active()||!network||network.mode!==policy.proxy.mode||network.dns!=='system'||Object.keys(network).some(k=>!['mode','dns','ca','proxyCa','credentials'].includes(k)))throw refused();
assertSupportedDns(policy,network,{numericOriginSupported:true});
const security=await import('../../packages/core/security.mjs'),resolve=createDnsPolicyRoute({current:active,blocked:a=>security.dnsAddressBlocked(a,policy)});
const transport={authority:accepted.token,current:active,ca:deploymentRoots(network.ca)?.join('\n'),deadlineMs,maxRequestBytes:16384,maxResponseBytes:5242880};
const forward=network.mode==='fixed_servers'?createProxyForwarder({...transport,proxyPolicy:policy.proxy,proxyCa:deploymentRoots(network.proxyCa)?.join('\n'),credentials:network.credentials,resolveProxyRoute:resolve,resolveOriginRoute:policy.security?.dns?.enabled!==false&&policy.security?.dns?.blockedIPs?.length?resolve:undefined}):network.mode==='direct'?createHttpsForwarder({...transport,resolveRoute:resolve}):(()=>{throw refused();})();
let closing;const originals=new Set(),jobs=createWorkAdmission({maxJobs:1}),controllers=new Set(),credentialBinding=Object.freeze({origin:binding.origin,deviceId:plan.bootstrap.deviceId,policyVersion:plan.policyVersion,policyDigest:plan.policyDigest,policyKeySha256:createHash('sha256').update(createPublicKey(plan.bootstrap.publicKey).export({type:'spki',format:'der'})).digest('hex')});
if(loadCredential===undefined){const {createNativeManagementCredentialLoader}=require('./management-credential.cjs');ownedLoader=createNativeManagementCredentialLoader({expectedBinding:credentialBinding,current:active,deadlineMs});loadCredential=ownedLoader.load;}
const close=()=>{if(closing)return closing;authority.revoke();jobs.close();for(const controller of controllers)controller.abort();closing=Promise.allSettled([...originals,ownedLoader?.close()]).then(()=>undefined);return closing;};
const operation=async(kind,input)=>{
if(input!==undefined||!active())throw refused();
const controller=new AbortController();controllers.add(controller);let timer;
const cancelled=new Promise((_,reject)=>{controller.signal.addEventListener('abort',()=>reject(refused()),{once:true});timer=setTimeout(()=>controller.abort(),deadlineMs);});
const work=Promise.resolve().then(()=>jobs.run(async()=>{
let credentialCopy,ownedToken,context;
try{
const credential=await loadCredential(credentialBinding,{signal:controller.signal});
// Buffer credentials are private one-use leases from the protected loader,
// transferred to this operation, never a reusable backing-store buffer.
ownedToken=Buffer.isBuffer(credential?.token)?credential.token:undefined;
if(!active()||controller.signal.aborted||!credential||Object.keys(credential).sort().join(',')!=='deviceId,origin,token'||credential.deviceId!==credentialBinding.deviceId||credential.origin!==binding.origin)throw refused();
credentialCopy=Buffer.isBuffer(credential.token)?Buffer.from(credential.token):typeof credential.token==='string'?Buffer.from(credential.token):undefined;
if(!credentialCopy||credentialCopy.length<16||credentialCopy.length>4096||! /^[A-Za-z0-9._~-]+$/.test(credentialCopy.toString('utf8')))throw refused();
const request=async(route,method,body,limit)=>{if(!active()||controller.signal.aborted)throw refused();const bytes=body===undefined?Buffer.alloc(0):Buffer.from(JSON.stringify(body));let result;try{result=await forward({request:{url:binding.origin+route,method,headers:{authorization:'Bearer '+credentialCopy.toString('utf8'),...(body===undefined?{}:{'content-type':'application/json'})}},body:bytes,signal:controller.signal,maxResponseBytes:limit});if(result.statusCode!==200||!active()||controller.signal.aborted)throw refused();return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(result.body));}finally{bytes.fill(0);result?.body.fill(0);}};
if(kind==='identity'){const identity=await request('/api/device/channel/identity','GET',undefined,20000);channel.verifyIdentity(identity,plan.bootstrap.publicKey,binding.signatureKey);if(!active())throw refused();return Object.freeze(identity);}
const challenge=await request('/api/device/channel/challenge','POST',{},20000);channel.verifyChallenge(challenge,plan.bootstrap.deviceId,plan.bootstrap.publicKey,binding.signatureKey);if(!active()||controller.signal.aborted)throw refused();context=channel.prepare(challenge);
const response=await request('/api/device/channel/policy','POST',context.request,5242880),envelope=channel.finish(response,challenge,context);
if(!envelope||Object.keys(envelope).sort().join(',')!=='payload,signature'||typeof envelope.payload!=='string'||typeof envelope.signature!=='string')throw refused();
const core=await import('../../packages/core/policy.mjs'),next=core.verifyPolicy(envelope,plan.bootstrap.publicKey,plan.policyVersion);
if(next.assignment?.deviceId!==plan.bootstrap.deviceId||next.version===plan.policyVersion&&createHash('sha256').update(envelope.payload).digest('hex')!==plan.policyDigest||!active()||controller.signal.aborted)throw refused();
// Returned policy is verified but NOT activated or durably accepted here;
// the privileged policy transition must commit its new floor separately.
return Object.freeze({payload:envelope.payload,signature:envelope.signature});
}catch{throw refused();}finally{credentialCopy?.fill(0);ownedToken?.fill(0);context?.responseKey.fill(0);}
}));
originals.add(work);work.finally(()=>originals.delete(work)).catch(()=>{});
try{return await Promise.race([work,cancelled]);}catch{throw refused();}finally{clearTimeout(timer);controllers.delete(controller);}
};
return Object.freeze({close,identity:input=>operation('identity',input),policy:input=>operation('policy',input)});
}catch{authority?.revoke();await ownedLoader?.close();throw refused();}
}
module.exports={createManagementIdentityOperation};
+92
View File
@@ -0,0 +1,92 @@
'use strict';
class MultipartError extends Error{
constructor(code){super(code);this.name='MultipartError';this.code=code;}
}
const fail=code=>{throw new MultipartError(code);};
const token=/^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
function parameters(value){
const main=/^([^;]+)(.*)$/.exec(value);if(!main)fail('invalid-multipart');
const result=Object.create(null);let rest=main[2];
while(rest){
const key=/^;[ \t]*([!#$%&'*+.^_`|~0-9a-z-]+)[ \t]*=[ \t]*/i.exec(rest);
if(!key)fail('invalid-multipart');rest=rest.slice(key[0].length);
let parameter;
if(rest.startsWith('"')){
let index=1;parameter='';let closed=false;
for(;index<rest.length;index++){
const character=rest[index];
if(character==='"'){closed=true;index++;break;}
if(character==='\\'){
index++;if(!['"','\\'].includes(rest[index]))fail('invalid-multipart');parameter+=rest[index];
}else parameter+=character;
}
if(!closed)fail('invalid-multipart');rest=rest.slice(index);
}else{
const match=/^([^; \t]+)([ \t]*)(.*)$/.exec(rest);if(!match||!token.test(match[1]))fail('invalid-multipart');
parameter=match[1];rest=match[3];
}
const name=key[1].toLowerCase();if(Object.hasOwn(result,name))fail('invalid-multipart');
result[name]=parameter;rest=rest.replace(/^[ \t]+/,'');
}
return {main:main[1].trim().toLowerCase(),values:result};
}
function metadata(header){
let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(header);}catch{fail('invalid-multipart');}
const headers=Object.create(null);
for(const line of text.split('\r\n')){
const match=/^([^:]+):[ \t]*(.*)$/.exec(line);
if(!match||!token.test(match[1])||[...match[2]].some(character=>{const code=character.charCodeAt(0);return code<32&&code!==9||code===127;}))fail('invalid-multipart');
const name=match[1].toLowerCase();if(Object.hasOwn(headers,name))fail('invalid-multipart');headers[name]=match[2];
}
if(Object.keys(headers).some(name=>!['content-disposition','content-type'].includes(name)))fail('unsupported-multipart');
if(!headers['content-disposition'])fail('invalid-multipart');
const disposition=parameters(headers['content-disposition']),values=disposition.values;
if(Object.keys(values).some(name=>name.endsWith('*')))fail('unsupported-multipart');
if(disposition.main!=='form-data'||Object.keys(values).some(name=>!['name','filename'].includes(name))||typeof values.name!=='string'||!values.name||Buffer.byteLength(values.name)>256||[values.name,values.filename].some(value=>value!==undefined&&(Buffer.byteLength(value)>1024||[...value].some(character=>character.charCodeAt(0)<32||character.charCodeAt(0)===127))))fail('invalid-multipart');
let contentType;
if(headers['content-type']!==undefined){
if(headers['content-type'].length>256)fail('multipart-limit');
const type=parameters(headers['content-type']);
if(!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(type.main))fail('invalid-multipart');
if(type.main.startsWith('multipart/'))fail('unsupported-multipart');
contentType=headers['content-type'];
}
return {name:values.name,...values.filename!==undefined?{filename:values.filename}:{},...contentType!==undefined?{contentType}:{}};
}
/** Full-buffer parser. No filesystem access or content logging. Returned part
* buffers are independent private copies; dispose only after scanners finish.
* Strict unsupported forms fail closed instead of claiming partial coverage.
*/
function parseMultipart(body,contentType,{maxBytes=16777216,maxParts=64,maxHeaderBytes=8192}={}){
if(!Buffer.isBuffer(body)||typeof contentType!=='string'||![maxBytes,maxParts,maxHeaderBytes].every(Number.isSafeInteger)||maxBytes<0||maxBytes>16777216||maxParts<1||maxParts>256||maxHeaderBytes<1||maxHeaderBytes>32768)fail('invalid-multipart');
if(body.length>maxBytes||contentType.length>256)fail('multipart-limit');
const type=parameters(contentType);
if(type.main!=='multipart/form-data'||Object.keys(type.values).some(name=>name!=='boundary')||!/^[0-9a-z'()+_,./:=?-]{1,70}$/i.test(type.values.boundary||''))fail('invalid-multipart');
const boundary=Buffer.from('--'+type.values.boundary),separator=Buffer.from('\r\n--'+type.values.boundary),parts=[];
const dispose=()=>{for(const part of parts)part.body.fill(0);};
try{
if(!body.subarray(0,boundary.length).equals(boundary))fail('invalid-multipart');
let position=boundary.length;
while(true){
if(body.subarray(position,position+2).equals(Buffer.from('--'))){
position+=2;if(body.subarray(position).equals(Buffer.from('\r\n')))position+=2;
if(position!==body.length)fail('invalid-multipart');break;
}
if(!body.subarray(position,position+2).equals(Buffer.from('\r\n')))fail('invalid-multipart');position+=2;
if(parts.length>=maxParts)fail('multipart-limit');
const headerEnd=body.indexOf('\r\n\r\n',position);
if(headerEnd<0)fail('invalid-multipart');if(headerEnd-position>maxHeaderBytes)fail('multipart-limit');
const record=metadata(body.subarray(position,headerEnd));position=headerEnd+4;
let end=position;
while(true){
end=body.indexOf(separator,end);if(end<0)fail('invalid-multipart');
const suffix=body.subarray(end+separator.length,end+separator.length+2);
if(suffix.equals(Buffer.from('\r\n'))||suffix.equals(Buffer.from('--')))break;
end++;
}
parts.push(Object.freeze({...record,body:Buffer.from(body.subarray(position,end))}));position=end+separator.length;
}
return Object.freeze({coverage:Object.freeze({complete:true}),parts:Object.freeze(parts),dispose});
}catch(error){dispose();throw error;}
}
module.exports={parseMultipart,MultipartError};
+147
View File
@@ -0,0 +1,147 @@
# Native Windows service host
This component uses Windows SCM, named pipes and Job objects around the stock
Node broker. It does not rebuild Electron/Chromium or modify installed Chrome.
It has not been installed or proven as the production network boundary.
Run `node scripts/build-windows-service-host.mjs --self-test` on Windows with
Visual Studio C++ Build Tools and SDK 10.0.26100.0. The builder uses explicit
compiler/include/library paths, /W4 /WX, static runtime, CFG, ASLR and DEP. Output
and fixed metadata evidence remain under ignored `artifacts/native-service-host`.
It changes no execution policy, service, firewall, DNS setting or user profile.
The builder accepts `--self-test` or `--compile-only`. Compile-only never launches
the executable and writes separate evidence. It bakes a disposable public key, retains
private test keys only in memory, and signs synthetic inventory fixtures after
compilation. It never creates a production release key or registers a service.
The actual negative tests verify: missing SCM service identity, unauthorized
client image, oversized frame, wrong file hash, second pipe instance denied by
its DACL, and a child plus grandchild retained in a non-breakaway kill-on-close
Job and both observed terminal after closing the Job. These are meaningful
native component tests; they do not prove an installed SCM or WFP deployment.
The fixed bridge is `\\.\pipe\SafetyNet.Broker.v1`, local-only, one authenticated
browser connection at a time. An enabled SCM service SID, protected code and
independently pinned deployment material are required before broker launch. The
client is the protected installed helper launched by the protected installed
SafetyNet.exe. Both must have matching medium/high, non-AppContainer user
tokens and sessions. Its process token and impersonated pipe token must agree
on user SID and session. The process handle remains retained and monitored;
client exit terminates the broker Job. The ACL excludes FILE_CREATE_PIPE_INSTANCE
from employee rights. No trust arises from a pipe name or claimed PID in JSON.
Frames are uint32 little-endian length followed by UTF-8 JSON. The first
parent-to-child frame is bounded to 2 MiB; subsequent frames and child responses
are 64 KiB. The constant child receives only inherited stdin/stdout handles and
`--service-stdio`; stderr goes to NUL. Seven Windows profile/runtime variables
and one fixed OS-derived `SAFETYNET_SERVICE_CLIENT_SID` are passed. No caller
environment supplies that SID. The Node adapter must validate the existing
v1 generation, monotonic sequence, typed audit/certificate receipts and pinned
service deployment authority. Framing does not authenticate policy or allow a
caller to replace its public key, device identity or version floor.
The `--client-stdio` mode connects once and authenticates the actual connected
handle's retained server process against signed release hashes, the exact
installed path and an enabled SCM service SID. It never probes and reconnects.
`--trust-snapshot` requires a service-token helper and a retained signed Node
parent. It returns bounded framed public anchor/SID/registry-floor metadata,
without accepting a registry path or trust key from its caller.
The fixed child entry is `resources/broker/apps/network-broker/service-entry.cjs`.
The prepared `--topology-snapshot` helper uses the compiled broker port 43120,
fixed installed browser/broker paths and the real SCM service SID. Service-token,
signed Node-parent and signed user-assignment checks precede its read-only BFE
enumeration. It verifies all eight owned filter keys, actions, conditions,
weights and service descriptor bytes, checks equal-priority collisions and BFE
state, and reports bounded metadata. It never accepts a plan, port or visibility
flag from IPC. The new topology source compiled, but its latest self-test launch
failed with `UNKNOWN:spawn`; compile-only evidence is separate from earlier
successful native tests. This helper has no installed execution proof yet.
Windows enumerators omit unreadable objects and notifications are asynchronous
and unreliable. Therefore the helper reports `allObjectsReadable:false` even at
successful EOF. Production admission requires an independently reviewed,
protected deployment prerequisite; token elevation or an ordinary JSON flag
cannot establish it. Ordinary-user enforcement assumes trusted administrators;
user-mode monitoring cannot promise zero packets during hostile privileged
kernel/BFE changes. See Microsoft's [access control](https://learn.microsoft.com/en-us/windows/win32/fwp/access-control)
and [notification contract](https://learn.microsoft.com/en-us/windows/win32/fwp/change-notifications).
The fixed 64-bit HKLM key `SOFTWARE\SafetyNet\Trust` contains one canonical
`AcceptanceState` REG_BINARY value, including release/deployment digests and
sorted per-user policy floors. Missing or corrupt state fails closed. The
snapshot exposes `{version, authenticatedSid, releaseAnchor, acceptanceState}`.
`--commit-acceptance` accepts only `{expectedPreviousSHA, candidateState}`,
checks a full monotonic transition under an authenticated fixed mutex, writes
one value, flushes and verifies readback before returning
`{persisted:true,stateSha256}`. State bytes are capped at 2 MiB minus 1024 bytes;
all stdio frames have a single bounded deadline. The trusted Node gate must
verify the actual signed policy before requesting a commit. Policy digests hash
the original base64url payload string; manifest digests hash canonical decoded
JSON bytes.
The prepared `--management-credential` command accepts exactly device ID,
canonical HTTPS origin, policy version, policy-key DER SHA256 and the accepted
policy-payload digest. It binds them to the native service-minted user SID,
signed v2 deployment and exact protected acceptance record. Its private reply
contains only the same fields and the token, bounded to 8 KiB. The separate fixed
key `SOFTWARE\SafetyNet\ManagementCredentials` requires a protected DACL owned
by SYSTEM/administrators, granting access only to them and the exact service SID.
Encrypted records use LocalMachine DPAPI with canonical metadata as entropy;
this is Windows encryption, not a post-quantum storage claim.
Each read first validates the exact current accepted policy request. It then
selects the unique highest stored version at or below that request for the same
SID, device, origin and policy key. Same-version digest conflicts, ambiguous
duplicates and future versions fail closed. Decryption uses the record's own
original metadata; the reply echoes the verified current request. Advancing a
policy therefore does not require re-encrypting an otherwise valid credential.
Provisioning replaces all records for that stable identity and preserves other
identities, preventing a new record for every policy revision.
Internal `--provision-management-credential` additionally requires the previous
encrypted-store SHA256 and token. It uses the same authenticated service role,
serialized mutex, bounded canonical store (256 records/1 MiB), CAS, flush and
readback before returning a receipt. It accepts no registry path, user SID or
key selector. Neither command creates keys or supplies an initial empty store.
No renderer/enrollment provisioning endpoint is exposed. Initial protected
provisioning and actual service-token DPAPI/registry execution remain unproven;
the latest native source has compile-only evidence, not execution evidence.
The mutex uses an OWNER_RIGHTS read-control ACE rather than assuming the service
SID can own objects. Actual native tests show owner WRITE_DAC denial while
synchronization and modification remain allowed. Commit source is prepared,
but no registry write, installed service positive or durable commit has been
performed. Current uninstalled snapshot/commit/client tests require exit 5.
`signed-manifest.h` verifies canonical manifests using Windows CNG P-256/SHA256
and fixed 64-byte IEEE-P1363 signatures. The baked vendor key verifies release
inventory; its delegated customer key verifies deployment and user assignment.
This is a classical release signature, not a post-quantum signature. Actual
native tests verify positive delegation and reject tampering, the wrong customer
key, expiry and equal-sequence digest mismatch. A larger signed inventory tests
real envelope parsing. These fixture tests do not attest installed resource ACLs.
Before deployment, implement and independently verify these remaining gates:
- Actual vendor signing/provisioning and complete final inventory, including
every broker, inspector, model and browser resource. The external signature
avoids circular browser/host hashes; synthetic fixtures are not a release.
- Installed positive client/server authentication and lifecycle tests; current
native negatives cannot prove a correctly provisioned SCM service.
- Protected initial acceptance-state provisioning and an actual service-token
CAS/flush/readback proof before listen. Prepared source and uninstalled
negative tests do not establish durable installed behavior.
- Real SCM provisioning under a least-privileged service identity, protected
resource/state ACLs, actual service-start/stop and bounded I/O timeout tests.
- Per-user signed assignment binding and typed management/inspector/OTLP RPC.
Interactive DPAPI audit persistence stays in the authenticated user parent.
- Transactional WFP outbound filters and receiver service-descriptor soft-permit
plus lower block in the same owned sublayer, preserving corporate veto.
- Native readback and physical IPv4/IPv6/UDP/DNS/port-takeover tests, including
another instance of the same stock Node executable and service crashes.
- Effective WFP hard-permit topology validation and BFE change monitoring;
a low-priority sublayer or a static rule inventory cannot prove enforcement.
Browser inbound-first IPv4/IPv6 TCP/UDP denies need physical receiver tests.
Until those gates pass, retain existing browser quarantine and enforcement.
@@ -0,0 +1,114 @@
#pragma once
#include "signed-manifest.h"
// Canonical monotonic state contract. No registry operations in this header.
namespace acceptance_contract {
using release_contract::Json;
static bool sha(const std::string &value) {
if (value.size() != 64)
return false;
for (char c : value)
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')))
return false;
return true;
}
static bool userSid(const std::string &value) {
if ((value.rfind("S-1-5-21-", 0) != 0 && value.rfind("S-1-12-1-", 0) != 0) ||
std::count(value.begin(), value.end(), '-') != 7)
return false;
PSID sid = nullptr;
if (!ConvertStringSidToSidA(value.c_str(), &sid))
return false;
LPSTR text = nullptr;
bool valid = ConvertSidToStringSidA(sid, &text) && text && value == text;
if (text)
LocalFree(text);
LocalFree(sid);
return valid;
}
static std::string identity(const Json &record) {
return release_contract::str(record.object.at("sid")) + "|" +
release_contract::str(record.object.at("deviceId")) + "|" +
release_contract::str(record.object.at("policyKeySha256"));
}
static void floor(const Json &value, bool deployment) {
if (!release_contract::exact(
value,
deployment
? std::initializer_list<const char *>{"minimumSequence",
"acceptedDigest", "tenant"}
: std::initializer_list<const char *>{"minimumSequence",
"acceptedDigest"}) ||
!sha(release_contract::str(value.object.at("acceptedDigest"))) ||
!release_contract::number(value.object.at("minimumSequence")) ||
(deployment && !release_contract::uuid(
release_contract::str(value.object.at("tenant")))))
throw std::runtime_error("fixed-state-invalid");
}
static Json validate(const std::string &text) {
if (text.size() > 2096128)
throw std::runtime_error("fixed-state-invalid");
Json state = release_contract::json(text);
if (!release_contract::exact(
state, {"format", "release", "deployment", "policies"}) ||
release_contract::str(state.object.at("format")) !=
"safetynet-service-floors-v1")
throw std::runtime_error("fixed-state-invalid");
floor(state.object.at("release"), false);
floor(state.object.at("deployment"), true);
const auto &policies = state.object.at("policies");
if (policies.kind != Json::Array || policies.array.size() > 8192)
throw std::runtime_error("fixed-state-invalid");
std::string previous;
for (const auto &record : policies.array) {
if (!release_contract::exact(record,
{"sid", "deviceId", "policyKeySha256",
"minimumVersion", "acceptedDigest"}) ||
!userSid(release_contract::str(record.object.at("sid"))) ||
!sha(release_contract::str(record.object.at("policyKeySha256"))) ||
!sha(release_contract::str(record.object.at("acceptedDigest"))) ||
!release_contract::number(record.object.at("minimumVersion")))
throw std::runtime_error("fixed-state-invalid");
auto device = release_contract::str(record.object.at("deviceId"));
if (device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])))
throw std::runtime_error("fixed-state-invalid");
for (unsigned char c : device)
if (!isalnum(c) && c != '.' && c != '_' && c != '-')
throw std::runtime_error("fixed-state-invalid");
auto id = identity(record);
if (!previous.empty() && id <= previous)
throw std::runtime_error("fixed-state-invalid");
previous = id;
}
return state;
}
static bool nondecreasing(const Json &previous, const Json &candidate,
const char *field) {
auto old = release_contract::number(previous.object.at(field)),
next = release_contract::number(candidate.object.at(field));
return next >= old &&
(next != old ||
release_contract::str(previous.object.at("acceptedDigest")) ==
release_contract::str(candidate.object.at("acceptedDigest")));
}
static bool transition(const Json &old, const Json &candidate) {
if (!nondecreasing(old.object.at("release"), candidate.object.at("release"),
"minimumSequence") ||
!nondecreasing(old.object.at("deployment"),
candidate.object.at("deployment"), "minimumSequence") ||
release_contract::str(old.object.at("deployment").object.at("tenant")) !=
release_contract::str(
candidate.object.at("deployment").object.at("tenant")))
return false;
std::map<std::string, const Json *> records;
for (const auto &record : candidate.object.at("policies").array)
records.emplace(identity(record), &record);
for (const auto &record : old.object.at("policies").array) {
auto match = records.find(identity(record));
if (match == records.end() ||
!nondecreasing(record, *match->second, "minimumVersion"))
return false;
}
return true;
}
} // namespace acceptance_contract
@@ -0,0 +1,276 @@
#pragma once
#include "acceptance-state.h"
#include <wincrypt.h>
#pragma comment(lib, "crypt32.lib")
namespace credential_contract {
using release_contract::Json;
static constexpr DWORD STORE_LIMIT = 1048576;
struct Wipe {
std::vector<BYTE> bytes;
~Wipe() {
if (!bytes.empty())
SecureZeroMemory(bytes.data(), bytes.size());
}
};
struct WipeText {
std::string &text;
~WipeText() {
if (!text.empty())
SecureZeroMemory(text.data(), text.size());
}
};
static Json parse(const std::string &text) {
release_contract::Parser parser{text};
auto result = parser.parse();
if (parser.offset != text.size())
throw 1;
return result;
}
static bool tokenValid(const std::string &token) {
if (token.size() < 16 || token.size() > 4096)
return false;
for (char c : token)
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '~' ||
c == '-'))
return false;
return true;
}
static Json binding(const Json &request,
const release_contract::Assignment &assignment,
const Json &state) {
if (assignment.managementOrigin.empty() ||
release_contract::str(request.object.at("deviceId")) !=
assignment.deviceId ||
release_contract::str(request.object.at("origin")) !=
assignment.managementOrigin)
throw 1;
auto der = release_contract::pemDer(assignment.policyPem);
const std::string keyBytes(der.begin(), der.end());
if (release_contract::str(request.object.at("policyKeySha256")) !=
release_contract::digest(keyBytes))
throw 1;
const auto version =
release_contract::number(request.object.at("policyVersion"));
const auto digest = release_contract::str(request.object.at("policyDigest"));
if (!acceptance_contract::sha(digest) || version < assignment.minimumVersion)
throw 1;
bool matched = false;
for (const auto &record : state.object.at("policies").array) {
if (release_contract::str(record.object.at("sid")) == assignment.sid &&
release_contract::str(record.object.at("deviceId")) ==
assignment.deviceId &&
release_contract::str(record.object.at("policyKeySha256")) ==
release_contract::str(request.object.at("policyKeySha256"))) {
matched =
release_contract::number(record.object.at("minimumVersion")) ==
version &&
release_contract::str(record.object.at("acceptedDigest")) == digest;
}
}
if (!matched)
throw 1;
Json result = request;
result.object.erase("token");
result.object.erase("expectedPreviousSHA");
Json sid;
sid.kind = Json::String;
sid.string = assignment.sid;
result.object["sid"] = sid;
return result;
}
static Json validateStore(const std::string &text) {
if (text.empty() || text.size() > STORE_LIMIT)
throw 1;
auto value = parse(text);
if (release_contract::canonical(value) != text ||
!release_contract::exact(value, {"format", "records"}) ||
release_contract::str(value.object.at("format")) !=
"safetynet-management-credentials-v1" ||
value.object.at("records").kind != Json::Array ||
value.object.at("records").array.size() > 256)
throw 1;
std::string previous;
for (const auto &record : value.object.at("records").array) {
if (!release_contract::exact(record, {"sid", "deviceId", "origin",
"policyVersion", "policyKeySha256",
"policyDigest", "protectedToken"}) ||
!acceptance_contract::userSid(
release_contract::str(record.object.at("sid"))) ||
!release_contract::managementOrigin(
release_contract::str(record.object.at("origin"))) ||
!acceptance_contract::sha(
release_contract::str(record.object.at("policyKeySha256"))) ||
!acceptance_contract::sha(
release_contract::str(record.object.at("policyDigest"))))
throw 1;
release_contract::number(record.object.at("policyVersion"));
const auto device = release_contract::str(record.object.at("deviceId"));
if (device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])))
throw 1;
for (char c : device)
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' &&
c != '-')
throw 1;
const auto cipher =
release_contract::str(record.object.at("protectedToken"));
if (cipher.empty() || cipher.size() > 16384)
throw 1;
release_contract::decode(cipher);
Json identity = record;
identity.object.erase("protectedToken");
const auto key = release_contract::canonical(identity);
if (!previous.empty() && key <= previous)
throw 1;
previous = key;
}
return value;
}
static std::string stableIdentity(Json metadata) {
metadata.object.erase("policyVersion");
metadata.object.erase("policyDigest");
metadata.object.erase("protectedToken");
return release_contract::canonical(metadata);
}
static const Json *selectRecord(const Json &metadata,
const std::vector<Json> &records) {
const auto identity = stableIdentity(metadata);
const auto current =
release_contract::number(metadata.object.at("policyVersion"));
const Json *selected = nullptr;
ULONGLONG highest = 0;
std::set<ULONGLONG> versions;
for (const auto &record : records) {
if (stableIdentity(record) != identity)
continue;
const auto version =
release_contract::number(record.object.at("policyVersion"));
if (!versions.insert(version).second)
throw 1;
if (version > current)
throw 1;
if (version == current &&
release_contract::str(record.object.at("policyDigest")) !=
release_contract::str(metadata.object.at("policyDigest")))
throw 1;
if (version > highest) {
highest = version;
selected = &record;
}
}
return selected;
}
static void replaceRecord(std::vector<Json> &records, const Json &record) {
const auto identity = stableIdentity(record);
records.erase(std::remove_if(records.begin(), records.end(),
[&](const Json &old) {
return stableIdentity(old) == identity;
}),
records.end());
records.push_back(record);
std::sort(records.begin(), records.end(), [](Json a, Json b) {
a.object.erase("protectedToken");
b.object.erase("protectedToken");
return release_contract::canonical(a) < release_contract::canonical(b);
});
}
#ifdef SAFETYNET_NATIVE_SELF_TEST
static bool selectionSelfTest() {
auto current =
parse("{\"deviceId\":\"synthetic\",\"origin\":\"https://"
"management.example\",\"policyDigest\":\"" +
std::string(64, 'b') + "\",\"policyKeySha256\":\"" +
std::string(64, 'a') +
"\",\"policyVersion\":8,\"sid\":\"S-1-5-21-1-2-3-1001\"}");
auto old = current;
old.object.at("policyVersion").integer = 7;
old.object.at("policyDigest").string = std::string(64, 'c');
auto oldest = old;
oldest.object.at("policyVersion").integer = 6;
std::vector<Json> records{oldest, old};
if (selectRecord(current, records) != &records[1])
return false;
auto other = old;
other.object.at("origin").string = "https://other.example";
records.push_back(other);
replaceRecord(records, current);
if (records.size() != 2 || !selectRecord(current, records))
return false;
auto fails = [&](std::vector<Json> rows) {
try {
selectRecord(current, rows);
return false;
} catch (...) {
return true;
}
};
auto conflicting = current;
conflicting.object.at("policyDigest").string = std::string(64, 'd');
auto future = current;
future.object.at("policyVersion").integer = 9;
if (!fails({old, old}) || !fails({conflicting}) || !fails({future}))
return false;
auto wrongKey = old;
wrongKey.object.at("policyKeySha256").string = std::string(64, 'e');
return !selectRecord(current, {other, wrongKey});
}
#endif
static std::string load(HKEY key) {
DWORD type = 0, size = 0;
if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type, nullptr,
&size) ||
type != REG_BINARY || !size || size > STORE_LIMIT)
throw 1;
std::string value(size, '\0');
if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type,
reinterpret_cast<BYTE *>(value.data()), &size) ||
type != REG_BINARY || size != value.size())
throw 1;
validateStore(value);
return value;
}
static std::vector<BYTE> entropy(const Json &metadata) {
const auto value =
std::string("SafetyNet management credential v1\0",
sizeof("SafetyNet management credential v1\0") - 1) +
release_contract::canonical(metadata);
auto digest = release_contract::unhex(release_contract::digest(value));
return digest;
}
static std::string protect(const std::string &token, const Json &metadata) {
if (!tokenValid(token))
throw 1;
auto bindingBytes = entropy(metadata);
Wipe plain;
plain.bytes.assign(token.begin(), token.end());
DATA_BLOB input{static_cast<DWORD>(plain.bytes.size()), plain.bytes.data()},
additional{static_cast<DWORD>(bindingBytes.size()), bindingBytes.data()},
output{};
if (!CryptProtectData(
&input, L"SafetyNet credential", &additional, nullptr, nullptr,
CRYPTPROTECT_LOCAL_MACHINE | CRYPTPROTECT_UI_FORBIDDEN, &output))
throw 1;
std::vector<BYTE> cipher(output.pbData, output.pbData + output.cbData);
LocalFree(output.pbData);
return release_contract::base64url(cipher);
}
static std::string unprotect(const std::string &cipher, const Json &metadata) {
auto encrypted = release_contract::decode(cipher),
bindingBytes = entropy(metadata);
DATA_BLOB input{static_cast<DWORD>(encrypted.size()), encrypted.data()},
additional{static_cast<DWORD>(bindingBytes.size()), bindingBytes.data()},
output{};
if (!CryptUnprotectData(&input, nullptr, &additional, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &output))
throw 1;
std::string token(reinterpret_cast<char *>(output.pbData), output.cbData);
SecureZeroMemory(output.pbData, output.cbData);
LocalFree(output.pbData);
if (!tokenValid(token)) {
SecureZeroMemory(token.data(), token.size());
throw 1;
}
return token;
}
} // namespace credential_contract
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,790 @@
// Strict bounded native signed-release verification. No signer key in
// envelopes.
#pragma once
#include <algorithm>
#include <cctype>
#include <map>
#include <set>
#include <stdexcept>
#include <wincrypt.h>
#include <ws2tcpip.h>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ws2_32.lib")
namespace release_contract {
struct Json {
enum Kind { String, Integer, Object, Array } kind;
std::string string;
ULONGLONG integer = 0;
std::map<std::string, Json> object;
std::vector<Json> array;
};
struct Parser {
const std::string &text;
size_t offset = 0, nodes = 0, stringCap = 16384;
[[noreturn]] void fail() {
throw std::runtime_error("fixed-manifest-invalid");
}
char take() {
if (offset >= text.size())
fail();
return text[offset++];
}
std::string string() {
if (take() != '"')
fail();
std::string value;
while (true) {
char c = take();
if (c == '"')
break;
if (static_cast<unsigned char>(c) < 32)
fail();
if (c == '\\') {
char escaped = take();
switch (escaped) {
case '"':
c = '"';
break;
case '\\':
c = '\\';
break;
case 'b':
c = '\b';
break;
case 'f':
c = '\f';
break;
case 'n':
c = '\n';
break;
case 'r':
c = '\r';
break;
case 't':
c = '\t';
break;
case 'u': {
unsigned code = 0;
for (int i = 0; i < 4; i++) {
char h = take();
if (h >= '0' && h <= '9')
code = code * 16 + unsigned(h - '0');
else if (h >= 'a' && h <= 'f')
code = code * 16 + unsigned(h - 'a' + 10);
else
fail();
}
if (code > 127)
fail();
c = char(code);
break;
}
default:
fail();
}
}
if (static_cast<unsigned char>(c) > 127 || value.size() >= stringCap)
fail();
value += c;
}
return value;
}
Json parse(int depth = 0) {
if (depth > 8 || ++nodes > 100000 || offset >= text.size())
fail();
Json result;
char c = text[offset];
if (c == '"') {
result.kind = Json::String;
result.string = string();
return result;
}
if (c >= '0' && c <= '9') {
result.kind = Json::Integer;
size_t start = offset;
while (offset < text.size() && text[offset] >= '0' &&
text[offset] <= '9') {
result.integer = result.integer * 10 + ULONGLONG(text[offset++] - '0');
if (result.integer > 9007199254740991ULL)
fail();
}
if (offset - start > 1 && text[start] == '0')
fail();
return result;
}
if (c == '{') {
result.kind = Json::Object;
offset++;
if (offset < text.size() && text[offset] == '}') {
offset++;
return result;
}
while (true) {
std::string key = string();
if (take() != ':' || result.object.count(key))
fail();
result.object.emplace(key, parse(depth + 1));
char delimiter = take();
if (delimiter == '}')
break;
if (delimiter != ',')
fail();
}
return result;
}
if (c == '[') {
result.kind = Json::Array;
offset++;
if (offset < text.size() && text[offset] == ']') {
offset++;
return result;
}
while (true) {
result.array.push_back(parse(depth + 1));
char delimiter = take();
if (delimiter == ']')
break;
if (delimiter != ',')
fail();
}
return result;
}
fail();
}
};
static std::string escaped(const std::string &input) {
std::string output = "\"";
for (unsigned char c : input) {
switch (c) {
case '"':
output += "\\\"";
break;
case '\\':
output += "\\\\";
break;
case '\b':
output += "\\b";
break;
case '\f':
output += "\\f";
break;
case '\n':
output += "\\n";
break;
case '\r':
output += "\\r";
break;
case '\t':
output += "\\t";
break;
default:
if (c < 32) {
char bytes[7]{};
sprintf_s(bytes, 7, "\\u%04x", unsigned(c));
output += bytes;
} else
output += char(c);
}
}
return output + '"';
}
static std::string canonical(const Json &value) {
if (value.kind == Json::String)
return escaped(value.string);
if (value.kind == Json::Integer)
return std::to_string(value.integer);
std::string result;
bool first = true;
if (value.kind == Json::Object) {
result = "{";
for (const auto &pair : value.object) {
if (!first)
result += ',';
first = false;
result += escaped(pair.first) + ':' + canonical(pair.second);
}
return result + '}';
}
result = "[";
for (const auto &item : value.array) {
if (!first)
result += ',';
first = false;
result += canonical(item);
}
return result + ']';
}
static Json json(const std::string &input, size_t maximum = 2097152,
size_t stringCap = 16384) {
if (input.empty() || input.size() > maximum)
throw std::runtime_error("fixed-manifest-invalid");
Parser parser{input, 0, 0, stringCap};
Json value = parser.parse();
if (parser.offset != input.size() || canonical(value) != input)
parser.fail();
return value;
}
static bool exact(const Json &value,
std::initializer_list<const char *> names) {
if (value.kind != Json::Object || value.object.size() != names.size())
return false;
for (auto name : names)
if (!value.object.count(name))
return false;
return true;
}
static const std::string &str(const Json &value) {
if (value.kind != Json::String)
throw std::runtime_error("fixed-manifest-invalid");
return value.string;
}
static ULONGLONG number(const Json &value) {
if (value.kind != Json::Integer || !value.integer)
throw std::runtime_error("fixed-manifest-invalid");
return value.integer;
}
static std::vector<BYTE> unhex(const std::string &input) {
if (input.empty() || input.size() % 2)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> result;
auto digit = [](char c) {
if (c >= '0' && c <= '9')
return int(c - '0');
if (c >= 'a' && c <= 'f')
return int(c - 'a' + 10);
throw std::runtime_error("fixed-manifest-invalid");
};
for (size_t i = 0; i < input.size(); i += 2)
result.push_back(BYTE(digit(input[i]) * 16 + digit(input[i + 1])));
return result;
}
static std::string digest(const std::string &text) {
BCRYPT_ALG_HANDLE alg = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
std::array<BYTE, 32> bytes{};
bool good = false;
if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) >=
0 &&
BCryptCreateHash(alg, &hash, nullptr, 0, nullptr, 0, 0) >= 0)
good = BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(text.data())),
ULONG(text.size()), 0) >= 0 &&
BCryptFinishHash(hash, bytes.data(), 32, 0) >= 0;
if (hash)
BCryptDestroyHash(hash);
if (alg)
BCryptCloseAlgorithmProvider(alg, 0);
if (!good)
throw std::runtime_error("fixed-manifest-invalid");
char output[65]{};
for (size_t i = 0; i < 32; i++)
sprintf_s(output + i * 2, 3, "%02x", bytes[i]);
return output;
}
static std::string base64url(const std::vector<BYTE> &bytes) {
DWORD size = 0;
if (!CryptBinaryToStringA(bytes.data(), DWORD(bytes.size()),
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr,
&size))
throw std::runtime_error("fixed-manifest-invalid");
std::string text(size, '\0');
if (!CryptBinaryToStringA(bytes.data(), DWORD(bytes.size()),
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
text.data(), &size))
throw std::runtime_error("fixed-manifest-invalid");
text.resize(size);
while (!text.empty() && (text.back() == '\0' || text.back() == '='))
text.pop_back();
std::replace(text.begin(), text.end(), '+', '-');
std::replace(text.begin(), text.end(), '/', '_');
return text;
}
static std::vector<BYTE> decode(const std::string &input) {
if (input.empty() || input.size() > 3000000)
throw std::runtime_error("fixed-manifest-invalid");
std::string text = input;
for (char &c : text) {
if (c == '-')
c = '+';
else if (c == '_')
c = '/';
else if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') &&
!(c >= '0' && c <= '9'))
throw std::runtime_error("fixed-manifest-invalid");
}
while (text.size() % 4)
text += '=';
DWORD size = 0;
if (!CryptStringToBinaryA(text.c_str(), DWORD(text.size()),
CRYPT_STRING_BASE64, nullptr, &size, nullptr,
nullptr) ||
size > 2097152)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> result(size);
if (!CryptStringToBinaryA(text.c_str(), DWORD(text.size()),
CRYPT_STRING_BASE64, result.data(), &size, nullptr,
nullptr) ||
base64url(result) != input)
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
static bool verify(const std::vector<BYTE> &xy, const std::string &payload,
const std::vector<BYTE> &signature,
const std::string &domain) {
if (xy.size() != 64 || signature.size() != 64)
return false;
BCRYPT_ALG_HANDLE algorithm = nullptr, sha = nullptr;
BCRYPT_KEY_HANDLE key = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
bool okay = false;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_ECDSA_P256_ALGORITHM,
nullptr, 0) < 0)
return false;
BCRYPT_ECCKEY_BLOB header{BCRYPT_ECDSA_PUBLIC_P256_MAGIC, 32};
std::vector<BYTE> blob(sizeof(header) + 64);
memcpy(blob.data(), &header, sizeof(header));
memcpy(blob.data() + sizeof(header), xy.data(), 64);
if (BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key,
blob.data(), DWORD(blob.size()), 0) >= 0 &&
BCryptOpenAlgorithmProvider(&sha, BCRYPT_SHA256_ALGORITHM, nullptr, 0) >=
0 &&
BCryptCreateHash(sha, &hash, nullptr, 0, nullptr, 0, 0) >= 0) {
std::array<BYTE, 32> digest{};
if (BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(domain.data())),
ULONG(domain.size()), 0) >= 0 &&
BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(payload.data())),
ULONG(payload.size()), 0) >= 0 &&
BCryptFinishHash(hash, digest.data(), 32, 0) >= 0)
okay = BCryptVerifySignature(key, nullptr, digest.data(), 32,
const_cast<PUCHAR>(signature.data()), 64,
0) >= 0;
}
if (hash)
BCryptDestroyHash(hash);
if (sha)
BCryptCloseAlgorithmProvider(sha, 0);
if (key)
BCryptDestroyKey(key);
BCryptCloseAlgorithmProvider(algorithm, 0);
return okay;
}
static std::string read(const std::wstring &file) {
Handle input(CreateFileW(file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT,
nullptr));
if (input.h == INVALID_HANDLE_VALUE)
throw std::runtime_error("fixed-manifest-invalid");
FILE_ATTRIBUTE_TAG_INFO info{};
LARGE_INTEGER size{};
if (!GetFileInformationByHandleEx(input, FileAttributeTagInfo, &info,
sizeof(info)) ||
(info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ||
!GetFileSizeEx(input, &size) || size.QuadPart < 1 ||
size.QuadPart > 3000000)
throw std::runtime_error("fixed-manifest-invalid");
std::string content(size_t(size.QuadPart), '\0');
DWORD bytes = 0;
if (!ReadFile(input, content.data(), DWORD(content.size()), &bytes,
nullptr) ||
bytes != content.size())
throw std::runtime_error("fixed-manifest-invalid");
return content;
}
static Json signedPayload(const std::wstring &file, const std::vector<BYTE> &xy,
const std::string &domain, size_t maximum = 2097152) {
Json envelope = json(read(file), 3000000, 3000000);
if (!exact(envelope, {"payload", "signature"}))
throw std::runtime_error("fixed-manifest-invalid");
auto bytes = decode(str(envelope.object.at("payload"))),
signature = decode(str(envelope.object.at("signature")));
if (bytes.size() > maximum)
throw std::runtime_error("fixed-manifest-invalid");
std::string payload(bytes.begin(), bytes.end());
if (!verify(xy, payload, signature, domain))
throw std::runtime_error("fixed-manifest-invalid");
return json(payload);
}
static bool uuid(const std::string &value) {
if (value.size() != 36)
return false;
for (size_t i = 0; i < value.size(); i++) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (value[i] != '-')
return false;
} else if (!((value[i] >= 'a' && value[i] <= 'f') ||
(value[i] >= '0' && value[i] <= '9')))
return false;
}
return true;
}
static bool relative(const std::string &value) {
if (value.empty() || value.size() > 240 || value.front() == '/' ||
value.back() == '/')
return false;
size_t start = 0;
for (size_t i = 0; i <= value.size(); i++) {
if (i < value.size() && value[i] != '/') {
char c = value[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-'))
return false;
continue;
}
std::string part = value.substr(start, i - start);
if (part.empty() || part == "." || part == ".." || part.back() == '.')
return false;
std::transform(part.begin(), part.end(), part.begin(),
[](unsigned char c) { return char(toupper(c)); });
part = part.substr(0, part.find('.'));
if (part == "CON" || part == "PRN" || part == "AUX" || part == "NUL" ||
(part.size() == 4 &&
(part.substr(0, 3) == "COM" || part.substr(0, 3) == "LPT") &&
part[3] >= '1' && part[3] <= '9'))
return false;
start = i + 1;
}
return true;
}
struct Release {
std::string releaseId, tenant, customerPem;
ULONGLONG sequence;
std::map<std::string, std::string> hashes;
};
static Release release(const std::wstring &root,
const std::vector<BYTE> &anchor, ULONGLONG floor,
const std::string &acceptedDigest = "") {
Json value =
signedPayload(root + L"\\release-envelope.json", anchor,
std::string("SafetyNet Windows release v1\0",
sizeof("SafetyNet Windows release v1\0") - 1));
if (!exact(value, {"format", "releaseId", "releaseSequence", "serviceName",
"files", "deploymentTenant", "deploymentPublicKey"}))
throw std::runtime_error("fixed-manifest-invalid");
if (str(value.object.at("format")) != "safetynet-windows-release-v1" ||
str(value.object.at("serviceName")) != "SafetyNetNetworkBroker" ||
!uuid(str(value.object.at("releaseId"))) ||
!uuid(str(value.object.at("deploymentTenant"))))
throw std::runtime_error("fixed-manifest-invalid");
Release result{str(value.object.at("releaseId")),
str(value.object.at("deploymentTenant")),
str(value.object.at("deploymentPublicKey")),
number(value.object.at("releaseSequence")),
{}};
if (!floor || result.sequence < floor ||
(result.sequence == floor && !acceptedDigest.empty() &&
digest(canonical(value)) != acceptedDigest))
throw std::runtime_error("fixed-manifest-invalid");
const Json &files = value.object.at("files");
if (files.kind != Json::Array || files.array.empty() ||
files.array.size() > 10000)
throw std::runtime_error("fixed-manifest-invalid");
std::string previous;
std::set<std::string> folded;
for (const auto &item : files.array) {
if (!exact(item, {"path", "bytes", "sha256"}))
throw std::runtime_error("fixed-manifest-invalid");
auto name = str(item.object.at("path")),
digest = str(item.object.at("sha256"));
if (!relative(name) || (!previous.empty() && name <= previous) ||
digest.size() != 64 || unhex(digest).size() != 32 ||
item.object.at("bytes").kind != Json::Integer)
throw std::runtime_error("fixed-manifest-invalid");
previous = name;
std::string lower = name;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return char(tolower(c)); });
if (!folded.insert(lower).second)
throw std::runtime_error("fixed-manifest-invalid");
result.hashes.emplace(name, digest);
}
for (const char *required :
{"SafetyNet.exe", "safetynet-service-host.exe", "resources/app.asar",
"resources/broker/node.exe", "resources/python/python.exe",
"resources/inspector/server.py"})
if (!result.hashes.count(required))
throw std::runtime_error("fixed-manifest-invalid");
bool semantic = false;
for (const auto &item : result.hashes) {
if (item.first.rfind("resources/semantic/", 0) == 0)
semantic = true;
if (item.first == "release-envelope.json" ||
item.first == "deployment-envelope.json")
throw std::runtime_error("fixed-manifest-invalid");
}
if (!semantic)
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
static std::vector<BYTE> pemDer(const std::string &pem, size_t pemLimit = 4096,
DWORD derLimit = 1024) {
if (pem.size() > pemLimit)
throw std::runtime_error("fixed-manifest-invalid");
DWORD size = 0;
if (!CryptStringToBinaryA(pem.c_str(), DWORD(pem.size()),
CRYPT_STRING_BASE64HEADER, nullptr, &size, nullptr,
nullptr) ||
size > derLimit)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> bytes(size);
if (!CryptStringToBinaryA(pem.c_str(), DWORD(pem.size()),
CRYPT_STRING_BASE64HEADER, bytes.data(), &size,
nullptr, nullptr))
throw std::runtime_error("fixed-manifest-invalid");
std::string encoded = base64url(bytes);
std::replace(encoded.begin(), encoded.end(), '-', '+');
std::replace(encoded.begin(), encoded.end(), '_', '/');
while (encoded.size() % 4)
encoded += '=';
std::string canonicalPem = "-----BEGIN PUBLIC KEY-----\n";
for (size_t offset = 0; offset < encoded.size(); offset += 64)
canonicalPem += encoded.substr(offset, 64) + '\n';
canonicalPem += "-----END PUBLIC KEY-----\n";
if (canonicalPem != pem)
throw std::runtime_error("fixed-manifest-invalid");
return bytes;
}
static std::vector<BYTE> customerKey(const std::string &pem) {
auto bytes = pemDer(pem),
prefix = unhex("3059301306072a8648ce3d020106082a8648ce3d03010703420004");
if (bytes.size() != 91 ||
!std::equal(prefix.begin(), prefix.end(), bytes.begin()))
throw std::runtime_error("fixed-manifest-invalid");
return std::vector<BYTE>(bytes.begin() + 27, bytes.end());
}
static bool policyKey(const std::string &pem) {
auto bytes = pemDer(pem), prefix = unhex("302a300506032b6570032100");
return bytes.size() == 44 &&
std::equal(prefix.begin(), prefix.end(), bytes.begin());
}
// SPKI metadata binding only; actual ML-DSA signatures use the existing Node
// policy channel. No unknown algorithm or opaque-key fallback is accepted.
static bool managementKey(const std::string &pem) {
auto bytes = pemDer(pem, 20000, 4096),
prefix = unhex("30820a32300b060960864801650304031303820a2100");
return bytes.size() == 2614 &&
std::equal(prefix.begin(), prefix.end(), bytes.begin());
}
static std::string ipv6Canonical(const IN6_ADDR &address) {
std::array<unsigned, 8> groups{};
for (size_t i = 0; i < 8; ++i)
groups[i] =
unsigned(address.u.Byte[i * 2]) * 256 + address.u.Byte[i * 2 + 1];
size_t best = 8, longest = 1;
for (size_t i = 0; i < 8;) {
if (groups[i]) {
++i;
continue;
}
size_t start = i;
while (i < 8 && !groups[i])
++i;
if (i - start > longest) {
best = start;
longest = i - start;
}
}
std::string output;
for (size_t i = 0; i < 8;) {
if (i == best) {
output += "::";
i += longest;
continue;
}
if (!output.empty() && output.back() != ':')
output += ':';
char hex[5]{};
sprintf_s(hex, "%x", groups[i]);
output += hex;
++i;
}
return output;
}
static bool managementOrigin(const std::string &origin) {
if (origin.size() > 2048 || origin.rfind("https://", 0) != 0)
return false;
std::string authority = origin.substr(8), host, port;
for (unsigned char c : authority)
if (c <= 32 || c >= 127 || c == '/' || c == '?' || c == '#' || c == '@' ||
c == '\\' || c == '%')
return false;
if (authority.empty())
return false;
if (authority[0] == '[') {
auto end = authority.find(']');
if (end == std::string::npos)
return false;
host = authority.substr(1, end - 1);
IN6_ADDR address{};
if (InetPtonA(AF_INET6, host.c_str(), &address) != 1 ||
ipv6Canonical(address) != host)
return false;
if (end + 1 < authority.size()) {
if (authority[end + 1] != ':')
return false;
port = authority.substr(end + 2);
if (port.empty())
return false;
}
} else {
auto colon = authority.find(':');
host = authority.substr(0, colon);
if (colon != std::string::npos) {
port = authority.substr(colon + 1);
if (port.empty())
return false;
}
if (host.empty() || host.size() > 253)
return false;
IN_ADDR address{};
if (InetPtonA(AF_INET, host.c_str(), &address) == 1) {
const BYTE *bytes = reinterpret_cast<const BYTE *>(&address);
std::string ip =
std::to_string(bytes[0]) + "." + std::to_string(bytes[1]) + "." +
std::to_string(bytes[2]) + "." + std::to_string(bytes[3]);
if (ip != host)
return false;
} else {
bool numeric = true;
for (char c : host)
if (!(c >= '0' && c <= '9') && c != '.')
numeric = false;
if (numeric)
return false;
size_t start = 0;
while (start < host.size()) {
auto end = host.find('.', start);
if (end == std::string::npos)
end = host.size();
auto label = host.substr(start, end - start);
if (label.empty() || label.size() > 63 || label.front() == '-' ||
label.back() == '-')
return false;
for (char c : label)
if (!(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '-')
return false;
if (label.rfind("0x", 0) == 0 && label.size() > 2 &&
std::all_of(label.begin() + 2, label.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
}))
return false;
if (end == host.size())
break;
start = end + 1;
if (start == host.size())
return false;
}
}
}
if (!port.empty()) {
if (port.size() > 5 || port[0] == '0')
return false;
unsigned number = 0;
for (char c : port) {
if (c < '0' || c > '9')
return false;
number = number * 10 + unsigned(c - '0');
}
if (!number || number > 65535 || number == 443)
return false;
}
return true;
}
struct Assignment {
std::string sid, deviceId, policyPem;
ULONGLONG minimumVersion, deploymentSequence;
std::string managementOrigin;
};
static Assignment deployment(const std::wstring &root, const Release &release,
const std::string &sid, ULONGLONG floor,
const std::string &acceptedDigest = "") {
Json value = signedPayload(
root + L"\\deployment-envelope.json", customerKey(release.customerPem),
std::string("SafetyNet Windows deployment v1\0",
sizeof("SafetyNet Windows deployment v1\0") - 1),
262144);
const auto format = str(value.object.at("format"));
const bool v2 = format == "safetynet-windows-deployment-v2";
if ((!v2 && format != "safetynet-windows-deployment-v1") ||
(v2 ? !exact(value, {"format", "releaseId", "deploymentTenant",
"deploymentSequence", "policyPublicKey", "expiresAt",
"users", "management"})
: !exact(value, {"format", "releaseId", "deploymentTenant",
"deploymentSequence", "policyPublicKey", "expiresAt",
"users"})) ||
str(value.object.at("releaseId")) != release.releaseId ||
str(value.object.at("deploymentTenant")) != release.tenant || !floor ||
number(value.object.at("deploymentSequence")) < floor)
throw std::runtime_error("fixed-manifest-invalid");
if (number(value.object.at("deploymentSequence")) == floor &&
!acceptedDigest.empty() && digest(canonical(value)) != acceptedDigest)
throw std::runtime_error("fixed-manifest-invalid");
if (v2) {
const auto &management = value.object.at("management");
if (!exact(management, {"origin", "signatureKey", "protocol"}) ||
!managementOrigin(str(management.object.at("origin"))) ||
str(management.object.at("protocol")) !=
"safetynet-policy-mlkem1024-mldsa87-v1" ||
!managementKey(str(management.object.at("signatureKey"))))
throw std::runtime_error("fixed-manifest-invalid");
}
FILETIME now;
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER time{};
time.LowPart = now.dwLowDateTime;
time.HighPart = now.dwHighDateTime;
ULONGLONG millis = time.QuadPart / 10000 - 11644473600000ULL;
if (number(value.object.at("expiresAt")) <= millis)
throw std::runtime_error("fixed-manifest-invalid");
const auto &users = value.object.at("users");
if (users.kind != Json::Array || users.array.empty() ||
users.array.size() > 1024)
throw std::runtime_error("fixed-manifest-invalid");
Assignment result{};
std::string previous;
std::set<std::string> devices;
for (const auto &user : users.array) {
if (!exact(user, {"sid", "deviceId", "minimumPolicyVersion"}))
throw std::runtime_error("fixed-manifest-invalid");
auto userSid = str(user.object.at("sid")),
device = str(user.object.at("deviceId"));
PSID parsed = nullptr;
if (!ConvertStringSidToSidA(userSid.c_str(), &parsed))
throw std::runtime_error("fixed-manifest-invalid");
LPSTR canonicalSid = nullptr;
bool valid = ConvertSidToStringSidA(parsed, &canonicalSid) &&
canonicalSid && userSid == canonicalSid;
if (canonicalSid)
LocalFree(canonicalSid);
LocalFree(parsed);
bool sidFamily = (userSid.rfind("S-1-5-21-", 0) == 0 ||
userSid.rfind("S-1-12-1-", 0) == 0) &&
std::count(userSid.begin(), userSid.end(), '-') == 7;
if (!valid || !sidFamily || (!previous.empty() && userSid <= previous) ||
device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])) ||
!devices.insert(device).second)
throw std::runtime_error("fixed-manifest-invalid");
for (char c : device)
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' &&
c != '-')
throw std::runtime_error("fixed-manifest-invalid");
previous = userSid;
ULONGLONG minimum = number(user.object.at("minimumPolicyVersion"));
if (userSid == sid)
result = {sid,
device,
str(value.object.at("policyPublicKey")),
minimum,
number(value.object.at("deploymentSequence")),
v2 ? str(value.object.at("management").object.at("origin"))
: ""};
}
if (result.sid.empty() || result.policyPem.empty() ||
!policyKey(result.policyPem))
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
} // namespace release_contract
+378
View File
@@ -0,0 +1,378 @@
#pragma once
// clang-format off
#include <windows.h>
#include <sddl.h>
#include <objbase.h>
#include <fwpmu.h>
// clang-format on
#include <string>
#include <vector>
#include <windows.h>
#pragma comment(lib, "fwpuclnt.lib")
#pragma comment(lib, "ole32.lib")
namespace topology_contract {
static constexpr UINT16 BROKER_PORT = 43120;
static GUID guid(const wchar_t *text) {
GUID value{};
if (FAILED(CLSIDFromString(text, &value)))
throw 1;
return value;
}
struct Memory {
void *p = nullptr;
~Memory() {
if (p)
FwpmFreeMemory0(&p);
}
};
struct Engine {
HANDLE h = nullptr;
~Engine() {
if (h)
FwpmEngineClose0(h);
}
};
struct Enumeration {
HANDLE engine, h = nullptr;
explicit Enumeration(HANDLE e) : engine(e) {}
~Enumeration() {
if (h)
FwpmFilterDestroyEnumHandle0(engine, h);
}
};
struct Result {
DWORD code = ERROR_ACCESS_DENIED;
UINT32 accessibleFilters = 0;
UINT32 ownedFilters = 0;
bool ownedVerified = false;
bool collision = false;
bool bfeRunning = false;
// Enumeration omits unreadable objects even at successful EOF. Never infer
// this from token elevation, caller JSON or the number of returned rows.
bool allObjectsReadable = false;
};
static bool blobEqual(const FWP_BYTE_BLOB *actual,
const std::vector<BYTE> &expected) {
return actual && actual->data && actual->size == expected.size() &&
memcmp(actual->data, expected.data(), expected.size()) == 0;
}
struct ExpectedCondition {
GUID field;
FWP_MATCH_TYPE match;
FWP_DATA_TYPE type;
UINT32 value;
std::vector<BYTE> bytes;
};
static bool marker(const FWP_BYTE_BLOB &value) {
static constexpr char expected[] = "SafetyNet.NetworkBoundary.v1";
return value.data && value.size == sizeof(expected) - 1 &&
memcmp(value.data, expected, sizeof(expected) - 1) == 0;
}
using SecurityQuery = DWORD(WINAPI *)(HANDLE, const GUID *,
SECURITY_INFORMATION, PSID *, PSID *,
PACL *, PACL *, PSECURITY_DESCRIPTOR *);
static bool protectedObject(HANDLE engine, const GUID &key,
SecurityQuery query) {
PSECURITY_DESCRIPTOR descriptor = nullptr;
PACL dacl = nullptr;
PSID owner = nullptr;
DWORD code = query(engine, &key,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&owner, nullptr, &dacl, nullptr, &descriptor);
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE];
DWORD ss = sizeof(system), as = sizeof(admins);
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
SECURITY_DESCRIPTOR_CONTROL control{};
DWORD revision = 0;
bool okay = !code && dacl && owner &&
(EqualSid(owner, system) || EqualSid(owner, admins)) &&
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
(control & SE_DACL_PROTECTED);
const DWORD write = FWPM_ACTRL_ADD | FWPM_ACTRL_ADD_LINK |
FWPM_ACTRL_BEGIN_WRITE_TXN | FWPM_ACTRL_WRITE | DELETE |
WRITE_DAC | WRITE_OWNER | GENERIC_ALL | GENERIC_WRITE;
if (okay)
for (DWORD i = 0; i < dacl->AceCount; ++i) {
void *raw = nullptr;
if (!GetAce(dacl, i, &raw)) {
okay = false;
break;
}
auto header = static_cast<ACE_HEADER *>(raw);
if (header->AceFlags & INHERIT_ONLY_ACE)
continue;
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
if ((ace->Mask & write) && !EqualSid(&ace->SidStart, system) &&
!EqualSid(&ace->SidStart, admins)) {
okay = false;
break;
}
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
okay = false;
break;
}
}
if (descriptor)
FwpmFreeMemory0(reinterpret_cast<void **>(&descriptor));
return okay;
}
struct Rule {
GUID key, layer;
FWP_ACTION_TYPE action;
UINT64 weight;
std::vector<ExpectedCondition> conditions;
};
static bool matches(const FWPM_FILTER0 &actual, const Rule &rule,
const GUID &provider, const GUID &sublayer) {
if (!actual.providerKey || !IsEqualGUID(*actual.providerKey, provider) ||
!IsEqualGUID(actual.subLayerKey, sublayer) ||
!IsEqualGUID(actual.layerKey, rule.layer) ||
actual.action.type != rule.action ||
actual.flags != FWPM_FILTER_FLAG_PERSISTENT ||
actual.numFilterConditions != rule.conditions.size() ||
!actual.filterCondition)
return false;
if (rule.weight) {
if (actual.weight.type != FWP_UINT64 || !actual.weight.uint64 ||
*actual.weight.uint64 != rule.weight)
return false;
} else if (actual.weight.type != FWP_EMPTY)
return false;
for (size_t i = 0; i < rule.conditions.size(); ++i) {
const auto &e = rule.conditions[i];
const auto &a = actual.filterCondition[i];
if (!IsEqualGUID(a.fieldKey, e.field) || a.matchType != e.match ||
a.conditionValue.type != e.type)
return false;
switch (e.type) {
case FWP_UINT8:
if (a.conditionValue.uint8 != e.value)
return false;
break;
case FWP_UINT16:
if (a.conditionValue.uint16 != e.value)
return false;
break;
case FWP_UINT32:
if (a.conditionValue.uint32 != e.value)
return false;
break;
case FWP_BYTE_BLOB_TYPE:
if (!blobEqual(a.conditionValue.byteBlob, e.bytes))
return false;
break;
case FWP_SECURITY_DESCRIPTOR_TYPE:
if (!blobEqual(a.conditionValue.sd, e.bytes))
return false;
break;
default:
return false;
}
}
return true;
}
static std::vector<BYTE> appId(const std::wstring &path) {
Memory memory;
FWP_BYTE_BLOB *blob = nullptr;
if (FwpmGetAppIdFromFileName0(path.c_str(), &blob))
throw 1;
memory.p = blob;
if (!blob || !blob->data || blob->size < 8 || blob->size > 65536)
throw 1;
return {blob->data, blob->data + blob->size};
}
static Result inspect(const std::wstring &root,
const std::wstring &serviceSid) {
Result result;
try {
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
if (!manager) {
result.code = GetLastError();
return result;
}
SC_HANDLE bfe = OpenServiceW(manager, L"BFE", SERVICE_QUERY_STATUS);
SERVICE_STATUS_PROCESS status{};
DWORD needed = 0;
result.bfeRunning = bfe &&
QueryServiceStatusEx(bfe, SC_STATUS_PROCESS_INFO,
reinterpret_cast<BYTE *>(&status),
sizeof(status), &needed) &&
status.dwCurrentState == SERVICE_RUNNING;
if (bfe)
CloseServiceHandle(bfe);
CloseServiceHandle(manager);
if (!result.bfeRunning)
return result;
const GUID provider = guid(L"{2a7bcbf2-e4f5-4e94-bd33-d5442a3bef50}"),
sub = guid(L"{403a69b8-fc7c-460a-a247-72e0d72e6bf8}");
const GUID c4 = FWPM_LAYER_ALE_AUTH_CONNECT_V4,
c6 = FWPM_LAYER_ALE_AUTH_CONNECT_V6,
r4 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
r6 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
const auto browser = appId(root + L"\\SafetyNet.exe"),
broker = appId(root + L"\\resources\\broker\\node.exe");
PSECURITY_DESCRIPTOR descriptor = nullptr;
ULONG size = 0;
std::wstring sddl = L"D:(A;;CC;;;" + serviceSid + L")";
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.c_str(), SDDL_REVISION_1, &descriptor, &size))
throw 1;
std::vector<BYTE> sd(reinterpret_cast<BYTE *>(descriptor),
reinterpret_cast<BYTE *>(descriptor) + size);
LocalFree(descriptor);
auto scalar = [](GUID field, FWP_MATCH_TYPE match, FWP_DATA_TYPE type,
UINT32 value) {
return ExpectedCondition{field, match, type, value, {}};
};
auto bytes = [](GUID field, FWP_DATA_TYPE type,
const std::vector<BYTE> &value) {
return ExpectedCondition{field, FWP_MATCH_EQUAL, type, 0, value};
};
const auto app = bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE,
browser),
tcp = scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_EQUAL,
FWP_UINT8, 6),
local = scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_EQUAL,
FWP_UINT32, 0x7f000001);
const std::vector<ExpectedCondition> tuple = {
tcp,
scalar(FWPM_CONDITION_IP_LOCAL_ADDRESS, FWP_MATCH_EQUAL, FWP_UINT32,
0x7f000001),
scalar(FWPM_CONDITION_IP_LOCAL_PORT, FWP_MATCH_EQUAL, FWP_UINT16,
BROKER_PORT)};
auto permit = tuple;
permit.push_back(
bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE, broker));
permit.push_back(
bytes(FWPM_CONDITION_ALE_USER_ID, FWP_SECURITY_DESCRIPTOR_TYPE, sd));
const std::vector<Rule> rules = {
{guid(L"{4ac4e1d1-f08b-50fe-83fe-1872672c42b9}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_NOT_EQUAL,
FWP_UINT8, 6)}},
{guid(L"{07130743-8828-58b5-9476-3a0c2a001796}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, tcp,
scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_NOT_EQUAL,
FWP_UINT32, 0x7f000001)}},
{guid(L"{51401be2-30ce-5808-878f-7e181d0f6fde}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, tcp, local,
scalar(FWPM_CONDITION_IP_REMOTE_PORT, FWP_MATCH_NOT_EQUAL, FWP_UINT16,
BROKER_PORT)}},
{guid(L"{1ee3c54d-84f8-5461-bd50-b74f32335ff3}"),
c6,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{a71f6123-ef64-5836-90ee-f624c6841f18}"),
r4,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{e537682c-948d-5bbf-a231-5db3ba896eda}"),
r6,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{dcbbb5f1-5936-5978-8349-21a2e791ffbf}"), r4, FWP_ACTION_PERMIT,
2, permit},
{guid(L"{7c4dba0f-904a-5685-a3ed-ae774a45136a}"), r4, FWP_ACTION_BLOCK,
1, tuple}};
Engine engine;
result.code = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, nullptr,
&engine.h);
if (result.code)
return result;
Enumeration enumeration(engine.h);
Memory providerMemory;
FWPM_PROVIDER0 *p = nullptr;
result.code = FwpmProviderGetByKey0(engine.h, &provider, &p);
providerMemory.p = p;
if (result.code)
return result;
if (p->flags != FWPM_PROVIDER_FLAG_PERSISTENT || !p->serviceName ||
wcscmp(p->serviceName, L"SafetyNetNetworkBroker") ||
!marker(p->providerData) ||
!protectedObject(engine.h, provider,
FwpmProviderGetSecurityInfoByKey0)) {
result.code = ERROR_INVALID_DATA;
return result;
}
result.code =
FwpmFilterCreateEnumHandle0(engine.h, nullptr, &enumeration.h);
if (result.code)
return result;
std::vector<GUID> seen;
for (;;) {
Memory memory;
FWPM_FILTER0 **entries = nullptr;
UINT32 count = 0;
result.code =
FwpmFilterEnum0(engine.h, enumeration.h, 128, &entries, &count);
memory.p = entries;
if (result.code)
return result;
if (count > 128 || result.accessibleFilters + count > 65536) {
result.code = ERROR_MORE_DATA;
return result;
}
result.accessibleFilters += count;
for (UINT32 i = 0; i < count; ++i) {
const auto &f = *entries[i];
if (IsEqualGUID(f.subLayerKey, sub) ||
(f.providerKey && IsEqualGUID(*f.providerKey, provider))) {
bool okay = false;
for (const auto &rule : rules)
if (IsEqualGUID(f.filterKey, rule.key) &&
matches(f, rule, provider, sub))
okay = true;
if (!okay || !protectedObject(engine.h, f.filterKey,
FwpmFilterGetSecurityInfoByKey0)) {
result.code = ERROR_INVALID_DATA;
return result;
}
seen.push_back(f.filterKey);
++result.ownedFilters;
} else if (IsEqualGUID(f.layerKey, c4) || IsEqualGUID(f.layerKey, c6) ||
IsEqualGUID(f.layerKey, r4) || IsEqualGUID(f.layerKey, r6)) {
Memory sm;
FWPM_SUBLAYER0 *s = nullptr;
result.code = FwpmSubLayerGetByKey0(engine.h, &f.subLayerKey, &s);
sm.p = s;
if (result.code)
return result;
if (s->weight == 65535)
result.collision = true;
}
}
if (count < 128)
break;
}
Memory sm;
FWPM_SUBLAYER0 *s = nullptr;
result.code = FwpmSubLayerGetByKey0(engine.h, &sub, &s);
sm.p = s;
if (result.code)
return result;
result.ownedVerified =
marker(s->providerData) &&
protectedObject(engine.h, sub, FwpmSubLayerGetSecurityInfoByKey0) &&
result.ownedFilters == 8 && !result.collision && s->weight == 65535 &&
s->providerKey && IsEqualGUID(*s->providerKey, provider) &&
s->flags == FWPM_SUBLAYER_FLAG_PERSISTENT;
result.code = result.ownedVerified ? ERROR_SUCCESS : ERROR_INVALID_DATA;
} catch (...) {
result.code = ERROR_INVALID_DATA;
}
return result;
}
} // namespace topology_contract
+38
View File
@@ -0,0 +1,38 @@
'use strict';
const {createPublicKey,createHash,randomBytes}=require('node:crypto');
const {assertPolicyAssignment}=require('../browser/policy-assignment.cjs');
class PolicyAuthorityError extends Error {
constructor(code){super(code);this.name='PolicyAuthorityError';this.code=code;}
}
function freeze(value){if(value&&typeof value==='object'){for(const item of Object.values(value))freeze(item);Object.freeze(value);}return value;}
/** Trusted bootstrap pins enrollment key/device/minimum version. No policy
* enabling, network requests or fixture classifiers are performed here.
* Persist the accepted minimum version in protected enrollment storage when
* wiring production; this instance cannot supply durable anti-rollback alone.
*/
async function createPolicyAuthority({publicKey,deviceId,minimumVersion=0}) {
let pinned;
try {
if(typeof publicKey!=='string'||publicKey.length>16384||typeof deviceId!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(deviceId)||!Number.isSafeInteger(minimumVersion)||minimumVersion<0)throw Error('invalid');
pinned=createPublicKey(publicKey).export({type:'spki',format:'pem'});
} catch {throw new PolicyAuthorityError('policy-bootstrap-invalid');}
const {verifyPolicy}=await import('../../packages/core/policy.mjs');
const namespace=randomBytes(16).toString('hex');
let generation=0,active=null,floor=minimumVersion,lastDigest=null,lastVersion=null;
const revoke=()=>{generation++;active=null;};
const current=token=>typeof token==='string'&&active!==null&&token===active.token&&Date.parse(active.policy.expiresAt)>Date.now();
function accept(envelope) {
try {
if(!envelope||typeof envelope!=='object'||Array.isArray(envelope)||Object.keys(envelope).sort().join(',')!=='payload,signature'||typeof envelope.payload!=='string'||!envelope.payload||envelope.payload.length>2097152||!/^[A-Za-z0-9_-]+$/.test(envelope.payload)||typeof envelope.signature!=='string'||!envelope.signature||envelope.signature.length>16384||!/^[A-Za-z0-9_-]+$/.test(envelope.signature))throw Error('invalid-envelope');
const policy=assertPolicyAssignment(verifyPolicy({payload:envelope.payload,signature:envelope.signature},pinned,floor),deviceId);
const digest=createHash('sha256').update(envelope.payload).digest('hex');
if(lastVersion===policy.version&&lastDigest!==digest)throw Error('version-equivocation');
generation++;floor=policy.version;lastVersion=policy.version;lastDigest=digest;
active=Object.freeze({token:`${namespace}:${generation}`,generation,version:policy.version,policy:freeze(policy)});
return active;
} catch {revoke();throw new PolicyAuthorityError('policy-rejected');}
}
return Object.freeze({accept,current,revoke,snapshot:()=>active&&current(active.token)?active:null,status:()=>Object.freeze({generation,minimumVersion:floor,active:active!==null&&current(active.token)})});
}
module.exports={createPolicyAuthority,PolicyAuthorityError};
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/** Inherited private parent IPC only. The reviewed launcher supplies a fixed
* factory in code; IPC cannot select modules, executable paths or commands.
* This supplies lifecycle isolation, not a packaged/WFP-constrained broker.
*/
function runBrokerProcess(factory,{maxMessageBytes=65536}={}) {
if(typeof factory!=='function'||typeof process.send!=='function'||!process.connected||!Number.isSafeInteger(maxMessageBytes)||maxMessageBytes<1||maxMessageBytes>65536)throw Error('broker-process-invalid');
let state='waiting',broker,closing;
const send=value=>{if(process.connected)process.send(value,()=>{});};
const close=()=>{
if(closing)return closing;
state='closing';
closing=Promise.resolve().then(()=>broker?.close()).catch(()=>{}).finally(()=>{if(process.connected)process.disconnect();});
return closing;
};
process.once('disconnect',()=>{void close();});
process.on('message',message=>{
let serialized;
try {serialized=JSON.stringify(message);} catch {serialized='';}
if(!serialized||Buffer.byteLength(serialized)>maxMessageBytes||!message||typeof message!=='object'||Array.isArray(message)){send({type:'error',code:'invalid-message'});void close();return;}
if(message.type==='stop'&&Object.keys(message).length===1){void close();return;}
if(state!=='waiting'||message.type!=='start'||Object.keys(message).sort().join(',')!=='config,type'||!message.config||typeof message.config!=='object'||Array.isArray(message.config)){send({type:'error',code:'invalid-message'});void close();return;}
state='starting';
Promise.resolve().then(()=>factory(message.config)).then(async created=>{
if(!created||typeof created.listen!=='function'||typeof created.close!=='function'||typeof created.capability!=='string'||!/^[a-zA-Z0-9_-]{43}$/.test(created.capability)||!['basic','bearer'].includes(created.authMode))throw Error('invalid-factory');
if(created.authMode==='basic'&&(!created.credentials||!/^safetynet-[a-f0-9]{32}$/.test(created.credentials.username)||created.credentials.password!==created.capability))throw Error('invalid-factory');
broker=created;
if(state==='closing'){await broker.close();return;}
const port=await broker.listen();
if(state==='closing'){await broker.close();return;}
if(!Number.isSafeInteger(port)||port<1||port>65535)throw Error('invalid-listener');
state='ready';send({type:'ready',port,authMode:broker.authMode,capability:broker.capability,...(broker.authMode==='basic'?{credentials:{username:broker.credentials.username,password:broker.credentials.password}}:{})});
}).catch(()=>{send({type:'error',code:'startup-failed'});void close();});
});
return Object.freeze({close});
}
module.exports={runBrokerProcess};
+63
View File
@@ -0,0 +1,63 @@
'use strict';
const {randomUUID}=require('node:crypto');
const {BrokerAuditEventSchema}=require('../../packages/core/broker-audit.cjs');
const {createProductionBroker}=require('./production-factory.cjs');
const UUID=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
const exact=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).sort().join(',')===keys.sort().join(',');
/** Inherited IPC is the only bootstrap/control surface. No CLI, environment
* credential, public management listener, dynamic module or private key IPC.
*/
function runProductionProcess(factory=createProductionBroker,channel=process){
if(typeof factory!=='function'||typeof channel.send!=='function'||!channel.connected)throw Error('broker-process-invalid');
let state='waiting',context,broker,construction,initialization,closing,cleanup,sequence=0;const pending=new Map();
const closeBroker=created=>{if(!cleanup){cleanup=Promise.resolve().then(()=>created.close());cleanup.catch(()=>{});}return cleanup;};
const close=()=>{
if(closing)return closing;state='closing';for(const value of pending.values()){clearTimeout(value.timer);value.reject(Error('broker-parent-unavailable'));}pending.clear();
closing=Promise.resolve().then(async()=>{
// Construction and listen retain ownership until their ORIGINAL promises
// settle. A late factory result cannot become ready or escape cleanup.
let created=broker;try{created=created||await construction;}catch{}
try{if(created)await closeBroker(created);}finally{await initialization?.catch(()=>{});}
}).finally(()=>{if(channel.connected)channel.disconnect();});
// Event-driven callers may discard the promise; explicit close callers still
// receive the real sticky cleanup failure, never a fabricated confirmation.
closing.catch(()=>{});return closing;
};
const send=message=>{if(!channel.connected||state==='closing')throw Error('broker-parent-unavailable');channel.send(message,error=>{if(error)void close();});};
const rpc=(type,payload)=>new Promise((resolve,reject)=>{
if(pending.size>=8||state==='closing'||!context){reject(Error('broker-parent-unavailable'));return;}
if(sequence>=Number.MAX_SAFE_INTEGER){reject(Error('broker-parent-unavailable'));void close();return;}
const nextSequence=++sequence,requestId=randomUUID(),timer=setTimeout(()=>{void close();},30000);
pending.set(requestId,{type,sequence:nextSequence,eventId:payload.event?.eventId,resolve,reject,timer});
try{send({type,...context,requestId,sequence:nextSequence,...payload});}catch{void close();}
});
channel.once('disconnect',()=>{void close();});
channel.on('message',message=>{
try{
const bytes=Buffer.byteLength(JSON.stringify(message));if(!message||bytes>(state==='waiting'?2097152:65536))throw Error();
if(state==='waiting'){
if(!exact(message,['type','version','generation','policyVersion','config'])||message.type!=='start'||message.version!==1||!UUID.test(message.generation)||!Number.isSafeInteger(message.policyVersion)||message.policyVersion<1)throw Error();
context={version:1,generation:message.generation,policyVersion:message.policyVersion};state='starting';
construction=Promise.resolve().then(()=>factory(message.config,{expectedPolicyVersion:context.policyVersion,persistAudit:async event=>{
BrokerAuditEventSchema.parse(event);if(event.policyVersion!==context.policyVersion)throw Error('broker-parent-unavailable');
return rpc('audit',{event});
},acknowledgeCertificate:async({authority,cert})=>rpc('certificate',{authority,cert})}));
initialization=construction.then(async created=>{
broker=created;if(state==='closing')return;
const port=await broker.listen();if(state==='closing')return;
if(created.authMode!=='basic'||port!==message.config.brokerPort||!/^safetynet-[a-f0-9]{32}$/.test(created.credentials?.username)||!/^[A-Za-z0-9_-]{43}$/.test(created.credentials?.password))throw Error();
state='ready';send({type:'ready',...context,sequence:0,authMode:'basic',port,credentials:{...created.credentials}});
}).catch(error=>{const codes=new Set(['broker-bootstrap-invalid','broker-route-unsupported','broker-startup-inspector','broker-startup-certificate','broker-startup-transport','broker-startup-managed']);const code=codes.has(error?.message)?error.message:'broker-startup-failed';try{send({type:'startup-fault',...context,code});}catch{}void close();});return;
}
if(message.version!==1||message.generation!==context.generation||message.policyVersion!==context.policyVersion)throw Error();
if(message.type==='stop'&&exact(message,['type','version','generation','policyVersion'])){void close();return;}
const value=pending.get(message.requestId);if(!value||message.sequence!==value.sequence)throw Error();
if(value.type==='audit'&&exact(message,['type','version','generation','policyVersion','requestId','sequence','receipt'])&&message.type==='audit-receipt'&&exact(message.receipt,['persisted','eventId'])&&message.receipt.persisted===true&&message.receipt.eventId===value.eventId){clearTimeout(value.timer);pending.delete(message.requestId);value.resolve(Object.freeze({...message.receipt}));return;}
if(value.type==='certificate'&&exact(message,['type','version','generation','policyVersion','requestId','sequence','accepted'])&&message.type==='certificate-receipt'&&message.accepted===true){clearTimeout(value.timer);pending.delete(message.requestId);value.resolve(true);return;}
throw Error();
}catch{void close();}
});
return Object.freeze({close});
}
if(require.main===module)runProductionProcess();
module.exports={runProductionProcess};
@@ -0,0 +1,62 @@
'use strict';
const path=require('node:path');
const {createDnsPolicyRoute,assertSupportedDns}=require('./dns-policy-route.cjs');
const tls=require('node:tls');
const {X509Certificate}=require('node:crypto');
const {z}=require('zod');
const {createPolicyAuthority}=require('./policy-authority.cjs');
const {createManagedProxy}=require('./managed-proxy.cjs');
const {createSessionCertificates}=require('./tls-certificates.cjs');
const {createProxyForwarder,parseFixedProxy}=require('./proxy-forwarder.cjs');
const {createHttpsForwarder}=require('./https-forwarder.cjs');
const {createDownloadResponseGate}=require('./download-response-gate.cjs');
const {startInspector}=require('../browser/inspection.cjs');
const {scanEnterprise}=require('../browser/enterprise-inspection.cjs');
const networkBase={dns:z.literal('system'),ca:z.string().min(1).max(131072).optional()};
const NetworkSchema=z.discriminatedUnion('mode',[z.object({mode:z.literal('direct'),...networkBase}).strict(),z.object({mode:z.literal('fixed_servers'),...networkBase,proxyCa:z.string().min(1).max(131072).optional(),credentials:z.object({username:z.string().min(1).max(1024).refine(value=>!value.includes(':')&&![...value].some(character=>character.charCodeAt(0)<32||character.charCodeAt(0)===127)),password:z.string().min(1).max(3072).refine(value=>![...value].some(character=>character.charCodeAt(0)<32||character.charCodeAt(0)===127))}).strict().optional()}).strict()]);
const BootstrapSchema=z.object({publicKey:z.string().min(1).max(16384),deviceId:z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/),minimumVersion:z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),envelope:z.object({payload:z.string().min(1).max(2097152),signature:z.string().min(1).max(16384)}).strict(),brokerPort:z.number().int().min(1).max(65535),network:NetworkSchema}).strict();
function deploymentRoots(pem){
if(pem===undefined)return undefined;
const matches=pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
if(!matches||matches.length>32||pem.replace(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g,'').trim())throw Error('broker-bootstrap-invalid');
for(const item of matches){let certificate;try{certificate=new X509Certificate(item);}catch{throw Error('broker-bootstrap-invalid');}if(!certificate.ca||Date.parse(certificate.validFrom)>Date.now()||Date.parse(certificate.validTo)<=Date.now())throw Error('broker-bootstrap-invalid');}
return Object.freeze([...tls.rootCertificates,...matches]);
}
/** Real packaged-resource factory. Parent verifies its signed package before
* spawn; IPC cannot choose modules, resource paths, keys or scanner callbacks.
* Direct or one signed fixed HTTP(S) proxy only; PAC/system/bypass fail closed.
*/
async function createProductionBroker(config,{persistAudit,acknowledgeCertificate,expectedPolicyVersion}={}){
let c;try{c=BootstrapSchema.parse(config);}catch{throw Error('broker-bootstrap-invalid');}
if(typeof persistAudit!=='function'||typeof acknowledgeCertificate!=='function')throw Error('broker-bootstrap-invalid');
const ca=deploymentRoots(c.network.ca),proxyCa=deploymentRoots(c.network.proxyCa);
const brokerRoot=path.resolve(__dirname,'../..'),scannerResourcesRoot=path.dirname(brokerRoot);
const authority=await createPolicyAuthority({publicKey:c.publicKey,deviceId:c.deviceId,minimumVersion:c.minimumVersion});
const snapshot=authority.accept(c.envelope),policy=snapshot.policy;
if(expectedPolicyVersion!==policy.version)throw Error('broker-bootstrap-invalid');
if(policy.proxy.mode!==c.network.mode)throw Error('broker-route-unsupported');
assertSupportedDns(policy,c.network,{numericOriginSupported:true});
if(c.network.mode==='fixed_servers'){try{parseFixedProxy(policy.proxy);}catch{throw Error('broker-route-unsupported');}}
const core=await import('../../packages/core/policy.mjs'),securityCore=await import('../../packages/core/security.mjs');
const current=token=>authority.current(token),active=()=>current(snapshot.token);
const approved=binding=>active()&&core.destinationAllowed(`https://${binding.authority}/`,policy);
let inspector,issuer,proxy,closed=false,stage='inspector';
const close=async()=>{if(closed)return;closed=true;authority.revoke();try{await proxy?.close();}finally{issuer?.close();inspector?.stop();}};
try{
inspector=startInspector(scannerResourcesRoot,true);if(!inspector)throw Error('broker-inspector-unavailable');await inspector.waitReady();
stage='certificate';issuer=await createSessionCertificates({current:active,authorized:approved});
const maxResponseBytes=Math.min(policy.fileTransfers.download.maxBytes,16777216);
stage='transport';
const resolve=createDnsPolicyRoute({current:active,blocked:address=>securityCore.dnsAddressBlocked(address,policy)});
const transport={authority:snapshot.token,current,ca:ca?.join('\n'),maxRequestBytes:policy.dlp.maxBytes,maxResponseBytes,deadlineMs:30000};
const resolveOriginRoute=async route=>{if(!core.destinationAllowed(`https://${route.hostname.includes(':')?'['+route.hostname+']':route.hostname}:${route.port}/`,policy))throw Error('broker-route-denied');return resolve(route);};
const rawForward=c.network.mode==='fixed_servers'?createProxyForwarder({...transport,proxyPolicy:policy.proxy,proxyCa:proxyCa?.join('\n'),credentials:c.network.credentials,resolveProxyRoute:resolve,resolveOriginRoute:policy.security?.dns?.enabled!==false&&policy.security?.dns?.blockedIPs?.length?resolveOriginRoute:undefined}):createHttpsForwarder({...transport,resolveRoute:resolveOriginRoute});
const forward=createDownloadResponseGate({forward:rawForward,policy,current:active,inspector,maxBytes:maxResponseBytes});
stage='managed';proxy=await createManagedProxy({deadlineMs:30000,policyAuthority:authority,snapshot,persistAudit,localInspector:(bytes,p)=>inspector.inspect(bytes,p),enterpriseInspector:(bytes,p)=>scanEnterprise(bytes,p,'upload'),forward,resolveCertificate:async binding=>{
if(!approved(binding))throw Error('broker-certificate-denied');
const leaf=issuer.resolve(binding);if(await acknowledgeCertificate({authority:leaf.authority,cert:leaf.cert})!==true||!active())throw Error('broker-certificate-denied');return leaf;
}});
return Object.freeze({authMode:proxy.authMode,credentials:proxy.credentials,capability:proxy.capability,listen:()=>proxy.listen(c.brokerPort),close});
}catch{await close();throw Error('broker-startup-'+stage); }
}
module.exports={createProductionBroker,BootstrapSchema,deploymentRoots};
+78
View File
@@ -0,0 +1,78 @@
'use strict';
const net=require('node:net'),tls=require('node:tls'),http=require('node:http');
const {parseAuthority}=require('./tls-authority.cjs');
const {validateResponse}=require('./http-response-gate.cjs');
const {GateError}=require('./http-request-gate.cjs');
const fail=code=>{throw new GateError(code);};
// Deliberately narrow signed subset: one HTTP(S) proxy for all HTTPS origins.
// PAC, system, SOCKS, fallback lists and bypass syntax are not interpreted.
function parseFixedProxy(policy){
if(!policy||policy.mode!=='fixed_servers'||typeof policy.rules!=='string'||policy.bypass!=='')fail('proxy-policy-unsupported');
const match=/^(https?):\/\/([^/?#]+)$/.exec(policy.rules);if(!match)fail('proxy-policy-unsupported');
let binding;try{binding=parseAuthority(match[2]);if(!/:[0-9]+$/.test(match[2])&&match[1]==='http')binding=parseAuthority(match[2]+':80');}catch{fail('proxy-policy-unsupported');}
return Object.freeze({protocol:match[1],...binding});
}
function createProxyForwarder({authority,current,proxyPolicy,resolveProxyRoute,resolveOriginRoute,credentials,ca,proxyCa,deadlineMs=5000,maxRequestBytes=16777216,maxResponseBytes=1048576}={}){
const proxy=parseFixedProxy(proxyPolicy);
if(typeof authority!=='string'||typeof current!=='function'||typeof resolveProxyRoute!=='function'||!Number.isInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000)fail('invalid-forward-configuration');
if(resolveOriginRoute!==undefined&&typeof resolveOriginRoute!=='function')fail('invalid-forward-configuration');
if(![maxRequestBytes,maxResponseBytes].every(n=>Number.isSafeInteger(n)&&n>=0&&n<=16777216))fail('invalid-forward-configuration');
if([ca,proxyCa].some(value=>value!==undefined&&!Buffer.isBuffer(value)&&typeof value!=='string'))fail('invalid-forward-configuration');
ca=Buffer.isBuffer(ca)?Buffer.from(ca):ca;proxyCa=Buffer.isBuffer(proxyCa)?Buffer.from(proxyCa):proxyCa;
let authorization;
if(credentials){if(Object.keys(credentials).sort().join(',')!=='password,username'||typeof credentials.username!=='string'||typeof credentials.password!=='string'||credentials.username.includes(':')||/[\r\n\0]/.test(credentials.username+credentials.password)||credentials.username.length+credentials.password.length>4096)fail('invalid-forward-configuration');authorization='Basic '+Buffer.from(credentials.username+':'+credentials.password).toString('base64');}
const active=()=>{try{return current(authority)===true;}catch{return false;}};
let pending=0;
return async function forward({request,body,signal,maxResponseBytes:limit=maxResponseBytes}){
if(pending>=8)fail('proxy-capacity-exhausted');pending++;
let raw,secure,outgoing,agent,timer,abort,heldResolution=false;const controller=new AbortController();
const privateBody=Buffer.isBuffer(body)&&body.length<=maxRequestBytes?Buffer.from(body):undefined;
try{
if(!privateBody||privateBody.length>maxRequestBytes||!Number.isSafeInteger(limit)||limit<0||limit>maxResponseBytes||!request||!['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS'].includes(request.method))fail('invalid-forward-request');
const url=new URL(request.url);if(url.protocol!=='https:'||url.username||url.password||url.hash||[...request.url].some(c=>c.charCodeAt(0)<=32||c.charCodeAt(0)===127||c==='\\'))fail('invalid-forward-request');
const destination=parseAuthority(url.host),headers=Object.create(null);let headerBytes=0;
for(const [name,value] of Object.entries(request.headers||{})){
if(!/^[a-z0-9-]+$/i.test(name)||typeof value!=='string'||[...value].some(c=>{const code=c.charCodeAt(0);return code!==9&&(code<32||code===127||code>255);})||headers[name.toLowerCase()]!==undefined)fail('invalid-forward-request');
if(name.toLowerCase()==='connection'&&!['close','keep-alive'].includes(value.toLowerCase()))fail('invalid-forward-request');
headerBytes+=name.length+value.length;if(headerBytes>32768)fail('invalid-forward-request');
if(!['host','content-length','connection','proxy-connection','proxy-authorization','proxy-authenticate','keep-alive','te','trailer','transfer-encoding','upgrade'].includes(name.toLowerCase()))headers[name.toLowerCase()]=value;
}
headers.host=url.host;headers.connection='close';headers['content-length']=String(privateBody.length);headers['accept-encoding']='identity';
let rejectAbort;const cancelled=new Promise((_,reject)=>{rejectAbort=reject;});cancelled.catch(()=>{});
abort=()=>{controller.abort();raw?.destroy();secure?.destroy();outgoing?.destroy();rejectAbort(new GateError('upstream-cancelled'));};
signal?.addEventListener('abort',abort,{once:true});timer=setTimeout(abort,deadlineMs);
const guarded=promise=>Promise.race([promise,cancelled]);
if(signal?.aborted)fail('upstream-cancelled');if(!active())fail('authority-expired');
// Resolve the origin first when IP policy is active. Numeric CONNECT binds
// this exact approval to the proxy tunnel; original hostname remains TLS/SNI.
// Each outstanding original resolver retains admission after outer timeout.
const resolveBound=async(resolver,binding)=>{const work=Promise.resolve().then(()=>resolver(Object.freeze({hostname:binding.hostname,port:binding.port,signal:controller.signal})));let value;try{value=await guarded(work);}catch(error){heldResolution=true;work.finally(()=>{pending--;}).catch(()=>{});throw error;}if(controller.signal.aborted)fail('upstream-cancelled');if(!active())fail('authority-expired');return Object.freeze({approved:value?.approved,address:value?.address,port:value?.port});};
let connectAuthority=destination.authority;
if(resolveOriginRoute){const origin=await resolveBound(resolveOriginRoute,destination);if(origin.approved!==true||!net.isIP(origin.address)||origin.port!==destination.port)fail('proxy-route-unapproved');connectAuthority=parseAuthority(`${net.isIP(origin.address)===6?'['+origin.address+']':origin.address}:${origin.port}`).authority;}
const route=await resolveBound(resolveProxyRoute,proxy);
if(route.approved!==true||!net.isIP(route.address)||route.port!==proxy.port)fail('proxy-route-unapproved');if(!active())fail('authority-expired');
raw=proxy.protocol==='https'?tls.connect({host:route.address,port:route.port,servername:net.isIP(proxy.hostname)?undefined:proxy.hostname,ca:proxyCa,rejectUnauthorized:true,checkServerIdentity:(_,cert)=>tls.checkServerIdentity(proxy.hostname,cert)}):net.connect({host:route.address,port:route.port});
await guarded(new Promise((resolve,reject)=>{raw.once(proxy.protocol==='https'?'secureConnect':'connect',resolve);raw.once('error',()=>reject(new GateError('proxy-connect-failed')));}));
if(!active())fail('authority-expired');
await guarded(new Promise((resolve,reject)=>{
let bytes=Buffer.alloc(0);const data=chunk=>{if(chunk.length>32768-bytes.length){cleanup();reject(new GateError('proxy-response-invalid'));return;}bytes=Buffer.concat([bytes,chunk]);const end=bytes.indexOf('\r\n\r\n');if(end<0)return;cleanup();if(end+4!==bytes.length||!/^HTTP\/1\.[01] 200(?: [^\r\n]*)?\r\n/.test(bytes.toString('latin1'))){reject(new GateError('proxy-response-invalid'));return;}resolve();};
const error=()=>{cleanup();reject(new GateError('proxy-connect-failed'));};const cleanup=()=>{raw.off('data',data);raw.off('error',error);raw.off('end',error);};raw.on('data',data);raw.once('error',error);raw.once('end',error);
raw.write(`CONNECT ${connectAuthority} HTTP/1.1\r\nHost: ${connectAuthority}\r\n${authorization?'Proxy-Authorization: '+authorization+'\r\n':''}Connection: keep-alive\r\n\r\n`);
}));
if(!active())fail('authority-expired');
secure=tls.connect({socket:raw,servername:net.isIP(destination.hostname)?undefined:destination.hostname,ca,rejectUnauthorized:true,checkServerIdentity:(_,cert)=>tls.checkServerIdentity(destination.hostname,cert)});
await guarded(new Promise((resolve,reject)=>{secure.once('secureConnect',resolve);secure.once('error',()=>reject(new GateError('upstream-failed')));}));
if(!active())fail('authority-expired');
agent=new http.Agent({keepAlive:false});agent.createConnection=()=>secure;
const result=await guarded(new Promise((resolve,reject)=>{
outgoing=http.request({host:destination.hostname,port:destination.port,path:url.pathname+url.search,method:request.method,headers,agent,maxHeaderSize:32768},incoming=>{
const chunks=[];let bytes=0;const wipe=()=>{chunks.forEach(chunk=>chunk.fill(0));chunks.length=0;};incoming.once('close',wipe);incoming.on('data',chunk=>{bytes+=chunk.length;if(bytes>limit){incoming.destroy();reject(new GateError('upstream-response-too-large'));return;}chunks.push(Buffer.from(chunk));});
incoming.once('error',()=>reject(new GateError('upstream-failed')));incoming.once('aborted',()=>reject(new GateError('upstream-failed')));
incoming.once('end',()=>{try{if(!incoming.complete)fail('upstream-failed');resolve(validateResponse({statusCode:incoming.statusCode,headers:incoming.headers,body:Buffer.concat(chunks)},limit));}catch(error){reject(error);}finally{wipe();}});
});outgoing.once('error',()=>reject(new GateError('upstream-failed')));if(!active()){reject(new GateError('authority-expired'));return;}outgoing.end(privateBody);
}));if(!active())fail('authority-expired');return result;
}catch(error){if(error instanceof GateError)throw error;throw new GateError('upstream-failed');}
finally{clearTimeout(timer);signal?.removeEventListener('abort',abort);outgoing?.destroy();secure?.destroy();raw?.destroy();agent?.destroy();privateBody?.fill(0);if(!heldResolution)pending--;}
};
}
module.exports={parseFixedProxy,createProxyForwarder};
@@ -0,0 +1,48 @@
'use strict';
const {z}=require('zod');
const {createHash,createPublicKey}=require('node:crypto');
const {verifyReleaseEnvelope,verifyDeploymentEnvelope}=require('../../packages/core/windows-trust.cjs');
const {createPolicyAuthority}=require('./policy-authority.cjs');
const digest=z.string().regex(/^[a-f0-9]{64}$/),positive=z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
const sid=z.string().regex(/^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/).refine(value=>value.split('-').slice(4).every(n=>Number(n)<=4294967295));
const device=z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/);
const floor=z.object({minimumSequence:positive,acceptedDigest:digest}).strict();
const policyFloor=z.object({sid,deviceId:device,policyKeySha256:digest,minimumVersion:positive,acceptedDigest:digest}).strict();
const StateSchema=z.object({format:z.literal('safetynet-service-floors-v1'),release:floor,deployment:floor.extend({tenant:z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)}),policies:z.array(policyFloor).max(8192)}).strict();
const identity=value=>`${value.sid}|${value.deviceId}|${value.policyKeySha256}`;
// Reserve room for the fixed snapshot/CAS envelope inside its 2 MiB frame.
const MAX_STATE_BYTES=2097152-1024;
function freeze(value){if(value&&typeof value==='object'){for(const child of Object.values(value))freeze(child);Object.freeze(value);}return value;}
function canonical(value){if(Array.isArray(value))return '['+value.map(canonical).join(',')+']';if(value&&typeof value==='object')return '{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}';return JSON.stringify(value);}
function encodeAcceptanceState(value){
const state=StateSchema.parse(value),ids=state.policies.map(identity);
if(ids.some((id,index)=>index>0&&id<=ids[index-1]))throw Error('broker-bootstrap-invalid');
const bytes=Buffer.from(canonical(state));if(bytes.length>MAX_STATE_BYTES)throw Error('broker-bootstrap-invalid');return bytes;
}
function acceptanceStateDigest(value){return createHash('sha256').update(encodeAcceptanceState(value)).digest('hex');}
// Pure verification/transition. The fixed native store must supply this state
// and commit nextState durably BEFORE listen. Returning a plan is not persistence.
async function verifyServiceAcceptance(input){
let authority,stage='state';
try{
const source=structuredClone(input),state=StateSchema.parse(source.state);
const previousStateDigest=acceptanceStateDigest(state);
stage='release';const release=verifyReleaseEnvelope(source.releaseEnvelope,{publicKey:source.releaseAnchor,floor:state.release});
if(release.manifest.deploymentTenant!==state.deployment.tenant)throw Error();
stage='deployment';const deployment=verifyDeploymentEnvelope(source.deploymentEnvelope,{release,floor:state.deployment,authenticatedSid:source.authenticatedSid});
const binding=deployment.bootstrap,keySha=createHash('sha256').update(createPublicKey(binding.publicKey).export({type:'spki',format:'der'})).digest('hex');
const recordId={sid:source.authenticatedSid,deviceId:binding.deviceId,policyKeySha256:keySha};
const previous=state.policies.find(value=>identity(value)===identity(recordId));
const minimumVersion=Math.max(binding.minimumVersion,previous?.minimumVersion||0);
stage='policy';authority=await createPolicyAuthority({...binding,minimumVersion});const snapshot=authority.accept(source.policyEnvelope);
if(snapshot.policy.assignment?.deviceId!==binding.deviceId)throw Error();
const acceptedDigest=createHash('sha256').update(source.policyEnvelope.payload).digest('hex');
if(previous?.minimumVersion===snapshot.version&&previous.acceptedDigest!==acceptedDigest)throw Error();
stage='transition';const policies=state.policies.filter(value=>identity(value)!==identity(recordId));
policies.push({...recordId,minimumVersion:snapshot.version,acceptedDigest});policies.sort((a,b)=>identity(a)<identity(b)?-1:identity(a)>identity(b)?1:0);
const nextState=StateSchema.parse({format:state.format,release:{minimumSequence:release.manifest.releaseSequence,acceptedDigest:release.digest},deployment:{tenant:state.deployment.tenant,minimumSequence:deployment.manifest.deploymentSequence,acceptedDigest:deployment.digest},policies});
const nextStateDigest=acceptanceStateDigest(nextState);
return freeze({deployment,bootstrap:{...binding,minimumVersion:snapshot.version},policyVersion:snapshot.version,policyDigest:acceptedDigest,previousStateDigest,nextStateDigest,nextState});
}catch{throw Object.assign(Error('broker-bootstrap-invalid'),{failureStage:stage});}finally{authority?.revoke();}
}
module.exports={verifyServiceAcceptance,StateSchema,encodeAcceptanceState,acceptanceStateDigest,MAX_STATE_BYTES};
+37
View File
@@ -0,0 +1,37 @@
'use strict';
const {BootstrapSchema}=require('./production-factory.cjs');
const {createHash}=require('node:crypto');
// Matches the installed native service's fixed receiver/WFP contract. Caller
// bootstrap JSON cannot choose a port outside that protected boundary.
const SERVICE_BROKER_PORT=43120;
function freeze(value){if(value&&typeof value==='object'){for(const child of Object.values(value))freeze(child);Object.freeze(value);}return value;}
// loadTrust is fixed privileged code: verify protected release/deployment files,
// rollback state and native-authenticated SID before returning its bootstrap.
// No IPC field may select this callback, trust paths, keys or SID.
function createServiceFactory({loadTrust,factory}={}){
if(typeof loadTrust!=='function'||typeof factory!=='function')throw Error('broker-bootstrap-invalid');
return async(config,callbacks)=>{
let parsed,binding,expiresAt;
try{
parsed=freeze(BootstrapSchema.parse(config));
if(parsed.brokerPort!==SERVICE_BROKER_PORT)throw Error();
const verified=await loadTrust(parsed);binding=verified?.bootstrap;expiresAt=verified?.deployment?.manifest?.expiresAt;
if(!Number.isSafeInteger(expiresAt)||expiresAt<=Date.now())throw Error();
if(!Number.isSafeInteger(verified?.policyVersion)||verified.policyVersion<1||verified.policyVersion!==callbacks?.expectedPolicyVersion||typeof verified.policyDigest!=='string'||!/^[a-f0-9]{64}$/.test(verified.policyDigest)||verified.policyDigest!==createHash('sha256').update(parsed.envelope.payload).digest('hex'))throw Error();
if(!binding||typeof binding.publicKey!=='string'||typeof binding.deviceId!=='string'||!Number.isSafeInteger(binding.minimumVersion)||binding.minimumVersion<1||
parsed.publicKey!==binding.publicKey||parsed.deviceId!==binding.deviceId||parsed.minimumVersion<binding.minimumVersion)throw Error();
// Reparse the trusted projection to prevent a malformed privileged loader
// from weakening the production bootstrap contract.
parsed=BootstrapSchema.parse({...parsed,publicKey:binding.publicKey,deviceId:binding.deviceId,minimumVersion:Math.max(parsed.minimumVersion,binding.minimumVersion)});
}catch{throw Error('broker-bootstrap-invalid');}
const created=await factory(parsed,callbacks);
if(!created||typeof created.listen!=='function'||typeof created.close!=='function')throw Error('broker-bootstrap-invalid');
let closed=false,timer,closeWork;
const close=()=>{if(closeWork)return closeWork;closed=true;clearTimeout(timer);closeWork=Promise.resolve().then(()=>created.close());return closeWork;};
const live=()=>!closed&&Date.now()<expiresAt;
const expire=()=>{if(!live()){void close().catch(()=>{});return;}timer=setTimeout(expire,Math.min(expiresAt-Date.now(),2147483647));timer.unref?.();};
expire();
return Object.freeze({...created,async listen(){if(!live()){await close();throw Error('broker-bootstrap-invalid');}const result=await created.listen();if(!live()){await close();throw Error('broker-bootstrap-invalid');}return result;},close});
};
}
module.exports={createServiceFactory,SERVICE_BROKER_PORT};
@@ -0,0 +1,22 @@
'use strict';
const {verifyServiceAcceptance}=require('./service-acceptance.cjs');
// Privileged, one-bootstrap gate. commitState must invoke the fixed protected
// native compare-and-swap store and acknowledge only after RegFlushKey succeeds.
// An in-memory callback in a test is not proof of Windows persistence.
function createServiceCommitGate({current,commitState}={}){
if(typeof current!=='function'||typeof commitState!=='function')throw Error('broker-bootstrap-invalid');
let used=false,failed=false;
const active=()=>{try{return !failed&&current()===true;}catch{return false;}};
return Object.freeze({async prepare(input){
if(used||!active())throw Error('broker-bootstrap-invalid');used=true;
try{
const plan=await verifyServiceAcceptance(input);if(!active())throw Error();
const request=Object.freeze({expectedPreviousSHA:plan.previousStateDigest,candidateState:plan.nextState});
if(Buffer.byteLength(JSON.stringify(request))>2097152)throw Error();
const receipt=await commitState(request);
if(!receipt||typeof receipt!=='object'||Array.isArray(receipt)||Object.keys(receipt).sort().join(',')!=='persisted,stateSha256'||receipt.persisted!==true||receipt.stateSha256!==plan.nextStateDigest||!active()||Date.now()>=plan.deployment.manifest.expiresAt)throw Error();
return plan;
}catch{failed=true;throw Error('broker-bootstrap-invalid');}
}});
}
module.exports={createServiceCommitGate};
+33
View File
@@ -0,0 +1,33 @@
'use strict';
const {createFramedChannel}=require('./framed-channel.cjs');
const {runProductionProcess}=require('./production-entry.cjs');
const {createProductionBroker}=require('./production-factory.cjs');
const {createServiceFactory}=require('./service-bootstrap.cjs');
const {createServiceCommitGate}=require('./service-commit-gate.cjs');
const {createServiceStoreClient}=require('./service-store-client.cjs');
const {createWfpRevalidation}=require('./wfp-revalidation.cjs');
// Fixed service entry only. Native pipe peer authentication and protected store
// authorization precede policy verification; IPC cannot nominate files or keys.
function runServiceProcess({channel,store=createServiceStoreClient(),factory=createProductionBroker,readTopology=async()=>{throw Error('native-topology-unavailable');}}={}){
if(!channel?.connected||typeof store.snapshot!=='function'||typeof store.envelopes!=='function'||typeof store.commit!=='function')throw Error('broker-bootstrap-invalid');
let live=true,handle;
const topology=createWfpRevalidation({readTopology,quarantine:()=>{live=false;void handle?.close();}});
channel.once('disconnect',()=>{live=false;topology.close();});
const gate=createServiceCommitGate({current:()=>live&&channel.connected,commitState:request=>store.commit(request)});
const wrapped=createServiceFactory({factory:async(...args)=>{
if(!await topology.start()||!live||!channel.connected)throw Error('broker-bootstrap-invalid');
return factory(...args);
},loadTrust:async parsed=>{
const snapshot=await store.snapshot();if(!live||!channel.connected)throw Error('broker-bootstrap-invalid');
const envelopes=await store.envelopes();if(!live||!channel.connected)throw Error('broker-bootstrap-invalid');
return gate.prepare({...envelopes,releaseAnchor:snapshot.releaseAnchor,authenticatedSid:snapshot.authenticatedSid,state:snapshot.acceptanceState,policyEnvelope:parsed.envelope});
}});
handle=runProductionProcess(wrapped,channel);
const originalClose=handle.close;
return {...handle,close:()=>{live=false;topology.close();return originalClose();}};
}
if(require.main===module){
if(process.argv.length!==3||process.argv[2]!=='--service-stdio'){process.exitCode=5;}
else{try{const channel=createFramedChannel({readable:process.stdin,writable:process.stdout,inboundFirstLimit:2097152,outboundFirstLimit:65536});runServiceProcess({channel});}catch{process.exitCode=5;}}
}
module.exports={runServiceProcess};
@@ -0,0 +1,62 @@
'use strict';
const fs=require('node:fs/promises');
const path=require('node:path');
const {spawn}=require('node:child_process');
const {TextDecoder}=require('node:util');
const {StateSchema}=require('./service-acceptance.cjs');
const MAX=2097152;
const SID=/^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/;
const exact=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join(',')===keys.sort().join(',');
const fail=()=>Error('broker-bootstrap-invalid');
// These paths derive exclusively from the reviewed installed module location.
// Native helper verifies its service SID and retained signed Node parent. A JSON
// SID alone is never evidence of peer authentication.
const installationRoot=path.resolve(__dirname,'../../../..');
async function readEnvelope(root,name,io=fs){
if(!['release-envelope.json','deployment-envelope.json'].includes(name))throw fail();
const cap=Math.ceil((name==='release-envelope.json'?2097152:262144)*4/3)+256;
const target=path.join(root,name);let handle;
try{
let current=path.parse(target).root;
for(const component of target.slice(current.length).split(path.sep)){current=path.join(current,component);const info=await io.lstat(current);if(info.isSymbolicLink())throw fail();}
handle=await io.open(target,'r');const info=await handle.stat();
if(!info.isFile()||info.size<2||info.size>cap)throw fail();
const bytes=Buffer.alloc(info.size+1);let count=0;
try{
while(count<info.size){const {bytesRead}=await handle.read(bytes,count,info.size-count,count);if(!bytesRead)throw fail();count+=bytesRead;}
if((await handle.read(bytes,info.size,1,info.size)).bytesRead!==0)throw fail();
const after=await handle.stat(),named=await io.lstat(target);
for(const key of ['dev','ino','size','mtimeMs','ctimeMs'])if(info[key]!==after[key]||info[key]!==named[key])throw fail();
if(named.isSymbolicLink())throw fail();
const value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes.subarray(0,count)));if(!exact(value,['payload','signature'])||typeof value.payload!=='string'||typeof value.signature!=='string')throw fail();return value;
}finally{bytes.fill(0);}
}catch{throw fail();}finally{await handle?.close();}
}
function invokeHelper(mode,input,{launch=spawn,root=installationRoot,deadlineMs=10000}={}){
if(!['--trust-snapshot','--commit-acceptance'].includes(mode)||typeof launch!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>30000)throw fail();
return new Promise((resolve,reject)=>{
let child,timer,failed=false,closed=false,total=0;const limit=mode==='--commit-acceptance'?65536:MAX,output=Buffer.alloc(limit+4);
const stop=()=>{failed=true;try{child?.kill();}catch{}};
try{
const env={SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR},sid=process.env.SAFETYNET_SERVICE_CLIENT_SID;
if(sid!==undefined){if(!SID.test(sid)||sid.split('-').slice(4).some(n=>Number(n)>4294967295))throw fail();env.SAFETYNET_SERVICE_CLIENT_SID=sid;}
child=launch(path.join(root,'safetynet-service-host.exe'),[mode],{cwd:root,windowsHide:true,shell:false,stdio:['pipe','pipe','ignore'],env});
timer=setTimeout(stop,deadlineMs);
child.once('error',stop);child.stdout.on('error',stop);child.stdin.on('error',stop);
child.stdout.on('data',chunk=>{if(failed)return;if(!Buffer.isBuffer(chunk)||total+chunk.length>limit+4){stop();return;}chunk.copy(output,total);total+=chunk.length;if(total>=4){const size=output.readUInt32LE(0);if(size<2||size>limit||total>size+4)stop();}});
child.once('close',(code,signal)=>{
closed=true;clearTimeout(timer);
try{if(failed||code!==0||signal||total<6||output.readUInt32LE(0)!==total-4)throw fail();const value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(output.subarray(4,total)));resolve(value);}catch{reject(fail());}finally{output.fill(0);}
});
if(input===undefined)child.stdin.end();else{const payload=Buffer.from(JSON.stringify(input));if(payload.length<2||payload.length>MAX)throw fail();const frame=Buffer.alloc(payload.length+4);frame.writeUInt32LE(payload.length);payload.copy(frame,4);payload.fill(0);child.stdin.end(frame,()=>frame.fill(0));}
}catch{stop();if(!child||closed){clearTimeout(timer);output.fill(0);reject(fail());}}
// Timeout cannot accept output or invent process termination. A killed child
// must emit close; an unconfirmed terminal state remains a failed pending job.
});
}
function createServiceStoreClient({launch=spawn,root=installationRoot,deadlineMs=10000}={}){
const invoke=(mode,input)=>invokeHelper(mode,input,{launch,root,deadlineMs});
const safe=fn=>async(...args)=>{try{return await fn(...args);}catch{throw fail();}};
return Object.freeze({snapshot:safe(async()=>{const value=await invoke('--trust-snapshot');if(!exact(value,['version','authenticatedSid','releaseAnchor','acceptanceState'])||value.version!==1||typeof value.authenticatedSid!=='string'||typeof value.releaseAnchor!=='string'||value.releaseAnchor.length>16384)throw fail();StateSchema.parse(value.acceptanceState);return value;}),commit:safe(async request=>{if(!exact(request,['expectedPreviousSHA','candidateState'])||!/^[a-f0-9]{64}$/.test(request.expectedPreviousSHA))throw fail();StateSchema.parse(request.candidateState);const value=await invoke('--commit-acceptance',request);if(!exact(value,['persisted','stateSha256'])||value.persisted!==true||!/^[a-f0-9]{64}$/.test(value.stateSha256))throw fail();return value;}),envelopes:safe(async()=>({releaseEnvelope:await readEnvelope(root,'release-envelope.json'),deploymentEnvelope:await readEnvelope(root,'deployment-envelope.json')}))});
}
module.exports={createServiceStoreClient,invokeHelper,installationRoot,readEnvelope};
+76
View File
@@ -0,0 +1,76 @@
'use strict';
const {isIP}=require('node:net');
const bindings=new WeakSet();
class AuthorityError extends Error {
constructor(code){super(code);this.name='AuthorityError';this.code=code;}
}
const deny=code=>{throw new AuthorityError(code);};
function parseAuthority(value,{requirePort=false}={}) {
if(typeof value!=='string'||!value||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))deny('invalid-authority');
let hostname,portText;
if(value.startsWith('[')) {
const match=/^\[([^\]]+)\](?::([^:]+))?$/.exec(value);
if(!match||isIP(match[1])!==6)deny('invalid-authority');
hostname=new URL(`https://[${match[1]}]/`).hostname.slice(1,-1);
portText=match[2];
} else {
const match=/^([^:]+)(?::([^:]+))?$/.exec(value);
if(!match)deny('invalid-authority');
hostname=match[1].toLowerCase();portText=match[2];
if(isIP(hostname)!==4) {
// Forbid URL-parser legacy numerical IPv4 aliases (127.1, octal,
// decimal and hex). DNS labels remain explicit ASCII / punycode.
if(/^[0-9.]+$/.test(hostname)||hostname.split('.').some(label=>/^0x[0-9a-f]+$/i.test(label)))deny('invalid-authority');
if(hostname.length>253||hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))deny('invalid-authority');
}
}
if(requirePort&&portText===undefined)deny('missing-port');
if(portText!==undefined&&!/^[1-9][0-9]{0,4}$/.test(portText))deny('invalid-port');
const port=portText===undefined?443:Number(portText);
if(port>65535)deny('invalid-port');
return Object.freeze({hostname,port,authority:`${isIP(hostname)===6?'['+hostname+']':hostname}:${port}`});
}
/** Caller authenticates CONNECT before creating this immutable binding.
* Parsing does not authenticate a user, authorize a destination, terminate
* TLS, validate upstream certificates or constrain process/OS egress.
*/
function bindConnectAuthority(value) {
const binding=parseAuthority(value,{requirePort:true});bindings.add(binding);return binding;
}
function verifyInnerAuthority(binding,{rawHeaders,url,servername}) {
if(!binding||!bindings.has(binding))deny('invalid-binding');
const matches=value=>parseAuthority(value).authority===binding.authority;
if(!Array.isArray(rawHeaders)||rawHeaders.length%2)deny('invalid-headers');
let host;
for(let index=0;index<rawHeaders.length;index+=2) {
if(typeof rawHeaders[index]!=='string'||typeof rawHeaders[index+1]!=='string')deny('invalid-headers');
if(rawHeaders[index].toLowerCase()==='host') {
if(host!==undefined)deny('duplicate-host');
host=rawHeaders[index+1];
}
}
if(host===undefined)deny('missing-host');
if(!matches(host))deny('authority-mismatch');
if(typeof url!=='string'||!url||url.includes('\\')||[...url].some(character=>character.charCodeAt(0)<=32||character.charCodeAt(0)===127))deny('invalid-target');
if(url.startsWith('/')) {
if(url.startsWith('//')||url.includes('#'))deny('invalid-target');
} else {
const match=/^https:\/\/([^/;?#]+)(?:\/[^#]*)?$/.exec(url);
if(!match)deny('invalid-target');
if(!matches(match[1]))deny('authority-mismatch');
}
if(servername===undefined||servername==='') {
if(!isIP(binding.hostname))deny('missing-sni');
} else {
// TLS SNI is a hostname, never an authority carrying a port.
const sni=isIP(servername)===6?`[${servername}]`:servername;
const parsed=parseAuthority(sni);
if(sni.includes(':')&&isIP(servername)!==6)deny('invalid-sni');
if(parsed.hostname!==binding.hostname)deny('sni-mismatch');
}
return binding;
}
module.exports={AuthorityError,parseAuthority,bindConnectAuthority,verifyInnerAuthority};
+64
View File
@@ -0,0 +1,64 @@
'use strict';
// Session-local TLS leaves only. No CA installation, persistent key or browser
// trust bypass. resolve() is broker-internal: its key must NEVER cross IPC.
// Parent receives only publicMetadata(), bound separately to broker generation
// and current policy. Destroy TLS sockets/contexts before close(); wiping this
// JS key buffer does not erase copies already held by OpenSSL contexts.
const crypto=require('node:crypto');
const {isIP}=require('node:net');
const {parseAuthority}=require('./tls-authority.cjs');
const fail=()=>{throw Error('broker-certificate-unavailable');};
function der(tag,bytes){
const length=bytes.length;
if(length>65535)fail();
const size=length<128?Buffer.from([length]):length<256?Buffer.from([0x81,length]):Buffer.from([0x82,length>>8,length&255]);
return Buffer.concat([Buffer.from([tag]),size,bytes]);
}
const seq=(...items)=>der(0x30,Buffer.concat(items));
const oid=hex=>Buffer.from(hex,'hex');
const algorithm=seq(oid('06082a8648ce3d040302')); // ECDSA with SHA-256; absent parameters.
const extension=(id,value,critical=false)=>seq(oid(id),...(critical?[Buffer.from([1,1,255])]:[]),der(4,value));
function ipBytes(host){
if(isIP(host)===4)return Buffer.from(host.split('.').map(Number));
let value=host;
if(value.includes('.')){const split=value.lastIndexOf(':');const tail=ipBytes(value.slice(split+1));value=value.slice(0,split)+':'+tail.readUInt16BE(0).toString(16)+':'+tail.readUInt16BE(2).toString(16);}
const halves=value.split('::');if(halves.length>2)fail();
const left=halves[0]?halves[0].split(':'):[],right=halves[1]?halves[1].split(':'):[];
const words=halves.length===2?[...left,...Array(8-left.length-right.length).fill('0'),...right]:left;
if(words.length!==8)fail();const bytes=Buffer.alloc(16);words.forEach((word,index)=>bytes.writeUInt16BE(parseInt(word,16),index*2));return bytes;
}
function time(milliseconds){return der(0x18,Buffer.from(new Date(milliseconds).toISOString().replace(/[-:T]/g,'').replace(/\.\d{3}Z$/,'Z')));}
function pem(bytes){return '-----BEGIN CERTIFICATE-----\n'+bytes.toString('base64').match(/.{1,64}/g).join('\n')+'\n-----END CERTIFICATE-----\n';}
async function createSessionCertificates({maxAuthorities=128,lifetimeMs=3600000,current,authorized}={}){
if(!Number.isInteger(maxAuthorities)||maxAuthorities<1||maxAuthorities>1024||!Number.isInteger(lifetimeMs)||lifetimeMs<60000||lifetimeMs>3600000||typeof current!=='function'||typeof authorized!=='function')fail();
let keys=await new Promise((resolve,reject)=>crypto.generateKeyPair('ec',{namedCurve:'prime256v1'},(error,publicKey,privateKey)=>error?reject(Error('broker-certificate-unavailable')):resolve({publicKey,privateKey})));
const keyBytes=Buffer.from(keys.privateKey.export({type:'pkcs8',format:'pem'}));
const leaves=new Map();let closed=false;
const active=()=>{try{return !closed&&current()===true;}catch{return false;}};
const close=()=>{closed=true;leaves.clear();keyBytes.fill(0);keys=null;};
const issuer={close,
resolve(binding){
if(!active()||!binding||typeof binding.authority!=='string')fail();
const authority=parseAuthority(binding.authority);if(authority.authority!==binding.authority)fail();
try{if(authorized(authority)!==true)fail();}catch{fail();}
const now=Date.now(),existing=leaves.get(authority.authority);
if(existing&&existing.expiresAt>now+1000)return existing;
for(const [name,leaf]of leaves)if(leaf.expiresAt<=now)leaves.delete(name);
if(leaves.size>=maxAuthorities)fail();
const serial=crypto.randomBytes(20);serial[0]&=127;serial[0]|=1;
const name=seq(der(0x31,seq(oid('0603550403'),der(0x0c,Buffer.from('SafetyNet session leaf')))));
const san=seq(der(isIP(authority.hostname)?0x87:0x82,isIP(authority.hostname)?ipBytes(authority.hostname):Buffer.from(authority.hostname,'ascii')));
const expiresAt=now+lifetimeMs;
const extensions=der(0xa3,seq(extension('0603551d13',seq(),true),extension('0603551d0f',der(3,Buffer.from([7,128])),true),extension('0603551d25',seq(oid('06082b06010505070301'))),extension('0603551d11',san)));
const tbs=seq(der(0xa0,der(2,Buffer.from([2]))),der(2,serial),algorithm,name,seq(time(now-60000),time(expiresAt)),name,keys.publicKey.export({type:'spki',format:'der'}),extensions);
const certificate=pem(seq(tbs,algorithm,der(3,Buffer.concat([Buffer.from([0]),crypto.sign('sha256',tbs,keys.privateKey)]))));
const x509=new crypto.X509Certificate(certificate);
if(x509.ca||!x509.verify(keys.publicKey)||!(isIP(authority.hostname)?x509.checkIP(authority.hostname):x509.checkHost(authority.hostname,{subject:'never',wildcards:false})))fail();
const leaf=Object.freeze({authority:authority.authority,key:keyBytes,cert:certificate,fingerprint256:x509.fingerprint256,expiresAt});
if(!active())fail();leaves.set(authority.authority,leaf);return leaf;
},
publicMetadata(binding){const leaf=issuer.resolve(binding);return Object.freeze({authority:leaf.authority,cert:leaf.cert,fingerprint256:leaf.fingerprint256,expiresAt:leaf.expiresAt});}
};
return Object.freeze(issuer);
}
module.exports={createSessionCertificates};
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const {validateTopology}=require('./wfp-topology.cjs');
// Adapter must come from a protected native service authority, never IPC JSON.
// Periodic reads supplement unreliable BFE notifications. Neither closes the
// privileged-change packet window; no atomic kernel claim is made here.
function createWfpRevalidation({readTopology,quarantine,intervalMs=500,deadlineMs=2000}={}){
if(typeof readTopology!=='function'||typeof quarantine!=='function'||!Number.isInteger(intervalMs)||intervalMs<100||intervalMs>1000||!Number.isInteger(deadlineMs)||deadlineMs<10||deadlineMs>5000)throw TypeError('Invalid WFP monitor');
let state='new',pending=false,timer,timeout;
function fail(reason){if(state==='closed'||state==='quarantined')return;state='quarantined';clearInterval(timer);clearTimeout(timeout);quarantine(reason);}
async function check(){
if(pending||state==='closed'||state==='quarantined')return false;
pending=true;
timeout=setTimeout(()=>fail('wfp-revalidation-timeout'),deadlineMs);timeout.unref?.();
try{
const snapshot=await readTopology();
if(state==='closed'||state==='quarantined')return false;
if(snapshot?.bfeRunning!==true){fail('wfp-bfe-unavailable');return false;}
const result=validateTopology(snapshot);
if(!result.safe){fail(result.reason);return false;}
return true;
}catch{fail('wfp-revalidation-unavailable');return false;}
finally{pending=false;clearTimeout(timeout);}
}
return Object.freeze({
async start(){if(state!=='new')throw Error('WFP monitor already started');state='starting';if(!await check())return false;if(state!=='starting')return false;state='verified';timer=setInterval(()=>{void check();},intervalMs);timer.unref?.();return true;},
changed(){fail('wfp-topology-changed');},
current(){return state==='verified';},
close(){state='closed';clearInterval(timer);clearTimeout(timeout);},
status(){return Object.freeze({state,pending});}
});
}
module.exports={createWfpRevalidation};
+60
View File
@@ -0,0 +1,60 @@
'use strict';
// Contract only; not an installer and not proof of kernel enforcement.
// https://learn.microsoft.com/en-us/windows/win32/fwp/filter-arbitration
const {GUIDS,planBrowserNetworkBoundary}=require('./windows-wfp-plan.cjs');
const OWNED_PRIORITY=65535;
const RELEVANT_LAYERS=Object.freeze([GUIDS.connectV4,GUIDS.connectV6,GUIDS.recvV4,GUIDS.recvV6]);
const USER_ID='af043a0a-b34d-4f86-979c-c90371af6e66';
function planServiceReceiver({appId,brokerPort,serviceSid}={}){
const base=planBrowserNetworkBoundary({appId,brokerPort});
if(typeof serviceSid!=='string'||!/^S-1-5-80-(?:\d+-){4}\d+$/.test(serviceSid)||serviceSid.split('-').slice(4).some(x=>String(Number(x))!==x||Number(x)>4294967295))throw new TypeError('Actual SCM service SID required');
const tuple=Object.freeze([
{field:GUIDS.protocol,match:'equal',type:'FWP_UINT8',value:6},
{field:GUIDS.localAddress,match:'equal',type:'FWP_UINT32',value:0x7f000001},
{field:GUIDS.localPort,match:'equal',type:'FWP_UINT16',value:brokerPort}
].map(Object.freeze));
// Higher normal PERMIT skips the lower BLOCK in OUR sublayer. No clear-action-
// right: corporate blocks in other sublayers retain their veto.
const filters=Object.freeze([
Object.freeze({id:'broker-service-receiver',layer:GUIDS.recvV4,weight:2,action:'FWP_ACTION_PERMIT',clearActionRight:false,conditions:Object.freeze([...tuple,Object.freeze({field:GUIDS.appId,match:'equal',type:'FWP_BYTE_BLOB_TYPE',value:base.applicationId}),Object.freeze({field:USER_ID,match:'equal',type:'FWP_SECURITY_DESCRIPTOR_TYPE',value:`D:(A;;CC;;;${serviceSid})`})])}),
Object.freeze({id:'broker-receiver-deny',layer:GUIDS.recvV4,weight:1,action:'FWP_ACTION_BLOCK',clearActionRight:false,conditions:tuple})
]);
return Object.freeze({activation:'not-installed',sublayerPriority:OWNED_PRIORITY,filters,serviceSid,requiresActualServiceToken:true,completeBoundary:false});
}
function validateTopology({ownedSublayer,sublayers,filters,enumerationComplete,allObjectsReadable}={}){
const fail=reason=>Object.freeze({safe:false,reason});
if(enumerationComplete!==true||allObjectsReadable!==true)return fail('topology-incomplete');
if(typeof ownedSublayer!=='string'||!Array.isArray(sublayers)||!Array.isArray(filters)||sublayers.length>4096||filters.length>65536)return fail('topology-invalid');
const map=new Map();
for(const s of sublayers){if(!s||typeof s.key!=='string'||map.has(s.key)||!Number.isInteger(s.weight)||s.weight<0||s.weight>65535)return fail('topology-invalid');map.set(s.key,s.weight);}
if(map.get(ownedSublayer)!==OWNED_PRIORITY)return fail('owned-priority-invalid');
let ownedCount=0;const ownedIds=new Set();
for(const f of filters){
if(!f||typeof f!=='object'||typeof f.layer!=='string'||typeof f.sublayer!=='string')return fail('snapshot-invalid');
if(!RELEVANT_LAYERS.includes(f.layer))continue;
if(!map.has(f.sublayer))return fail('unknown-sublayer');
if(f.sublayer!==ownedSublayer&&map.get(f.sublayer)>=OWNED_PRIORITY)return fail('priority-collision');
// Our complete filter readback must additionally match exact trusted plans.
if(f.sublayer===ownedSublayer){
if(f.ownedVerified!==true||typeof f.key!=='string'||ownedIds.has(f.key))return fail('owned-filter-unverified');
ownedIds.add(f.key);ownedCount++;
}
}
if(ownedCount!==8)return fail('owned-filter-count');
return Object.freeze({safe:true,reason:'topology-compatible',zeroChangeWindowProven:false});
}
function createTopologyGuard({quarantine}={}){
if(typeof quarantine!=='function')throw new TypeError('Quarantine callback required');
let state='unverified';
function invalidate(reason){if(state==='quarantined')return;state='quarantined';quarantine(reason);}
return Object.freeze({
admit(snapshot){if(state!=='unverified')return false;const result=validateTopology(snapshot);if(!result.safe){invalidate(result.reason);return false;}state='verified';return true;},
// Subscribe ALL filter/sublayer/BFE state changes before snapshot, on a
// separate read-only session. Invalidate synchronously on any notification;
// never re-admit automatically after a change or failed subscription.
changed(){invalidate('wfp-topology-changed');},
monitoringFailed(){invalidate('wfp-monitor-unavailable');},
current(){return state==='verified';},status(){return state;}
});
}
module.exports={OWNED_PRIORITY,RELEVANT_LAYERS,planServiceReceiver,validateTopology,createTopologyGuard};
+54
View File
@@ -0,0 +1,54 @@
'use strict';
// Planner only: it never opens BFE, installs filters, or changes Windows policy.
// GUIDs checked against Windows SDK 10.0.26100.0 fwpmu.h.
// https://learn.microsoft.com/en-us/windows/win32/fwp/filtering-condition-identifiers-
// https://learn.microsoft.com/en-us/windows/win32/fwp/filtering-conditions-available-at-each-filtering-layer
// https://learn.microsoft.com/en-us/windows/win32/fwp/filter-arbitration
const GUIDS=Object.freeze({
connectV4:'c38d57d1-05a7-4c33-904f-7fbceee60e82',connectV6:'4a72393b-319f-44bc-84c3-ba54dcb3b6b4',
appId:'d78e1e87-8644-4ea5-9437-d809ecefc971',protocol:'3971ef2b-623e-4f9a-8cb1-6e79b806b9a7',
remoteAddress:'b235ae9a-1d64-49b8-a44c-5ff3d9095045',remotePort:'c35a604d-d22b-4e1a-91b4-68f674ee674b',
recvV4:'e1cd9fe7-f4b5-4273-96c0-592e487b8650',recvV6:'a3b42c97-9f04-4672-b87e-cee9c483257f',
localAddress:'d9ee00de-c1ef-4617-bfe3-ffd8f5a08957',localPort:'0c1ba1af-5765-453f-af22-a8f791ac775b'
});
function condition(field,match,type,value){return Object.freeze({field:GUIDS[field],match,type,value});}
function planBrowserNetworkBoundary({appId,brokerPort}={}){
// App ID must come from FwpmGetAppIdFromFileName0 for the verified installed EXE.
// Immutable hex prevents callers mutating a Buffer after planning.
if(!Buffer.isBuffer(appId)||appId.length<8||appId.length>65536||appId.length%2||!Number.isInteger(brokerPort)||brokerPort<1||brokerPort>65535)throw new TypeError('Invalid browser network boundary');
const devicePath=appId.toString('utf16le');
if(!/^\\device\\harddiskvolume[0-9]+\\[^\0]+\.exe\0$/.test(devicePath)||devicePath!==devicePath.toLowerCase()||devicePath.slice(0,-1).split('\\').some(part=>part==='.'||part==='..')||devicePath.includes(':'))throw new TypeError('Native canonical application ID required');
const applicationId=appId.toString('hex');
const identity=condition('appId','equal','FWP_BYTE_BLOB_TYPE',applicationId);
const tcp=condition('protocol','equal','FWP_UINT8',6);
const localhost=condition('remoteAddress','equal','FWP_UINT32',0x7f000001);
function rule(id,layer,conditions){return Object.freeze({id,layer,action:'FWP_ACTION_BLOCK',clearActionRight:false,conditions:Object.freeze([identity,...conditions])});}
// Disjoint deny predicates cover the complement of the one approved tuple.
// No PERMIT filter: existing corporate firewall blocks retain their veto.
const filters=Object.freeze([
rule('v4-non-tcp',GUIDS.connectV4,[condition('protocol','not-equal','FWP_UINT8',6)]),
rule('v4-tcp-non-broker-address',GUIDS.connectV4,[tcp,condition('remoteAddress','not-equal','FWP_UINT32',0x7f000001)]),
rule('v4-tcp-other-local-port',GUIDS.connectV4,[tcp,localhost,condition('remotePort','not-equal','FWP_UINT16',brokerPort)]),
rule('v6-all',GUIDS.connectV6,[]),
// ALE authorizes both directions of an inbound-first flow. Denying only
// CONNECT would allow replies to an accepted hostile UDP/TCP peer.
// Approved outbound broker responses already belong to their CONNECT flow.
rule('browser-v4-incoming',GUIDS.recvV4,[]),
rule('browser-v6-incoming',GUIDS.recvV6,[])
]);
return Object.freeze({version:2,activation:'not-installed',applicationId,devicePath:devicePath.slice(0,-1),broker:Object.freeze({family:4,address:'127.0.0.1',protocol:6,port:brokerPort}),filters,
prerequisites:Object.freeze(['verified-native-app-id','signed-installer-transaction','persistent-owned-provider-and-sublayer','native-filter-enumeration','physical-ipv4-ipv6-and-loopback-tests','physical-inbound-first-tcp-udp-tests','browser-management-inspector-telemetry-and-agent-ipc-migration','browser-local-dns-denial','authenticated-http-only-broker'])});
}
// Separate supplemental plan; the existing six-filter native installer does
// NOT install this plan. It prevents a different executable accepting the
// approved TCP tuple, but does not distinguish instances of reusable node.exe.
// A service-token or continuously protected listener remains mandatory.
function planBrokerListenerProtection({appId,brokerPort}={}){
const validated=planBrowserNetworkBoundary({appId,brokerPort});
const filters=Object.freeze([Object.freeze({id:'broker-v4-other-executable',layer:GUIDS.recvV4,action:'FWP_ACTION_BLOCK',clearActionRight:false,conditions:Object.freeze([
condition('protocol','equal','FWP_UINT8',6),condition('localAddress','equal','FWP_UINT32',0x7f000001),condition('localPort','equal','FWP_UINT16',brokerPort),condition('appId','not-equal','FWP_BYTE_BLOB_TYPE',validated.applicationId)
])})]);
return Object.freeze({version:1,activation:'not-installed',completeBoundary:false,applicationId:validated.applicationId,filters,
prerequisites:Object.freeze(['native-recv-accept-filter-verification','same-executable-instance-protection','physical-port-takeover-tests','corporate-firewall-veto-preserved'])});
}
module.exports={GUIDS,planBrowserNetworkBoundary,planBrokerListenerProtection};
+241
View File
@@ -0,0 +1,241 @@
"""Trusted signed-installer library. No CLI, automatic elevation or live apply.
The installer must authenticate its manifest, verify installation ACLs and service
provenance, stop browser processes, and own rollback. This module does not prove
those prerequisites or replace physical receiver tests. Filters never permit.
"""
import ctypes as C
import copy
import hashlib
import os
import uuid
from contextlib import ExitStack
from pathlib import Path
from windows_wfp_probe import GUID,Blob,Display,Value,Condition,Filter,U32,ReadOnlyWfp,_blob,_decode,verify_observed
PROVIDER='2a7bcbf2-e4f5-4e94-bd33-d5442a3bef50'
SUBLAYER='403a69b8-fc7c-460a-a247-72e0d72e6bf8'
SERVICE='SafetyNetNetworkBroker'
MARKER=b'SafetyNet.NetworkBoundary.v1'
OWNED_PRIORITY=65535
L4='c38d57d1-05a7-4c33-904f-7fbceee60e82'
L6='4a72393b-319f-44bc-84c3-ba54dcb3b6b4'
R4='e1cd9fe7-f4b5-4273-96c0-592e487b8650'
R6='a3b42c97-9f04-4672-b87e-cee9c483257f'
APP='d78e1e87-8644-4ea5-9437-d809ecefc971'
PROTO='3971ef2b-623e-4f9a-8cb1-6e79b806b9a7'
ADDR='b235ae9a-1d64-49b8-a44c-5ff3d9095045'
PORT='c35a604d-d22b-4e1a-91b4-68f674ee674b'
RULE_IDS=['v4-non-tcp','v4-tcp-non-broker-address','v4-tcp-other-local-port','v6-all','browser-v4-incoming','browser-v6-incoming']
_MANAGED_TOKEN=object()
class InstallError(RuntimeError):
def __init__(self,reason,code=0):super().__init__(reason);self.reason=reason;self.code=int(code)
def _guid(text):return GUID.from_buffer_copy(uuid.UUID(text).bytes_le)
def _key(rule):return str(uuid.uuid5(uuid.UUID(PROVIDER),rule))
def _owned_metadata(value,kind):
if not value:return False
return value.get('flags')==1 and value.get('data')==MARKER.hex() and (value.get('service')==SERVICE if kind=='provider' else value.get('provider')==PROVIDER and value.get('weight')==OWNED_PRIORITY)
def _validate(plan,identity,port):
if not isinstance(port,int) or isinstance(port,bool) or not 1<=port<=65535:raise InstallError('invalid-broker-port')
def c(field,match,type_,value):return {'field':field,'match':match,'type':type_,'value':value}
app=c(APP,'equal','FWP_BYTE_BLOB_TYPE',identity)
tcp=c(PROTO,'equal','FWP_UINT8',6)
specifications=[(L4,[app,c(PROTO,'not-equal','FWP_UINT8',6)]),(L4,[app,tcp,c(ADDR,'not-equal','FWP_UINT32',0x7f000001)]),(L4,[app,tcp,c(ADDR,'equal','FWP_UINT32',0x7f000001),c(PORT,'not-equal','FWP_UINT16',port)]),(L6,[app]),(R4,[app]),(R6,[app])]
managed=plan.get('version')==3
if not isinstance(plan,dict) or plan.get('applicationId')!=identity or plan.get('version') not in (2,3) or len(plan.get('filters',[]))!=(8 if managed else 6):raise InstallError('plan-identity-mismatch')
for rule,rule_id,(layer,conditions) in zip(plan['filters'],RULE_IDS,specifications):
if rule.get('id')!=rule_id or rule.get('layer')!=layer or rule.get('action')!='FWP_ACTION_BLOCK' or rule.get('clearActionRight') is not False or rule.get('conditions')!=conditions:raise InstallError('unsafe-plan')
if managed:
from windows_wfp_receiver import validate_receiver_rule,service_sddl
pair=plan['filters'][6:]
if [rule.get('id') for rule in pair]!=['broker-service-receiver','broker-receiver-deny']:raise InstallError('unsafe-receiver-plan')
for rule in pair:validate_receiver_rule(rule)
if any(rule['conditions'][2]['value']!=port for rule in pair) or pair[0]['conditions'][3]['value']!=plan.get('brokerApplicationId') or pair[0]['conditions'][4]['value']!=service_sddl(plan.get('serviceSid')):raise InstallError('unsafe-receiver-plan')
return plan
def elevated():
if os.name!='nt':return False
k=C.WinDLL('kernel32',use_last_error=True);a=C.WinDLL('advapi32',use_last_error=True)
k.GetCurrentProcess.restype=C.c_void_p;k.CloseHandle.argtypes=[C.c_void_p]
a.OpenProcessToken.argtypes=[C.c_void_p,U32,C.POINTER(C.c_void_p)]
a.GetTokenInformation.argtypes=[C.c_void_p,U32,C.c_void_p,U32,C.POINTER(U32)]
token=C.c_void_p();value=U32();size=U32()
if not a.OpenProcessToken(k.GetCurrentProcess(),8,C.byref(token)):return False
try:return bool(a.GetTokenInformation(token,20,C.byref(value),4,C.byref(size)) and value.value)
finally:k.CloseHandle(token)
class Provider(C.Structure):
_fields_=[('key',GUID),('display',Display),('flags',U32),('data',Blob),('service',C.c_wchar_p)]
class Sublayer(C.Structure):
_fields_=[('key',GUID),('display',Display),('flags',U32),('provider',C.POINTER(GUID)),('data',Blob),('weight',C.c_uint16)]
class NativeBackend:
def __init__(self):
self.reader=ReadOnlyWfp();self.api=self.reader.api;self.engine=C.c_void_p()
for name,args in {
'FwpmTransactionBegin0':[C.c_void_p,U32], 'FwpmTransactionCommit0':[C.c_void_p], 'FwpmTransactionAbort0':[C.c_void_p],
'FwpmProviderGetByKey0':[C.c_void_p,C.POINTER(GUID),C.POINTER(C.POINTER(Provider))],
'FwpmSubLayerGetByKey0':[C.c_void_p,C.POINTER(GUID),C.POINTER(C.POINTER(Sublayer))],
'FwpmFilterGetByKey0':[C.c_void_p,C.POINTER(GUID),C.POINTER(C.POINTER(Filter))],
'FwpmProviderAdd0':[C.c_void_p,C.POINTER(Provider),C.c_void_p],
'FwpmSubLayerAdd0':[C.c_void_p,C.POINTER(Sublayer),C.c_void_p],
'FwpmFilterAdd0':[C.c_void_p,C.POINTER(Filter),C.c_void_p,C.POINTER(C.c_uint64)],
'FwpmFilterDeleteByKey0':[C.c_void_p,C.POINTER(GUID)],
'FwpmSubLayerDeleteByKey0':[C.c_void_p,C.POINTER(GUID)],
'FwpmProviderDeleteByKey0':[C.c_void_p,C.POINTER(GUID)]}.items():
function=getattr(self.api,name);function.argtypes=args;function.restype=U32
def _call(self,name,*args):
code=getattr(self.api,name)(*args)
if code:raise InstallError('native-operation-failed',code)
def app_id(self,executable):
result=self.reader.app_id(executable)
if not result['ok']:raise InstallError(result['reason'],result['code'])
return result['blobHex']
def open(self):self._call('FwpmEngineOpen0',None,10,None,None,C.byref(self.engine))
def prepare_topology(self):
from windows_wfp_topology import TopologyObserver
self.topology=TopologyObserver()
result=self.topology.start()
if not result.get('subscribed'):return False
snapshot=self.topology.snapshot([L4,L6,R4,R6])
# The current observer cannot attest READ visibility over every object.
# Do not substitute successful enumeration for that deployment gate.
return bool(snapshot.get('complete') and snapshot.get('allObjectsReadable'))
def close(self):
if getattr(self,'topology',None):self.topology.close();self.topology=None
if self.engine:self.api.FwpmEngineClose0(self.engine);self.engine=C.c_void_p()
def begin(self):self._call('FwpmTransactionBegin0',self.engine,0)
def commit(self):self._call('FwpmTransactionCommit0',self.engine)
def abort(self):self._call('FwpmTransactionAbort0',self.engine)
def lookup(self,kind,key):
cls,method,not_found={'provider':(Provider,'FwpmProviderGetByKey0',0x80320005),'sublayer':(Sublayer,'FwpmSubLayerGetByKey0',0x80320007),'filter':(Filter,'FwpmFilterGetByKey0',0x80320003)}[kind]
pointer=C.POINTER(cls)();guid=_guid(key);code=getattr(self.api,method)(self.engine,C.byref(guid),C.byref(pointer))
if code==not_found:return None
if code:raise InstallError('native-lookup-failed',code)
try:
value=pointer.contents
if kind=='filter':return _decode(value)
result={'flags':value.flags,'data':_blob(value.data).hex()}
if kind=='provider':result['service']=value.service
else:result.update(provider=value.provider.contents.text() if value.provider else None,weight=value.weight)
return result
finally:
memory=C.cast(pointer,C.c_void_p);self.api.FwpmFreeMemory0(C.byref(memory))
def add_provider(self):
data=(C.c_ubyte*len(MARKER)).from_buffer_copy(MARKER);value=Provider();value.key=_guid(PROVIDER);value.flags=1;value.display=Display('SafetyNet network boundary','Owned browser egress restrictions');value.data=Blob(len(MARKER),data);value.service=SERVICE
self._call('FwpmProviderAdd0',self.engine,C.byref(value),None)
def add_sublayer(self):
data=(C.c_ubyte*len(MARKER)).from_buffer_copy(MARKER);provider=_guid(PROVIDER);value=Sublayer();value.key=_guid(SUBLAYER);value.flags=1;value.display=Display('SafetyNet network boundary','Owned priority; collisions rejected');value.provider=C.pointer(provider);value.data=Blob(len(MARKER),data);value.weight=OWNED_PRIORITY
self._call('FwpmSubLayerAdd0',self.engine,C.byref(value),None)
def add_filter(self,rule):
if rule.get('id') in ('broker-service-receiver','broker-receiver-deny'):
from windows_wfp_receiver import validate_receiver_rule
validate_receiver_rule(rule)
if any(item['type']=='FWP_SECURITY_DESCRIPTOR_TYPE' for item in rule['conditions']):
with ExitStack() as stack:
from windows_wfp_receiver import service_descriptor
descriptors={}
for index,item in enumerate(rule['conditions']):
if item['type']=='FWP_SECURITY_DESCRIPTOR_TYPE':
value=item['value']
if not value.startswith('D:(A;;CC;;;') or not value.endswith(')'):raise InstallError('unsafe-service-descriptor')
descriptors[index]=stack.enter_context(service_descriptor(value[len('D:(A;;CC;;;'):-1]))
return self._add_filter(rule,descriptors)
return self._add_filter(rule,{})
def _add_filter(self,rule,descriptors):
conditions=(Condition*len(rule['conditions']))();retained=[]
for index,item in enumerate(rule['conditions']):
target=conditions[index];target.field=_guid(item['field']);target.match={'equal':0,'not-equal':10}[item['match']]
target.value.type={'FWP_UINT8':1,'FWP_UINT16':2,'FWP_UINT32':3,'FWP_BYTE_BLOB_TYPE':12,'FWP_SECURITY_DESCRIPTOR_TYPE':14}[item['type']]
if target.value.type==14:
blob=descriptors[index];retained.append(blob);target.value.pointer=C.cast(C.pointer(blob),C.c_void_p)
continue
if target.value.type==12:
payload=bytes.fromhex(item['value']);data=(C.c_ubyte*len(payload)).from_buffer_copy(payload);blob=Blob(len(payload),data);retained.extend([data,blob]);target.value.pointer=C.cast(C.pointer(blob),C.c_void_p)
else:setattr(target.value,{1:'u8',2:'u16',3:'u32'}[target.value.type],item['value'])
provider=_guid(PROVIDER);value=Filter();value.key=_guid(_key(rule['id']));value.flags=1;value.display=Display('SafetyNet '+rule['id'],'Deny only');value.provider=C.pointer(provider);value.layer=_guid(rule['layer']);value.sublayer=_guid(SUBLAYER);value.weight.type=0;value.count=len(conditions);value.conditions=conditions;value.action.type=0x1001
if rule.get('weight') is not None:
if rule.get('id') not in ('broker-service-receiver','broker-receiver-deny') or rule['weight'] not in (1,2):raise InstallError('unsafe-receiver-weight')
weight=C.c_uint64(rule['weight']);retained.append(weight);value.weight.type=4;value.weight.pointer=C.cast(C.pointer(weight),C.c_void_p)
if rule['action']=='FWP_ACTION_PERMIT':
if rule.get('id')!='broker-service-receiver' or rule.get('weight')!=2 or not descriptors:raise InstallError('unsafe-permit')
value.action.type=0x1002
elif rule['action']!='FWP_ACTION_BLOCK':raise InstallError('unsafe-action')
filter_id=C.c_uint64();self._call('FwpmFilterAdd0',self.engine,C.byref(value),None,C.byref(filter_id))
def verify(self,plan):return self.reader.inspect(plan,PROVIDER,SUBLAYER)
def delete(self,kind,key):
guid=_guid(key);method={'filter':'FwpmFilterDeleteByKey0','sublayer':'FwpmSubLayerDeleteByKey0','provider':'FwpmProviderDeleteByKey0'}[kind]
self._call(method,self.engine,C.byref(guid))
class BoundaryInstaller:
def __init__(self,backend=None,is_elevated=elevated):self.backend=backend;self.is_elevated=is_elevated
def _prepare(self,installation_root,executable,expected_sha256,port,plan):
if not self.is_elevated():raise InstallError('administrator-required')
raw_root=Path(installation_root).absolute();raw_target=Path(executable).absolute()
try:
for component in {raw_root,raw_target,*raw_root.parents,*raw_target.parents}:
if getattr(component.lstat(),'st_file_attributes',0)&0x400 or component.is_symlink():raise InstallError('installation-reparse-point')
root=raw_root.resolve(strict=True);target=raw_target.resolve(strict=True)
except OSError:raise InstallError('installation-path-invalid') from None
if not root.is_dir() or not target.is_relative_to(root) or target==root or not target.is_file() or target.suffix.lower()!='.exe':raise InstallError('installation-path-invalid')
if not isinstance(expected_sha256,str) or len(expected_sha256)!=64 or any(c not in '0123456789abcdef' for c in expected_sha256):raise InstallError('manifest-hash-invalid')
digest=hashlib.sha256()
with target.open('rb') as stream:
for chunk in iter(lambda:stream.read(1024*1024),b''):digest.update(chunk)
if digest.hexdigest()!=expected_sha256:raise InstallError('installation-hash-mismatch')
backend=self.backend or NativeBackend();identity=backend.app_id(target);_validate(plan,identity,port)
return backend
def _existing(self,backend,plan):
provider=backend.lookup('provider',PROVIDER);sublayer=backend.lookup('sublayer',SUBLAYER)
filters=[backend.lookup('filter',_key(rule['id'])) for rule in plan['filters']]
if provider is None and sublayer is None and all(item is None for item in filters):return False
if not _owned_metadata(provider,'provider') or not _owned_metadata(sublayer,'sublayer') or any(item is None for item in filters):raise InstallError('existing-boundary-collision')
result=verify_observed(plan,filters,PROVIDER,SUBLAYER)
if not result['verified'] or not backend.verify(plan)['verified']:raise InstallError('existing-boundary-mismatch')
return True
def apply_managed(self,installation_root,executable,expected_sha256,broker_executable,broker_sha256,port,plan):
plan=copy.deepcopy(plan)
if plan.get('version')!=3:raise InstallError('managed-plan-required')
# Independently bind the receiver app ID to an exact verified installed
# broker executable, using the same containment/reparse/hash checks.
broker_plan=copy.deepcopy(plan);broker_plan['version']=2;broker_plan['filters']=broker_plan['filters'][:6]
broker_plan['applicationId']=plan.get('brokerApplicationId')
for rule in broker_plan['filters']:rule['conditions'][0]['value']=broker_plan['applicationId']
self._prepare(installation_root,broker_executable,broker_sha256,port,broker_plan)
return self.apply(installation_root,executable,expected_sha256,port,plan,_managed=_MANAGED_TOKEN)
def apply(self,installation_root,executable,expected_sha256,port,plan,_managed=None):
plan=copy.deepcopy(plan)
if plan.get('version')==3 and _managed is not _MANAGED_TOKEN:raise InstallError('receiver-install-requires-verified-broker')
backend=self._prepare(installation_root,executable,expected_sha256,port,plan);active=False;backend.open()
try:
if not backend.prepare_topology():raise InstallError('topology-admission-unavailable')
backend.begin();active=True
existing=self._existing(backend,plan)
if not existing:
backend.add_provider();backend.add_sublayer()
for rule in plan['filters']:backend.add_filter(rule)
# Read back transaction-owned objects before committing.
if not _owned_metadata(backend.lookup('provider',PROVIDER),'provider') or not _owned_metadata(backend.lookup('sublayer',SUBLAYER),'sublayer'):raise InstallError('added-metadata-mismatch')
observed=[backend.lookup('filter',_key(rule['id'])) for rule in plan['filters']]
if any(item is None for item in observed) or not verify_observed(plan,observed,PROVIDER,SUBLAYER)['verified']:raise InstallError('added-filter-mismatch')
backend.commit();active=False
result=backend.verify(plan)
if not result['verified']:raise InstallError('postcommit-verification-failed')
return {'installed':True,'existing':existing,'nativeVerified':True,'receiverTestsVerified':False}
finally:
try:
if active:backend.abort()
finally:backend.close()
def rollback(self,installation_root,executable,expected_sha256,port,plan):
plan=copy.deepcopy(plan)
backend=self._prepare(installation_root,executable,expected_sha256,port,plan);active=False;backend.open()
try:
backend.begin();active=True
if not self._existing(backend,plan):raise InstallError('owned-boundary-not-present')
for rule in plan['filters']:backend.delete('filter',_key(rule['id']))
backend.delete('sublayer',SUBLAYER);backend.delete('provider',PROVIDER)
backend.commit();active=False
return {'removedOwnedBoundary':True}
finally:
try:
if active:backend.abort()
finally:backend.close()
+126
View File
@@ -0,0 +1,126 @@
"""Read-only native Windows WFP observations; never adds/deletes filters."""
import ctypes as C
import os
import uuid
from pathlib import Path
U32=C.c_uint32
class GUID(C.Structure):
_fields_=[('data1',U32),('data2',C.c_uint16),('data3',C.c_uint16),('data4',C.c_ubyte*8)]
def text(self): return str(uuid.UUID(bytes_le=bytes(self)))
class Blob(C.Structure):
_fields_=[('size',U32),('data',C.POINTER(C.c_ubyte))]
class Data(C.Union):
_fields_=[('u8',C.c_uint8),('u16',C.c_uint16),('u32',U32),('pointer',C.c_void_p)]
class Value(C.Structure):
_anonymous_=['data']
_fields_=[('type',U32),('data',Data)]
class Condition(C.Structure):
_fields_=[('field',GUID),('match',U32),('value',Value)]
class Display(C.Structure):
_fields_=[('name',C.c_wchar_p),('description',C.c_wchar_p)]
class Action(C.Structure):
_fields_=[('type',U32),('key',GUID)]
class Context(C.Union):
_fields_=[('raw',C.c_uint64),('provider',GUID)]
class Filter(C.Structure):
_fields_=[('key',GUID),('display',Display),('flags',U32),('provider',C.POINTER(GUID)),('provider_data',Blob),('layer',GUID),('sublayer',GUID),('weight',Value),('count',U32),('conditions',C.POINTER(Condition)),('action',Action),('context',Context),('reserved',C.POINTER(GUID)),('id',C.c_uint64),('effective_weight',Value)]
TYPES={1:'FWP_UINT8',2:'FWP_UINT16',3:'FWP_UINT32',12:'FWP_BYTE_BLOB_TYPE',14:'FWP_SECURITY_DESCRIPTOR_TYPE'}
MATCHES={0:'equal',10:'not-equal'}
def _blob(blob):
if not blob.data or not 0<blob.size<=65536:raise ValueError('Invalid bounded blob')
return C.string_at(blob.data,blob.size)
def _decode(native):
if native.count>16 or (native.count and not native.conditions):raise ValueError('Invalid bounded conditions')
conditions=[]
for index in range(native.count):
condition=native.conditions[index];value=condition.value
if value.type==1: parsed=value.u8
elif value.type==2: parsed=value.u16
elif value.type==3: parsed=value.u32
elif value.type in (12,14):
if not value.pointer:raise ValueError('Missing blob')
parsed=_blob(C.cast(value.pointer,C.POINTER(Blob)).contents).hex()
else:raise ValueError('Unsupported condition type')
conditions.append({'field':condition.field.text(),'match':MATCHES.get(condition.match,'unsupported'),'type':TYPES[value.type],'value':parsed})
weight=C.cast(native.weight.pointer,C.POINTER(C.c_uint64)).contents.value if native.weight.type==4 and native.weight.pointer else None
return {'key':native.key.text(),'provider':native.provider.contents.text() if native.provider else None,'sublayer':native.sublayer.text(),'layer':native.layer.text(),'flags':native.flags,'action':native.action.type,'conditions':conditions,'weight':weight}
def verify_observed(plan,observed,provider,sublayer):
"""Compare complete native observations to trusted deployment metadata."""
provider=str(uuid.UUID(provider));sublayer=str(uuid.UUID(sublayer))
selected=[item for item in observed if item.get('provider')==provider or item.get('sublayer')==sublayer]
expected=plan.get('filters',[])
if len(expected) not in (6,8) or len(selected)!=len(expected):return {'verified':False,'reason':'filter-count-mismatch'}
def signature(conditions):return sorted((c['field'],c['match'],c['type'],str(c['value'])) for c in conditions)
pending=list(expected)
for item in selected:
if item['provider']!=provider or item['sublayer']!=sublayer or not item['flags']&1 or item['flags']&(8|32) or (len(expected)==6 and item['action']!=0x1001):return {'verified':False,'reason':'filter-metadata-mismatch'}
def expected_conditions(rule):
result=[]
for condition in rule['conditions']:
condition=dict(condition)
if condition['type']=='FWP_SECURITY_DESCRIPTOR_TYPE':
from windows_wfp_receiver import service_descriptor
with service_descriptor(condition['value'][len('D:(A;;CC;;;'):-1]) as blob:condition['value']=_blob(blob).hex()
result.append(condition)
return result
matches=[rule for rule in pending if item['action']=={'FWP_ACTION_BLOCK':0x1001,'FWP_ACTION_PERMIT':0x1002}.get(rule['action']) and rule['clearActionRight'] is False and rule['layer']==item['layer'] and item.get('weight')==rule.get('weight') and signature(expected_conditions(rule))==signature(item['conditions'])]
if len(matches)!=1:return {'verified':False,'reason':'filter-condition-mismatch'}
pending.remove(matches[0])
return {'verified':not pending,'reason':'verified' if not pending else 'filter-missing'}
class ReadOnlyWfp:
def __init__(self):
if os.name!='nt':raise RuntimeError('Windows required')
self.api=C.WinDLL('fwpuclnt',use_last_error=True)
definitions={
'FwpmEngineOpen0':([C.c_wchar_p,U32,C.c_void_p,C.c_void_p,C.POINTER(C.c_void_p)],U32),
'FwpmEngineClose0':([C.c_void_p],U32),
'FwpmGetAppIdFromFileName0':([C.c_wchar_p,C.POINTER(C.POINTER(Blob))],U32),
'FwpmFreeMemory0':([C.POINTER(C.c_void_p)],None),
'FwpmFilterCreateEnumHandle0':([C.c_void_p,C.c_void_p,C.POINTER(C.c_void_p)],U32),
'FwpmFilterDestroyEnumHandle0':([C.c_void_p,C.c_void_p],U32),
'FwpmFilterEnum0':([C.c_void_p,C.c_void_p,U32,C.POINTER(C.POINTER(C.POINTER(Filter))),C.POINTER(U32)],U32)}
for name,(arguments,result) in definitions.items():
method=getattr(self.api,name);method.argtypes=arguments;method.restype=result
def app_id(self,executable):
target=Path(executable).resolve(strict=True)
if not target.is_file() or target.suffix.lower()!='.exe':raise ValueError('Executable required')
pointer=C.POINTER(Blob)();code=self.api.FwpmGetAppIdFromFileName0(str(target),C.byref(pointer))
if code:return {'ok':False,'reason':'app-id-unavailable','code':int(code)}
try:
data=_blob(pointer.contents)
return {'ok':True,'blobHex':data.hex()}
finally:
memory=C.cast(pointer,C.c_void_p);self.api.FwpmFreeMemory0(C.byref(memory))
def inspect(self,plan,provider,sublayer,max_entries=4096):
provider=str(uuid.UUID(provider));sublayer=str(uuid.UUID(sublayer))
if not isinstance(max_entries,int) or not 6<=max_entries<=4096:raise ValueError('Invalid enumeration bound')
engine=C.c_void_p();code=self.api.FwpmEngineOpen0(None,10,None,None,C.byref(engine))
if code:return {'verified':False,'reason':'engine-unavailable','code':int(code)}
enumeration=C.c_void_p()
try:
code=self.api.FwpmFilterCreateEnumHandle0(engine,None,C.byref(enumeration))
if code:return {'verified':False,'reason':'enumeration-unavailable','code':int(code)}
selected=[];total=0
while True:
entries=C.POINTER(C.POINTER(Filter))();count=U32()
code=self.api.FwpmFilterEnum0(engine,enumeration,128,C.byref(entries),C.byref(count))
if code:return {'verified':False,'reason':'enumeration-failed','code':int(code)}
try:
if count.value>128 or total+count.value>max_entries:return {'verified':False,'reason':'enumeration-limit'}
total+=count.value
for index in range(count.value):
native=entries[index].contents
if (native.provider and native.provider.contents.text()==provider) or native.sublayer.text()==sublayer:selected.append(_decode(native))
if not count.value:break
finally:
if entries:
memory=C.cast(entries,C.c_void_p);self.api.FwpmFreeMemory0(C.byref(memory))
return verify_observed(plan,selected,provider,sublayer)
except (ValueError,KeyError,TypeError):return {'verified':False,'reason':'native-contract-invalid'}
finally:
if enumeration:self.api.FwpmFilterDestroyEnumHandle0(engine,enumeration)
self.api.FwpmEngineClose0(engine)
@@ -0,0 +1,55 @@
"""Exact service-SID receiver filter marshalling; installer integration pending.
No CLI or activation. Only FWP_ACTION_PERMIT without CLEAR_ACTION_RIGHT is used
for the exact service identity, followed by a lower-weight tuple BLOCK.
"""
import ctypes as C
import re
from contextlib import contextmanager
from windows_wfp_install import InstallError
from windows_wfp_probe import Blob
def validate_receiver_rule(rule):
expected = [('3971ef2b-623e-4f9a-8cb1-6e79b806b9a7','FWP_UINT8',6),
('d9ee00de-c1ef-4617-bfe3-ffd8f5a08957','FWP_UINT32',0x7f000001)]
conditions = rule.get('conditions', [])
permit = rule.get('id') == 'broker-service-receiver'
if rule.get('layer') != 'e1cd9fe7-f4b5-4273-96c0-592e487b8650' or rule.get('clearActionRight') is not False or len(conditions) != (5 if permit else 3):
raise InstallError('unsafe-receiver-plan')
for item, (field, type_, value) in zip(conditions, expected):
if item != {'field':field,'match':'equal','type':type_,'value':value}:raise InstallError('unsafe-receiver-plan')
port = conditions[2]
if port.get('field') != '0c1ba1af-5765-453f-af22-a8f791ac775b' or port.get('match') != 'equal' or port.get('type') != 'FWP_UINT16' or not isinstance(port.get('value'),int) or not 1 <= port['value'] <= 65535:
raise InstallError('unsafe-receiver-plan')
if rule.get('action') != ('FWP_ACTION_PERMIT' if permit else 'FWP_ACTION_BLOCK') or rule.get('weight') != (2 if permit else 1):raise InstallError('unsafe-receiver-plan')
if permit:
app, sd = conditions[3:]
if app.get('field') != 'd78e1e87-8644-4ea5-9437-d809ecefc971' or app.get('type') != 'FWP_BYTE_BLOB_TYPE' or app.get('match') != 'equal' or not isinstance(app.get('value'),str) or not re.fullmatch('[0-9a-f]{16,131072}',app['value']):raise InstallError('unsafe-receiver-plan')
if sd.get('field') != 'af043a0a-b34d-4f86-979c-c90371af6e66' or sd.get('type') != 'FWP_SECURITY_DESCRIPTOR_TYPE' or sd.get('match') != 'equal':raise InstallError('unsafe-receiver-plan')
def service_sddl(sid):
if not isinstance(sid, str) or not re.fullmatch(r'S-1-5-80-(?:\d+-){4}\d+', sid):
raise InstallError('invalid-service-sid')
if any(str(int(part)) != part or int(part) > 4294967295 for part in sid.split('-')[4:]):
raise InstallError('invalid-service-sid')
# FWP_ACTRL_MATCH_FILTER=1, represented by CC in SDDL. Not GENERIC_ALL.
return 'D:(A;;CC;;;' + sid + ')'
@contextmanager
def service_descriptor(sid):
advapi = C.WinDLL('advapi32', use_last_error=True)
kernel = C.WinDLL('kernel32', use_last_error=True)
convert = advapi.ConvertStringSecurityDescriptorToSecurityDescriptorW
convert.argtypes = [C.c_wchar_p, C.c_uint32, C.POINTER(C.c_void_p), C.POINTER(C.c_uint32)]
convert.restype = C.c_int
kernel.LocalFree.argtypes = [C.c_void_p]
kernel.LocalFree.restype = C.c_void_p
pointer, size = C.c_void_p(), C.c_uint32()
if not convert(service_sddl(sid), 1, C.byref(pointer), C.byref(size)):
raise InstallError('service-descriptor-invalid', C.get_last_error())
try:
if not 20 <= size.value <= 4096:
raise InstallError('service-descriptor-size')
yield Blob(size.value, C.cast(pointer, C.POINTER(C.c_ubyte)))
finally:
kernel.LocalFree(pointer)
+178
View File
@@ -0,0 +1,178 @@
"""Read-only BFE topology/notifications. No installer or packet-enforcement claim.
Notifications exclude inaccessible objects and are asynchronous. Successful
subscription does NOT establish atomic protection against privileged changes.
"""
import ctypes as C
import threading
from windows_wfp_probe import GUID, Display, Blob, Filter, U32, ReadOnlyWfp
class Sublayer(C.Structure):
_fields_ = [('key', GUID), ('display', Display), ('flags', U32),
('provider', C.POINTER(GUID)), ('data', Blob), ('weight', C.c_uint16)]
class Subscription(C.Structure):
_fields_ = [('template', C.c_void_p), ('flags', U32), ('session', GUID)]
CALLBACK = getattr(C, 'WINFUNCTYPE', C.CFUNCTYPE)(None, C.c_void_p, C.c_void_p)
def bfe_status():
"""SCM_QUERY_STATUS only; never start/stop/change service configuration."""
api = C.WinDLL('advapi32', use_last_error=True)
api.OpenSCManagerW.argtypes = [C.c_wchar_p, C.c_wchar_p, U32]
api.OpenSCManagerW.restype = C.c_void_p
api.OpenServiceW.argtypes = [C.c_void_p, C.c_wchar_p, U32]
api.OpenServiceW.restype = C.c_void_p
api.CloseServiceHandle.argtypes = [C.c_void_p]
api.QueryServiceStatusEx.argtypes = [C.c_void_p, U32, C.c_void_p, U32, C.POINTER(U32)]
manager = api.OpenSCManagerW(None, None, 1)
if not manager:return {'running':False,'code':C.get_last_error()}
service = None
try:
service = api.OpenServiceW(manager, 'BFE', 4)
if not service:return {'running':False,'code':C.get_last_error()}
status = (U32 * 9)()
needed = U32()
if not api.QueryServiceStatusEx(service, 0, status, C.sizeof(status), C.byref(needed)):
return {'running':False,'code':C.get_last_error()}
return {'running':status[1] == 4,'state':int(status[1])}
finally:
if service:api.CloseServiceHandle(service)
api.CloseServiceHandle(manager)
class TopologyObserver:
def __init__(self):
self.reader = ReadOnlyWfp()
self.api = self.reader.api
self.engine = C.c_void_p()
self.handles = []
self.changed = threading.Event()
self.callback = CALLBACK(lambda _context, _change: self.changed.set())
for kind in ('Filter', 'SubLayer'):
subscribe = getattr(self.api, 'Fwpm' + kind + 'SubscribeChanges0')
subscribe.argtypes = [C.c_void_p, C.POINTER(Subscription), CALLBACK,
C.c_void_p, C.POINTER(C.c_void_p)]
subscribe.restype = U32
unsubscribe = getattr(self.api, 'Fwpm' + kind + 'UnsubscribeChanges0')
unsubscribe.argtypes = [C.c_void_p, C.c_void_p]
unsubscribe.restype = U32
self.api.FwpmSubLayerGetByKey0.argtypes = [C.c_void_p, C.POINTER(GUID), C.POINTER(C.POINTER(Sublayer))]
self.api.FwpmSubLayerGetByKey0.restype = U32
def start(self):
if self.engine.value:
raise RuntimeError('Observer already started')
code = self.api.FwpmEngineOpen0(None, 10, None, None, C.byref(self.engine))
if code:
self.changed.set()
return {'subscribed': False, 'reason': 'engine-unavailable', 'code': int(code)}
for kind in ('Filter', 'SubLayer'):
handle = C.c_void_p()
subscription = Subscription()
# FWPM_SUBSCRIPTION_FLAG_NOTIFY_ON_ADD | NOTIFY_ON_DELETE.
subscription.flags = 3
code = getattr(self.api, 'Fwpm' + kind + 'SubscribeChanges0')(
self.engine, C.byref(subscription), self.callback, None, C.byref(handle))
if code:
self.changed.set()
self.close()
return {'subscribed': False, 'reason': 'subscription-unavailable', 'code': int(code)}
self.handles.append((kind, handle))
return {'subscribed': True, 'allObjectsReadable': False,
'zeroChangeWindowProven': False}
def snapshot(self, relevant_layers, limit=4096):
if not self.engine.value or self.changed.is_set():
return {'complete': False, 'reason': 'observer-unavailable'}
if not isinstance(limit, int) or not 1 <= limit <= 65536:
raise ValueError('Invalid snapshot bound')
enum = C.c_void_p()
layers = set(relevant_layers)
filters, sublayers, total = [], {}, 0
try:
code = self.api.FwpmFilterCreateEnumHandle0(self.engine, None, C.byref(enum))
if code:
return {'complete': False, 'reason': 'enumeration-unavailable', 'code': int(code)}
while True:
entries = C.POINTER(C.POINTER(Filter))()
count = U32()
code = self.api.FwpmFilterEnum0(self.engine, enum, 128, C.byref(entries), C.byref(count))
if code:
return {'complete': False, 'reason': 'enumeration-failed', 'code': int(code)}
try:
total += count.value
if count.value > 128 or total > limit:
return {'complete': False, 'reason': 'enumeration-limit'}
for index in range(count.value):
value = entries[index].contents
if value.layer.text() not in layers:
continue
key = value.sublayer.text()
if key not in sublayers:
pointer = C.POINTER(Sublayer)()
code = self.api.FwpmSubLayerGetByKey0(self.engine, C.byref(value.sublayer), C.byref(pointer))
if code:
return {'complete': False, 'reason': 'sublayer-unavailable', 'code': int(code)}
try:
sublayers[key] = {'key': key, 'weight': int(pointer.contents.weight)}
finally:
memory = C.cast(pointer, C.c_void_p)
self.api.FwpmFreeMemory0(C.byref(memory))
filters.append({'key':value.key.text(),'provider':value.provider.contents.text() if value.provider else None,
'layer': value.layer.text(), 'sublayer': key,
'action': int(value.action.type), 'flags': int(value.flags)})
if not count.value:
break
finally:
if entries:
memory = C.cast(entries, C.c_void_p)
self.api.FwpmFreeMemory0(C.byref(memory))
return {'complete': not self.changed.is_set(), 'allObjectsReadable': False,
'filters': filters, 'sublayers': list(sublayers.values()),
'zeroChangeWindowProven': False}
finally:
if enum:
self.api.FwpmFilterDestroyEnumHandle0(self.engine, enum)
def revalidate(self, relevant_layers):
# Notifications are unreliable. Every release gate must additionally
# revalidate; a service caller can periodically invoke this method.
# No result permits an automatic recovery after previous invalidation.
if self.changed.is_set():return {'complete':False,'reason':'observer-invalidated'}
if not bfe_status().get('running'):
self.changed.set()
return {'complete':False,'reason':'bfe-unavailable'}
result = self.snapshot(relevant_layers)
if not result.get('complete'):self.changed.set()
return result
def authorized_snapshot(self, trusted_plan, provider, sublayer, relevant_layers):
"""Exact protected-plan readback, never a caller-selected visibility flag.
Integration must derive trusted_plan from protected signed installation
and deployment material. No public CLI or untrusted JSON plan loader.
"""
result = self.revalidate(relevant_layers)
if not result.get('complete'):return result
verified = self.reader.inspect(trusted_plan, provider, sublayer)
if not verified.get('verified') or self.changed.is_set():
self.changed.set()
return {'complete':False,'reason':'owned-readback-unavailable'}
selected=[row for row in result['filters'] if row['sublayer']==sublayer or row['provider']==provider]
if len(selected)!=8 or any(row['sublayer']!=sublayer or row['provider']!=provider for row in selected):
self.changed.set()
return {'complete':False,'reason':'owned-readback-count'}
for row in selected:row['ownedVerified']=True
result.update(bfeRunning=True,ownedSublayer=sublayer,enumerationComplete=True)
# Remains false until a separate, authoritative protected deployment
# prerequisite proves visibility. EOF and token elevation cannot do so.
return result
def close(self):
self.changed.set()
for kind, handle in reversed(self.handles):
getattr(self.api, 'Fwpm' + kind + 'UnsubscribeChanges0')(self.engine, handle)
self.handles.clear()
if self.engine.value:
self.api.FwpmEngineClose0(self.engine)
self.engine = C.c_void_p()
+30
View File
@@ -0,0 +1,30 @@
'use strict';
// A deadline race cannot stop an underlying worker. Keep its admission slot
// until its real promise settles; unresponsive work consumes capacity and
// subsequent work fails closed. Worker termination requires separate proof.
class WorkAdmissionError extends Error{
constructor(code){super(code);this.name='WorkAdmissionError';this.code=code;}
}
function createWorkAdmission({maxJobs=8}={}){
if(!Number.isSafeInteger(maxJobs)||maxJobs<1||maxJobs>64)throw new WorkAdmissionError('work_configuration_invalid');
let active=0,closed=false;
return Object.freeze({
run(fn,{payload}={}){
if(typeof fn!=='function'||(payload!==undefined&&!Buffer.isBuffer(payload)))throw new WorkAdmissionError('work_configuration_invalid');
if(closed){payload?.fill(0);throw new WorkAdmissionError('work_admission_closed');}
if(active>=maxJobs){payload?.fill(0);throw new WorkAdmissionError('work_capacity_exhausted');}
active++;
let settled=false;
const release=()=>{if(settled)return;settled=true;try{payload?.fill(0);}finally{active--;}};
try{
// The private payload is transferred to this admission job. Caller
// must not reuse it; scanner may use it until underlying settlement.
const result=fn();
return Promise.resolve(result).then(value=>{release();return value;},error=>{release();throw error;});
}catch(error){release();throw error;}
},
status:()=>Object.freeze({active,maxJobs,closed,available:closed?0:maxJobs-active}),
close(){closed=true;}
});
}
module.exports={createWorkAdmission,WorkAdmissionError};
+6
View File
@@ -17,6 +17,12 @@ events. The standard `outcome` field reflects unknown authorization or failure;
Microsoft documents its mapping to `EventOutcome` in
[CommonSecurityLog](https://learn.microsoft.com/en-us/azure/sentinel/cef-name-mapping).
Typed download records use `download.scan` / `download.block` with the original
request authorization UUID and real file size, magic-derived type and SHA-256.
They never attach a generic request-body hash. A blocked download has a failure
outcome and a denied event type. A successful scan reports inspection success;
it does not prove that the user received or saved the file.
These format and exporter tests do not establish a customer's remote SIEM
deployment, retention, key custody or incident-response configuration. Production
browser-to-broker integration remains a separate acceptance gate.
+443
View File
@@ -0,0 +1,443 @@
# Windows DLP: aktuelle Abnahmegrenzen
Stand: 14. Oktober 2026. Diese Übersicht unterscheidet produktive Integration
von isolierten Baustein- und Empfängertests. Das MVP ist nicht abgenommen.
| Bereich | Autoritativer aktueller Nachweis | Noch erforderlich |
| --- | --- | --- |
| Unveränderte Runtime | Offizielles Electron; eigener Chromium-Build abgebrochen | Ersatz der abstürzenden WebRTC-Header-Sperre erst nach vollständigem Netzwerk-Nachweis |
| Broker-Transport | Gepackter Production-Child mit privater IPC, authentifiziertem CONNECT, TLS-Ausstellung und scoped Pin-Helper; öffentliche Originalbytes übertragen, Geheimnisse vor Zielverbindung gesperrt | Vollständige Anbindung an den Hauptbrowser und geschützten Windows-Dienst |
| Signierte Regeln | Bestehende Core-Signaturprüfung, Device-Assignment, Ablauf, Widerruf, Generation und Rollback geprüft | Geschützte dauerhafte Mindestversion und vollständiger produktiver Bootstrap |
| Transferprüfung | Header, URLs und Multipart-Verträge; tatsächlicher Production-Child blockiert geheime PDF-Teile in JSON und Prozent-Data-URLs mit lokalem Parser | Hauptbrowser-Abnahme einschließlich aller Clipboard-/Editor-Pfade; echte Unternehmens-Scanner-Endpunkte |
| Downloads | Production-Child prüft Inline-PNG/PDF samt tatsächlichem Datei-Hash, sperrt Attachment bei deaktiviertem Speichern und geheimes PDF ohne PDF-Bytes; kausale Audit-Ereignisse persistiert | Hauptbrowser-SaveAs-, Blob- und Cache-Pfade |
| PDF | Gepaarter nativer AppContainer-/PDFium-/OCR-Test besteht: Geheimnis erkannt, null Zielverbindungen; öffentliches statisches PDF exakt übertragen | Erweiterung und ausdrücklich dokumentierte Grenzen für weitere PDF-Funktionen; finale Browser-/Installer-Abnahme |
| Office | Nativer AppContainer-/Broker-Test besteht für reale DOCX/XLSX: öffentliche Originalbytes übertragen; Geheimnisse in Metadaten/Hidden Sheet erkannt und ohne Zielverbindung blockiert | Weitere Office-Funktionen; komplexes PowerPoint und unbekannte Inhalte bleiben unvollständig und gesperrt |
| Windows-Netzwerkgrenze | Resolver- und STUN-Teilnachweise; WFP-Plan und Installer-Fehlertests. Aktueller nicht erhöhter Token kann WFP öffnen, Filterenumeration scheitert mit Access Denied (5) | Geschützter SCM-Host und Dienst-SID, Receiver-/Portübernahmeschutz, erhöhte Installation und Filter-Readback; IPv4/IPv6/UDP/TCP/Loopback/DNS-Umgehungstests |
| Broker-Runtime | Neunter geprüfter Payload: offizielles Node 24.21.0, 313 Dateien / 64 Quellnachweise, sechs tatsächliche Isolationsprüfungen einschließlich Management- und WFP-Module | Neuere Credential-/Policy-Änderungen erneut paketieren und prüfen; produktiven nativen Host und signierte Installation ergänzen |
| Auslieferung | Standalone-Payload außerhalb des Repositorys getestet; historischer Node-24.20-Nachweis bleibt erhalten | Gemeinsamer finaler Installer mit Broker, Scanner und Modellen; Fresh-System-Test |
| Unternehmens-Proxy | Achter gepackter Production-Child besteht HTTPS-Proxy und aktive IP-Policy mit sechs numerisch gebundenen CONNECT-Routen, ursprünglicher TLS-Hostname-Prüfung und gesperrten kodierten Geheimnissen | Hauptbrowser-/Dienst-Anbindung; PAC/system/SOCKS/NTLM/Kerberos noch nicht unterstützt |
| Diagnose und Audit | Bestehende Browser-Diagnostik sowie tatsächliche isolierte Broker-OTLP-Tests | Broker-Diagnostik und dauerhafte Sicherheitsereignisse im produktiven Prozesspfad; zentrale Abschaltung beibehalten |
Die nächste Abnahme folgt dieser Reihenfolge: frisches gepacktes Proxy-/Download-
Verhalten, geschützter Dienst samt authentifizierter Benutzeranbindung, vollständiger
Hauptbrowserpfad, gemeinsamer Installer und native Netzwerk-/Clipboard-Umgehungstests.
Bausteintests erlauben kein Entfernen der bestehenden WebRTC-Sperre. Management,
Inspector und autorisierte OTLP-Exporte müssen vor einer Browser-EXE-Netzwerksperre
über ausdrücklich erlaubte Dienstoperationen laufen; eine breite Ausnahme wäre
kein Ersatz für diesen Pfad.
Konkrete Transport-Migration vor Aktivierung:
- Enrollment und Management benötigen einen im signierten Deployment gebundenen
HTTPS-Origin und den bestehenden ML-DSA-87-Kanal-Pin. Das bisherige Deployment
v1 enthält diese Bindung nicht; ein vom Renderer eingegebener Server darf keine
allgemeine Dienst-Fetch-Berechtigung erzeugen. Versionierter Vertrag in Arbeit.
- Device-Identität, Challenge, verschlüsselte Policy/Evidence, Heartbeat, Ereignisse,
Bookmarks und Library brauchen begrenzte typisierte Operationen statt beliebiger
URL-/Header-/Token-Weiterleitung. Pre-Enrollment bleibt gesondert nachzuweisen.
- Browserseitige Clipboard-/AI-/manuelle Scans verwenden noch authentifiziertes
Loopback-HTTP zum Inspector. Sie müssen über den Dienst laufen; bereits
brokerseitig erfolgreiche Upload-Scans beweisen diese Browser-Pfade nicht.
- Große Dokumente brauchen begrenzte Chunk-Transfers oder geschützte Handles;
der 2-MiB-Bootstrap und spätere 64-KiB-Frames sind keine 16-MiB-Datei-RPC.
- Autorisierte OTLP-Exporte brauchen ihren eigenen begrenzten Dienstpfad.
Agentensteuerung muss vom Browser-Loopback-Listener auf authentifizierte lokale
Pipes wechseln und vorhandene PQ-, Grant- und Replay-Prüfungen erhalten.
Der Dienstadapter ist unabhängig auf seine Paketauflösung geprüft: tatsächliches
electron-builder File-Matching, ASAR-Erstellung/Extraktion und isolierter Modul-
Load bestehen. Ausschließlich `apps/network-broker/framed-channel.cjs` wird als
zusätzliche Browser-Abhängigkeit eingebettet, kein allgemeines Broker-Wildcard.
Dies beweist keine fertig installierte Dienstverbindung.
Aktuelle Broker-Regression (ohne den separat zu prüfenden Payload-Staging-Test):
188 Fälle, davon 181 bestanden und sieben ausdrücklich aktivierbare native
Prüfungen übersprungen; keine Fehler. Die übersprungenen Tests werden nicht als
native Abnahme gezählt. Die vier gezielten Diagnose-/Factory-/Download-/Proxy-
Dateien bestanden zuvor mit 28 Fällen einschließlich tatsächlichem privatem
OTLP-Empfänger. Der vorhandene geschützte Collector meldet null Commitfehler und
null abgewiesene Schreibzugriffe; der bestehende Trace
`e5ebf887a1ad8d90f065739a86cd2261` wurde mit 55 Spans abgerufen. Das beweist keine
neuen produktiven Broker-Exporte. Lokale Evidenz:
`artifacts/core-broker-development-check-2026-10-14.json`.
Der erste native Lauf des fünften Payloads mit tatsächlichem HTTPS-Unternehmens-
Proxy ist fehlgeschlagen: öffentlicher Roh-Upload wurde übertragen, decodierte
Dokumentprüfungen erreichten jedoch das feste fünfsekündige Request-Limit statt
eines vollständigen Scannerurteils. Der Inspector erlaubt bis zu 29 Sekunden
begrenzte Arbeit. Diese Zeitbudget-Inkompatibilität und die Unterdrückung später
Urteile nach einem bereits protokollierten Timeout sind vor einer erneuten
Proxy-Abnahme zu korrigieren. Ein funktionierender Roh-Upload beweist hier keine
Dokumentabdeckung. Der Testprozess endete; ursprüngliche Fehlerevidenz bleibt.
Nach Korrektur auf ein absolutes 30-Sekunden-Gesamtbudget besteht der sechste
gepackte native Lauf (1/1, 26,5 Sekunden): geheime Raw-/JSON-/Data-URL-Inhalte
werden mit tatsächlicher Scanner-Regel vor zusätzlicher Zielverbindung gesperrt.
Öffentliche äußere JSON-Bytes bleiben unverändert. PNG-Inline-Inhalt funktioniert
trotz allowDownloads=false, Attachment-Speicherung wird gesperrt. Öffentliches
Inline-PDF funktioniert, geheimes PDF gibt keine PDF-Bytes frei. 13 persistierte
Ereignisse enthalten geprüfte echte Dateigrößen und SHA-256 sowie kausale UUIDs.
Privater Testempfänger: 22 echte OTLP-Spans, 33.635 Bytes, null ungültige Exporte,
null aktuelle/lebenslange Exportfehler; dauerhafte Trace-ID
`2943810ad3499cd0d331a45206b2780d` wurde tatsächlich abgerufen, Audit-Span und
Request-Elternspan verknüpft. Supervisor bestätigt geschlossen, ohne Quarantäne.
Nachweis: `artifacts/broker-production-native/proof-2026-10-14T10-31-22.318Z.json`.
Die Spans stammen vom privilegierten Test-Parent, nicht vom produktiven Child-
Exporter. Hauptbrowser, Windows-Dienst und Installer bleiben separate Gates.
Der Transportbaustein für den nativen Dienst verwendet längenbegrenztes UTF-8-
JSON (initial maximal 2 MiB, danach 64 KiB), höchstens acht ausstehende Writes
und feste Fehlercodes; acht Tests einschließlich fragmentierter Frames,
ungültigem UTF-8 und tatsächlichem Duplex-Transport des bestehenden Audit-RPC
bestehen. Disconnect bestätigt ausstehende Writes nicht als erfolgreich;
ein später Callback kann kein zweites Ergebnis liefern. Dieser Baustein
authentifiziert keinen Peer und ist
noch nicht aktiviert. Die native Dienst-/Client-Identitätsprüfung vor dem ersten
Frame sowie die Produktionsanbindung bleiben separate Abnahmebedingungen.
Der eigene native Dienst-Host wurde mit MSVC 14.44 als Testvariante kompiliert
(kein Chromium-Neubau). Ein tatsächlicher Windows-Selbsttest beweist die
Ablehnung fehlender Dienst-SID, Job-Mitgliedschaft von Kind und Enkel, deren
beendeten Zustand nach Job-Schließen und die Ablehnung eines falschen Hashes.
`artifacts/native-service-host/proof.json` bindet diesen Nachweis an den damaligen
Quell-/Binary-Hash und weist installed=false sowie wfpActivated=false aus.
Die unabhängige Quellprüfung verlangt weitere Korrekturen vor Aktivierung:
Pipe-DACL ohne FILE_CREATE_PIPE_INSTANCE für Benutzer, exakter geschützter
Browserpfad, geschützter Deployment-Vertrauensanker, absolute Frame-Zeitgrenzen
und abbrechbare Writes. Der Selbsttest beweist keine Dienstinstallation oder
authentifizierte produktive Benutzerverbindung.
Nachfolgende native Testvariante: explizite Benutzer-Pipe-Rechte verweigern eine
zweite Instanz auch ohne FIRST_PIPE_INSTANCE; unberechtigter Client und zu großer
Frame werden tatsächlich abgewiesen. Job-Tests bleiben grün. Ein zusätzlicher
anonymer Pipe-Test unterscheidet erfolgreich abgeschlossenen Write mit gesundem
Idle von einem blockierten, abgebrochenen Write. Die Installation bleibt
unbewiesen; das aktuell weiterentwickelte Quellfile muss jeweils erneut zum
Hash des kompilierten Nachweises passen.
Ein noch nicht aktivierter Service-Bootstrap-Baustein verwirft einen vom Aufrufer
gewählten Policy-Schlüssel, eine fremde Geräte-ID und eine Mindestversion unter
der geschützten Zuordnung vor dem Factory-Aufruf. Sechs Tests bestehen, darunter
eine echte Vendor-/Kundensignatur-Kette und die Ablehnung einer fremden in der
Testfixture übergebenen SID. Das ist keine native Pipe-Identitätsprüfung. Sein
privilegierter Trust-Loader muss zuvor Release-Signatur, delegierte Kundensignatur,
geschützte dauerhafte Rollback-Grenzen und die tatsächlich vom nativen Pipe-Peer
ermittelte SID prüfen; ein injizierter Test-Loader beweist diese Voraussetzungen
nicht. Die Benutzer-SID darf ausschließlich aus der nativen Peer-Prüfung stammen,
niemals aus dem START-JSON oder einer übernommenen Benutzer-Umgebungsvariable.
Der Baustein sperrt zudem einen Start nach Deployment-Ablauf und schließt einen
laufenden Broker bei Ablauf, auch wenn die Browser-Policy länger gültig wäre.
Mehrere Close-Aufrufe warten auf denselben ursprünglichen Cleanup und erhalten
dessen Fehler; ein bereits begonnener Cleanup gilt nicht als abgeschlossen.
Die neue Service-Acceptance-Prüfung verifiziert Release-, Kunden- und Policy-
Signaturen zusammen, erzwingt die Gerätezuordnung und erhält gespeicherte
Policy-Mindestversionen pro Nutzer-SID, Gerät und Policy-Schlüssel. Auch eine
neuere Deployment-Freigabe oder eine Schlüsselrotation löscht alte Grenzen nicht.
Der kanonische vollständige Zustand ist auf 2 MiB und 8.192 eindeutige Einträge
begrenzt; 1.024 Bytes des Frame-Budgets bleiben für den festen Snapshot-/CAS-
Umschlag reserviert. Der vollständige serialisierte Commit wird zusätzlich
gegen das 2-MiB-Limit geprüft. Überfüllung wird gesperrt. Der Commit-Gate wartet auf die exakte
native Persistenzbestätigung und akzeptiert weder unsichere Ergebnisse noch
automatische Wiederholung nach einem ungewissen Commit.
Die unabhängige Prüfung reproduzierte außerdem einen Start-Binding-Fehler:
eine andere gültig signierte Policy unter derselben Version konnte eine neue
Factory erreichen, obwohl ein vorheriger Plan andere Inhalte bestätigt hatte.
Der Start vergleicht jetzt zusätzlich den exakten Digest des bestätigten
Policy-Payload-Feldes und die Version, bevor die Factory aufgerufen wird.
Direkte Plan-/Commit-/Start-Komposition und gleichversionige Alternativ-Policy
werden zusammen geprüft: acht Akzeptanz-/Commit-Tests und sechs Bootstrap-Tests
bestanden. Ein weiterer Akzeptanztest beweist die Sperre von 8.000 einzeln gültigen
Einträgen durch das gesamte serialisierte Byte-Budget (neun Akzeptanztests grün).
Der Digest der Policy verwendet
das Base64url-Payload-Feld als String; Release-/Deployment-Digests verwenden
die decodierten kanonischen Bytes. Die native Implementierung muss diese
unterschiedlichen Verträge einhalten.
Diese Regeln und Callback-Bestätigungen beweisen noch keinen Registry-Commit.
Offen ist ein tatsächlich geschützter einzelner AcceptanceState-Wert mit
Vergleich des vorherigen Zustands, vollständiger monotoner Transition und
RegFlushKey vor Bestätigung. Fehlender Zustand darf nicht als Version null
initialisiert werden. Installation und Freigabe dieses Stores bleiben offen.
Der native Store verwendet dafür feste Helper-Rollen `--trust-snapshot` und
`--commit-acceptance`, einen einzelnen AcceptanceState-Registrywert und eine
begrenzte Compare-and-swap-Anfrage. Der Quellpfad prüft Dienst-SID, tatsächlichen
geschützten Node-Elternprozess, signierte Zuordnung, Flush und Rücklesen vor
Bestätigung. Bisher wurde kein produktiver HKLM-Write ausgeführt; die
uninstallierte Rollen-Ablehnung ersetzt keine positive Dienst-/Store-Prüfung.
Die JavaScript-Anbindung wird separat auf genau eine gerahmte Antwort und
bestätigtes Prozessende geprüft, ohne vom Renderer gewählte Pfade oder Schlüssel.
Offene native Ownership-Prüfung: Eine aktivierte Dienst-SID ist nicht automatisch
als Eigentümer zulässig. Windows verlangt für einen Gruppen-Owner das
SE_GROUP_OWNER-Attribut; siehe [Microsoft TOKEN_OWNER](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_token_owner).
Eine Alternative muss implizites WRITE_DAC des Kontoeigentümers ausdrücklich
einschränken und tatsächlich testen; [Microsofts SID-Dokumentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers)
beschreibt die OWNER-RIGHTS-SID. Ein Plan oder DACL-String allein ist kein
Nachweis der tatsächlichen LocalService-/SCM-Berechtigungen.
Der native Test bestätigt inzwischen eine Alternative ohne SeRestore und ohne
angenommene Service-SID-Owner-Eignung: Standardkontoeigentümer plus ausdrückliche
OWNER-RIGHTS-ReadControl-Berechtigung. Im tatsächlichen Test wird WRITE_DAC mit
Access Denied (5) verweigert, während Mutex-Synchronisation und Modify erlaubt
sind. Das prüft die Owner-Regel, nicht einen installierten SCM-Commit.
Die JavaScript-Service-Anbindung besitzt einen eigenen festen Entry-Pfad und
wartet auf die native Persistenzbestätigung vor Factory/Listen. 16 gezielte
Transport-/Entry-/Akzeptanztests bestehen. Die unabhängige Quellprüfung fand noch
Interoperabilitätskorrekturen vor einem nativen Lauf: Die vom Host ermittelte SID
muss ausdrücklich an den Helper weitergegeben werden; der begrenzte rohe
Release-Envelope muss den Base64-Overhead berücksichtigen; partielle Dateireads
benötigen vollständige begrenzte Verarbeitung und Vorher-/Nachher-Prüfung.
Die Fake-/Subprozess-Tests schließen diese tatsächlichen Dienst-Gates nicht.
Nach unabhängiger Korrektur bestehen 18 gezielte Service-Store-/Entry-/Akzeptanz-
Tests. Ein tatsächlicher Subprozess erhält nur die validierte vom Host ermittelte
SID und die beiden Windows-Systemvariablen. Ein echter signierter Release mit
6.007 Dateien und mehr als 2 MiB rohem Envelope wurde über begrenzte partielle
Reads vollständig gelesen und kryptografisch verifiziert. Dateiidentitätswechsel
und ungültige Antworten werden gesperrt. Das ist weiterhin keine positive
SCM-/HKLM-Abnahme.
Zusätzliche offene WFP-Abnahme: CONNECT-Filter allein reichen nicht aus.
[Microsofts ALE-Dokumentation](https://learn.microsoft.com/en-us/windows/win32/fwp/ale-stateful-filtering)
beschreibt, dass nach erster Freigabe eines eingehenden oder ausgehenden Flows
beide Richtungen zustandsbehaftet erlaubt sind. Deshalb braucht die Browser-EXE
auch RECV_ACCEPT-Sperren für neue eingehende IPv4-/IPv6-Flows und tatsächliche
inbound-first-UDP-/TCP-Tests. Vor einer Aktivierung müssen bisherige lokale
Agent-/Management-/Inspector-/OTLP-Listener bzw. Transporte über den Broker laufen.
Der bisherige Vier-CONNECT-Filter-Plan und eine normale Unternehmens-Firewall
beweisen diese Grenze nicht. Keine dieser neuen WFP-Regeln wurde aktiviert.
Der Plan und die Installer-/Probe-Quellen enthalten jetzt sechs Browser-Regeln:
vier CONNECT- und zwei RECV_ACCEPT-Sperren. Sechs JavaScript-Planprüfungen und
21 Python-Vertrags-/Fehlertests bestehen. Das sind keine aktivierten Windows-
Filter oder physischen Egress-Tests. Zusätzlich muss die tatsächliche WFP-
Arbitration geprüft werden: höher priorisierte Hard-PERMITS können gewöhnliche
Sperren beeinflussen; siehe [Microsofts Filterarbitration](https://learn.microsoft.com/en-us/windows/win32/fwp/filter-arbitration).
Die bisherige niedrige Sublayer-Priorität allein beweist keine wirksame Grenze.
Eindeutige Priorität/Kollisionsregeln, native Topologieprüfung, relevante
Änderungsüberwachung und tatsächliche TCP-/UDP-Tests bleiben vor Freigabe offen.
Die DNS-Migrationslücke ist im neuen Broker behoben: Alle aufgelösten Adressen
werden gegen security.dns.blockedIPs geprüft; eine gesperrte Adresse verwirft
die Route. Bei aktiver IP-Policy erhält der Unternehmens-Proxy ausschließlich
die freigegebene numerische CONNECT-Adresse. SNI, Host und TLS-Zertifikatsprüfung
bleiben an den ursprünglichen Namen gebunden. Der achte native Factory-Test
beweist sechs tatsächliche numerische Tunnel und unveränderte DLP-Blockierungen.
Der System-/VPN-Resolver bleibt der unterstützte Pfad. Konfiguriertes Secure DNS
wird ausdrücklich als nicht unterstützt abgewiesen statt still heruntergestuft.
Die Hauptbrowser-/Dienst-Migration und externe Kunden-Proxy-Abnahme bleiben offen.
Für diese Release-Verifikation wird standardisierte P-256/ECDSA mit Windows CNG
und getrennt delegiertem Kundenschlüssel entwickelt. Das ist klassische
Signaturkryptografie und kein Nachweis quantensicherer Artefakt-Signaturen.
Produktionssignierung und die vollständige Windows-Verifikation bleiben offen.
Die unabhängige Prüfung des Release-Inventars fand einen Integrationsfehler:
kanonisch signierte JSON-Objekte hatten eine andere Feldreihenfolge als das
Dateiinventar und wurden deshalb fälschlich abgewiesen. Der Vergleich prüft jetzt
explizit Pfad, Größe und SHA-256 bei unveränderter vollständiger Inventarabdeckung.
Eine echte Signieren-/Verifizieren-/Dateisystem-Prüfung und anschließende
gleichgroße Dateimanipulation prüfen beide Richtungen. Diese Testschlüssel liegen
nur im Speicher; daraus entsteht kein produktiver Signaturnachweis.
## Reihenfolge der verbleibenden Core-Arbeit
Der erste echte Start des neuen gepackten Production-Childs war vor `READY`
gesperrt. Der feste lokale Startcode `broker-startup-transport` identifizierte
eine konkrete Typinkompatibilität: Die Factory lieferte ein CA-Array, während
der geprüfte HTTPS-Forwarder ausschließlich PEM-String oder Buffer akzeptiert.
Die Korrektur verbindet die freigegebenen öffentlichen PEM-Zertifikate zu einem
String; TLS-Prüfung und Hostname-Prüfung bleiben aktiv. Der tatsächliche neu
gestagte Prozess-/Transfernachweis bestand nach dieser Korrektur: tatsächlicher
Production-Child mit offiziellem Node, enthaltenem Inspector und privater IPC,
öffentlicher Upload exakt 16 Bytes / 201 / eine Zielverbindung, Geheimnis 403
ohne zusätzliche Zielverbindung und tatsächliche erkannte Regel im fsync-Audit.
Nachweis: `broker-production-native/proof-2026-10-14T09-10-08.981Z.json` unter
`artifacts`; bestätigtes Prozessende. Dies ersetzt weder den Hauptbrowserpfad
noch Download-, Unternehmens-Proxy- und WFP-Abnahmen.
Ein später erfolgreicher direkter Inspector-Start ersetzt diesen Nachweis nicht.
Umgesetzt: bekannte Base64-verpackte binäre Dokumente in JSON/Data-URLs erhalten
vor Freigabe Magic-Byte-, Fingerprint- und lokale Parserprüfung. Native Nachweise
stehen unten. Download-Blockierungen besitzen typisierte Download-Fakten und
registrierte Scanner-Regel-IDs. Dies ersetzt keine allgemeine Abdeckung beliebiger
Kodierungen oder die noch offene Hauptbrowser-Anbindung.
Der neue begrenzte Carrier-Extractor ist implementiert und nach unabhängiger
Prüfung um JSON-interne Prozentkodierung, verschachtelte Text-Envelopes und
strikte JSON-Primitive erweitert. Acht Regressionstests und Lint bestehen.
Die Integration erhält ein gemeinsames Byte-/Teil-/Knoten-/Rekursionsbudget über
alle decodierten Teile und erzwingt tatsächliche Dokumentparser.
Unbekannte rohe Binärkodierungen und Vektor-/SVG-Rendering sind damit nicht
pauschal abgedeckt; ausdrücklich deklarierte unprüfbare Carrier müssen sperren.
Die kombinierte native Production-Child-Prüfung besteht nach kontrolliert
nachgewiesener Ergänzung von genau `USERPROFILE`, `LOCALAPPDATA` und `APPDATA` in
der Windows-Umgebung. Geheimnisse im Hex-PDF werden in JSON und Prozent-Data-URL
mit tatsächlicher registrierter Regel ohne zusätzliche Zielverbindung blockiert.
Öffentliche äußere JSON-Bytes werden nach vollständiger Parserprüfung unverändert
übertragen. Öffentlicher PDF-Download funktioniert; gesperrter PDF-Download gibt
keine PDF-Bytes frei und erhält ein kausal verknüpftes `download.block`-Ereignis.
Neun fsync-persistierte synthetische Ereignisse; bestätigtes Broker-Prozessende.
Der ursprüngliche fehlgeschlagene kombinierte Nachweis bleibt erhalten. Die
Umgebungskorrektur verändert weder ACLs noch Parser-, TLS- oder Policy-Prüfungen.
Factory-Unternehmens-Proxy und Hauptbrowser-/Installer-Abnahmen bleiben offen.
Nachweis: `broker-production-native/proof-2026-10-14T09-48-31.825Z.json` unter
dem ignorierten lokalen `artifacts`-Verzeichnis.
### Verbindlicher Auslieferungsvertrag
Ein gemeinsamer Windows-Installer enthält Browser, separaten DLP-Broker,
dessen private Runtime, lokale Scanner und Modelle. Auf dem Zielgerät ist keine
Entwicklungsinstallation von Node oder Python und kein manueller Broker-Start
erforderlich. Der Zielpfad startet und überwacht den enthaltenen Broker über einen
geschützten Windows-Dienst; dessen Installation und produktive Anbindung sind
noch nicht nachgewiesen. Fehlt
dieser, ist er manipuliert oder fällt er aus, dürfen geschützte Transfers nicht
auf einen ungeprüften direkten Netzwerkpfad ausweichen. Update und Deinstallation
müssen auch die zugehörigen verwalteten Komponenten berücksichtigen.
Ein entpacktes Entwicklungsverzeichnis allein erfüllt diesen Vertrag nicht.
### Neue lokale Integrationsnachweise
- Gemeinsamer Repository-Lauf: ESLint und sämtliche JavaScript-Syntaxprüfungen
bestehen; 1.119 Node-Tests, davon 1.107 bestanden, null Fehler und zwölf explizite
native Opt-in-Skips. Der Lauf ersetzt diese ausgelassenen nativen Abnahmen nicht.
Tatsächlicher privater OTLP-Trace `4a4cbe86f72921e7e1d9a2b94abcb4b8` enthält
fünf korrelierte Spans, null aktuelle/lebenslange Exportfehler. Zwei anschließend
ergänzte Credential-Lifecycle-Consumer-Regressionen bestehen separat.
- Credential-Lifecycle korrigiert: Der native Request muss zuerst exakt zur
aktuellen angenommenen Policy passen. Für denselben SID, Device, Origin und
Policy-Key kann der jüngste zulässige ältere Credential-Datensatz wiederverwendet
werden; seine eigenen Metadaten bleiben an die DPAPI-Entropie gebunden. Ein
gleiches Versionsdatum mit anderem Digest, zukünftige Versionen und Mehrdeutigkeit
werden abgewiesen. Provisionierung ersetzt diese stabile Identität statt ihre
Versionshistorie unbegrenzt anzuhäufen. Native Auswahl-/CAS-/Registry-Ausführung
ist weiterhin nicht nachgewiesen; die zwei JS-Tests prüfen die Consumer-Grenze.
- DPAPI-Capability-Helfer gehärtet: private Python-Runtime mit `-I -B` und
ausschließlich erlaubten Windows-Umgebungsvariablen. Ausgabe ist vorallokiert
und begrenzt; späte Daten nach einem Fehler werden ignoriert. Höchstens acht
ursprüngliche Helper-Prozesse bleiben bis zu ihrem tatsächlichen Ende belegt;
ein fehlgeschlagener Kill gibt keine Kapazität frei. Fehler bleiben bereinigt.
Tatsächliche Windows-DPAPI-Roundtrips bestehen auch für 32.768 Eingabebytes und
44.000 Ciphertext-Bytes sowie bei manipulierten Python-Umgebungsvariablen.
Fünf Tests, null Skips. Die früher kleinere Entschlüsselungsgrenze wurde an
den tatsächlich größeren Ciphertext angepasst, ohne Klartextgrenze zu erhöhen.
`artifacts/dpapi-helper/proof.json` enthält vier tatsächliche private OTLP-Spans,
Trace `a2f37753f5620155c4944b26ef1189bd`, null Exportfehler. Die beiden
Credential-Stufen wurden mit tatsächlichem Parent und Zeiten abgerufen.
Der Audit-Korrelationsanker ist synthetisch; kein Browser-Upload fand statt.
- Begrenzte Management-Operationen prüfen jetzt Identität und verschlüsselte
Policy über fest vorgegebene HTTPS-Routen. ML-KEM-1024, gepinnte ML-DSA-87-
Signaturen, Device-Bindung, Mindestversion und gleicher-Version-Digest werden
geprüft. Beide Policy-Aufrufe teilen ein Deadline-/Job-Budget; Widerruf nach
Challenge verhindert den zweiten POST. 16 Tests bestehen. Das Policy-Ergebnis
aktiviert keine Regeln und ersetzt keinen geschützten Floor-Commit.
- Native Topologie-/Credential-Quellen sind erweitert und kompilieren. Ihre
aktuelle Ausführung ist ausdrücklich nicht nachgewiesen: Beim Topologie-
Selftest war die EXE vor dem Start vorhanden und gehasht; der Start scheiterte
mit `UNKNOWN:spawn`, danach fehlte die Datei. Gezielte schreibgeschützte
Defender-/CodeIntegrity-Abfragen belegen keine Ursache dieses Fehlers.
`topology-compile-only-proof.json` und `current-compile-only-proof.json` unter
`artifacts/native-service-host` unterscheiden Kompilierung von Ausführung.
Frühere native v2-Erfolge gelten für den damaligen Hash. Kein Endpoint-Schutz
wurde deaktiviert, kein neuer Registry-/SCM-/WFP-Zustand aktiviert.
- Neunter Payload: `broker-prepared-service-proof-ninth.json` unter `artifacts`,
313 Dateien / 64 Quellnachweise, sechs tatsächliche Paket-Isolationsprüfungen.
Native Test-EXEs, Signing-Assets und damals unreviewte Credential-Helfer sind
ausgeschlossen. Spätere Policy-/Credential-Quellen benötigen einen neuen
expliziten Snapshot; dieser Komponenten-Nachweis ist keine Installer-Abnahme.
- Dienst-Lifecycle weiter geprüft (`broker-service-lifecycle-proof-2026-10-14.json`
unter `artifacts`): Paketprüfung, Startbereitschaft und Herunterfahren besitzen
benannte tatsächliche OTLP-Spans. Trace `d0e4c73944250d8f96864c043e6a83d9`
enthält fünf korrelierte Spans; aktuelle und lebenslange Exportfehler null.
Instrumentierung kann Prüfungen nicht überspringen, ersetzen oder wiederholen.
Der Prozesspfad wartet ursprüngliche Factory-/Listen-/Cleanup-Promises ab;
verspätete Ergebnisse geben keine Bereitschaft frei. Cleanup-Fehler bleiben
beim wiederholten Close erhalten statt als erfolgreicher Abschluss zu gelten.
25 gezielte Prozess-/Dienst-/Framing-Tests bestehen. Ein parallel noch mit
altem Testinhalt gestarteter Lauf hing an unterdrücktem Startup-Fault; der
korrigierte Test wartet die tatsächliche Quarantäne-Disconnection ab.
- Deployment v2 bindet HTTPS-Management-Origin und bestehenden ML-DSA-87-Pin.
Native MSVC-Kompilierung besteht; sechs positive und 23 tatsächlich signierte
negative native Manifestfälle geprüft. Der native Parser validiert PQ-Key-
Metadaten; die tatsächliche PQ-Signaturprüfung bleibt im vorhandenen Node-
Kanal. Die erste begrenzte Management-Operation prüft Identität über HTTPS mit
Ed25519 und gepinntem ML-DSA-87. Sechs physische HTTPS-Tests bestehen. Geschützte
Credentials, Enrollment und produktive IPC-Anbindung sind noch nicht umgesetzt.
- Dienst-Bootstrap akzeptiert ausschließlich Receiver-Port 43120, passend zur
festen nativen/WFP-Grenze. Andere Ports scheitern vor Trust-Load und Konstruktion;
20 kombinierte Bootstrap-/Acceptance-/Service-Tests bestehen. Der separate
isolierte Production-Child-Empfängertest darf weiterhin Testports verwenden.
Neue Trust-/Lifecycle-/Topologiequellen erfordern vor dem nächsten Paketnachweis
einen frischen explizit geprüften Payload; der achte bleibt historische Evidenz.
- Achter Payload: `broker-prepared-service-proof-eighth.json` unter `artifacts`,
309 Dateien / 60 Quellnachweise, sechs physische Paket-Isolationsprüfungen.
Aktualisierter nativer Production-Child-Test besteht:
`broker-production-native/proof-2026-10-14T11-28-12.697Z.json`, sechs numerische
HTTPS-Proxy-Tunnel mit ursprünglichem SNI, 13 persistierte Ereignisse und
22 tatsächliche private OTLP-Spans. Trace `40c28b579ce1adb8010503bae74b40f5`
wurde mit seinem Audit-Kindspan abgerufen; aktuelle und lebenslange Exportfehler
null. Die Exporte stammen vom privilegierten Test-Parent, nicht vom Dienst-SDK.
- Neuer Browser-Dienstadapter startet ausschließlich den fest installierten
nativen Host mit `--client-stdio`. Installation und Broker-Paket müssen vorher
unabhängig verifiziert sein. Fünf Adaptertests mit tatsächlichem Node-Testchild
plus zehn bestehende Supervisor-Tests bestehen. Das ist kein SCM-Positivnachweis.
Der zusätzliche OTLP-Test persistiert ein synthetisches Audit-Ereignis mit fsync
und ruft zwei tatsächliche, kausal verbundene Spans ab:
`8be57a1790a8429140f5eeb71e0e219c`, null Exportfehler. Ein erster Test scheiterte
an einer ungültigen Fixture-UUID; die strikte Validierung blieb unverändert.
Hauptbrowser-Aktivierung und native Dienst-Authentifizierung stehen noch aus.
- Office-Timings mit tatsächlichem AppContainer-Inspector und privatem OTLP-
Testempfänger geprüft: `private-otlp-proof-2026-10-14T08-29-16.980Z.json`
unter `artifacts/office-native-observability`. Vier benannte Office-Spans hängen
am tatsächlichen Parse-Span; defektes Archiv erzeugt einen bereinigten Fehler-
Span. Aktuelle und lebenslange Exportfehler: null. Dies ist kein Nachweis der
produktiven Collector-Konfiguration; diese wurde nicht geändert.
- Kurzlebige Broker-TLS-Zertifikate werden im Speicher erstellt, mit exakter SAN,
ohne CA-Berechtigung und mit begrenztem Cache. Physischer Node-HTTPS-Test prüft
die Zertifikatskette und verwirft einen falschen Hostnamen. Die Browser-
Vertrauensbindung und die produktive Factory bleiben gesondert offen.
- Sitzungsgebundene Zertifikat-Pins mit tatsächlichem Stock-Electron 44.7.0 /
Chromium 152.0.7977.130 geprüft (`broker-certificate-pins/proof.json` unter
`artifacts`): registriertes kurzlebiges Leaf liefert 200, unregistriertes Leaf
wird ohne Anwendungsanfrage abgewiesen. Der Fingerprint wird aus Electrons
tatsächlichem `Certificate.data` berechnet. Generation, SAN, Ablauf und Policy
werden geprüft. Electrons Callback liefert keinen Port und cached Ergebnisse;
CONNECT-/SNI-/Host-Bindung sowie Schließen alter Verbindungen bleiben zwingend.
Der produktive Browser ist noch nicht auf diesen Helper umgestellt.
- Der Parent-Audit-Adapter wurde mit tatsächlichem Windows-DPAPI und einer
synchronen `fsync`-/Rename-Persistenz geprüft (`broker-parent-audit/proof-7IKfqn`
unter `artifacts`). Die ursprüngliche Broker-Ereignis-ID wird erst nach Commit
bestätigt; Klartext-ID fehlt in der geschützten Datei. Fehler, asynchrones
Einreihen und uneindeutige boolesche Writer-Ergebnisse geben keine Bestätigung.
Die produktive Private-IPC-/Browser-Anbindung dieses Adapters bleibt offen.
- Der tatsächliche lokale SIEM-Dateiexporter erhält die Broker-Ereignis-UUID und
die Verbindung zwischen Freigabe und anschließendem Übertragungsfehler.
ECS/CEF-Felder und deren Grenzen stehen in [broker-siem.md](broker-siem.md).
Eine Freigabe hat einen unbekannten Lieferstatus; Terminalfehler werden als
Fehler ausgewiesen. Dieser Nachweis ersetzt keine externe SIEM-Abnahme.
1. Nativer PDF-/Office-Nachweis mit verpflichtender Audit-Persistenz bestanden:
`office-proof-2026-10-14T08-23-30.852Z.json` unter dem ignorierten lokalen
Evidenzverzeichnis `artifacts/broker-native-inspection`. Sieben mit `fsync`
persistierte synthetische Ereignisse, drei tatsächliche erkannte Geheimnisregeln,
keine Zielverbindung für gesperrte Dokumente und keine verbleibenden eigenen
Inspector-Prozesse. Die Fixture beweist weder produktive Audit-Verschlüsselung
noch SIEM-Auslieferung; Diagnostik war zentral deaktiviert.
2. Sicherheitsereignisse für frühe CONNECT-/Framing-Blockierungen und Fehler nach
Transferfreigabe ergänzen. Eine protokollierte Freigabe ist kein Nachweis einer
erfolgreichen Übertragung. Erlaubte Dokumente müssen funktionieren; Geheimnisse
dürfen beim kontrollierten Empfänger nicht ankommen.
3. Zertifikats-/Trust-, Proxy-, Audit- und Management-Pfade vollständig integrieren.
Kein ungeprüftes CONNECT-Piping oder DIRECT-Fallback als Ersatz.
4. Windows-Regeln über den geprüften Installer-/Service-Pfad aktivieren und
unabhängig nativ prüfen. Erst dann die bisherige Header-Sperre ersetzen.
5. Gemeinsamen Installer bauen und ohne Entwicklungsruntime abnehmen, einschließlich
Ausfall, Widerruf, Update und fehlender/manipulierter Broker-Dateien.
Die detaillierten Anforderungen bleiben in [windows-dlp-mvp.md](windows-dlp-mvp.md).
Diese Übersicht ersetzt weder deren Kriterien noch die übrige Enterprise-Roadmap.
+83
View File
@@ -1,11 +1,39 @@
# Chromium 152 WebRTC binder backport
## Cancelled implementation path — stock runtime required
The user explicitly rejected rebuilding Chromium/Electron on2026-10-14. The
owned native build was stopped and its terminal failure/cancellation evidence
retained. No custom binary was activated or shipped. This document's backport,
compiler and source-checkout records are historical evidence, not instructions
to resume that implementation. **Do not restart this build or make a custom
Chromium runtime a product dependency.**
The current direction is an unchanged official Electron/Chromium runtime plus
an independently enforced Windows network boundary and separate controlled DLP
broker. This alternative is not yet implemented or accepted. The existing relay
allows opaque CONNECT forwarding and shares Electron's executable/process with
upstream management traffic, so merely adding per-executable firewall blocking
or removing the WebRTC header would not provide the required boundary. Retain
the present protection until hostile native transport and DNS tests prove its
replacement. A JavaScript constructor wrapper alone is insufficient.
Status: source patch prepared and checked; **not compiled or runtime verified**.
Do not distribute this patch as a proven security fix until the acceptance gates
below pass against the resulting binary.
## Latest stable candidate checked
On2026-10-14 the newer official Electron44.7.2 Windows x64 binary was also tested.
Its ZIP matches the official SHA256 manifest:
`10a89447ef64ac5cb9f782af48ffd83734a40ae91681259d1048ca3c9e6c5da4`.
Both initialabout:blank andsrcdoc positive controls delivered exact synthetic
RTC data and benign POSTs. Both corresponding managed-policy cases still
crashed with missing MdnsResponder, bad IPC123 and renderer exit3. The earlier
run's positive timeout remains preserved separately. Evidence:
`artifacts/native-rtc-4472-second-proof.json`. No project dependency was upgraded.
Thus this newly available binary does not replace the native backport either.
The official [Electron44.7.1 release](https://github.com/electron/electron/releases/tag/v44.7.1)
uses Chromium152.0.7977.149. Its Windows x64 ZIP was downloaded into an isolated
ignored directory, checked against the official SHA256 manifest and extracted
@@ -254,6 +282,61 @@ is not identified as a compiler tool, but the official full sync remains failed.
This read-only analysis authorizes neither skipping dependencies nor changing
antivirus policy. ATL/MFC remains independently missing at the host check.
## Alternative dependency installation, 2026-10-14
The normal Microsoft installer subsequently completed. `vswhere` now finds
`Microsoft.VisualStudio.Component.VC.ATLMFC` in the existing BuildTools instance
and reports both `isComplete=true` and `isLaunchable=true`; the installer process
has ended. Ignored evidence: `artifacts/native-build/atl-mfc-installed-proof.json`.
Thus the earlier missing-component prerequisite is resolved. This does not by
itself prove a generated GN graph or a working native compilation.
The user's explicit alternative-build request was implemented without changing
Defender. A reviewed CIPD ensure manifest preserves all forty original requested
package/subdirectory/version bindings from the existing `.gclient_entries`,
except the single rejected Chrome-x86 updater fixture described above. The
thirty-nine remaining bindings were processed by the official CIPD client with
exit0 in119.7seconds. No new antivirus error occurred. Original version tags
were preserved; this is not a claim that every tag was resolved offline to an
immutable instance ID. The source entries and both manifests retain their
verified hashes. Native Chromium/Electron HEADs and captured references remained
unchanged before/after; no full Git sync or patch replay occurred.
Ignored evidence: `cipd-manifest-offline-proof.json`,
`cipd-reviewed-ensure-journal.json` and `cipd-reviewed-ensure.log` under
`artifacts/native-build`. The original failed full sync remains retained.
Updater regression tests using the omitted fixture are not accepted. A generated
Electron/native-test GN dependency graph must confirm that the omitted target is
not needed before compiling. Required hooks, ATL/MFC, compilation and real native
acceptance remain separate gates. The signed Microsoft installer was launched
through normal Windows UAC; installation is not confirmed by starting it.
The selected original RC and four version-generation hooks subsequently exited0.
RC matches its checked-in SHA1 and has recorded SHA256 provenance. Electron's
Yarn4.18.0 immutable install also completed with unchanged lockfile and config.
`enableScripts:false` stayed set, but the upstream workspace fixtures and explicit
`dependenciesMeta.built:true` entries executed their permitted builds; this is
not an all-scripts-disabled claim. All native HEAD/reference snapshots remained
unchanged, with no Defender modification or new antivirus error. Evidence:
`selected-hooks-journal.json` and per-action logs. The official local toolchain
check also exited0, recognizing VS2022 and Windows SDK10.0.26100.0 without a Google
toolchain download (`local-toolchain-proof.json`). GN generation, target-graph
verification and product compilation remain separate steps.
GN generation subsequently succeeded in the fresh `out/SafetyNetTestingPython314`
directory with the supported explicit Python interpreter switch; the original
Windows-Store-alias failure remains retained. Actual recursive GN graphs for
Electron and content_unittests contain no omitted updater-fixture dependency.
Both NoOp test sources are present and `enable_mdns`/`is_p2p_enabled` are true.
Evidence: `gn-generation-python314-proof.json` and `gn-target-graph-proof.json`.
Physical Ninja aliases were also checked. The initial compile launch was
controlled-stopped before observed compiler progress for insufficient memory;
that failed attempt remains retained. A fresh continuation started only after
the measured available commit memory exceeded4GiB, using one compiler job,
bounded disk/memory guards and no remote execution. This running build is not
yet a completed compilation or native acceptance. The shipped runtime remains
unchanged until the required real tests pass.
### Patch readiness before compilation
ATL/MFC is required for the native toolchain, not for the Python patch import
+18 -2
View File
@@ -7,7 +7,23 @@ Die breiteren Anforderungen und Erweiterungen unten bleiben Roadmap.
## Aktuelle nächste Abnahmen
**Aktualisierung des Windows-MVP:** 784 Tests bestehen mit begrenzter
**Stand 14. Oktober 2026:** Das neue Windows-DLP-MVP ist nicht abgenommen.
Maßgeblich ist die [aktuelle Abnahmematrix](core-dlp-current-status.md).
Ein gepackter separater Broker ist mit privater Runtime und realen lokalen
Scannern geprüft, aber noch nicht im Hauptbrowser aktiviert. Der eigene
Chromium-Build wurde abgebrochen; stock Electron bleibt Voraussetzung. Der
geschützte Windows-Dienst, native Netzwerkdurchsetzung, gemeinsame Installation
und erneute vollständige Clipboard-/Upload-/Download-Abnahme sind offen.
Aktuelle Arbeit: Timeout-Korrektur für die reale Unternehmens-Proxy-Prüfung,
authentifizierter Diensttransport und getrennte Release-/Kundendeployment-
Signaturen. Klassische Release-Signaturen sind kein quantensicherer Nachweis.
Baustein-/Fixture-Tests und frühere Browser-Builds ersetzen weder den aktuellen
Hauptbrowserpfad noch einen final installierten gemeinsamen Broker-Installer.
### Historische Integrationsnachweise
**Vorangegangener Windows-MVP-Stand:** 784 Tests bestehen mit begrenzter
Parallelität. Der aktuelle Windows-Build besteht Paket-, Fuse- und
Manipulationsprüfungen, ist aber nicht signiert. Native Clipboard-/Upload-Tests,
Prozessneustart bestehen auf dem finalen Quellstand. Die lokalen semantischen
@@ -21,7 +37,7 @@ WebRTC-Absturz bleibt offen, bis ein korrigiertes Binary geprüft ist.
Die folgenden älteren Integrationszahlen beschreiben historische Zwischenstände;
die aktuelle verbindliche Abnahme steht im [Windows-MVP-Ziel](windows-dlp-mvp.md).
Aktueller Integrationsstand: 713 Tests bestehen
Früherer Integrationsstand: 713 Tests bestehen
(`artifacts/worker-integration-suite.log`). Der echte Admin-Workflow für typisierte
Login-Regeln, unabhängige Veröffentlichung und Auditor-Leserechte ist geprüft:
193 abgeschlossene Anfragen mit korrelierten exportierten Traces, eine abgebrochene
+195 -3
View File
@@ -1,5 +1,192 @@
# Verbindliches Ziel: Windows-DLP-MVP
## Verbindliche Runtime-Grenze
Nutzerpräzisierung vom14. Oktober2026: Chromium/Electron darf für dieses Produkt
nicht selbst gepatcht oder neu gebaut werden. Offizielle, unveränderte Runtime-
Updates bleiben Voraussetzung. Der bisherige native Backport-/Compilerpfad ist
abgebrochen; kein Custom-Binary wurde aktiviert. Eine unabhängige Windows-
Netzwerkgrenze mit separatem DLP-Broker muss die bisherige WebRTC-Sperre ersetzen,
bevor der abstürzende Response-Header entfernt werden darf. Dieser Ersatz ist
noch nicht implementiert oder nativ abgenommen. Das unveränderte Runtime-Ziel
reduziert die erforderlichen DLP-/Umgehungs-/Stabilitätsnachweise nicht.
## Gemeinsame Auslieferung
Nutzerpräzisierung: Browser, DLP-Broker, lokale Scanner und benötigte Modelle
werden gemeinsam in einer finalen Windows-Installer-EXE ausgeliefert. Der Broker
darf ein eigener interner Prozess sein, aber kein separat zu installierendes
Produkt oder manuell zu startendes Programm. Auf einem frischen Zielsystem dürfen
kein Node-/Python-/Broker-Download und keine Entwicklungsumgebung erforderlich
sein. Der Installer provisioniert die nötigen Schutzgrenzen und Dateirechte;
Startup und Paketprüfung müssen fehlende/manipulierte Broker-Dateien, falsche
Versionen sowie nicht aktive Schutzgrenzen erkennen und fail-closed reagieren.
Ein grüner Paketnachweis des alten In-Process-Relays beweist die neue Broker-
Auslieferung nicht; der neue vollständige Installer benötigt eigene Native-Tests.
## Aktueller Ersatzpfad: Nachweise und offene Grenzen
Die folgenden Einträge dokumentieren die schrittweise Entwicklung und enthalten
auch frühere Zwischenstände. Die aktuelle Abnahmematrix und neuesten Paket-,
Proxy-, Diagnose- und Dienstadapter-Nachweise stehen in
[core-dlp-current-status.md](core-dlp-current-status.md). Frühere offene Punkte
gelten nicht als aktuelle Lücke, wenn dort ein neuerer begrenzter Nachweis steht;
die vollständige Browser-/Dienst-/Installer-Abnahme bleibt erforderlich.
Der isolierte Baustein `apps/network-broker/http-request-gate.cjs` puffert explizite
HTTP-Requests vollständig vor der Freigabe. Er prüft Framing und Authority,
begrenzt Größe und Laufzeit und prüft nach dem Scan den Policy-Widerruf erneut.
Die Scan-Kopie kann die freigegebenen Originalbytes nicht verändern. Er ist noch
nicht im produktiven Relay oder Installer eingebunden. TLS-Terminierung,
Broker-Authentifizierung und WebSocket-/WebRTC-Prüfung sind dadurch nicht gelöst.
Die neun gezielten Tests umfassen einen echten HTTP-Empfänger und zeigen null
TCP-Verbindungen vor der Freigabe sowie keine Auslieferung blockierter Inhalte.
`scripts/broker-package-contract.mjs` erzeugt und prüft eine vollständige
Broker-Dateiliste mit SHA-256, Version und Protokollkennung. Neun gezielte Tests
prüfen fehlende, zusätzliche und manipulierte Dateien sowie unsichere Pfade und
Junctions. Der erwartete Manifest-Inhalt muss aus der integritätsgeschützten
Anwendung stammen. Das ist noch keine Installer-Integration und keine atomare
Startgarantie; geschützte Installationsrechte bleiben Voraussetzung. Beide
Testdateien und Bausteine bestehen den lokalen ESLint-Check. Die isolierten Tests
erzeugen noch keine produktiven Broker-OTEL-Traces.
Weitere isolierte Vorarbeiten: `tls-authority.cjs` bindet CONNECT, inneren Host,
HTTPS-Ziel und SNI an dieselbe unveränderliche Authority; fünf Tests bestanden.
Es terminiert noch kein TLS. `windows-package-plan.mjs` trennt Unpacked-Build,
Pflichtprüfungen und NSIS-Erstellung aus dem geprüften Verzeichnis; sechs Tests
bestanden. Die Freigabe ist an Source-, Runtime- und Paketidentität gebunden.
Der damalige Packaging-Aufruf verwendete nur `--dir`. Der aktuelle Aufruf
bereitet den verpflichtenden Broker vor, verifiziert das entpackte Paket und
erstellt standardmäßig anschließend NSIS; `--unpacked-only` bleibt ein expliziter
Entwicklungsmodus. Ein finaler Installer mit aktiviertem geschütztem Broker-
Dienst ist weiterhin nicht gebaut oder abgenommen.
`https-inspection-server.cjs` prüft jetzt einen echten entschlüsselten HTTPS-Leg
mit vorab authentifizierter CONNECT-Bindung und vom vertrauenswürdigen Aufrufer
bereitgestelltem Zertifikat. Fünf TLS-Tests prüfen erlaubte Originalbytes und
null Ziel-TCP-Verbindungen bei Geheimnissen, Widerruf, Host-/SNI-Fehlern sowie
vor verzögerter Freigabe. Client und Testziel validieren ihre TLS-Zertifikate.
Der Baustein ist kein vollständiger CONNECT-Proxy: Zertifikatsausstellung,
Browser-Vertrauensintegration, Zielantwort-Weiterleitung und produktive
Einbindung fehlen. Die Gesamtsuite vor diesem zusätzlichen TLS-Testlauf bestand
823 Tests; der anschließende separate TLS-Lauf bestand fünf Tests.
Inzwischen gibt der isolierte HTTPS-Leg die echte gepufferte Zielantwort zurück.
Ein Response-Gate begrenzt Body und Header, berechnet Content-Length neu und
entfernt Hop-by-hop-Header. Nicht unterstützte Kompression bleibt gesperrt.
Der vertrauenswürdige Forwarder muss seine eigene Socket-/Response-Sammlung
ebenfalls begrenzen. Die OTLP-Adaptertests prüfen echte Exporte mit Parent-/Child-
Korrelation und Laufzeiten, unveränderte Fehler bei deaktivierter Diagnostik und
den Ausschluss von Inhalt, Authority und Fehlermeldung. Der Adapter konfiguriert
keinen Exporter selbst und benötigt die bereits autorisierte Diagnostik des
Aufrufers. Das ist noch keine produktive Broker-Telemetrie-Integration.
Der Forwarding-Leg hat nun einen eigenen, nicht durch Netzwerkaktivität
verlängerbaren Zeitrahmen und signalisiert Abbruch bei Client-Disconnect oder
Fristüberschreitung. Neun HTTPS-Tests einschließlich realer Zielantwort,
Upstream-Fehler, einmaligem Abbruch und Konstruktor-Limits bestehen. Die sechs
Broker-Diagnosestufen besitzen getrennte feste `broker.*`-Operationsnamen.
`windows-wfp-plan.cjs` beschreibt vier komplementäre Deny-Filter für den nativen
Application-ID-Blob: IPv4 Nicht-TCP, TCP zu anderen Adressen, Loopback-TCP zu
anderen Ports und sämtliches IPv6. Es installiert keine Regeln und enthält
keinen Permit-Override für Unternehmens-Firewalls. Vier Tests prüfen alle
65.536 Portwerte, Protokoll-/Adressgrenzen und unveränderliche Identitäten.
Die GUIDs stimmen mit dem installierten Windows SDK 26100 überein. Native
BFE-Installation, Enumeration und physische Empfängertests sind noch offen;
der Entwicklungsprozess besitzt nur ein Medium-Integrity-Token.
Der isolierte `connect-inspection-proxy.cjs` führt nun einen echten CONNECT-
Durchlauf aus: zufällige Launch-Capability, konstante Tokenprüfung, autorisierte
Zielbindung, verifiziertes TLS, Inhaltsprüfung und echte HTTPS-Zielantwort.
Falsche Tokens werden vor Zertifikatsauflösung abgewiesen; gesperrte Inhalte
erzeugen keine Zielverbindung. Es gibt kein opaques Upstream-Piping und keinen
zusätzlichen ungeschützten lokalen Listener. Zertifikatsausstellung und produktive
Browser-Vertrauensintegration fehlen weiterhin. Die unabhängige Prüfung fand
eine noch offene Ressourcenlücke: Socket-Limits begrenzen nicht Scanner-/Forward-
Jobs, die Abbruchsignale ignorieren. Vor Integration ist gemeinsame Job-Admission
nötig, deren Plätze bis zum tatsächlichen Ende der zugrunde liegenden Arbeit
belegt bleiben. Timeout allein darf keine belegte Kapazität freigeben.
Diese Lücke ist im isolierten Prototyp geschlossen: gemeinsame Work-Admission
für Zertifikatsauflösung, Scan und Forwarding über alle CONNECT-Tunnel, Freigabe
erst nach tatsächlichem Promise-Ende. Wiederholte Timeouts überschreiten das
Job-Limit nicht; belegte Kapazität bleibt fail-closed verbraucht. Private Buffer
werden nach tatsächlichem Arbeitsende gelöscht. 37 Broker-Tests und Lint bestehen.
Der native read-only WFP-Adapter besteht neun Python-Tests. Der tatsächliche
Medium-Integrity-Prozess erhält einen nativen Application-ID-Blob, den der Plan
akzeptiert. Filter-Enumeration endet mit Access Denied (5), korrekt als
`verified: false`; native Ressourcen werden geschlossen. Der Adapter installiert
keine Regeln. Installer-Aktivierung, Service-Provenienz und native Abnahme bleiben
offen. Nachweis: ignorierter `artifacts/stock-wfp-readonly-proof.json`.
Der separate Broker-Prozess ist jetzt mit der offiziellen gebündelten Node-
24.20.0-Runtime getestet: drei echte Child-Prozess-Tests bestehen, einschließlich
CONNECT/TLS, blockiertem Geheimnis, exakten öffentlichen Bytes sowie geschlossenem
Listener nach privatem IPC-Disconnect oder Stop. Der aktuelle Factory-Code nutzt
nur eine explizite synthetische Testfixture; produktive Policy-/Scanner-Anbindung
ist damit noch nicht implementiert. Node-Binary und Lizenz liegen ignoriert unter
`.runtime/windows/broker`; offizielle Archiv- und Binary-SHA-256 stimmen,
Authenticode ist gültig. Das Browser-Binary bleibt unverändert. Installer-
Einbindung und Fresh-System-Abnahme fehlen weiterhin.
`policy-authority.cjs` verwendet die bestehende Core-Signatur-/Schema-Prüfung
und dieselbe Device-Assignment-Prüfung wie der Browser. Enrollment-Key, Device-ID
und Mindestversion stammen aus vertrauenswürdigem Bootstrap; unveränderliche
Snapshots und Generationstokens verhindern Freigabe nach Policywechsel. Sechs
Tests mit echten synthetischen Signaturen prüfen Ablauf, falsches Gerät, Änderung,
Rollback und Widerruf. Mindestversionen müssen bei produktiver Integration in
geschütztem Enrollment-Storage dauerhaft gespeichert werden. Die drei Child-
Prozesstests verlangen jetzt ausdrücklich die verifizierte gebündelte Node-Runtime
und besitzen keinen Entwicklungs-Node-Fallback. Fünf Runtime-Manifest-Tests prüfen
Version, Hashes, zusätzliche Dateien und Junctions. Kombinierter Lauf: 14/14.
Der integrierte signierte Managed-Proxy prüft Header/Cookies und volle URLs mit
dekodierten Darstellungen. Jeder nicht-textuelle Magic-Typ erfordert vollständige
lokale Dokumentanalyse, auch bei UTF-8-Rohbytes. Der begrenzte Multipart-Parser
prüft alle Teile: Metadaten, Dateityp und Inhalt müssen freigegeben sein, bevor
die ursprünglichen Request-Bytes weitergehen. 13 echte TLS-/Empfängertests und
neun Parser-Tests bestehen, ebenso Lint. Ihre Dokument-Scanner sind injizierte
Verträge; ein echter AppContainer-/Modell-Nachweis über diesen Broker fehlt noch.
Der Managed-Proxy ist weiterhin nicht produktiv im Browser eingebunden.
Stock-Resolver-Teilnachweis: `MAP * ^NOTFOUND, EXCLUDE 127.0.0.1` verhindert im
isolierten offiziellen Electron-44.7.2-Test `Session.resolveHost(example.com)`
mit `ERR_NAME_NOT_RESOLVED`, während der explizite Loopback-Proxy weiterhin
einen harmlosen Hostname-HTTP-Request empfängt. Der positive Ausgangslauf löst
den Namen auf. Das unveränderte Runtime-Binary bleibt hashgleich. Ohne exakte
Loopback-Ausnahme scheitert auch der Proxy; diese Einschränkung bleibt sichtbar.
Der bisherige RTC-UDP-Ausgangstest empfängt schon ohne Regeln null Pakete und
beweist daher keine Sperre. Physische DNS-/mDNS-/LLMNR- und WFP-Umgehungsnachweise
fehlen weiterhin. Nachweise: ignorierter `artifacts/stock-resolver-proof`-Ordner.
Ein zusätzlicher gepaarter nativer STUN-Test verwendet nun einen vorab nur auf
die eigene nicht-loopback IPv4 aufgelösten Testhostnamen. Der Ausgangslauf
empfängt fünf anhand der STUN-Magic validierte UDP-Pakete; mit derselben
Resolver-Regel empfängt der eigene Receiver null Pakete und die Namensauflösung
scheitert. Beide unveränderten Stock-Runtime-Läufe enden mit Exit 0 und bestehen
den harmlosen HTTP-Proxy-Durchlauf. Das bestätigt den getesteten hostnamebasierten
STUN-Pfad; Literal-IP, TURN/TLS, mDNS/LLMNR und physische DNS-Ausleitung sind
weiterhin keine bestandenen Abnahmen. Eigene Nachweise bleiben getrennt als
`baseline-stun-owned.log`, `mapped-stun-owned.log` und `stun-owned-summary.json`.
Die unveränderte offizielle Electron-44.7.2-Runtime startet im bisherigen
AppContainer-Versuch mit null Capabilities nicht bis `READY`. Auch isolierte
Profil-/Temp-Umgebungsvariablen ändern den Fehler nicht. Ein auf den synthetischen
Testprozess begrenzter Microsoft-Debugger erfasst `0x80000003`, anschließend
`0xC000001D`, bei Runtime-Offset `0x72ac2bb`. Ohne aufgelöste private Symbole
beweisen benachbarte Exportnamen keine Fehlerursache. Dieser Versuch beweist
weder Browser-Kompatibilität noch DNS-/Netzwerkisolation. Lokale Nachweise liegen
ignoriert unter `artifacts/stock-appcontainer-feasibility`.
Nächste Abnahme: funktionsfähige Windows-Netzwerkgrenze einschließlich DNS,
separater authentifizierter Broker mit tatsächlicher HTTPS-Inhaltsprüfung,
Integration ohne ungeprüfte Ausweichwege und gemeinsamer Installer-Test auf
einem System ohne Entwicklungsruntimes. Die bisherige Schutzmaßnahme bleibt
bis zum erfolgreichen Ersatznachweis aktiv; das MVP ist noch nicht abgenommen.
Der Nutzer hat am 13. Oktober 2026 den bisherigen breiten Funktionsumfang
ausdrücklich ersetzt. Dieses Ziel hat Vorrang vor älteren Aufgabenlisten:
Ein stabiler, zentral verwalteter Windows-Browser verhindert, dass vertrauliche
@@ -161,9 +348,14 @@ isolierten Build-Checkout als Commit
`6f8b80fef13bfdc9d9c70df567ebcad722d70467` angewendet, mit genau16 Dateien und
separatem Provenienzjournal. Er ist noch nicht kompiliert oder nativ abgenommen;
das ausgelieferte Runtime-Binary bleibt unverändert. Aktuelle Build-Grenzen:
ATL/MFC wird vom Visual-Studio-Komponentencheck nicht gefunden; der vollständige
Windows-Dependency-Sync scheiterte an einer protokollierten Antivirus-Ablehnung
des Chromium-Updater-Testpakets. Keine Antivirus-Ausnahme wurde vorgenommen.
ATL/MFC fehlte ursprünglich; die normale Microsoft-Installation ist inzwischen
abgeschlossen und `vswhere` bestätigt die Komponente in einer vollständigen,
startfähigen BuildTools-Instanz. Der ursprüngliche vollständige Windows-Sync
scheiterte an einer Antivirus-Ablehnung des Chromium-Updater-Testpakets. Ein
separat geprüftes CIPD-Manifest verarbeitete danach39 unveränderte Bindings mit
Exit0 und ließ ausschließlich die benannte Updater-Testfixture aus. Keine Antivirus-
Ausnahme wurde vorgenommen. Build-Hooks und GN-Targetabhängigkeiten werden
separat geprüft; ein vollständiger neuer Native-Build ist noch nicht belegt.
Quellkompatibilität und Strukturprüfungen ersetzen weder Binder-Unittests noch
die echten erlaubten und blockierten WebRTC-Übertragungen im neuen Binary.
+19
View File
@@ -74,11 +74,23 @@
"filter": [
"**/*"
]
},
{
"from": ".runtime/windows/broker-payload",
"to": "broker",
"filter": [
"**/*"
]
}
],
"files": [
"apps/browser/**/*",
"apps/network-broker/framed-channel.cjs",
"packages/core/**/*",
{
"from": ".runtime/windows/broker-payload.manifest.json",
"to": "apps/browser/broker-package-manifest.json"
},
"package.json"
],
"directories": {
@@ -90,6 +102,13 @@
],
"icon": "docs/brand/safetynet.ico"
},
"nsis": {
"perMachine": true,
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": false,
"include": "scripts/install/windows-security.nsh"
},
"mac": {
"target": [
"dmg"
+12 -6
View File
@@ -1,21 +1,27 @@
'use strict';
const {z}=require('zod');
const {FileFactsSchema}=require('./transfer-facts.cjs');
const {RequestFactsSchema}=require('./request-transfer-facts.cjs');
const BROKER_REASONS=Object.freeze(['broker-approved','broker-policy-block','broker-inspection-fault','broker-authority-expired','broker-audit-failed','broker-connect-denied','broker-auth-denied','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout','broker-upstream-failed','broker-response-invalid']);
const BrokerFactsSchema=z.object({requestId:z.string().uuid(),connectionId:z.string().uuid().optional(),phase:z.enum(['connect.block','request.authorized','request.block','request.fault','forward.fault']),reason:z.enum(BROKER_REASONS),authorizedEventId:z.string().uuid().optional()}).strict();
const BROKER_REASONS=Object.freeze(['broker-approved','broker-policy-block','broker-inspection-fault','broker-authority-expired','broker-audit-failed','broker-connect-denied','broker-auth-denied','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout','broker-upstream-failed','broker-response-invalid','broker-scan-timeout','broker-download-approved','broker-download-policy-block','broker-download-inspection-block']);
const BrokerFactsSchema=z.object({requestId:z.string().uuid(),connectionId:z.string().uuid().optional(),phase:z.enum(['connect.block','request.authorized','request.block','request.fault','forward.fault','response.scanned','response.block']),reason:z.enum(BROKER_REASONS),authorizedEventId:z.string().uuid().optional()}).strict();
const phaseReasons=Object.freeze({
'connect.block':['broker-connect-denied','broker-auth-denied','broker-framing-block','broker-authority-expired','broker-audit-failed','broker-request-timeout','broker-size-block','broker-request-aborted'],
'request.authorized':['broker-approved'],
'request.block':['broker-policy-block','broker-authority-expired','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout'],
'request.block':['broker-scan-timeout','broker-policy-block','broker-authority-expired','broker-framing-block','broker-size-block','broker-request-aborted','broker-request-timeout'],
'request.fault':['broker-inspection-fault','broker-audit-failed'],
'forward.fault':['broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired','broker-audit-failed']
'response.scanned':['broker-download-approved'],
'response.block':['broker-download-policy-block','broker-download-inspection-block'],
'forward.fault':['broker-scan-timeout','broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired','broker-audit-failed']
});
function validateBrokerSemantics(event,context){
if(!event.broker)return;
const value=event.broker;
if(!event.request||event.file||event.kind!==(value.phase==='request.authorized'?'upload.authorized':'upload.block')||!phaseReasons[value.phase].includes(value.reason)||value.phase==='forward.fault'&&!value.authorizedEventId||value.phase!=='forward.fault'&&value.authorizedEventId||value.authorizedEventId===event.eventId){
const download=event.kind==='download.scan'||event.kind==='download.block',linked=value.phase==='forward.fault'||value.phase.startsWith('response.');
const expectedKind=value.phase==='request.authorized'?'upload.authorized':value.phase==='response.scanned'?'download.scan':value.phase==='response.block'?'download.block':'upload.block';
if(download?(!event.file||event.request||!value.phase.startsWith('response.')):(!event.request||event.file||value.phase.startsWith('response.')))context.addIssue({code:'custom',path:['broker'],message:'Broker transfer facts rejected'});
if(event.kind!==expectedKind||!phaseReasons[value.phase].includes(value.reason)||linked&&!value.authorizedEventId||!linked&&value.authorizedEventId||value.authorizedEventId===event.eventId){
context.addIssue({code:'custom',path:['broker'],message:'Broker lifecycle semantics rejected'});
}
}
const BrokerAuditEventSchema=z.object({eventId:z.string().uuid(),kind:z.enum(['upload.authorized','upload.block']),ruleId:z.string().min(1).max(100),policyVersion:z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),host:z.string().max(253).optional(),request:RequestFactsSchema,broker:BrokerFactsSchema.optional(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional()}).strict().superRefine(validateBrokerSemantics);
const BrokerAuditEventSchema=z.object({eventId:z.string().uuid(),kind:z.enum(['upload.authorized','upload.block','download.scan','download.block']),ruleId:z.string().min(1).max(100),policyVersion:z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),host:z.string().max(253).optional(),request:RequestFactsSchema.optional(),file:FileFactsSchema.optional(),broker:BrokerFactsSchema.optional(),traceId:z.string().regex(/^[a-f0-9]{32}$/).optional(),spanId:z.string().regex(/^[a-f0-9]{16}$/).optional()}).strict().superRefine((event,context)=>{validateBrokerSemantics(event,context);const download=event.kind.startsWith('download.');if(download?(!event.file||event.request):(!event.request||event.file))context.addIssue({code:'custom',message:'Exclusive transfer facts required'});});
module.exports={BrokerAuditEventSchema,BrokerFactsSchema,BROKER_REASONS,validateBrokerSemantics};
+75
View File
@@ -0,0 +1,75 @@
'use strict';
const {createHash}=require('node:crypto');
const {detectType}=require('./file-transfer.cjs');
// Typed binary carriers supplement text normalization. No whole decoded file
// may be released on the strength of an allow verdict for its outer JSON text.
function extractEncodedDocuments(bytes,{maxDecodedBytes=16777216,maxParts=16,maxNodes=20000,maxDepth=32,jsonRequired=false}={}){
if(!Buffer.isBuffer(bytes)||bytes.length>16777216||![maxDecodedBytes,maxParts,maxNodes,maxDepth].every(Number.isSafeInteger)||maxDecodedBytes<1||maxDecodedBytes>16777216||maxParts<1||maxParts>64||maxNodes<1||maxNodes>20000||maxDepth<1||maxDepth>32)throw Error('encoded-document-configuration-invalid');
if(typeof jsonRequired!=='boolean')throw Error('encoded-document-configuration-invalid');
const parts=[],seen=new Set(),encodings=new Set();let total=0,nodes=0,complete=true;
const result=()=>Object.freeze({parts:Object.freeze(parts),complete,decodedBytes:total,visitedNodes:nodes,dispose});
const dispose=()=>{for(const part of parts)part.bytes.fill(0);};
function candidate(value){
if(!complete||typeof value!=='string')return;
let encoded=value.trim(),explicit=false,mime;
if(/^data:/i.test(encoded)){
const match=/^data:([a-z0-9.+/-]+)?(?:;[a-z0-9-]+=[a-z0-9._-]+)*;base64,([\s\S]*)$/i.exec(encoded);
if(!match){complete=false;return;}mime=(match[1]||'application/octet-stream').toLowerCase();encoded=match[2];explicit=true;
if(encoded.includes('%')){try{encoded=decodeURIComponent(encoded);}catch{complete=false;return;}}
}
if(!explicit&&encoded.length<16)return;
if(!/^[A-Za-z0-9+/_-]*={0,2}$/.test(encoded)||encoded.length%4===1){if(explicit)complete=false;return;}
const normalized=encoded.replace(/-/g,'+').replace(/_/g,'/').replace(/=+$/,'');
const encodingKey=createHash('sha256').update(normalized).update('\0'+(mime||'')).digest('hex');
if(encodings.has(encodingKey))return;
const projected=Math.floor(normalized.length*3/4);
if(total+projected>maxDecodedBytes){complete=false;return;}
const decoded=Buffer.from(normalized,'base64');
total+=decoded.length;encodings.add(encodingKey);
const type=detectType(decoded);
if(decoded.toString('base64').replace(/=+$/,'')!==normalized){decoded.fill(0);if(explicit||type!=='unknown')complete=false;return;}
if(!explicit&&type==='unknown'){decoded.fill(0);return;}
const digest=createHash('sha256').update(decoded).digest('hex');
const identity=digest+'\0'+(mime||'');
if(seen.has(identity)){decoded.fill(0);return;}
if(parts.length>=maxParts){decoded.fill(0);complete=false;return;}
seen.add(identity);parts.push(Object.freeze({bytes:decoded,...(mime?{mime}:{})}));
}
function scanText(value,percentDepth=0){
candidate(value);
const carriers=[];
for(const match of value.matchAll(/data:[^\s,"'<>]+,[A-Za-z0-9+/_=%-]+/gi)){if(++nodes>maxNodes){complete=false;break;}carriers.push([match.index,match.index+match[0].length]);candidate(match[0]);}
let carrier=0;
for(const match of value.matchAll(/[A-Za-z0-9+/_-]{16,}={0,2}/g)){
while(carrier<carriers.length&&carriers[carrier][1]<=match.index)carrier++;
if(carrier<carriers.length&&match.index>=carriers[carrier][0]&&match.index<carriers[carrier][1])continue;
if(++nodes>maxNodes){complete=false;break;}candidate(match[0]);
}
if(complete&&value.includes('%')){
if(percentDepth>=4){complete=false;return;}
try{const decoded=decodeURIComponent(value);if(decoded!==value)scanText(decoded,percentDepth+1);}catch{if(/data%3a/i.test(value))complete=false;}
}
}
try{
const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes),trimmed=text.trim();
if(jsonRequired||trimmed.startsWith('{')||trimmed.startsWith('[')){
let root;try{root=JSON.parse(trimmed);}catch{if(jsonRequired)complete=false;else scanText(trimmed);return result();}
const stack=[{value:root,depth:0}];
while(stack.length&&complete){const {value,depth}=stack.pop();if(++nodes>maxNodes||depth>maxDepth){complete=false;break;}
if(typeof value==='string')scanText(value);
else if(Array.isArray(value)){
if(value.length>maxNodes-nodes||stack.length+value.length>maxNodes){complete=false;break;}
for(const child of value)stack.push({value:child,depth:depth+1});
}else if(value&&typeof value==='object'){
// Avoid Object.entries allocating an unbounded array of key/value pairs
// before the traversal budget is checked on a hostile JSON collection.
for(const key in value){if(!Object.hasOwn(value,key))continue;scanText(key);stack.push({value:value[key],depth:depth+1});if(stack.length>maxNodes||!complete){complete=false;break;}}
}
}
}else{
scanText(trimmed);
}
}catch{complete=false;}
return result();
}
module.exports={extractEncodedDocuments};
+2 -2
View File
@@ -20,7 +20,7 @@ export function toECS(event,installationId='safetynet') {
'@timestamp':event.time,ecs:{version:'8.17.0'},
event:{id:`${installationId}:${event.id}`,kind:alert?'alert':'event',module:'safetynet',dataset:'safetynet.security',
action:event.kind,category:[alert?'intrusion_detection':auth?'authentication':config?'configuration':clipboard?'process':'web'],
type:[transportEvent||alert||interaction?.outcome==='observed'?'info':failure||brokerFailure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:transportEvent||interaction?.outcome==='observed'||broker?.phase==='request.authorized'?'unknown':failure||brokerFailure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
type:[transportEvent||alert||interaction?.outcome==='observed'?'info':failure||brokerFailure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:transportEvent||interaction?.outcome==='observed'||broker?.phase==='request.authorized'?'unknown':failure||brokerFailure||broker?.phase==='response.block'?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
observer:{vendor:'SafetyNet',product:'Managed Enterprise Browser',type:'dlp',version:'0.1.0'},
...(event.device_id?{host:{id:event.device_id}}:{}),
...(m.actor?{user:{id:m.actor}}:{}),...(m.host&&(!transportEvent||transport)?{destination:{domain:m.host}}:{}),
@@ -53,7 +53,7 @@ export function toCEF(event,installationId='safetynet') {
...(event.kind.startsWith('download.')&&facts(m)?{cn2Label:'FileBytes',cn2:facts(m).bytes,cs6Label:'DetectedFileType',cs6:facts(m).detectedType,fileHash:facts(m).sha256}:{}),
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{cs4Label:'TraceId',cs4:m.traceId}:{}),
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{cs5Label:'SpanId',cs5:m.spanId}:{}),
...(broker?{cs2Label:'BrokerLifecycle',cs2:JSON.stringify(broker),cs3Label:'BrokerEventId',cs3:broker.eventId,requestContext:broker.requestId,reason:broker.reason,outcome:broker.phase==='request.authorized'?'unknown':['request.fault','forward.fault'].includes(broker.phase)?'failure':'success'}:{}),
...(broker?{cs2Label:'BrokerLifecycle',cs2:JSON.stringify(broker),cs3Label:'BrokerEventId',cs3:broker.eventId,requestContext:broker.requestId,reason:broker.reason,outcome:broker.phase==='request.authorized'?'unknown':['request.fault','forward.fault','response.block'].includes(broker.phase)?'failure':'success'}:{}),
...(alert?{cs2Label:'SourceEventId',cs2:`${installationId}:${m.sourceEventId}`,cs3Label:'AlertLevel',cs3:m.level}:{})};
return `CEF:0|SafetyNet|Enterprise Browser|0.1.0|${header(event.kind)}|${header(event.kind)}|${alert?severity(m.level):!transportEvent&&blocked?7:3}|`+Object.entries(fields).map(([k,v])=>`${k}=${ext(v)}`).join(' ');
}
+71
View File
@@ -0,0 +1,71 @@
'use strict';
// Signed external manifests avoid embedding the final browser hash in its own
// native helper. This classical P-256 contract is not a post-quantum signature.
const {createPublicKey,createPrivateKey,createHash,sign,verify}=require('node:crypto');
const {isIP}=require('node:net');
const MAX_RELEASE_BYTES=2097152,MAX_DEPLOYMENT_BYTES=262144;
const DOMAINS=Object.freeze({release:'SafetyNet Windows release v1\0',deployment:'SafetyNet Windows deployment v1\0'});
const verifiedReleases=new WeakSet(),verifiedDeployments=new WeakSet();
const MANAGEMENT_PROTOCOL=require('./policy-channel.cjs').VERSION;
const UUID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
function fail(code){throw Object.assign(new Error(code),{code});}
function exact(value,keys){return value&&typeof value==='object'&&!Array.isArray(value)&&Object.getPrototypeOf(value)===Object.prototype&&Object.keys(value).sort().join(',')===keys.slice().sort().join(',');}
function positive(value){return Number.isSafeInteger(value)&&value>0;}
function relative(value){return typeof value==='string'&&value.length<=240&&value.split('/').every(part=>/^[A-Za-z0-9_.-]+$/.test(part)&&part!=='.'&&part!=='..'&&!/[. ]$/.test(part)&&!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part));}
function freeze(value){if(value&&typeof value==='object'){for(const item of Object.values(value))freeze(item);Object.freeze(value);}return value;}
function canonical(value){if(Array.isArray(value))return '['+value.map(canonical).join(',')+']';if(value&&typeof value==='object')return '{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}';return JSON.stringify(value);}
function publicKey(pem,type){
if(typeof pem!=='string'||pem.length>(type==='ml-dsa-87'?20000:4096)||!pem.startsWith('-----BEGIN PUBLIC KEY-----\n'))fail('windows-trust-key-invalid');
const key=createPublicKey(pem);if(key.export({format:'pem',type:'spki'})!==pem||key.asymmetricKeyType!==type||type==='ec'&&key.asymmetricKeyDetails?.namedCurve!=='prime256v1')fail('windows-trust-key-invalid');return key;
}
function releaseSchema(value){
if(!exact(value,['format','releaseId','releaseSequence','serviceName','deploymentTenant','deploymentPublicKey','files'])||value.format!=='safetynet-windows-release-v1'||!UUID.test(value.releaseId)||!UUID.test(value.deploymentTenant)||!positive(value.releaseSequence)||value.serviceName!=='SafetyNetNetworkBroker'||!Array.isArray(value.files)||!value.files.length||value.files.length>10000)fail('windows-release-invalid');
publicKey(value.deploymentPublicKey,'ec');let previous='';const seen=new Set();
for(const file of value.files){if(!exact(file,['path','bytes','sha256'])||!relative(file.path)||!Number.isSafeInteger(file.bytes)||file.bytes<0||!/^[a-f0-9]{64}$/.test(file.sha256)||file.path<=previous||seen.has(file.path.toLowerCase()))fail('windows-release-invalid');previous=file.path;seen.add(file.path.toLowerCase());}
for(const required of ['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py'])if(!value.files.some(file=>file.path===required))fail('windows-release-invalid');
if(!value.files.some(file=>file.path.startsWith('resources/semantic/'))||value.files.some(file=>['release-envelope.json','deployment-envelope.json'].includes(file.path)))fail('windows-release-invalid');
return value;
}
function managementSchema(value){
if(!exact(value,['origin','signatureKey','protocol'])||typeof value.origin!=='string'||value.origin.length>2048||[...value.origin].some(character=>character.charCodeAt(0)<=32||character.charCodeAt(0)>=127)||value.protocol!==MANAGEMENT_PROTOCOL)fail('windows-management-invalid');
const url=new URL(value.origin);
if(url.protocol!=='https:'||url.origin!==value.origin||url.username||url.password||url.search||url.hash||url.pathname!=='/'||url.hostname.endsWith('.'))fail('windows-management-invalid');
const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname;
if(!isIP(hostname)&&(hostname.length>253||hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)||/^0x[0-9a-f]+$/i.test(label))))fail('windows-management-invalid');
publicKey(value.signatureKey,'ml-dsa-87');return value;
}
function deploymentSchema(value){
const v2=value?.format==='safetynet-windows-deployment-v2';
if(!exact(value,['format','releaseId','deploymentTenant','deploymentSequence','policyPublicKey','expiresAt','users',...(v2?['management']:[])])||!['safetynet-windows-deployment-v1','safetynet-windows-deployment-v2'].includes(value.format)||!UUID.test(value.releaseId)||!UUID.test(value.deploymentTenant)||!positive(value.deploymentSequence)||!positive(value.expiresAt)||!Array.isArray(value.users)||!value.users.length||value.users.length>1024)fail('windows-deployment-invalid');
if(v2)managementSchema(value.management);
publicKey(value.policyPublicKey,'ed25519');let previous='';const devices=new Set();
for(const user of value.users){if(!exact(user,['sid','deviceId','minimumPolicyVersion'])||typeof user.sid!=='string'||!/^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/.test(user.sid)||user.sid.split('-').slice(4).some(part=>Number(part)>4294967295)||user.sid<=previous||typeof user.deviceId!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(user.deviceId)||devices.has(user.deviceId)||!positive(user.minimumPolicyVersion))fail('windows-deployment-invalid');previous=user.sid;devices.add(user.deviceId);}
return value;
}
function decodeEnvelope(envelope,kind,key){
const cap=kind==='release'?MAX_RELEASE_BYTES:MAX_DEPLOYMENT_BYTES;
if(!exact(envelope,['payload','signature'])||typeof envelope.payload!=='string'||envelope.payload.length>Math.ceil(cap*4/3)||! /^[A-Za-z0-9_-]+$/.test(envelope.payload)||typeof envelope.signature!=='string'||! /^[A-Za-z0-9_-]{86}$/.test(envelope.signature))fail('windows-trust-envelope-invalid');
const bytes=Buffer.from(envelope.payload,'base64url'),signature=Buffer.from(envelope.signature,'base64url');
if(bytes.length>cap||bytes.toString('base64url')!==envelope.payload||signature.length!==64||signature.toString('base64url')!==envelope.signature||!verify('sha256',Buffer.concat([Buffer.from(DOMAINS[kind]),bytes]),{key,dsaEncoding:'ieee-p1363'},signature))fail('windows-trust-signature-invalid');
const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes),value=JSON.parse(text);if(canonical(value)!==text)fail('windows-trust-canonical-invalid');
return {value,digest:createHash('sha256').update(bytes).digest('hex')};
}
function checkFloor(sequence,digest,{minimumSequence=0,acceptedDigest}={}){
if(!Number.isSafeInteger(minimumSequence)||minimumSequence<0||acceptedDigest!==undefined&&!/^[a-f0-9]{64}$/.test(acceptedDigest))fail('windows-trust-floor-invalid');
if(sequence<minimumSequence||sequence===minimumSequence&&acceptedDigest!==undefined&&digest!==acceptedDigest)fail('windows-trust-rollback');
}
function verifyReleaseEnvelope(envelope,{publicKey:anchor,floor}={}){
try{const decoded=decodeEnvelope(envelope,'release',publicKey(anchor,'ec'));releaseSchema(decoded.value);checkFloor(decoded.value.releaseSequence,decoded.digest,floor);const result=freeze({manifest:decoded.value,digest:decoded.digest});verifiedReleases.add(result);return result;}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-release-invalid');}
}
function verifyDeploymentEnvelope(envelope,{release,floor,now=Date.now(),authenticatedSid}={}){
try{if(!verifiedReleases.has(release)||!Number.isSafeInteger(now)||now<0)fail('windows-deployment-invalid');const decoded=decodeEnvelope(envelope,'deployment',publicKey(release.manifest.deploymentPublicKey,'ec'));deploymentSchema(decoded.value);const value=decoded.value;if(value.releaseId!==release.manifest.releaseId||value.deploymentTenant!==release.manifest.deploymentTenant||value.expiresAt<=now)fail('windows-deployment-binding-invalid');checkFloor(value.deploymentSequence,decoded.digest,floor);const user=value.users.find(item=>item.sid===authenticatedSid);if(!user)fail('windows-deployment-user-denied');const result=freeze({manifest:value,digest:decoded.digest,bootstrap:{publicKey:value.policyPublicKey,deviceId:user.deviceId,minimumVersion:user.minimumPolicyVersion}});verifiedDeployments.add(result);return result;}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-deployment-invalid');}
}
function verifiedManagementBinding(deployment){
if(!verifiedDeployments.has(deployment)||deployment.manifest.format!=='safetynet-windows-deployment-v2'||deployment.manifest.expiresAt<=Date.now())fail('windows-management-denied');
return deployment.manifest.management;
}
function signManifest(value,privatePem,kind){
try{if(!Object.hasOwn(DOMAINS,kind))fail('windows-trust-kind-invalid');(kind==='release'?releaseSchema:deploymentSchema)(value);const payload=Buffer.from(canonical(value)),key=createPrivateKey(privatePem);if(key.asymmetricKeyType!=='ec'||key.asymmetricKeyDetails?.namedCurve!=='prime256v1')fail('windows-trust-key-invalid');if(payload.length>(kind==='release'?MAX_RELEASE_BYTES:MAX_DEPLOYMENT_BYTES))fail('windows-trust-envelope-invalid');return Object.freeze({payload:payload.toString('base64url'),signature:sign('sha256',Buffer.concat([Buffer.from(DOMAINS[kind]),payload]),{key,dsaEncoding:'ieee-p1363'}).toString('base64url')});}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-trust-signing-invalid');}
}
function publicKeyCoordinates(pem){try{const key=publicKey(pem,'ec'),jwk=key.export({format:'jwk'});return Buffer.concat([Buffer.from(jwk.x,'base64url'),Buffer.from(jwk.y,'base64url')]);}catch{fail('windows-trust-key-invalid');}}
module.exports={verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest,publicKeyCoordinates,verifiedManagementBinding,DOMAINS};
+95
View File
@@ -0,0 +1,95 @@
// Preparation for a separately packaged broker; NOT wired into production yet.
// The expected manifest must be embedded in the integrity-checked application,
// never accepted from the writable broker directory. Hashes alone do not replace
// installation ACLs, signed distribution, or a handle-bound process launch.
import {lstat, readdir, open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
const MAX_FILES=4096;
function fail(code,failureFacts){throw Object.assign(new Error(code),{code,...(failureFacts?{failureFacts}:{})});}
function version(value){return typeof value==='string'&&/^\d{1,5}\.\d{1,5}\.\d{1,5}$/.test(value);}
function relative(value){
return typeof value==='string'&&value.length<=240&&!value.includes('\\')&&value.split('/').every(segment=>
/^[a-zA-Z0-9_.-]+$/.test(segment)&&segment!=='.'&&segment!=='..'&&!/[. ]$/.test(segment)&&
!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment));
}
function validate(manifest,expected){
if(!manifest||manifest.format!=='safetynet-broker-package-v1'||!version(manifest.version)||
!Number.isSafeInteger(manifest.protocol)||manifest.protocol<1||manifest.protocol>65535||
!relative(manifest.entrypoint)||!manifest.entrypoint.endsWith('.exe')||
!Array.isArray(manifest.files)||!manifest.files.length||manifest.files.length>MAX_FILES)fail('broker_manifest_invalid');
if(expected&&(manifest.version!==expected.version||manifest.protocol!==expected.protocol))fail('broker_package_incompatible');
const names=new Set();
for(const file of manifest.files){
if(!file||!relative(file.path)||!Number.isSafeInteger(file.bytes)||file.bytes<0||
!/^[a-f0-9]{64}$/.test(file.sha256)||names.has(file.path.toLowerCase()))fail('broker_manifest_invalid');
names.add(file.path.toLowerCase());
}
if(!names.has(manifest.entrypoint.toLowerCase()))fail('broker_manifest_invalid');
}
async function inventory(root){
const files=[];let entries=0;
// Reject symlinks/junctions at every ancestor, including the supplied root.
for(let current=path.resolve(root);;current=path.dirname(current)){
const stat=await lstat(current);
if(stat.isSymbolicLink()||!stat.isDirectory())fail('broker_path_unsafe');
if(path.dirname(current)===current)break;
}
async function walk(directory,prefix=''){
for(const item of await readdir(directory,{withFileTypes:true})){
if(++entries>MAX_FILES)fail('broker_package_too_large');
const name=prefix+item.name;
if(!relative(name))fail('broker_path_unsafe');
const stat=await lstat(path.join(root,...name.split('/')));
if(stat.isSymbolicLink())fail('broker_path_unsafe');
if(stat.isDirectory())await walk(path.join(directory,item.name),name+'/');
else if(stat.isFile()){
files.push(name);if(files.length>MAX_FILES)fail('broker_package_too_large');
}else fail('broker_path_unsafe');
}
}
await walk(root);
if(new Set(files.map(file=>file.toLowerCase())).size!==files.length)fail('broker_path_unsafe');
return files.sort();
}
async function digest(root,name){
const handle=await open(path.join(root,...name.split('/')),'r');
try{
const before=await handle.stat();if(!before.isFile())fail('broker_path_unsafe');
const hash=createHash('sha256');let bytes=0;
for await(const chunk of handle.createReadStream({autoClose:false})){hash.update(chunk);bytes+=chunk.length;}
const after=await handle.stat();
const changed=[];
if(before.size!==bytes)changed.push('size-before-read');
if(after.size!==bytes)changed.push('size-after-read');
if(before.mtimeMs!==after.mtimeMs)changed.push('modification-time');
if(before.ctimeMs!==after.ctimeMs)changed.push('change-time');
if(before.dev!==after.dev||before.ino!==after.ino)changed.push('file-identity');
// Fixed bounded facts preserve the reason for a failed check without paths,
// file content, raw timestamps, OS error text or automatic retry.
if(changed.length)fail('broker_package_changed',Object.freeze({changed:Object.freeze(changed)}));
return {path:name,bytes,sha256:hash.digest('hex')};
}finally{await handle.close();}
}
export async function buildBrokerPackageManifest(root,{version:release,protocol,entrypoint}){
try{
const manifest={format:'safetynet-broker-package-v1',version:release,protocol,entrypoint,files:[]};
// Validate metadata before touching executable payloads.
validate({...manifest,files:[{path:entrypoint,bytes:0,sha256:'0'.repeat(64)}]});
for(const name of await inventory(root))manifest.files.push(await digest(root,name));
validate(manifest);return manifest;
}catch(error){if(error.code?.startsWith('broker_'))throw error;fail('broker_package_unavailable');}
}
export async function verifyBrokerPackage(root,trustedManifest,{version:release,protocol}){
try{
// Copy before await so a caller cannot mutate the trusted contract mid-check.
const manifest=structuredClone(trustedManifest);validate(manifest,{version:release,protocol});
const actual=await inventory(root),expected=manifest.files.map(file=>file.path).sort();
if(JSON.stringify(actual)!==JSON.stringify(expected))fail('broker_package_inventory_mismatch');
for(const file of manifest.files){const observed=await digest(root,file.path);
if(observed.bytes!==file.bytes||observed.sha256!==file.sha256)fail('broker_package_integrity_mismatch');}
// Verification is an installation/startup check, not an atomic launch lease.
return Object.freeze({version:manifest.version,protocol:manifest.protocol,entrypoint:path.join(path.resolve(root),...manifest.entrypoint.split('/')),files:manifest.files.length});
}catch(error){if(error.code?.startsWith('broker_'))throw error;fail('broker_package_unavailable');}
}
+85
View File
@@ -0,0 +1,85 @@
// Compile an owned native SCM helper; never installs services or Windows rules.
import {spawn,execFileSync} from 'node:child_process';
import {readFile,writeFile,mkdir,lstat,realpath} from 'node:fs/promises';
import {createHash,generateKeyPairSync,sign} from 'node:crypto';
import {createRequire} from 'node:module';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
const args=process.argv.slice(2),compileOnly=args.length===1&&args[0]==='--compile-only',selfTest=args.length===1&&(args[0]==='--self-test'||compileOnly);
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
const fail=()=>{throw Error('native-service-build-invalid');};
const {signManifest,publicKeyCoordinates}=createRequire(import.meta.url)('../packages/core/windows-trust.cjs');
const {encodeAcceptanceState}=createRequire(import.meta.url)('../apps/network-broker/service-acceptance.cjs');
async function safe(directory){const info=await lstat(directory);if(info.isSymbolicLink()||await realpath(directory)!==directory)fail();}
async function run(executable,arguments_,expectedCode=0){const started=performance.now();return await new Promise((resolve,reject)=>{const child=spawn(executable,arguments_,{cwd:root,windowsHide:true,stdio:['ignore','pipe','pipe'],shell:false});let bytes=0;for(const stream of [child.stdout,child.stderr])stream.on('data',chunk=>{bytes+=chunk.length;if(bytes>65536){child.kill();reject(Error('native-build-output-limit'));}else process.stdout.write(chunk);});const timer=setTimeout(()=>{child.kill();reject(Error('native-build-timeout'));},60000);child.once('error',error=>{error.nativeStage=path.basename(executable);reject(error);});child.once('exit',code=>{clearTimeout(timer);(code===expectedCode)?resolve({code,milliseconds:performance.now()-started}):reject(Error('native-build-failed'));});});}
async function main(){
if(process.platform!=='win32'||!selfTest)fail();
const output=path.join(root,'artifacts/native-service-host');await mkdir(output,{recursive:true});await safe(path.join(root,'artifacts'));await safe(output);
const vswhere='C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe';
const installation=execFileSync(vswhere,['-latest','-products','*','-requires','Microsoft.VisualStudio.Component.VC.Tools.x86.x64','-property','installationPath'],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();if(!installation)fail();
const version=(await readFile(path.join(installation,'VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt'),'utf8')).trim();if(!/^\d+\.\d+\.\d+$/.test(version))fail();
const tool=path.join(installation,'VC/Tools/MSVC',version),sdk='C:/Program Files (x86)/Windows Kits/10',sdkVersion='10.0.26100.0';
const vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),policy=generateKeyPairSync('ed25519');
const publicPem=key=>key.export({format:'pem',type:'spki'}),privatePem=key=>key.export({format:'pem',type:'pkcs8'});
const header='static constexpr char RELEASE_ANCHOR_XY[]="'+publicKeyCoordinates(publicPem(vendor.publicKey)).toString('hex')+'";';
await writeFile(path.join(output,'service-pins.h'),header);
const source=path.join(root,'apps/network-broker/native/service-host.cpp'),nativeHeader=path.join(root,'apps/network-broker/native/signed-manifest.h'),stateHeader=path.join(root,'apps/network-broker/native/acceptance-state.h'),topologyHeader=path.join(root,'apps/network-broker/native/wfp-topology.h'),credentialHeader=path.join(root,'apps/network-broker/native/management-credential.h'),beforeCredentialHeader=hash(await readFile(credentialHeader)),before=hash(await readFile(source)),beforeHeader=hash(await readFile(nativeHeader)),beforeStateHeader=hash(await readFile(stateHeader)),beforeTopologyHeader=hash(await readFile(topologyHeader));
const executable=path.join(output,'safetynet-service-host-test.exe');
const compiler=path.join(tool,'bin/Hostx64/x64/cl.exe');
const arguments_=['/nologo','/std:c++17','/W4','/WX','/EHsc','/O2','/MT','/guard:cf','/DUNICODE','/D_UNICODE','/DSAFETYNET_NATIVE_SELF_TEST',... [output,path.join(tool,'include'),...['ucrt','um','shared'].map(v=>path.join(sdk,'Include',sdkVersion,v))].map(v=>'/I'+v),'/Fo'+path.join(output,'service-host.obj'),'/Fe'+executable,source,'/link','/DYNAMICBASE','/NXCOMPAT','/HIGHENTROPYVA',... [path.join(tool,'lib/x64'),...['ucrt','um'].map(v=>path.join(sdk,'Lib',sdkVersion,v,'x64'))].map(v=>'/LIBPATH:'+v)];
const stages={};stages.compile=await run(compiler,arguments_);process.stdout.write('native.compile.completed\n');const compiledStat=await lstat(executable);process.stdout.write('native.compile.output.bytes='+compiledStat.size+'\n');const compiledHash=hash(await readFile(executable));process.stdout.write('native.compile.output.sha256='+compiledHash+'\n');if(hash(await readFile(source))!==before||hash(await readFile(nativeHeader))!==beforeHeader||hash(await readFile(stateHeader))!==beforeStateHeader||hash(await readFile(topologyHeader))!==beforeTopologyHeader||hash(await readFile(credentialHeader))!==beforeCredentialHeader)fail();if(compileOnly){await writeFile(path.join(output,'current-compile-only-proof.json'),JSON.stringify({compiled:true,selfTestExecuted:false,installed:false,wfpActivated:false,helperSha256:compiledHash,sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,stages},null,2));return;}process.stdout.write('native.selftest.starting\n');stages.pipeJob=await run(executable,['--self-test']);stages.credentialSelection=await run(executable,['--verify-credential-selection']);
const helper=await readFile(executable),releaseId='11111111-1111-1111-1111-111111111111',deploymentTenant='22222222-2222-2222-2222-222222222222';
const fixturePaths=['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py','resources/semantic/synthetic.bin'];
const files=[{path:'safetynet-service-host-test.exe',bytes:helper.length,sha256:hash(helper)}];
for(const name of fixturePaths){const destination=path.join(output,name);await mkdir(path.dirname(destination),{recursive:true});const synthetic=Buffer.from('fixed native signed inventory fixture');await writeFile(destination,synthetic);files.push({path:name,bytes:synthetic.length,sha256:hash(synthetic)});}
files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
const release={format:'safetynet-windows-release-v1',releaseId,releaseSequence:2,serviceName:'SafetyNetNetworkBroker',deploymentTenant,deploymentPublicKey:publicPem(customer.publicKey),files};
const deployment={format:'safetynet-windows-deployment-v1',releaseId,deploymentTenant,deploymentSequence:2,policyPublicKey:publicPem(policy.publicKey),expiresAt:Date.now()+60000,users:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',minimumPolicyVersion:3}]};
const releaseEnvelope=signManifest(release,privatePem(vendor.privateKey),'release'),deploymentEnvelope=signManifest(deployment,privatePem(customer.privateKey),'deployment');
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));
stages.trustPositive=await run(executable,['--verify-fixture']);
// Native negatives are genuinely customer-signed, intentionally bypassing
// JS schema validation so the independent C++ parser must reject them.
const canonical=value=>Array.isArray(value)?'['+value.map(canonical).join(',')+']':value&&typeof value==='object'?'{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}':JSON.stringify(value);
const rawDeployment=value=>{const bytes=Buffer.from(canonical(value));return {payload:bytes.toString('base64url'),signature:sign('sha256',Buffer.concat([Buffer.from('SafetyNet Windows deployment v1\0'),bytes]),{key:customer.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64url')};};
const pq=generateKeyPairSync('ml-dsa-87'),wrongPq=generateKeyPairSync('ml-dsa-65');
const management={origin:'https://management.example',signatureKey:publicPem(pq.publicKey),protocol:'safetynet-policy-mlkem1024-mldsa87-v1'};
const deploymentV2={...deployment,format:'safetynet-windows-deployment-v2',management};
stages.managementV2={};
for(const [index,origin] of ['https://management.example','https://management.example:8443','https://127.0.0.1','https://[::1]','https://[::ffff:c0a8:1]'].entries()){
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deploymentV2,management:{...management,origin}},privatePem(customer.privateKey),'deployment')));
stages.managementV2['positive'+index]=await run(executable,['--verify-fixture']);
}
const invalidManagement=[...['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://management.example:','https://[::1]:','https://[0:0:0:0:0:0:0:1]','https://[::ffff:192.168.0.1]','https://127.1','https://bad_host.example'].map(origin=>({...management,origin})),{...management,protocol:'safetynet-policy-mlkem768-mldsa65-v1'},{...management,signatureKey:publicPem(wrongPq.publicKey)},{...management,signatureKey:publicPem(policy.publicKey)},{...management,signatureKey:management.signatureKey.trim()},{...management,token:'synthetic-denied'}];
for(const [index,value] of invalidManagement.entries()){
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,management:value})));
stages.managementV2['invalid'+index]=await run(executable,['--verify-fixture'],86);
}
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v3'})));stages.managementV2.unknownFormat=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v1'})));stages.managementV2.v1ExtraCapability=await run(executable,['--verify-fixture'],86);
const signedV2=signManifest(deploymentV2,privatePem(customer.privateKey),'deployment');
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify({...signedV2,payload:Buffer.from(canonical({...deploymentV2,management:{...management,origin:'https://attacker.example'}})).toString('base64url')}));stages.managementV2.signatureTamper=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.managementV2.v1HistoricalPreserved=await run(executable,['--verify-fixture']);
const changedPayload=JSON.parse(Buffer.from(releaseEnvelope.payload,'base64url'));changedPayload.releaseSequence=3;
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify({...releaseEnvelope,payload:Buffer.from(JSON.stringify(changedPayload)).toString('base64url')}));stages.signatureTamper=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,deploymentPublicKey:publicPem(vendor.publicKey)},privatePem(vendor.privateKey),'release')));stages.wrongDelegatedKey=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
stages.uninstalledSnapshotRejected=await run(executable,['--trust-snapshot'],5);
stages.uninstalledCommitRejected=await run(executable,['--commit-acceptance'],5);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deployment,expiresAt:Date.now()-1},privatePem(customer.privateKey),'deployment')));stages.expiredDeployment=await run(executable,['--verify-fixture'],86);
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.uninstalledClientRejected=await run(executable,['--client-stdio'],5);
stages.sameSequenceDigestRejected=await run(executable,['--verify-equivocation'],86);
const manyFiles=[...files,...Array.from({length:500},(_,i)=>({path:`resources/semantic/synthetic-${String(i).padStart(4,'0')}.bin`,bytes:0,sha256:'0'.repeat(64)}))].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,files:manyFiles},privatePem(vendor.privateKey),'release')));stages.largeSignedInventory=await run(executable,['--verify-fixture']);
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
const oldState={format:'safetynet-service-floors-v1',release:{minimumSequence:2,acceptedDigest:hash(Buffer.from(releaseEnvelope.payload,'base64url'))},deployment:{tenant:deploymentTenant,minimumSequence:2,acceptedDigest:hash(Buffer.from(deploymentEnvelope.payload,'base64url'))},policies:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',policyKeySha256:hash(policy.publicKey.export({format:'der',type:'spki'})),minimumVersion:3,acceptedDigest:'a'.repeat(64)}]};
await writeFile(path.join(output,'acceptance-old.json'),encodeAcceptanceState(oldState));
const nextState=structuredClone(oldState);nextState.policies[0].minimumVersion=4;nextState.policies[0].acceptedDigest='b'.repeat(64);
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(nextState));stages.monotonicState=await run(executable,['--verify-state-fixture']);
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState({...nextState,policies:[]}));stages.policyDropRejected=await run(executable,['--verify-state-fixture'],86);
const lowered=structuredClone(nextState);lowered.policies[0].minimumVersion=2;await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(lowered));stages.policyRollbackRejected=await run(executable,['--verify-state-fixture'],86);
const equivocation=structuredClone(oldState);equivocation.policies[0].acceptedDigest='b'.repeat(64);await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(equivocation));stages.policyEquivocationRejected=await run(executable,['--verify-state-fixture'],86);
await writeFile(path.join(output,'proof.json'),JSON.stringify({sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,helperSha256:hash(await readFile(executable)),selfTest:true,installed:false,wfpActivated:false,msvc:version,sdk:sdkVersion,stages},null,2));
}
main().catch(error=>{process.stderr.write('native-service-build-failed'+(typeof error?.nativeStage==='string'&&/^[a-zA-Z0-9.-]+$/.test(error.nativeStage)?':'+error.nativeStage:'')+(typeof error?.code==='string'&&/^[A-Z_]+$/.test(error.code)?':'+error.code:'')+(typeof error?.syscall==='string'&&/^[a-z_]+$/.test(error.syscall)?':'+error.syscall:'')+'\n');process.exitCode=1;});
+14
View File
@@ -0,0 +1,14 @@
; Called after resources are copied into the protected per-machine location.
; nsExec hides the console and bounds captured output by NSIS_MAX_STRLEN.
; Its timeout is an inactivity timeout; the trusted Python helper also owns
; a hard wall-clock deadline and kill-on-close Job for its grant descendants.
!macro customInstall
nsExec::ExecToStack /TIMEOUT=245000 '"$INSTDIR\resources\python\python.exe" -I -B "$INSTDIR\resources\inspector\provision_runtime_acl.py" --provision'
Pop $0
Pop $1
StrCpy $1 ""
StrCmp $0 "0" safetynet_runtime_acl_ready
SetErrorLevel 1
Abort "SafetyNet could not establish the required scanner security permissions. Installation stopped."
safetynet_runtime_acl_ready:
!macroend
+69
View File
@@ -0,0 +1,69 @@
// Mandatory component packaging only; this does not activate a broker or
// install/attest its Windows network boundary.
import {createRequire} from 'node:module';
import {randomUUID} from 'node:crypto';
import {createHash} from 'node:crypto';
import {createReadStream} from 'node:fs';
import {readdir,lstat} from 'node:fs/promises';
import path from 'node:path';
import {stageBrokerPayload,BROKER_PAYLOAD,verifySourceSnapshot} from './stage-broker-payload.mjs';
import {verifyBrokerRuntime} from './provision-broker-runtime.mjs';
import {verifyBrokerPackage} from './broker-package-contract.mjs';
const require=createRequire(import.meta.url);
const RESOURCE_FROM='.runtime/windows/broker-payload',MANIFEST_FROM=RESOURCE_FROM+'.manifest.json';
const EMBEDDED_MANIFEST='apps/browser/broker-package-manifest.json';
function fail(code){throw Object.assign(new Error(code),{code});}
export function brokerResourceConfiguration(build,{payloadRoot,manifestPath}){
if(!build||!Array.isArray(build.extraResources)||!Array.isArray(build.files))fail('broker_package_configuration_missing');
const destination=value=>typeof value==='string'?value.replaceAll('\\','/').replace(/\/+$/,'').toLowerCase():'';
const resources=build.extraResources.filter(item=>item&&typeof item==='object'&&(destination(item.to)==='broker'||destination(item.to).startsWith('broker/')));
const manifests=build.files.filter(item=>item&&typeof item==='object'&&destination(item.to)===EMBEDDED_MANIFEST);
if(resources.length!==1||resources[0].from!==RESOURCE_FROM||manifests.length!==1||manifests[0].from!==MANIFEST_FROM)fail('broker_package_configuration_missing');
if(!path.isAbsolute(payloadRoot)||!path.isAbsolute(manifestPath))fail('broker_package_configuration_invalid');
return {...structuredClone(build),extraResources:build.extraResources.map(item=>item===resources[0]?{from:payloadRoot,to:'broker',filter:['**/*']}:structuredClone(item)),files:build.files.map(item=>item===manifests[0]?{from:manifestPath,to:EMBEDDED_MANIFEST}:structuredClone(item))};
}
export async function prepareBrokerResources({root,build,dependencyArchive}){
await verifyBrokerRuntime(path.join(root,'.runtime/windows/broker'));
const destination=path.join(root,'.runtime/windows','broker-payload-package-'+randomUUID());
const staged=await stageBrokerPayload({root,destination,dependencyArchive});
const configuration=brokerResourceConfiguration(build,{payloadRoot:staged.root,manifestPath:staged.root+'.manifest.json'});
return {configuration,manifest:staged.manifest,sourceSnapshot:staged.sources,
verifySources:()=>verifySourceSnapshot(root,staged.sources)};
}
export async function verifyPackagedBroker({resources,expectedManifest,readEmbeddedManifest}){
if(typeof readEmbeddedManifest!=='function')fail('broker_package_manifest_reader_required');
// Production supplies the ASAR extractor after embedded ASAR integrity/fuse
// verification. Never read expected state from resources/broker metadata.
let embedded;try{const bytes=await readEmbeddedManifest(EMBEDDED_MANIFEST);if(!Buffer.isBuffer(bytes)||bytes.length>1048576)fail('broker_package_manifest_invalid');embedded=JSON.parse(bytes.toString('utf8'));}catch{fail('broker_package_manifest_invalid');}
if(JSON.stringify(embedded)!==JSON.stringify(expectedManifest))fail('broker_package_manifest_mismatch');
return verifyBrokerPackage(path.join(resources,'broker'),embedded,BROKER_PAYLOAD);
}
export function embeddedBrokerManifestReader(archive){
const {extractFile}=require('@electron/asar');
return relative=>extractFile(archive,path.normalize(relative));
}
const scannerRoots=[['apps/inspector','inspector'],['.runtime/windows/python','python'],['.runtime/windows/semantic','semantic']];
async function fileDigest(file){const stat=await lstat(file);if(stat.isSymbolicLink()||!stat.isFile())fail('scanner_package_path_unsafe');const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);const after=await lstat(file);if(stat.size!==after.size||stat.mtimeMs!==after.mtimeMs||stat.ctimeMs!==after.ctimeMs)fail('scanner_package_changed');return {bytes:stat.size,sha256:hash.digest('hex')};}
async function scannerInventory(base,component,filter){
const records=[];
async function walk(directory,prefix=''){
for(const entry of await readdir(directory,{withFileTypes:true})){
if(filter&&(entry.name==='__pycache__'||component==='inspector'&&(entry.name.startsWith('test')||entry.name==='appcontainer_probe.py')))continue;
const file=path.join(directory,entry.name),relative=prefix+entry.name;
if(entry.isSymbolicLink())fail('scanner_package_path_unsafe');
if(entry.isDirectory())await walk(file,relative+'/');else if(entry.isFile())records.push({path:component+'/'+relative,...await fileDigest(file)});
else fail('scanner_package_path_unsafe');
if(records.length>20000)fail('scanner_package_too_large');
}
}
const stat=await lstat(base);if(stat.isSymbolicLink()||!stat.isDirectory())fail('scanner_package_path_unsafe');
await walk(base);return records.sort((a,b)=>a.path.localeCompare(b.path));
}
export async function snapshotScannerResources(root){
const records=[];for(const [from,to] of scannerRoots)records.push(...await scannerInventory(path.join(root,...from.split('/')),to,to!=='semantic'));
return records;
}
export async function verifyScannerResources(resources,expected){
const actual=[];for(const [,component] of scannerRoots)actual.push(...await scannerInventory(path.join(resources,component),component,false));
if(JSON.stringify(actual)!==JSON.stringify(expected))fail('scanner_package_integrity_mismatch');return true;
}
+60 -17
View File
@@ -1,26 +1,69 @@
import {hardenedElectronFuses,verifyPackagedFuses} from './package-fuses.mjs';
import { spawnSync } from 'node:child_process';
import {readFileSync,readdirSync} from 'node:fs';
import {createHash} from 'node:crypto';
import {spawnSync} from 'node:child_process';
import {readFileSync,readdirSync,writeFileSync,createReadStream} from 'node:fs';
import {createHash,randomUUID} from 'node:crypto';
import {createRequire} from 'node:module';
import path from 'node:path';
const platform=process.argv[2];if(platform!=='win'||process.platform!=='win32')throw Error('Windows is the active build target; other platforms are deferred');
const runtime=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-windows-runtime.py'],{stdio:'inherit'});if(runtime.status!==0)process.exit(runtime.status??1);
const semantic=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-semantic-runtime.py','--verify'],{stdio:'inherit'});if(semantic.status!==0)process.exit(semantic.status??1);
import {provisionBrokerRuntime,BROKER_RUNTIME} from './provision-broker-runtime.mjs';
import {prepareBrokerResources,verifyPackagedBroker,embeddedBrokerManifestReader,snapshotScannerResources,verifyScannerResources} from './package-broker-resources.mjs';
import {createWindowsPackagePlan} from './windows-package-plan.mjs';
import {buildVerifiedInstaller} from './verified-installer.mjs';
const platform=process.argv[2];
if(platform!=='win'||process.platform!=='win32'||process.argv.slice(3).some(value=>value!=='--unpacked-only'))throw Error('Windows is the active build target; only --unpacked-only is additionally supported');
const workspace=path.resolve('.');
for(const args of [['scripts/provision-windows-runtime.py'],['scripts/provision-semantic-runtime.py','--verify']]){
const result=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',args,{stdio:'inherit'});if(result.status!==0)process.exit(result.status??1);
}
await provisionBrokerRuntime(workspace);
const name=process.env.BRAND_NAME||'SafetyNet',id=process.env.BRAND_APP_ID||'enterprise.safetynet.browser';
if(!/^[\w .-]{1,40}$/.test(name)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(id))throw Error('Invalid white-label product name or app ID');
const configuration=JSON.parse(readFileSync('package.json','utf8')),workspace=path.resolve('.'),output=path.resolve(configuration.build.directories.output);
if(!output.startsWith(workspace+path.sep))throw Error('Build output must stay inside the workspace');
const configuration=JSON.parse(readFileSync('package.json','utf8')),output=path.resolve(configuration.build.directories.output);
// Reuse the pinned installed distribution already exercised by native tests.
// This avoids a second archive extraction and Windows directory-rename locks.
const electronDirectory=path.dirname(createRequire(import.meta.url)('electron'));
if(readFileSync(path.join(electronDirectory,'version'),'utf8').trim().replace(/^v/,'')!==configuration.devDependencies.electron)throw Error('Installed Electron does not match the build pin');
function sourceSnapshot(){const entries=[];function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}for(const directory of ['apps/browser','packages/core'])walk(directory);entries.push(['package.json',createHash('sha256').update(readFileSync('package.json')).digest('hex')]);return JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])));}
const plan=createWindowsPackagePlan({workspace,outputDirectory:output,electronDirectory,productName:name,appId:id});
const broker=await prepareBrokerResources({root:workspace,build:configuration.build});
const scannerSnapshot=await snapshotScannerResources(workspace);
const artifactName=`${name}-Setup-${configuration.version}.exe`;
if(!/^[\w .-]+\.exe$/.test(artifactName))throw Error('Invalid installer artifact name');
const buildConfiguration={...broker.configuration,artifactName,electronFuses:hardenedElectronFuses};
const configPath=path.join(workspace,'.runtime/windows',`package-build-${randomUUID()}.json`);
writeFileSync(configPath,JSON.stringify(buildConfiguration,null,2)+'\n',{flag:'wx'});
const configArgs=[`--config=${configPath}`];
function sourceSnapshot(){
const entries=[];
function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isSymbolicLink())throw Error('Build source reparse point rejected');if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}
for(const directory of ['apps/browser','apps/network-broker','apps/inspector','packages/core','scripts'])walk(directory);
for(const file of ['package.json','package-lock.json',configuration.build.win.icon,configPath])entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);
return createHash('sha256').update(JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])))).digest('hex');
}
const sourcesBefore=sourceSnapshot();
const r=spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',`--${platform}`,'--dir',`--config.electronDist=${electronDirectory}`,`--config.productName=${name}`,`--config.appId=${id}`,...Object.entries(hardenedElectronFuses).map(([key,value])=>`--config.electronFuses.${key}=${value}`)],{stdio:'inherit',env:process.env});
if(r.status!==0)process.exit(r.status??1);
if(sourceSnapshot()!==sourcesBefore)throw Error('Browser sources changed during packaging; discard this artifact and rebuild from stable sources');
const resources=path.join(output,'win-unpacked/resources');
await verifyPackagedFuses(path.join(output,'win-unpacked',`${name}.exe`),path.join(resources,'app.asar'));
console.log('Packaged Electron security fuses and embedded ASAR integrity verified.');
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});process.exit(acl.status??1);
const run=args=>spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',...args,...configArgs],{stdio:'inherit',env:process.env});
const result=run(plan.unpacked.args);if(result.status!==0)process.exit(result.status??1);
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
const resources=path.join(plan.unpacked.directory,'resources'),archive=path.join(resources,'app.asar'),executable=path.join(plan.unpacked.directory,`${name}.exe`);
async function verify(){
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
await verifyPackagedFuses(executable,archive);
await verifyPackagedBroker({resources,expectedManifest:broker.manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)});
await verifyScannerResources(resources,scannerSnapshot);
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py')],{stdio:'inherit',windowsHide:true});if(acl.status!==0)throw Error('Packaged scanner ACL verification failed');
return {identity:await readIdentity(),fuses:true,resources:true,broker:true,runtimeAcl:true};
}
// This verifies/provisions the unpacked build location only. NSIS extraction
// does not preserve these ACEs: a reviewed installer post-install ACL step and
// actual standard-user Program Files acceptance remain required. Browser
// startup must NEVER silently provision or broaden installation permissions.
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});if(acl.status!==0)process.exit(acl.status??1);
async function treeHash(directory){
const hash=createHash('sha256');
async function walk(current){for(const item of readdirSync(current,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const file=path.join(current,item.name);if(item.isSymbolicLink())throw Error('Packaged reparse point rejected');if(item.isDirectory())await walk(file);else if(item.isFile()){const content=createHash('sha256');let bytes=0;for await(const chunk of createReadStream(file)){content.update(chunk);bytes+=chunk.length;}hash.update(JSON.stringify([path.relative(directory,file).replaceAll('\\','/'),bytes,content.digest('hex')])+'\n');}}}
await walk(directory);return hash.digest('hex');
}
async function readIdentity(){await broker.verifySources();const scanners=await snapshotScannerResources(workspace);if(JSON.stringify(scanners)!==JSON.stringify(scannerSnapshot))throw Error('Scanner sources changed; discard this package');return {sourceSha256:sourceSnapshot(),runtimeSha256:createHash('sha256').update(JSON.stringify({electron:configuration.devDependencies.electron,broker:BROKER_RUNTIME,scanners})).digest('hex'),packageSha256:await treeHash(plan.unpacked.directory)};}
await verify();
console.log('Unpacked browser, embedded broker manifest, complete broker payload, security fuses and scanner ACLs verified.');
if(!process.argv.includes('--unpacked-only')){
const installer=await buildVerifiedInstaller({plan,expectedIdentity:await readIdentity(),readIdentity,verify,run,installerPath:path.join(output,artifactName)});
console.log(JSON.stringify({installer:installer.path,bytes:installer.bytes,sha256:installer.sha256,brokerIncluded:true,productionActivationVerified:false}));
}
+96
View File
@@ -0,0 +1,96 @@
// Official standalone Node executable gives the broker a distinct Windows
// application identity; Electron utilityProcess shares the browser executable.
// This provisioning helper is not yet wired into production packaging.
import {createHash} from 'node:crypto';
import {createReadStream,createWriteStream} from 'node:fs';
import {lstat,mkdir,mkdtemp,readFile,rename,writeFile,readdir} from 'node:fs/promises';
import {pipeline} from 'node:stream/promises';
import {Readable,Transform} from 'node:stream';
import {spawn} from 'node:child_process';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
export const BROKER_RUNTIME=Object.freeze({version:'24.21.0',platform:'win32',arch:'x64',
archive:'node-v24.21.0-win-x64.zip',
archiveSha256:'158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541',
executableSha256:'ba4e6d110e8c1592a1ecd390f6b05f3da124b13871a5be62b341a07a853c6c32',
licenseSha256:'ed34dd8e3f0a78dbaf00d0444ce8e285b015b765379c2e17880455f70370f8e9',
baseUrl:'https://nodejs.org/dist/v24.21.0/'});
function fail(code){throw Object.assign(new Error(code),{code});}
async function sha256(file){const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);return hash.digest('hex');}
export function verifyOfficialChecksums(text){
if(typeof text!=='string'||text.length>65536)fail('broker_runtime_checksums_invalid');
for(const [name,expected] of [[BROKER_RUNTIME.archive,BROKER_RUNTIME.archiveSha256],['win-x64/node.exe',BROKER_RUNTIME.executableSha256]]){
const rows=text.split(/\r?\n/).filter(row=>row.endsWith(' '+name));
if(rows.length!==1||rows[0]!==expected+' '+name)fail('broker_runtime_checksums_mismatch');
}
}
export async function downloadOfficialArchive(destination){
const checksumResponse=await fetch(BROKER_RUNTIME.baseUrl+'SHASUMS256.txt',{redirect:'error',signal:AbortSignal.timeout(30000)});
if(!checksumResponse.ok)fail('broker_runtime_download_failed');
const chunks=[];let checksumBytes=0;
for await(const chunk of checksumResponse.body){checksumBytes+=chunk.length;if(checksumBytes>65536)fail('broker_runtime_checksums_invalid');chunks.push(Buffer.from(chunk));}
const checksumText=Buffer.concat(chunks).toString();verifyOfficialChecksums(checksumText);
const response=await fetch(BROKER_RUNTIME.baseUrl+BROKER_RUNTIME.archive,{redirect:'error',signal:AbortSignal.timeout(120000)});
if(!response.ok)fail('broker_runtime_download_failed');
let bytes=0;const hash=createHash('sha256');
const bounded=new Transform({transform(chunk,_encoding,callback){bytes+=chunk.length;if(bytes>67108864){callback(new Error('broker_runtime_archive_too_large'));return;}hash.update(chunk);callback(null,chunk);}});
await pipeline(Readable.fromWeb(response.body),bounded,createWriteStream(destination,{flags:'wx',mode:0o600}));
if(hash.digest('hex')!==BROKER_RUNTIME.archiveSha256)fail('broker_runtime_archive_mismatch');
return {bytes,archiveSha256:BROKER_RUNTIME.archiveSha256,checksumText};
}
function psQuote(value){return "'"+value.replaceAll("'","''")+"'";}
async function extractSelected(archive,destination){
// Extract only exact reviewed members from the already hash-verified ZIP.
const script=`$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem; $zip=[IO.Compression.ZipFile]::OpenRead(${psQuote(archive)}); try { foreach($name in @('node.exe','LICENSE')) { $member='node-v${BROKER_RUNTIME.version}-win-x64/'+$name; $entries=@($zip.Entries | Where-Object { $_.FullName -ceq $member }); if($entries.Count -ne 1 -or $entries[0].Length -gt 100000000) { throw 'broker_runtime_archive_members_invalid' }; $inputStream=$entries[0].Open(); $outputStream=[IO.File]::Open([IO.Path]::Combine(${psQuote(destination)},$name),[IO.FileMode]::CreateNew); try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose(); $inputStream.Dispose() } } } finally { $zip.Dispose() }`;
const child=spawn('powershell.exe',['-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:'ignore'});
const code=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',resolve);});
if(code!==0)fail('broker_runtime_extraction_failed');
}
async function verifyDirectoryAncestors(directory){
for(let current=path.resolve(directory);;current=path.dirname(current)){
const stat=await lstat(current);if(stat.isSymbolicLink()||!stat.isDirectory())fail('broker_runtime_path_unsafe');
if(path.dirname(current)===current)break;
}
}
export async function verifyBrokerRuntime(directory){
await verifyDirectoryAncestors(directory);
const manifestPath=path.join(directory,'runtime-manifest.json'),manifestStat=await lstat(manifestPath);
if(manifestStat.isSymbolicLink()||!manifestStat.isFile()||manifestStat.size>16384)fail('broker_runtime_manifest_invalid');
let manifest;try{manifest=JSON.parse(await readFile(manifestPath,'utf8'));}catch{fail('broker_runtime_manifest_invalid');}
if(!manifest||typeof manifest!=='object')fail('broker_runtime_manifest_invalid');
if(manifest.format!=='safetynet-node-broker-runtime-v1'||manifest.version!==BROKER_RUNTIME.version||manifest.arch!=='x64'||manifest.platform!=='win32'||manifest.archiveSha256!==BROKER_RUNTIME.archiveSha256)fail('broker_runtime_manifest_invalid');
if(!Array.isArray(manifest.files)||manifest.files.length!==2||manifest.files.some(file=>!file||typeof file.path!=='string')||new Set(manifest.files.map(file=>file.path)).size!==2)fail('broker_runtime_manifest_invalid');
const files=await readdir(directory);if(JSON.stringify(files.sort())!==JSON.stringify(['LICENSE','node.exe','runtime-manifest.json'].sort()))fail('broker_runtime_inventory_mismatch');
for(const name of ['node.exe','LICENSE']){
const stat=await lstat(path.join(directory,name));if(stat.isSymbolicLink()||!stat.isFile())fail('broker_runtime_path_unsafe');
const record=manifest.files?.find(item=>item.path===name);
if(!record||record.bytes!==stat.size||!/^[a-f0-9]{64}$/.test(record.sha256)||await sha256(path.join(directory,name))!==record.sha256)fail('broker_runtime_integrity_mismatch');
if(record.sha256!==(name==='node.exe'?BROKER_RUNTIME.executableSha256:BROKER_RUNTIME.licenseSha256))fail('broker_runtime_integrity_mismatch');
}
return manifest;
}
export async function provisionBrokerRuntime(workspace){
if(process.platform!=='win32'||process.arch!=='x64')fail('broker_runtime_platform_unsupported');
const parent=path.join(path.resolve(workspace),'.runtime/windows');await mkdir(parent,{recursive:true});
await verifyDirectoryAncestors(parent);
const destination=path.join(parent,'broker');
try{await lstat(destination);return await verifyBrokerRuntime(destination);}catch(error){if(error.code!=='ENOENT')throw error;}
const staging=await mkdtemp(path.join(parent,'broker-provision-'));
const archive=path.join(staging,BROKER_RUNTIME.archive),payload=path.join(staging,'payload');await mkdir(payload);
// Failed and verified archives/proof remain ignored for diagnosis; no
// unverified executable is started and no global Node installation changes.
const download=await downloadOfficialArchive(archive);
await writeFile(path.join(staging,'SHASUMS256.txt'),download.checksumText);
await extractSelected(archive,payload);
const manifest={format:'safetynet-node-broker-runtime-v1',version:BROKER_RUNTIME.version,platform:'win32',arch:'x64',archiveSha256:BROKER_RUNTIME.archiveSha256,files:[]};
for(const name of ['node.exe','LICENSE'])manifest.files.push({path:name,bytes:(await lstat(path.join(payload,name))).size,sha256:await sha256(path.join(payload,name))});
await writeFile(path.join(payload,'runtime-manifest.json'),JSON.stringify(manifest,null,2)+'\n');
await verifyBrokerRuntime(payload);await rename(payload,destination);
return await verifyBrokerRuntime(destination);
}
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
const workspace=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
const result=process.argv.includes('--verify')?await verifyBrokerRuntime(path.join(workspace,'.runtime/windows/broker')):await provisionBrokerRuntime(workspace);
console.log(JSON.stringify({version:result.version,files:result.files.length,verified:true}));
}
+55
View File
@@ -0,0 +1,55 @@
// Standalone payload preparation only. No browser wiring, installer or service activation.
import {readFile,writeFile,lstat,mkdir,mkdtemp,rename} from 'node:fs/promises';
import {createReadStream} from 'node:fs';
import {createHash} from 'node:crypto';
import {builtinModules} from 'node:module';
import {gunzipSync} from 'node:zlib';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {BROKER_RUNTIME} from './provision-broker-runtime.mjs';
import {buildBrokerPackageManifest,verifyBrokerPackage} from './broker-package-contract.mjs';
export const BROKER_PAYLOAD=Object.freeze({version:'0.1.0',protocol:1,entrypoint:'node.exe'});
const BROKER_FILES=Object.freeze(['audit-gate.cjs','bounded-audit.cjs','connect-inspection-proxy.cjs','diagnostics-adapter.cjs','dns-policy-route.cjs','download-response-gate.cjs','http-request-gate.cjs','http-response-gate.cjs','https-forwarder.cjs','https-inspection-server.cjs','lifecycle-audit.cjs','managed-proxy.cjs','management-identity-operation.cjs','multipart.cjs','policy-authority.cjs','process-entry.cjs','production-entry.cjs','production-factory.cjs','proxy-forwarder.cjs','service-acceptance.cjs','service-bootstrap.cjs','service-commit-gate.cjs','service-entry.cjs','service-store-client.cjs','framed-channel.cjs','tls-authority.cjs','tls-certificates.cjs','windows-wfp-plan.cjs','wfp-topology.cjs','wfp-revalidation.cjs','work-admission.cjs']);
const BROWSER_HELPERS=new Set(['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs']);
const builtins=new Set(builtinModules.flatMap(name=>[name,name.replace(/^node:/,''),'node:'+name.replace(/^node:/,'')]));
function fail(code){throw Object.assign(new Error(code),{code});}
async function safe(file,directory=false){
for(let current=path.resolve(file);;current=path.dirname(current)){
const stat=await lstat(current);if(stat.isSymbolicLink())fail('broker_stage_path_unsafe');
if(current===path.resolve(file)&&((directory&&!stat.isDirectory())||(!directory&&!stat.isFile())))fail('broker_stage_path_unsafe');
if(path.dirname(current)===current)break;
}
}
async function digest(file){await safe(file);const before=await lstat(file);if(before.size>100000000)fail('broker_stage_file_limit');const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);const after=await lstat(file);if(before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)fail('broker_stage_source_changed');return {bytes:after.size,sha256:hash.digest('hex')};}
export async function snapshotSources(root,names){const records=[];for(const name of names){if(!/^[a-zA-Z0-9_./-]+$/.test(name)||name.split('/').some(p=>p==='..'||p==='.'||!p))fail('broker_stage_path_unsafe');records.push({path:name,...await digest(path.join(root,...name.split('/')))});}return records;}
export async function verifySourceSnapshot(root,records){for(const record of records){const actual=await digest(path.join(root,...record.path.split('/')));if(actual.bytes!==record.bytes||actual.sha256!==record.sha256)fail('broker_stage_source_changed');}}
function allowedSource(name){return /^packages\/core\/[a-z0-9-]+\.(?:cjs|mjs)$/.test(name)||(name.startsWith('apps/network-broker/')&&BROKER_FILES.includes(name.slice(20)))||(name.startsWith('apps/browser/')&&BROWSER_HELPERS.has(name.slice(13)));}
export async function reviewedSourceClosure(root){
const queue=BROKER_FILES.map(name=>'apps/network-broker/'+name);queue.push('packages/core/policy.mjs');const seen=new Set(),external=new Set();
while(queue.length){const name=queue.shift();if(seen.has(name))continue;if(!allowedSource(name))fail('broker_stage_unreviewed_source');await safe(path.join(root,...name.split('/')));const source=await readFile(path.join(root,...name.split('/')),'utf8');if(Buffer.byteLength(source)>1048576)fail('broker_stage_source_limit');seen.add(name);
for(const call of source.matchAll(/\b(?:require|import)\s*\(([^)]*)\)/g))if(!/^\s*['"][^'"]+['"]\s*$/.test(call[1]))fail('broker_stage_dynamic_dependency');
const imports=[...source.matchAll(/(?:require\s*\(|import\s*\()\s*['"]([^'"]+)['"]/g),...source.matchAll(/\bfrom\s*['"]([^'"]+)['"]/g),...source.matchAll(/\bimport\s*['"]([^'"]+)['"]/g)].map(match=>match[1]);
for(const specifier of imports){if(builtins.has(specifier))continue;if(specifier.startsWith('.')){const next=path.posix.normalize(path.posix.join(path.posix.dirname(name),specifier));if(!allowedSource(next))fail('broker_stage_unreviewed_source');queue.push(next);}else external.add(specifier);}
}
if([...external].some(name=>name!=='zod'))fail('broker_stage_unreviewed_dependency');return {files:[...seen].sort(),dependencies:[...external].sort()};
}
export function extractVerifiedZod(bytes,locked){
if(!Buffer.isBuffer(bytes)||bytes.length>10485760||locked?.version!=='4.6.7'||locked.dev||locked.resolved!==`https://registry.npmjs.org/zod/-/zod-${locked.version}.tgz`||!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(locked.integrity||'')||'sha512-'+createHash('sha512').update(bytes).digest('base64')!==locked.integrity)fail('broker_stage_dependency_integrity');
let tar;try{tar=gunzipSync(bytes,{maxOutputLength:67108864});}catch{fail('broker_stage_dependency_archive');}
const files=[];const names=new Set();for(let offset=0;offset+512<=tar.length;){const header=tar.subarray(offset,offset+512);if(header.every(byte=>byte===0))break;const text=buffer=>buffer.toString('utf8').replace(/\0.*$/s,'');const name=text(header.subarray(0,100)),prefix=text(header.subarray(345,500));const full=prefix?prefix+'/'+name:name;const size=parseInt(text(header.subarray(124,136)).trim(),8),type=header[156];const checksum=parseInt(text(header.subarray(148,156)).trim(),8);let sum=0;for(let i=0;i<512;i++)sum+=i>=148&&i<156?32:header[i];if(sum!==checksum||!Number.isSafeInteger(size)||size<0||offset+512+size>tar.length||![0,48,53].includes(type)||!full.startsWith('package/')||full.includes('\\')||full.split('/').filter(Boolean).some(part=>! /^[a-zA-Z0-9_.-]+$/.test(part)||part==='..'||part==='.'||/[. ]$/.test(part)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))||names.has(full.toLowerCase()))fail('broker_stage_dependency_archive');names.add(full.toLowerCase());if(names.size>4096)fail('broker_stage_dependency_archive');
const relative=full.slice(8);if(type!==53&&relative&&!relative.startsWith('src/')&&!/(^|\/)(tests?|fixtures?|benchmarks?)(\/|$)/i.test(relative)&&(relative==='package.json'||/^license(?:\.[a-z]+)?$/i.test(relative)||/\.(?:js|cjs|mjs)$/.test(relative)))files.push({path:'node_modules/zod/'+relative,bytes:Buffer.from(tar.subarray(offset+512,offset+512+size))});offset+=512+Math.ceil(size/512)*512;
}
if(files.length>2048||!files.some(file=>file.path==='node_modules/zod/package.json')||!files.some(file=>/\/license(?:\.[a-z]+)?$/i.test(file.path)))fail('broker_stage_dependency_archive');let metadata;try{metadata=JSON.parse(files.find(file=>file.path==='node_modules/zod/package.json').bytes.toString());}catch{fail('broker_stage_dependency_archive');}if(metadata.name!=='zod'||metadata.version!==locked.version)fail('broker_stage_dependency_integrity');return files;
}
async function archive(locked){const response=await fetch(locked.resolved,{redirect:'error',signal:AbortSignal.timeout(30000)});if(!response.ok)fail('broker_stage_dependency_download');const chunks=[];let count=0;for await(const chunk of response.body){count+=chunk.length;if(count>10485760)fail('broker_stage_dependency_limit');chunks.push(Buffer.from(chunk));}return Buffer.concat(chunks);}
export async function stageBrokerPayload({root=fileURLToPath(new URL('../',import.meta.url)),destination,dependencyArchive}={}){
root=path.resolve(root);const parent=path.join(root,'.runtime','windows');destination=path.resolve(destination||path.join(parent,'broker-payload'));if(path.dirname(destination)!==parent||!/^broker-payload(?:-[a-zA-Z0-9_-]+)?$/.test(path.basename(destination)))fail('broker_stage_destination_unsafe');await safe(root,true);await safe(parent,true);try{await lstat(destination);fail('broker_stage_destination_exists');}catch(error){if(error.code!=='ENOENT')throw error;}
const closure=await reviewedSourceClosure(root);const runtimeNames=['node.exe','LICENSE'];const sourceNames=[...closure.files,'package-lock.json',...runtimeNames.map(name=>'.runtime/windows/broker/'+name)];const sources=await snapshotSources(root,sourceNames);for(const [name,expected] of [['node.exe',BROKER_RUNTIME.executableSha256],['LICENSE',BROKER_RUNTIME.licenseSha256]])if(sources.find(item=>item.path==='.runtime/windows/broker/'+name).sha256!==expected)fail('broker_stage_runtime_integrity');
const lock=JSON.parse(await readFile(path.join(root,'package-lock.json'),'utf8'));const locked=lock.packages?.['node_modules/zod'];if(!locked)fail('broker_stage_dependency_missing');const dependencies=extractVerifiedZod(dependencyArchive||await archive(locked),locked);
const temporary=await mkdtemp(path.join(parent,'broker-payload-stage-'));async function put(name,bytes){const target=path.join(temporary,...name.split('/'));await mkdir(path.dirname(target),{recursive:true});await writeFile(target,bytes,{flag:'wx'});}
for(const name of runtimeNames)await put(name,await readFile(path.join(root,'.runtime/windows/broker',name)));for(const name of closure.files)await put(name,await readFile(path.join(root,...name.split('/'))));for(const file of dependencies)await put(file.path,file.bytes);
await verifySourceSnapshot(root,sources);const manifest=await buildBrokerPackageManifest(temporary,BROKER_PAYLOAD);for(const source of sources.filter(item=>item.path!=='package-lock.json')){const target=source.path.startsWith('.runtime/windows/broker/')?source.path.slice('.runtime/windows/broker/'.length):source.path;const copied=manifest.files.find(item=>item.path===target);if(!copied||copied.bytes!==source.bytes||copied.sha256!==source.sha256)fail('broker_stage_source_changed');}await verifyBrokerPackage(temporary,manifest,BROKER_PAYLOAD);await verifySourceSnapshot(root,sources);await rename(temporary,destination);
// Adjacent metadata is evidence; only an embedded/signed installer copy is trusted.
await writeFile(destination+'.manifest.json',JSON.stringify(manifest,null,2)+'\n',{flag:'wx'});return {root:destination,manifest,sources,sourceFiles:closure.files.length,dependencies:closure.dependencies,productionReady:false};
}
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){try{const result=await stageBrokerPayload();console.log(JSON.stringify({files:result.manifest.files.length,dependencies:result.dependencies,productionReady:result.productionReady}));}catch(error){console.error(error.code?.startsWith('broker_')?error.code:'broker_stage_failed');process.exitCode=1;}}
+30
View File
@@ -0,0 +1,30 @@
import {lstat,open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
function fail(code){throw Object.assign(new Error(code),{code});}
function same(expected,actual){return actual&&['sourceSha256','runtimeSha256','packageSha256'].every(key=>actual[key]===expected[key]);}
async function stat(file){try{return await lstat(file);}catch(error){if(error.code==='ENOENT')return null;throw error;}}
// Guard actual invocation and result. Caller owns/protects the build directory.
export async function buildVerifiedInstaller({plan,expectedIdentity,readIdentity,verify,run,installerPath}){
if(typeof run!=='function'||!plan||!path.isAbsolute(installerPath)||path.dirname(installerPath)!==path.dirname(plan.unpacked.directory)||!installerPath.endsWith('.exe'))fail('installer_configuration_invalid');
const stage=await plan.installer({expectedIdentity,readIdentity,verify});
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const previous=await stat(installerPath);if(previous?.isSymbolicLink())fail('installer_artifact_invalid');
const result=await run(stage.args);
if(result?.status!==0)fail('installer_build_failed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const receipt=await verify(stage.directory,stage.identity);
if(!receipt||!same(stage.identity,receipt.identity)||!['fuses','resources','broker','runtimeAcl'].every(key=>receipt[key]===true))fail('installer_verification_failed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const produced=await stat(installerPath);
if(!produced||!produced.isFile()||produced.isSymbolicLink()||produced.size<1024||produced.size>4294967295||previous&&previous.mtimeMs===produced.mtimeMs&&previous.ctimeMs===produced.ctimeMs&&previous.size===produced.size)fail('installer_artifact_invalid');
const file=await open(installerPath,'r');
try{
const magic=Buffer.alloc(2);await file.read(magic,0,2,0);if(magic.toString('ascii')!=='MZ')fail('installer_artifact_invalid');
const hash=createHash('sha256');for await(const chunk of file.createReadStream({start:0,autoClose:false}))hash.update(chunk);
const after=await file.stat();if(after.size!==produced.size||after.mtimeMs!==produced.mtimeMs||after.ctimeMs!==produced.ctimeMs)fail('installer_artifact_changed');
if(!same(stage.identity,await readIdentity(stage.directory)))fail('installer_identity_changed');
const final=await file.stat();if(final.size!==after.size||final.mtimeMs!==after.mtimeMs||final.ctimeMs!==after.ctimeMs)fail('installer_artifact_changed');
return Object.freeze({path:installerPath,bytes:after.size,sha256:hash.digest('hex'),identity:stage.identity});
}finally{await file.close();}
}
+38
View File
@@ -0,0 +1,38 @@
// Not wired into package.mjs until a real packaged broker is available.
// Official CLI: https://www.electron.build/docs/cli/
// --dir produces only an unpacked app; --win nsis --prepackaged distributes
// that verified directory without silently rebuilding its application payload.
import path from 'node:path';
function fail(code){throw Object.assign(new Error(code),{code});}
function identity(value){
if(!value||!['sourceSha256','runtimeSha256','packageSha256'].every(key=>/^[a-f0-9]{64}$/.test(value[key])))fail('package_identity_invalid');
return Object.freeze(Object.fromEntries(['sourceSha256','runtimeSha256','packageSha256'].map(key=>[key,value[key]])));
}
function same(left,right){return Object.keys(left).every(key=>left[key]===right[key]);}
export function createWindowsPackagePlan({workspace,outputDirectory,electronDirectory,productName='SafetyNet',appId='enterprise.safetynet.browser',platform=process.platform}){
if(platform!=='win32')fail('package_platform_unsupported');
if(![workspace,outputDirectory,electronDirectory].every(value=>typeof value==='string'&&path.isAbsolute(value)))fail('package_path_invalid');
const root=path.resolve(workspace),output=path.resolve(outputDirectory),runtime=path.resolve(electronDirectory);
const relative=path.relative(root,output);
if(!relative||relative.startsWith('..'+path.sep)||relative==='..'||path.isAbsolute(relative))fail('package_output_unsafe');
if(!/^[\w .-]{1,40}$/.test(productName)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(appId))fail('package_brand_invalid');
const directory=path.join(output,'win-unpacked');
const common=[`--config.productName=${productName}`,`--config.appId=${appId}`,`--config.directories.output=${output}`,'--publish','never'];
return Object.freeze({
unpacked:Object.freeze({stage:'build-unpacked',args:Object.freeze(['--win','--dir',`--config.electronDist=${runtime}`,...common]),directory}),
verification:Object.freeze({stage:'verify-unpacked',required:Object.freeze(['fuses','resources','broker','runtimeAcl']),directory}),
async installer({expectedIdentity,readIdentity,verify}){
if(typeof readIdentity!=='function'||typeof verify!=='function')fail('package_verifier_required');
const expected=identity(expectedIdentity);
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
const receipt=await verify(directory,expected);
if(!receipt||!same(expected,identity(receipt.identity))||!['fuses','resources','broker','runtimeAcl'].every(key=>receipt[key]===true))fail('package_verification_incomplete');
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
// This plans arguments only. Caller must protect the verified directory
// against changes through invocation and inspect the real installer result.
return Object.freeze({stage:'build-installer',directory,identity:expected,
args:Object.freeze(['--win','nsis','--prepackaged',directory,...common])});
}
});
}
+33
View File
@@ -0,0 +1,33 @@
// Build the inventory AFTER final binaries exist; signing is performed by an
// external trusted release signer. No private key is generated or stored here.
import {lstat,readdir,open} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import path from 'node:path';
const OMIT=new Set(['release-envelope.json','deployment-envelope.json']);
function fail(code){throw Object.assign(new Error(code),{code});}
async function ancestors(root){for(let current=path.resolve(root);;current=path.dirname(current)){const stat=await lstat(current);if(!stat.isDirectory()||stat.isSymbolicLink())fail('windows-release-path-unsafe');if(path.dirname(current)===current)break;}}
async function hash(file){const input=await open(file,'r');try{const before=await input.stat(),sha=createHash('sha256');let bytes=0;for await(const chunk of input.createReadStream({autoClose:false})){bytes+=chunk.length;sha.update(chunk);}const after=await input.stat();if(!before.isFile()||before.size!==bytes||after.size!==bytes||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs||before.ino!==after.ino||before.dev!==after.dev)fail('windows-release-source-changed');return {bytes,sha256:sha.digest('hex')};}finally{await input.close();}}
export async function buildReleaseInventory(root,{browserPath='SafetyNet.exe',nativeHostPath='safetynet-service-host.exe'}={}){
await ancestors(root);const files=[],seen=new Set();let count=0;
async function walk(directory,prefix=''){
for(const entry of await readdir(directory,{withFileTypes:true})){
const relative=prefix+entry.name;
if(++count>15000||!relative.split('/').every(part=>/^[A-Za-z0-9_.-]+$/.test(part)&&part!=='.'&&part!=='..'&&!/[. ]$/.test(part)&&!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))||relative.length>240||entry.isSymbolicLink())fail('windows-release-path-unsafe');
const target=path.join(directory,entry.name),stat=await lstat(target);if(stat.isSymbolicLink())fail('windows-release-path-unsafe');
if(stat.isDirectory())await walk(target,relative+'/');else if(stat.isFile()){
if(OMIT.has(relative))continue;
if(seen.has(relative.toLowerCase())||files.length>=10000)fail('windows-release-inventory-invalid');seen.add(relative.toLowerCase());files.push({path:relative,...await hash(target)});
}else fail('windows-release-path-unsafe');
}
}
await walk(root);files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
for(const required of [browserPath,nativeHostPath,'resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py'])if(!files.some(file=>file.path===required))fail('windows-release-component-missing');
if(!files.some(file=>file.path.startsWith('resources/semantic/')))fail('windows-release-component-missing');
return files;
}
export async function verifyReleaseInventory(root,verifiedRelease,options){
// Cryptographic authenticity is established separately by windows-trust.cjs.
// This API only compares an already trusted immutable inventory to disk.
const actual=await buildReleaseInventory(root,options),expected=verifiedRelease?.manifest?.files;
if(!Array.isArray(expected)||actual.length!==expected.length||actual.some((file,index)=>file.path!==expected[index]?.path||file.bytes!==expected[index]?.bytes||file.sha256!==expected[index]?.sha256))fail('windows-release-inventory-mismatch');return true;
}
+89
View File
@@ -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');
});
+40
View File
@@ -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();
}
});
+39
View File
@@ -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();}
});
+77
View File
@@ -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']);
});
+61
View File
@@ -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'});
});
+28
View File
@@ -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'}));
});
+67
View File
@@ -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');
});
+51
View File
@@ -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));
});
+74
View File
@@ -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);
});
+104
View File
@@ -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'));
}
});
+15
View File
@@ -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));
});
+92
View File
@@ -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);
});
+99
View File
@@ -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);
});
+173
View File
@@ -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();
});
+42
View File
@@ -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;}});
+50
View File
@@ -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'});
});
+11
View File
@@ -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);
});
+156
View File
@@ -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');
}
});
+85
View File
@@ -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);
});
+72
View File
@@ -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'});
});
+37
View File
@@ -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/);}));
+53
View File
@@ -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'});
});
+55
View File
@@ -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);
});
+97
View File
@@ -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/);
}
});
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more