Files
safetynet/tests/broker-service-acceptance.test.mjs

76 lines
9.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {generateKeyPairSync} from 'node:crypto';
import {createRequire} from 'node:module';
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
const require=createRequire(import.meta.url),{verifyServiceAcceptance}=require('../apps/network-broker/service-acceptance.cjs');
const {acceptanceStateDigest,StateSchema}=require('../apps/network-broker/service-acceptance.cjs'),{createServiceCommitGate}=require('../apps/network-broker/service-commit-gate.cjs');
const {verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest}=require('../packages/core/windows-trust.cjs');
const pub=key=>key.publicKey.export({type:'spki',format:'pem'}),priv=key=>key.privateKey.export({type:'pkcs8',format:'pem'});
function fixture(){
const vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),key=generateKeyPairSync('ed25519'),sid='S-1-12-1-1-2-3-4';
const release={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:1,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:pub(customer),files:['SafetyNet.exe','resources/app.asar','resources/broker/node.exe','resources/inspector/server.py','resources/python/python.exe','resources/semantic/model.bin','safetynet-service-host.exe'].map(path=>({path,bytes:1,sha256:'a'.repeat(64)}))};
const releaseEnvelope=signManifest(release,priv(vendor),'release'),verified=verifyReleaseEnvelope(releaseEnvelope,{publicKey:pub(vendor)});
const deployment={format:'safetynet-windows-deployment-v1',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:1,policyPublicKey:pub(key),expiresAt:Date.now()+60000,users:[{sid,deviceId:'device',minimumPolicyVersion:7}]};
const deploymentEnvelope=signManifest(deployment,priv(customer),'deployment'),d=verifyDeploymentEnvelope(deploymentEnvelope,{release:verified,authenticatedSid:sid});
const policy=defaultPolicy();policy.version=7;policy.assignment={deviceId:'device',groups:[]};
const input={state:{format:'safetynet-service-floors-v1',release:{minimumSequence:1,acceptedDigest:verified.digest},deployment:{tenant:release.deploymentTenant,minimumSequence:1,acceptedDigest:d.digest},policies:[]},releaseEnvelope,deploymentEnvelope,releaseAnchor:pub(vendor),authenticatedSid:sid,policyEnvelope:signPolicy(policy,priv(key))};
return {input,deployment,customer,key,policy};
}
test('genuine signed acceptance raises all in-memory floors and preserves the protected input',async()=>{
const f=fixture(),result=await verifyServiceAcceptance(f.input);assert.equal(result.bootstrap.minimumVersion,7);assert.equal(result.nextState.policies.length,1);assert.equal(result.nextState.policies[0].minimumVersion,7);assert.equal(f.input.state.policies.length,0);assert.ok(Object.isFrozen(result.nextState.policies));
const replay=await verifyServiceAcceptance({...f.input,state:result.nextState});assert.deepEqual(replay.nextState,result.nextState);
});
test('newer deployment cannot lower an accepted policy floor; same policy version cannot equivocate',async()=>{
const f=fixture(),accepted=await verifyServiceAcceptance(f.input);f.deployment.deploymentSequence=2;f.deployment.users[0].minimumPolicyVersion=1;
const input={...f.input,state:accepted.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment')};f.policy.version=6;
await assert.rejects(verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
f.policy.version=7;f.policy.brand.name='Changed same version';await assert.rejects(verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
f.policy.version=8;const next=await verifyServiceAcceptance({...input,policyEnvelope:signPolicy(f.policy,priv(f.key))});assert.equal(next.bootstrap.minimumVersion,8);assert.equal(next.nextState.deployment.minimumSequence,2);
});
test('missing state, duplicate floor identities, unknown peer and absent device assignment fail closed',async()=>{
const f=fixture(),accepted=await verifyServiceAcceptance(f.input);
for(const input of [{...f.input,state:undefined},{...f.input,state:{...accepted.nextState,policies:[...accepted.nextState.policies,...accepted.nextState.policies]}},{...f.input,authenticatedSid:'S-1-12-1-5-6-7-8'}])await assert.rejects(verifyServiceAcceptance(input),{message:'broker-bootstrap-invalid'});
delete f.policy.assignment;await assert.rejects(verifyServiceAcceptance({...f.input,policyEnvelope:signPolicy(f.policy,priv(f.key))}),{message:'broker-bootstrap-invalid'});
});
test('approved key rotation retains old-key floors when that key is selected again',async()=>{
const f=fixture(),first=await verifyServiceAcceptance(f.input),replacement=generateKeyPairSync('ed25519');
f.deployment.deploymentSequence=2;f.deployment.policyPublicKey=pub(replacement);f.deployment.users[0].minimumPolicyVersion=1;f.policy.version=1;
const rotated=await verifyServiceAcceptance({...f.input,state:first.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment'),policyEnvelope:signPolicy(f.policy,priv(replacement))});assert.equal(rotated.nextState.policies.length,2);
f.deployment.deploymentSequence=3;f.deployment.policyPublicKey=pub(f.key);f.policy.version=6;
await assert.rejects(verifyServiceAcceptance({...f.input,state:rotated.nextState,deploymentEnvelope:signManifest(f.deployment,priv(f.customer),'deployment'),policyEnvelope:signPolicy(f.policy,priv(f.key))}),error=>error.message==='broker-bootstrap-invalid'&&error.failureStage==='policy');
});
test('bootstrap gate releases no plan before exact protected commit receipt',async()=>{
const f=fixture();let enter,finish,completed=false;const entered=new Promise(resolve=>enter=resolve),hold=new Promise(resolve=>finish=resolve);
const gate=createServiceCommitGate({current:()=>true,commitState:async request=>{assert.equal(request.expectedPreviousSHA,acceptanceStateDigest(f.input.state));assert.ok(Object.isFrozen(request.candidateState));enter();await hold;return {persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)};}});
const pending=gate.prepare(f.input).then(value=>{completed=true;return value;});await entered;assert.equal(completed,false);finish();const plan=await pending;assert.equal(plan.policyVersion,7);await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});
});
test('failed or uncertain native commit is sticky without automatic retry',async()=>{
const f=fixture();
for(const answer of [undefined,true,{persisted:true,stateSha256:'0'.repeat(64)},{persisted:false,stateSha256:'0'.repeat(64)}]){
let commits=0;const gate=createServiceCommitGate({current:()=>true,commitState:async()=>{commits++;return answer;}});
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});assert.equal(commits,1);
}
});
test('revocation during original commit blocks even a correct persisted receipt',async()=>{
const f=fixture();let active=true;
const gate=createServiceCommitGate({current:()=>active,commitState:async request=>{active=false;return {persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)};}});
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});
});
test('commit plan composes with service startup and rejects another signed policy at the same version',async()=>{
const {createServiceFactory}=require('../apps/network-broker/service-bootstrap.cjs'),f=fixture();
const gate=createServiceCommitGate({current:()=>true,commitState:async request=>({persisted:true,stateSha256:acceptanceStateDigest(request.candidateState)})});
const plan=await gate.prepare(f.input);let launches=0;
const factory=createServiceFactory({loadTrust:async parsed=>{assert.ok(Object.isFrozen(parsed.envelope));return plan;},factory:async()=>{launches++;return {listen:async()=>43120,close:async()=>{}};}});
const config={publicKey:pub(f.key),deviceId:'device',minimumVersion:7,envelope:f.input.policyEnvelope,brokerPort:43120,network:{mode:'direct',dns:'system'}},callbacks={expectedPolicyVersion:7};
f.policy.brand.name='Differently signed same version';const other=signPolicy(f.policy,priv(f.key));
await assert.rejects(factory({...config,envelope:other},callbacks),{message:'broker-bootstrap-invalid'});assert.equal(launches,0);
const accepted=await factory(config,callbacks);assert.equal(await accepted.listen(),43120);await accepted.close();assert.equal(launches,1);
});
test('valid record count cannot bypass total serialized state and CAS frame budget',async()=>{
const f=fixture();f.input.state.policies=Array.from({length:8000},(_,index)=>({sid:'S-1-12-1-4294967295-4294967295-4294967295-4294967295',deviceId:'device'+String(index).padStart(74,'0'),policyKeySha256:'a'.repeat(64),minimumVersion:Number.MAX_SAFE_INTEGER,acceptedDigest:'b'.repeat(64)}));
assert.doesNotThrow(()=>StateSchema.parse(f.input.state));
let commits=0;const gate=createServiceCommitGate({current:()=>true,commitState:async()=>{commits++;}});
await assert.rejects(gate.prepare(f.input),{message:'broker-bootstrap-invalid'});assert.equal(commits,0);
});