import { resolve } from 'node:path'; import { BusError } from './broker.mjs'; import { verifyHuman, readProcess as defaultRead } from './human.mjs'; import { verifyTester, testerNonce } from './tester.mjs'; // Trusted transport API. Identity is held only by this connection's transient session. // User mapping and live grant authorization belong to R3, not to this verifier. export function verifyPrincipal(request, { peer, paths, businesses, launches = [], readProcess = defaultRead, readTesterNonce = testerNonce, consoleOwners = [] }) { const kinds = ['human', 'tester', 'user', 'console'].filter(k => request?.[k]); if (kinds.length !== 1 || request.cap) throw new BusError('invalid-envelope'); const kind = kinds[0], proof = request[kind]; const error = kind === 'tester' ? 'tester-required' : 'human-required'; const refuse = () => { throw new BusError(error); }; if (!peer || peer.uid !== process.getuid() || peer.pid !== proof.pid) refuse(); if (kind === 'tester') { const { business } = verifyTester(proof, { cliPath: paths.tester, launches, readProcess, readNonce: readTesterNonce }); return { kind: 'tester', business, id: 'tester' }; } if (kind === 'console') return verifyConsole(proof, { peer, paths, businesses, launches, readProcess, consoleOwners }); let checked = proof, parent; if (kind === 'user') { if (!proof || typeof proof.user !== 'string' || !proof.user || Object.keys(proof).some(k => !['pid', 'startTime', 'nonce', 'business', 'user'].includes(k))) refuse(); const { user, ...rest } = proof; checked = rest; try { const child = readProcess(proof.pid); parent = readProcess(child.ppid); if (parent.uid !== peer.uid || parent.argv[1] !== resolve(paths.console) || parent.argv[2] !== 'serve') refuse(); } catch { refuse(); } } const { business } = verifyHuman(checked, { cliPath: paths[kind], launches, readProcess, nonceKey: kind === 'user' ? 'MOSAIC_BUS_USER_NONCE' : 'MOSAIC_BUS_CLI_NONCE' }); if (kind === 'user') { try { const child = readProcess(proof.pid), again = readProcess(parent.pid); if (child.ppid !== parent.pid || again.startTime !== parent.startTime || again.ppid !== parent.ppid || again.uid !== parent.uid || again.argv[1] !== parent.argv[1] || again.argv[2] !== 'serve') refuse(); } catch { refuse(); } return { kind: 'user', business, id: proof.user }; } const id = businesses[business]?.human; if (!id) throw new BusError('unknown-business'); return { kind: 'owner', business, id }; } // Row 252 (#1753, design section 2): the Console owner. The same-tree console-cli.mjs wrapper, its own // nonce namespace and the human walk, with the exact same-tree webui `cli.mjs serve` as its direct // parent, read again after the walk as the user kind does. Business and (provider, iss, sub) must be // the wrapper's initial spawn environment and one of the checked console.sso subjects the runtime // was started with (Webby N1: every configured subject is an owner). The Console's mapping is trusted // as row 196 permits; the bus does not exchange an ID token itself. const SUBJECT_KEYS = ['provider', 'iss', 'sub']; const SUBJECT_ENV = { business: 'MOSAIC_BUS_CONSOLE_BUSINESS', provider: 'MOSAIC_BUS_CONSOLE_PROVIDER', iss: 'MOSAIC_BUS_CONSOLE_ISS', sub: 'MOSAIC_BUS_CONSOLE_SUB' }; function verifyConsole(proof, { peer, paths, businesses, launches, readProcess, consoleOwners }) { const refuse = () => { throw new BusError('human-required'); }; if (!proof || typeof proof !== 'object' || Object.keys(proof).some(k => !['pid', 'startTime', 'nonce', 'business', ...SUBJECT_KEYS].includes(k)) || SUBJECT_KEYS.some(k => typeof proof[k] !== 'string' || !proof[k] || proof[k].length > 512)) refuse(); if (typeof paths.consoleCli !== 'string' || typeof paths.console !== 'string') refuse(); const { provider, iss, sub, ...checked } = proof; let child, parent; try { child = readProcess(proof.pid); parent = readProcess(child.ppid); if (parent.uid !== peer.uid || parent.argv[1] !== resolve(paths.console) || parent.argv[2] !== 'serve') refuse(); for (const [key, name] of Object.entries(SUBJECT_ENV)) if (child.env?.[name] !== proof[key]) refuse(); } catch { refuse(); } const { business } = verifyHuman(checked, { cliPath: paths.consoleCli, launches, readProcess, nonceKey: 'MOSAIC_BUS_CONSOLE_NONCE' }); try { const again = readProcess(proof.pid), up = readProcess(parent.pid); if (again.ppid !== parent.pid || again.startTime !== child.startTime || up.startTime !== parent.startTime || up.ppid !== parent.ppid || up.uid !== parent.uid || up.argv[1] !== parent.argv[1] || up.argv[2] !== 'serve') refuse(); for (const [key, name] of Object.entries(SUBJECT_ENV)) if (again.env?.[name] !== proof[key]) refuse(); } catch { refuse(); } if (!Array.isArray(consoleOwners) || !consoleOwners.some(o => o && o.provider === provider && o.iss === iss && o.sub === sub)) refuse(); const id = businesses[business]?.human; if (!id) throw new BusError('unknown-business'); return { kind: 'console', business, id, via: 'console', subject: { provider, iss, sub } }; }