42 lines
12 KiB
JavaScript
42 lines
12 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import https from 'node:https';
|
||
|
|
import {once} from 'node:events';
|
||
|
|
import {readFile} from 'node:fs/promises';
|
||
|
|
import {generateKeyPairSync} from 'node:crypto';
|
||
|
|
import {createRequire} from 'node:module';
|
||
|
|
import {defaultPolicy,signPolicy,verifyPolicy} from '../packages/core/policy.mjs';
|
||
|
|
const require=createRequire(import.meta.url),{createManagementIdentityOperation}=require('../apps/network-broker/management-identity-operation.cjs');
|
||
|
|
const channel=require('../packages/core/policy-channel.cjs'),{verifyServiceAcceptance}=require('../apps/network-broker/service-acceptance.cjs');
|
||
|
|
const {signManifest,verifyReleaseEnvelope,verifyDeploymentEnvelope}=require('../packages/core/windows-trust.cjs');
|
||
|
|
const pub=k=>k.publicKey.export({type:'spki',format:'pem'}),priv=k=>k.privateKey.export({type:'pkcs8',format:'pem'});
|
||
|
|
async function fixture(t){
|
||
|
|
const cert=await readFile(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=await readFile(new URL('./fixtures/icap/key.pem',import.meta.url)),ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87'),vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'});let requests=0,tcp=0;
|
||
|
|
let result=channel.identity(ed.privateKey,pq.privateKey,pub(pq)),status=200,handler;
|
||
|
|
const server=https.createServer({key,cert},async(req,res)=>{requests++;assert.equal(req.headers.authorization,'Bearer synthetic-protected-token');if(handler){await handler(req,res);return;}assert.equal(req.method,'GET');assert.equal(req.url,'/api/device/channel/identity');res.writeHead(status,{'content-type':'application/json'});res.end(JSON.stringify(result));});server.on('connection',()=>tcp++);server.listen(0,'::');await once(server,'listening');t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));});const origin=`https://localhost:${server.address().port}`,sid='S-1-12-1-1-2-3-4';
|
||
|
|
const release={format:'safetynet-windows-release-v1',releaseId:'12345678-1234-1234-1234-123456789abc',releaseSequence:1,serviceName:'SafetyNetNetworkBroker',deploymentTenant:'22345678-1234-1234-1234-123456789abc',deploymentPublicKey:pub(customer),files:['SafetyNet.exe','resources/app.asar','resources/broker/node.exe','resources/inspector/server.py','resources/python/python.exe','resources/semantic/model.bin','safetynet-service-host.exe'].map(path=>({path,bytes:1,sha256:'a'.repeat(64)}))};const releaseEnvelope=signManifest(release,priv(vendor),'release'),r=verifyReleaseEnvelope(releaseEnvelope,{publicKey:pub(vendor)});
|
||
|
|
const deployment={format:'safetynet-windows-deployment-v2',releaseId:release.releaseId,deploymentTenant:release.deploymentTenant,deploymentSequence:1,policyPublicKey:pub(ed),expiresAt:Date.now()+60000,users:[{sid,deviceId:'device',minimumPolicyVersion:7}],management:{origin,signatureKey:pub(pq),protocol:channel.VERSION}},deploymentEnvelope=signManifest(deployment,priv(customer),'deployment'),d=verifyDeploymentEnvelope(deploymentEnvelope,{release:r,authenticatedSid:sid});
|
||
|
|
const policy=defaultPolicy();policy.version=7;policy.assignment={deviceId:'device',groups:[]};policy.proxy={mode:'direct'};const policyEnvelope=signPolicy(policy,priv(ed));
|
||
|
|
const snapshot={version:1,authenticatedSid:sid,releaseAnchor:pub(vendor),acceptanceState:{format:'safetynet-service-floors-v1',release:{minimumSequence:1,acceptedDigest:r.digest},deployment:{tenant:release.deploymentTenant,minimumSequence:1,acceptedDigest:d.digest},policies:[]}},files={releaseEnvelope,deploymentEnvelope};const plan=await verifyServiceAcceptance({...files,releaseAnchor:snapshot.releaseAnchor,authenticatedSid:sid,state:snapshot.acceptanceState,policyEnvelope});snapshot.acceptanceState=plan.nextState;
|
||
|
|
const store={async snapshot(){return snapshot;},async envelopes(){return files;}};const config={store,policyEnvelope,network:{mode:'direct',dns:'system',ca:cert.toString()},current:()=>true,loadCredential:async binding=>{assert.equal(binding.deviceId,'device');assert.equal(binding.origin,origin);return {deviceId:'device',origin,token:'synthetic-protected-token'};}};
|
||
|
|
return {config,origin,pq,ed,snapshot,get requests(){return requests;},get tcp(){return tcp;},set result(v){result=v;},set status(v){status=v;},set handler(v){handler=v;}};
|
||
|
|
}
|
||
|
|
test('actual HTTPS identity verifies genuine Ed25519 plus deployment-pinned MLDSA87',async t=>{const f=await fixture(t),op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());const value=await op.identity();assert.equal(value.signatureKey,pub(f.pq));assert.equal(f.requests,1);await assert.rejects(op.identity({url:'https://attacker.example',token:'override'}),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||
|
|
test('protected credentials missing or mismatched never start network',async t=>{let storeCalls=0;await assert.rejects(createManagementIdentityOperation({store:{snapshot(){storeCalls++;}}}),{message:'broker-management-refused'});assert.equal(storeCalls,0);const f=await fixture(t),op=await createManagementIdentityOperation({...f.config,loadCredential:async()=>({deviceId:'other',origin:f.origin,token:'synthetic-protected-token'})});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.tcp,0);});
|
||
|
|
test('identity tampering and redirect fail without retry',async t=>{const f=await fixture(t),op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());f.result={version:channel.VERSION,signatureKey:pub(f.pq),edSignature:'a',pqSignature:'b'};await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,1);f.status=302;await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,2);});
|
||
|
|
test('timed-out protected credential retains admission until original settlement',async t=>{const f=await fixture(t);let settle,calls=0;const pending=new Promise(r=>{settle=r;}),op=await createManagementIdentityOperation({...f.config,deadlineMs:20,loadCredential:()=>{calls++;return pending;}});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(calls,1);assert.equal(f.tcp,0);let closed=false;const closing=op.close().then(()=>{closed=true;});await new Promise(r=>setTimeout(r,10));assert.equal(closed,false);const lateToken=Buffer.from('synthetic-protected-token');settle({deviceId:'device',origin:f.origin,token:lateToken});await closing;assert.ok(lateToken.every(v=>v===0));assert.equal(f.tcp,0);});
|
||
|
|
test('validly dual-signed identity with another PQ key is refused by deployed pin',async t=>{const f=await fixture(t),other=generateKeyPairSync('ml-dsa-87');f.result=channel.identity(f.ed.privateKey,other.privateKey,pub(other));const op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||
|
|
test('pre-enrollment floors and revocation during credential load cannot dial',async t=>{const f=await fixture(t),saved=f.snapshot.acceptanceState;f.snapshot.acceptanceState={...saved,policies:[]};await assert.rejects(createManagementIdentityOperation(f.config),{message:'broker-management-refused'});f.snapshot.acceptanceState=saved;let active=true;const op=await createManagementIdentityOperation({...f.config,current:()=>active,loadCredential:async()=>{active=false;return {deviceId:'device',origin:f.origin,token:'synthetic-protected-token'};}});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});assert.equal(f.tcp,0);});
|
||
|
|
test('private protected token lease is erased after actual HTTPS use',async t=>{const f=await fixture(t),token=Buffer.from('synthetic-protected-token');const op=await createManagementIdentityOperation({...f.config,loadCredential:async binding=>{assert.match(binding.policyDigest,/^[a-f0-9]{64}$/);return {deviceId:'device',origin:f.origin,token};}});t.after(()=>op.close());await op.identity();assert.ok(token.every(byte=>byte===0));assert.equal(f.requests,1);});
|
||
|
|
function policyHandler(f,{version=8,deviceId='device',challengeDevice='device',tamper=false,onChallenge=()=>{}}={}){
|
||
|
|
let pair,used=false;return async(req,res)=>{
|
||
|
|
assert.equal(req.method,'POST');const chunks=[];for await(const part of req)chunks.push(part);const body=JSON.parse(Buffer.concat(chunks).toString());
|
||
|
|
if(req.url==='/api/device/channel/challenge'){assert.deepEqual(body,{});pair=channel.challenge(challengeDevice,f.ed.privateKey,f.pq.privateKey);onChallenge();res.end(JSON.stringify(pair.bundle));return;}
|
||
|
|
assert.equal(req.url,'/api/device/channel/policy');assert.equal(used,false);used=true;const context=channel.receive(body,pair.bundle,pair.privateKey);try{const p=defaultPolicy();p.version=version;p.assignment={deviceId,groups:[]};p.proxy={mode:'direct'};const envelope=signPolicy(p,priv(f.ed)),response=channel.response(envelope,pair.bundle,context);if(tamper)response.encrypted.tag='a'.repeat(22);res.end(JSON.stringify(response));}finally{context.responseKey.fill(0);}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
test('actual paired MLKEM1024/MLDSA87 HTTPS policy operation verifies new device-bound policy',async t=>{const f=await fixture(t);f.handler=policyHandler(f);const op=await createManagementIdentityOperation({...f.config,loadCredential:undefined,credentialLoader:{load:f.config.loadCredential}});t.after(()=>op.close());const result=await op.policy();assert.equal(verifyPolicy(result,pub(f.ed),7).version,8);assert.equal(f.requests,2);assert.equal(f.snapshot.acceptanceState.policies[0].minimumVersion,7);await assert.rejects(op.policy({url:'https://other.example'}),{message:'broker-management-refused'});assert.equal(f.requests,2);});
|
||
|
|
test('policy channel wrong device, rollback, same-version equivocation and encrypted tampering fail',async t=>{for(const options of [{challengeDevice:'other'},{deviceId:'other'},{version:6},{version:7},{tamper:true}]){const f=await fixture(t);f.handler=policyHandler(f,options);const op=await createManagementIdentityOperation(f.config);t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.requests,options.challengeDevice?1:2);}});
|
||
|
|
test('revocation after signed challenge prevents policy POST and uses one total job budget',async t=>{const f=await fixture(t);let active=true;f.handler=policyHandler(f,{onChallenge:()=>{active=false;}});const op=await createManagementIdentityOperation({...f.config,current:()=>active});t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.requests,1);});
|
||
|
|
test('challenge and policy share one deadline rather than renewing per request',async t=>{const f=await fixture(t),handle=policyHandler(f);f.handler=async(req,res)=>{const end=res.end.bind(res);res.end=(...args)=>{setTimeout(()=>end(...args),180);return res;};await handle(req,res);};const token=Buffer.from('synthetic-protected-token'),op=await createManagementIdentityOperation({...f.config,deadlineMs:300,loadCredential:async()=>({deviceId:'device',origin:f.origin,token})});t.after(()=>op.close());await assert.rejects(op.policy(),{message:'broker-management-refused'});await new Promise(r=>setTimeout(r,250));assert.equal(f.requests,2);assert.ok(token.every(v=>v===0));});
|
||
|
|
test('default fixed native loader refuses unavailable authenticated service context before network',async t=>{const f=await fixture(t),previous=process.env.SAFETYNET_SERVICE_CLIENT_SID;delete process.env.SAFETYNET_SERVICE_CLIENT_SID;try{const op=await createManagementIdentityOperation({...f.config,loadCredential:undefined});t.after(()=>op.close());await assert.rejects(op.identity(),{message:'broker-management-refused'});await assert.rejects(op.policy(),{message:'broker-management-refused'});assert.equal(f.tcp,0);}finally{if(previous!==undefined)process.env.SAFETYNET_SERVICE_CLIENT_SID=previous;}});
|