81 lines
5.1 KiB
JavaScript
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 } };
|
|
}
|