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
+11215 -60

No files matched your search

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