70 lines
8.2 KiB
JavaScript
70 lines
8.2 KiB
JavaScript
'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};
|