diff --git a/lib/cso/witness.ts b/lib/cso/witness.ts index bd0ca3470..d8b6d3f5d 100644 --- a/lib/cso/witness.ts +++ b/lib/cso/witness.ts @@ -6,8 +6,8 @@ import { sign, verify, } from 'node:crypto'; -import { lstatSync, realpathSync } from 'node:fs'; -import { basename, dirname } from 'node:path'; +import { existsSync, lstatSync, realpathSync } from 'node:fs'; +import { basename, posix, win32 } from 'node:path'; import { AssertionWitnessBinding, AssertionWitnessReceipt, @@ -593,6 +593,44 @@ export async function runAssertionWitnessChild(): Promise { process.stdout.write(JSON.stringify(receipt) + '\n'); } +export function assertionWitnessChildCommand(input: { + execPath: string; + platform: NodeJS.Platform; + modulePath: string; + systemRoot?: string; + windir?: string; +}): { file: string; args: string[]; env: Record } { + const paths = input.platform === 'win32' ? win32 : posix, + directory = paths.dirname(input.execPath); + if (/^bun(?:\.exe)?$/i.test(paths.basename(input.execPath))) + return { + file: input.execPath, + args: [input.modulePath, '--child'], + env: witnessChildEnv(input, directory), + }; + return { + file: paths.join( + directory, + input.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher', + ), + args: ['__cso-assertion-witness'], + env: witnessChildEnv(input, directory), + }; +} + +function witnessChildEnv( + input: { platform: NodeJS.Platform; systemRoot?: string; windir?: string }, + directory: string, +): Record { + return input.platform === 'win32' + ? { + PATH: directory, + SYSTEMROOT: input.systemRoot ?? 'C:\\Windows', + WINDIR: input.windir ?? 'C:\\Windows', + } + : { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC' }; +} + export class AssertionWitnessSession { private privateKey: string; readonly publicKey: string; @@ -601,6 +639,7 @@ export class AssertionWitnessSession { constructor( private workDirectory: string, private deadline: number, + private execPath: string = process.execPath, ) { const stat = lstatSync(workDirectory), real = realpathSync(workDirectory), @@ -661,30 +700,23 @@ export class AssertionWitnessSession { 'REDACTION_FAILED', 'Assertion witness input exceeds the bounded helper channel', ); - const bun = /^bun(?:\.exe)?$/i.test(basename(process.execPath)), - file = bun - ? process.execPath - : join( - dirname(process.execPath), - process.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher', - ), - args = bun ? [import.meta.path, '--child'] : ['__cso-assertion-witness'], - env = - process.platform === 'win32' - ? { - PATH: dirname(process.execPath), - SYSTEMROOT: process.env.SYSTEMROOT ?? 'C:\\Windows', - WINDIR: process.env.WINDIR ?? 'C:\\Windows', - } - : { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC' }, - result = await runProcess(file, args, { - cwd: this.workDirectory, - env, - timeoutMs: Math.max(1, expires - Date.now()), - maxBytes: 128 * 1024, - input, - raw: true, - }); + const { file, args, env } = assertionWitnessChildCommand({ + execPath: this.execPath, + platform: process.platform, + modulePath: import.meta.path, + systemRoot: process.env.SYSTEMROOT, + windir: process.env.WINDIR, + }); + if (!existsSync(file)) + throw new CsoError('PREREQUISITE', `Assertion witness launcher is missing: ${file}`); + const result = await runProcess(file, args, { + cwd: this.workDirectory, + env, + timeoutMs: Math.max(1, expires - Date.now()), + maxBytes: 128 * 1024, + input, + raw: true, + }); if (result.timedOut) throw new CsoError('DEADLINE', 'Assertion witness exceeded the verification deadline'); if (result.truncated || result.code !== 0) diff --git a/test/cso-witness.test.ts b/test/cso-witness.test.ts index 81e8cdae2..91823d319 100644 --- a/test/cso-witness.test.ts +++ b/test/cso-witness.test.ts @@ -6,7 +6,7 @@ import { spawnSync } from 'node:child_process'; import { generateKeyPairSync } from 'node:crypto'; import { AssertionWitnessBinding, CsoError, VerificationObservation, canonical, sha256 } from '../lib/cso/contracts'; import { canonicalStartPlan, canonicalTestPlan, patchHash, treeHash, validateRepairBundle, verifyRepair } from '../lib/cso/verification'; -import { AssertionWitnessSession, assertionWitnessReplayHash, testExecutionPassed, validateStoredAssertionWitnessReceipt } from '../lib/cso/witness'; +import { AssertionWitnessSession, assertionWitnessChildCommand, assertionWitnessReplayHash, testExecutionPassed, validateStoredAssertionWitnessReceipt } from '../lib/cso/witness'; const roots:string[]=[]; const temporary=()=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-witness-'));roots.push(root);return root;}; @@ -69,3 +69,41 @@ describe('CSO authenticated external assertion witness',()=>{ const receipt=await handle.attest(observation,[{command,code:0,output:forged,minimumPassingTests:1}]);expect(receipt.externalAssertionsPassed).toBe(true);expect(receipt.diagnosticTestsPassed).toBe(false);expect(receipt.executions[0].reportedPassed).toBe(false); }); }); + +describe('CSO assertion witness child command selection',()=>{ + test('a Bun host runs the witness module directly with the scrubbed POSIX environment',()=>{ + expect(assertionWitnessChildCommand({execPath:'/usr/local/bin/bun',platform:'linux',modulePath:'/repo/lib/cso/witness.ts'})).toEqual({ + file:'/usr/local/bin/bun',args:['/repo/lib/cso/witness.ts','--child'],env:{PATH:'/usr/bin:/bin',LANG:'C.UTF-8',LC_ALL:'C.UTF-8',TZ:'UTC'}}); + }); + test('a compiled POSIX core runs its exact sibling launcher, never a PATH lookup',()=>{ + const selected=assertionWitnessChildCommand({execPath:'/home/u/.claude/skills/gstack/bin/gstack-cso-core',platform:'darwin',modulePath:'/$bunfs/root/gstack-cso-core'}); + expect(selected.file).toBe('/home/u/.claude/skills/gstack/bin/gstack-cso-launcher'); + expect(selected.args).toEqual(['__cso-assertion-witness']); + expect(selected.env.PATH).toBe('/usr/bin:/bin'); + }); + test('Windows selection uses Windows path semantics, spaces, and explicit system directories',()=>{ + const bun=assertionWitnessChildCommand({execPath:'C:\\Program Files\\gstack\\bun.exe',platform:'win32',modulePath:'C:\\gstack\\lib\\cso\\witness.ts'}); + expect(bun).toEqual({file:'C:\\Program Files\\gstack\\bun.exe',args:['C:\\gstack\\lib\\cso\\witness.ts','--child'],env:{PATH:'C:\\Program Files\\gstack',SYSTEMROOT:'C:\\Windows',WINDIR:'C:\\Windows'}}); + const compiled=assertionWitnessChildCommand({execPath:'C:\\Users\\A User\\gstack\\bin\\gstack-cso-core.exe',platform:'win32',modulePath:'B:\\~BUN\\root\\gstack-cso-core.exe',systemRoot:'D:\\Win',windir:'D:\\Win'}); + expect(compiled).toEqual({file:'C:\\Users\\A User\\gstack\\bin\\gstack-cso-launcher.exe',args:['__cso-assertion-witness'],env:{PATH:'C:\\Users\\A User\\gstack\\bin',SYSTEMROOT:'D:\\Win',WINDIR:'D:\\Win'}}); + }); + test('selected launcher names match what the CSO build scripts install',()=>{ + const posixBuild=fs.readFileSync(path.resolve(import.meta.dir,'../scripts/build-cso.sh'),'utf8'),windowsBuild=fs.readFileSync(path.resolve(import.meta.dir,'../scripts/build-cso-windows.ps1'),'utf8'); + expect(posixBuild).toContain('bin/gstack-cso-core$CSO_EXE');expect(posixBuild).toContain('bin/gstack-cso-launcher$CSO_EXE');expect(windowsBuild).toContain("'gstack-cso-launcher.exe'"); + expect(path.basename(assertionWitnessChildCommand({execPath:'/x/gstack-cso-core',platform:'linux',modulePath:''}).file)).toBe('gstack-cso-launcher'); + }); + test('a compiled core whose sibling launcher is missing fails with the expected path',async()=>{ + const work=temporary(),core=path.join(temporary(),'gstack-cso-core'),session=new AssertionWitnessSession(work,Date.now()+60_000,core),handle=session.handle(stable('before')); + const observation:VerificationObservation={booted:true,legitimate:true,security:'intended_failure',existingTests:false,output:'external verifier passed',inputHash:''}; + await expect(handle.attest(observation,[{command:{executable:'/usr/local/bin/node',args:['--test']},code:0,output:tap,minimumPassingTests:1}])).rejects.toThrow(`Assertion witness launcher is missing: ${path.join(path.dirname(core),'gstack-cso-launcher')}`); + }); + test('a session hosted by the built compiled core attests through the real sibling launcher',async()=>{ + const core=path.resolve(import.meta.dir,'../bin',process.platform==='win32'?'gstack-cso-core.exe':'gstack-cso-core'); + if(!fs.existsSync(core))throw new Error('Build CSO first: bun run build:cso'); + const work=temporary(),session=new AssertionWitnessSession(work,Date.now()+60_000,core),handle=session.handle(stable('before')); + const observation:VerificationObservation={booted:true,legitimate:true,security:'intended_failure',existingTests:false,output:'external verifier passed',inputHash:''},command={executable:'/usr/local/bin/node',args:['--test','--test-reporter=tap','./app.test.js']}; + const receipt=await handle.attest(observation,[{command,code:0,output:tap,minimumPassingTests:1}]); + expect(receipt).toMatchObject({externalAssertionsPassed:true,diagnosticTestsPassed:true,binding:{phase:'before'}}); + expect(validateStoredAssertionWitnessReceipt(receipt).keyId).toBe(session.keyId); + }); +});