'use strict'; const assert = require('node:assert/strict'); const { spawnSync } = require('node:child_process'); const { createHash, webcrypto } = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const { createClusterConsoleEvidenceBundle, serializeClusterConsoleEvidenceBundle, } = require('../assets/copilot-console/evidence-bundle.js'); const { CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA, CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA, verifyClusterConsoleEvidenceBundleFile, } = require('../dist/copilot-console/evidenceVerifier.js'); const cliPath = path.resolve( __dirname, '../dist/copilot-console/evidenceVerifierCli.js', ); const requestSchema = 'qinglong/cluster-copilot-console-read-request@v1'; function canonicalize(value) { if (value === null || typeof value !== 'object') { return JSON.stringify(Object.is(value, -0) ? 0 : value); } if (Array.isArray(value)) { return `[${value.map(canonicalize).join(',')}]`; } return `{${Object.keys(value) .sort() .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`) .join(',')}}`; } function resign(bundle) { const unsigned = {}; for (const key of Object.keys(bundle)) { if (key !== 'contentDigest') unsigned[key] = bundle[key]; } bundle.contentDigest = createHash('sha256') .update(canonicalize(unsigned), 'utf8') .digest('hex'); return bundle; } async function validBundle() { return createClusterConsoleEvidenceBundle( [ { operation: 'run_read', observedAtMs: 1_700_000_000_000, request: { schema: requestSchema, operation: 'run_read', projectId: 'private-project', requestId: 'private-request', runId: 'private-run', }, fact: { schema: 'qinglong/bounded-run-projection@v1', schemaVersion: 1, status: 'succeeded', projectId: 'private-project', runId: 'private-run', createdAtMs: 1_700_000_000_000, outputAvailable: true, message: 'must-never-survive-redaction', }, }, ], 1_700_000_001_000, webcrypto, ); } function fixture(t, encoded, name = 'evidence.json') { const directory = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-evidence-verifier-')), ); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const filePath = path.join(directory, name); fs.writeFileSync(filePath, encoded, { mode: 0o644 }); return { directory, filePath }; } test('independently verifies the browser generator output without authority claims', async (t) => { const bundle = await validBundle(); const encoded = serializeClusterConsoleEvidenceBundle(bundle); const { filePath } = fixture(t, encoded); const result = verifyClusterConsoleEvidenceBundleFile(filePath); assert.deepEqual(result, { schema: CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA, status: 'verified', bundle: { schema: CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA, contentDigest: bundle.contentDigest, entryCount: 1, totalRawCanonicalBytes: bundle.source.totalRawCanonicalBytes, }, integrity: { bundleDigest: 'verified', rawFactDigests: 'not_recomputed_without_raw_facts', }, claims: { serverSignature: 'not_verified', attestation: 'not_verified', durableAudit: 'not_verified', actionAuthority: 'none', }, execution: { networkAccess: false, mutation: false, fileWrites: false }, }); assert.equal(Object.isFrozen(result), true); assert.equal(encoded.includes('must-never-survive-redaction'), false); }); test('cross-verifies every fixed Console read operation', async (t) => { const requests = { inspect: { projectId: 'p-1', requestId: 'q-1', sourceRunId: 'r-1' }, output: { projectId: 'p-2', requestId: 'q-2', sourceRunId: 'r-2' }, run_cancellation_status: { projectId: 'p-management', requestId: 'q-management-status', }, run_cancellation_blocked_list: { cursor: null, projectId: 'p-management', requestId: 'q-management-blocked', }, run_cancellation_inspect: { projectId: 'p-management', requestId: 'q-management-inspect', runId: 'r-management', }, worker_list: { afterWorkerId: null, projectId: 'p-worker', requestId: 'q-worker-list', }, worker_inspect: { projectId: 'p-worker', requestId: 'q-worker-inspect', workerId: 'worker-1', }, package_list: { afterPackageName: null, projectId: 'p-package', requestId: 'q-package-list', }, package_inspect: { packageName: 'ops-package', projectId: 'p-package', requestId: 'q-package-inspect', }, run_list: { afterCreatedAtMs: null, afterRunId: null, limit: 32, projectId: 'p-3', requestId: 'q-3', }, run_read: { projectId: 'p-4', requestId: 'q-4', runId: 'r-4' }, run_event_list: { afterSequence: null, limit: 32, projectId: 'p-5', requestId: 'q-5', runId: 'r-5', }, run_step_list: { afterStepKey: null, afterStepRunId: null, limit: 32, projectId: 'p-6', requestId: 'q-6', runId: 'r-6', }, task_list: { afterTaskId: null, limit: 32, projectId: 'p-7', requestId: 'q-7', }, task_read: { projectId: 'p-8', requestId: 'q-8', taskId: 't-8' }, workflow_list: { packageName: 'pkg-9', projectId: 'p-9', requestId: 'q-9', }, workflow_run_list: { afterAdmittedAtMs: null, afterRunId: null, limit: 32, packageName: 'pkg-10', projectId: 'p-10', requestId: 'q-10', workflowId: 'w-10', }, workflow_run_read: { packageName: 'pkg-11', projectId: 'p-11', requestId: 'q-11', runId: 'r-11', workflowId: 'w-11', }, workflow_event_list: { afterSequence: null, limit: 32, packageName: 'pkg-12', projectId: 'p-12', requestId: 'q-12', runId: 'r-12', workflowId: 'w-12', }, workflow_step_list: { afterStepKey: null, afterStepRunId: null, limit: 32, packageName: 'pkg-13', projectId: 'p-13', requestId: 'q-13', runId: 'r-13', workflowId: 'w-13', }, }; const records = Object.entries(requests).map( ([operation, request], index) => ({ operation, observedAtMs: 1_700_000_000_000 + index, request: { schema: requestSchema, operation, ...request }, fact: { schema: 'qinglong/test-fact@v1', schemaVersion: 1, operation, status: 'succeeded', }, }), ); let verified = 0; for (let index = 0; index < records.length; index += 16) { const bundle = await createClusterConsoleEvidenceBundle( records.slice(index, index + 16), 1_700_000_001_000 + index, webcrypto, ); const { filePath } = fixture( t, serializeClusterConsoleEvidenceBundle(bundle), `operations-${index}.json`, ); const result = verifyClusterConsoleEvidenceBundleFile(filePath); assert.equal(result.status, 'verified'); verified += result.bundle.entryCount; } assert.equal(verified, 20); }); test('CLI is secret-free on success, invalid input and usage errors', async (t) => { const bundle = await validBundle(); const { filePath } = fixture( t, serializeClusterConsoleEvidenceBundle(bundle), 'private-customer-run.json', ); const success = spawnSync( process.execPath, [cliPath, `--bundle=${filePath}`], { encoding: 'utf8', }, ); assert.equal(success.status, 0); assert.equal(JSON.parse(success.stdout).status, 'verified'); assert.equal(success.stderr, ''); assert.equal(success.stdout.includes(filePath), false); assert.equal(success.stdout.includes('private-customer-run'), false); const tampered = JSON.parse(fs.readFileSync(filePath, 'utf8')); tampered.entries[0].fact.status = 'failed'; fs.writeFileSync(filePath, `${JSON.stringify(tampered, null, 2)}\n`); const invalid = spawnSync( process.execPath, [cliPath, `--bundle=${filePath}`], { encoding: 'utf8', }, ); assert.equal(invalid.status, 65); assert.equal(invalid.stdout, ''); assert.equal( JSON.parse(invalid.stderr).code, 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID', ); assert.equal(invalid.stderr.includes(filePath), false); const usage = spawnSync(process.execPath, [cliPath], { encoding: 'utf8' }); assert.equal(usage.status, 64); assert.equal( JSON.parse(usage.stderr).code, 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFIER_USAGE_INVALID', ); const help = spawnSync(process.execPath, [cliPath, '--help'], { encoding: 'utf8', }); assert.equal(help.status, 0); assert.match(help.stdout, /^Usage: ql3-copilot-evidence-verify/); }); test('rejects re-signed structural widening, alias gaps and false proof claims', async (t) => { const baseline = JSON.parse( serializeClusterConsoleEvidenceBundle(await validBundle()), ); const mutations = [ (bundle) => { bundle.entries[0].fact.message = 'unsafe free text'; }, (bundle) => { bundle.entries[0].target.runId = 'run-002'; }, (bundle) => { bundle.integrity.serverSignature = true; }, (bundle) => { bundle.source.totalRawCanonicalBytes += 1; }, (bundle) => { bundle.entries[0].sequence = 2; }, ]; for (const [index, mutate] of mutations.entries()) { const candidate = structuredClone(baseline); mutate(candidate); resign(candidate); const { filePath } = fixture( t, `${JSON.stringify(candidate, null, 2)}\n`, `invalid-${index}.json`, ); assert.throws(() => verifyClusterConsoleEvidenceBundleFile(filePath), { code: 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID', }); } }); test('rejects non-canonical JSON, links, relative paths and oversized files', async (t) => { const encoded = serializeClusterConsoleEvidenceBundle(await validBundle()); for (const [name, contents] of [ ['minified.json', JSON.stringify(JSON.parse(encoded))], ['bom.json', `\ufeff${encoded}`], ['crlf.json', encoded.replaceAll('\n', '\r\n')], [ 'duplicate.json', encoded.replace( '{\n', `{\n "schema": "${CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA}",\n`, ), ], ]) { const { filePath } = fixture(t, contents, name); assert.throws( () => verifyClusterConsoleEvidenceBundleFile(filePath), undefined, name, ); } const { directory, filePath } = fixture(t, encoded, 'source.json'); const linkPath = path.join(directory, 'link.json'); fs.symlinkSync(filePath, linkPath); assert.throws(() => verifyClusterConsoleEvidenceBundleFile(linkPath)); assert.throws(() => verifyClusterConsoleEvidenceBundleFile('source.json')); const oversized = path.join(directory, 'oversized.json'); fs.writeFileSync(oversized, 'x'.repeat(512 * 1024 + 1)); assert.throws(() => verifyClusterConsoleEvidenceBundleFile(oversized)); });