Files

81 lines
5.1 KiB
JavaScript

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 } };
}