Files
safetynet/tests/broker-payload-staging.test.mjs
T

37 lines
7.9 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtemp,writeFile,readFile,cp,rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import path from 'node:path';
import {createHash} from 'node:crypto';
import {gzipSync} from 'node:zlib';
import {spawn} from 'node:child_process';
import {fileURLToPath} from 'node:url';
import {BROKER_PAYLOAD,extractVerifiedZod,snapshotSources,verifySourceSnapshot,stageBrokerPayload} from '../scripts/stage-broker-payload.mjs';
import {verifyBrokerPackage} from '../scripts/broker-package-contract.mjs';
import {BROKER_RUNTIME} from '../scripts/provision-broker-runtime.mjs';
const repo=fileURLToPath(new URL('../',import.meta.url));
// Explicit native proof uses a prepared local stage. Production expected
// state comes from the protected ASAR, never this adjacent test manifest.
const nativeStage=process.env.SAFETYNET_TEST_BROKER_ROOT;
async function currentStage(){
assert.equal(typeof nativeStage,'string','explicit native stage required');
const root=path.resolve(nativeStage),parent=path.resolve(repo,'.runtime/windows');
assert.equal(path.dirname(root),parent,'stage must stay in the reviewed runtime directory');
assert.match(path.basename(root),/^broker-payload-package-[a-z0-9-]+$/,'a unique current stage is required');
const manifest=JSON.parse(await readFile(root+'.manifest.json','utf8'));
await verifyBrokerPackage(root,manifest,BROKER_PAYLOAD);
const executable=manifest.files.find(file=>file.path==='node.exe');
assert.equal(executable?.sha256,BROKER_RUNTIME.executableSha256,'exact official runtime pin required');
return {root,manifest};
}
async function temporary(fn){const root=await mkdtemp(path.join(tmpdir(),'safetynet-broker-stage-test-'));try{return await fn(root);}finally{const absolute=path.resolve(root);assert.equal(path.dirname(absolute),path.resolve(tmpdir()));assert.ok(path.basename(absolute).startsWith('safetynet-broker-stage-test-'));await rm(absolute,{recursive:true,force:true});}}
function tar(entries){const chunks=[];for(const [name,content,type='0'] of entries){const data=Buffer.from(content),header=Buffer.alloc(512);header.write(name);header.write('0000644\0',100);header.write('0000000\0',108);header.write('0000000\0',116);header.write(data.length.toString(8).padStart(11,'0')+'\0',124);header.fill(32,148,156);header.write(type,156);header.write('ustar\0',257);header.write([...header].reduce((a,b)=>a+b,0).toString(8).padStart(6,'0')+'\0 ',148);chunks.push(header,data,Buffer.alloc((512-data.length%512)%512));}chunks.push(Buffer.alloc(1024));return gzipSync(Buffer.concat(chunks));}
function lock(bytes){return {version:'4.6.7',resolved:'https://registry.npmjs.org/zod/-/zod-4.6.7.tgz',integrity:'sha512-'+createHash('sha512').update(bytes).digest('base64')};}
const minimum=[['package/package.json',JSON.stringify({name:'zod',version:'4.6.7'})],['package/LICENSE','MIT synthetic fixture'],['package/index.js','export const z=1;']];
test('verified dependency archive copies runtime and license, excludes development and secrets',()=>{const bytes=tar([...minimum,['package/src/dev.ts','dev'],['package/test/fixture.js','dev'],['package/.env','SECRET'],['package/key.pem','KEY']]);const files=extractVerifiedZod(bytes,lock(bytes));assert.deepEqual(files.map(f=>f.path),['node_modules/zod/package.json','node_modules/zod/LICENSE','node_modules/zod/index.js']);});
test('archive rejects tampering, traversal, symlinks, duplicate-case paths and ADS before staging',()=>{const valid=tar(minimum);assert.throws(()=>extractVerifiedZod(Buffer.concat([valid,Buffer.from('tamper')]),lock(valid)),/integrity/);for(const entry of [['package/../../escape.js','x'],['package/link.js','x','2'],['package/INDEX.js','x'],['package/a.js:stream.js','x'],['package/CON.js','x']]){const bytes=tar([...minimum,entry]);assert.throws(()=>extractVerifiedZod(bytes,lock(bytes)),/archive/);}});
test('source snapshot detects changes, including a same-size mutation',()=>temporary(async root=>{await writeFile(path.join(root,'source.cjs'),'AAA');const snapshot=await snapshotSources(root,['source.cjs']);await writeFile(path.join(root,'source.cjs'),'BBB');await assert.rejects(verifySourceSnapshot(root,snapshot),/source_changed/);await assert.rejects(snapshotSources(root,['../outside']),/path_unsafe/);}));
test('stager refuses destination outside reviewed ignored root before copying',async()=>{await assert.rejects(stageBrokerPayload({root:repo,destination:path.join(repo,'dist','broker')}),/destination_unsafe/);});
test('actual staged package excludes activation scripts and has complete trusted inventory',{skip:!nativeStage?'requires explicit SAFETYNET_TEST_BROKER_ROOT native stage':false},async()=>{const {root,manifest}=await currentStage();assert.ok(root);for(const file of manifest.files){assert.ok(!/(^|\/)(tests?|artifacts?|models?|__pycache__)(\/|$)|\.env$|\.(?:pem|py|pyc|exe)$/.test(file.path)||file.path==='node.exe');assert.ok(!file.path.startsWith('apps/browser/')||['policy-assignment.cjs','enterprise-inspection.cjs','icap-client.cjs','inspect-text-representations.cjs','header-inspection.cjs','inspection.cjs','loopback-http.cjs'].includes(file.path.slice(13)));}assert.ok(manifest.files.some(f=>f.path==='node_modules/zod/LICENSE'));});
test('bundled official node loads real authority/policy outside repository with empty dependency environment',{skip:!nativeStage?'requires explicit SAFETYNET_TEST_BROKER_ROOT native stage':false},()=>temporary(async root=>{const {root:staged,manifest}=await currentStage();await cp(staged,root,{recursive:true});const script=`import {createRequire} from 'node:module';import {generateKeyPairSync} from 'node:crypto';const require=createRequire(process.cwd()+'/probe.cjs');const {createPolicyAuthority}=require('./apps/network-broker/policy-authority.cjs');const {defaultPolicy}=await import('./packages/core/policy.mjs');const service=require('./apps/network-broker/service-entry.cjs');const store=require('./apps/network-broker/service-store-client.cjs');const trust=require('./packages/core/windows-trust.cjs');const management=require('./apps/network-broker/management-identity-operation.cjs');const topology=require('./apps/network-broker/wfp-topology.cjs');const revalidation=require('./apps/network-broker/wfp-revalidation.cjs');const keys=generateKeyPairSync('ed25519');const authority=await createPolicyAuthority({publicKey:keys.publicKey.export({type:'spki',format:'pem'}),deviceId:'synthetic-staged-proof'});console.log(JSON.stringify({node:process.version,policyVersion:defaultPolicy().version,authorityActive:authority.status().active,dependency:require.resolve('zod'),nodePathPresent:!!process.env.NODE_PATH,serviceExportsLoaded:typeof service.runServiceProcess==='function'&&typeof store.createServiceStoreClient==='function'&&typeof trust.verifyDeploymentEnvelope==='function'&&typeof management.createManagementIdentityOperation==='function'&&typeof topology.validateTopology==='function'&&typeof revalidation.createWfpRevalidation==='function'}));`;const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP'])if(process.env[key])env[key]=process.env[key];const output=await new Promise((resolve,reject)=>{const child=spawn(path.join(root,'node.exe'),['--input-type=module','-e',script],{cwd:root,env,windowsHide:true,timeout:15000});let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);child.on('error',reject);child.on('close',code=>code===0?resolve(stdout):reject(Error('bundled probe failed '+code+' '+stderr)));});const proof=JSON.parse(output);assert.equal(proof.node,'v'+BROKER_RUNTIME.version);assert.equal(proof.policyVersion,1);assert.equal(proof.authorityActive,false);assert.equal(proof.nodePathPresent,false);assert.equal(proof.serviceExportsLoaded,true);assert.ok(path.resolve(proof.dependency).startsWith(path.join(root,'node_modules','zod')+path.sep));await writeFile(path.join(root,'apps/network-broker/policy-authority.cjs'),'TAMPER');await assert.rejects(verifyBrokerPackage(root,manifest,BROKER_PAYLOAD),/integrity_mismatch/);}));