Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
@@ -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};
|
||||
Reference in new issue
Block a user