/** * A handler receives runtime cells, and must not be able to set the trust state * of the transaction its writes go through: the implementation identity they * are authored by, or the acting principal. Every case here compiles an * attacker pattern handed a victim piece's cell, dispatches its handler, and * has it look for that transaction on the cell and try to set its trust state. * The victim's `name` may be written only by the victim's own `setName` * handler. */ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { Identity } from "@commonfabric/identity"; import { StorageManager } from "../src/storage/cache.deno.ts"; import { Runtime } from "../src/runtime.ts"; import type { Cell } from "../src/cell.ts"; import type { RuntimeProgram } from "../src/harness/types.ts"; import { computeModuleHashes } from "../src/harness/module-identity.ts"; import { ensureCompilerStack } from "../src/harness/deferred-compiler-stack.ts"; await ensureCompilerStack(); const signer = await Identity.fromPassphrase( "runner-cfc-implementation-identity-authorization", ); const space = signer.did(); const OTHER_PRINCIPAL = "did:key:z6MkotherOtherOtherOtherOtherOtherOtherOtherOth"; const VICTIM_FILE = "/packages/patterns/app/main.tsx"; const ATTACKER_FILE = "/attacker/main.tsx"; const victimProgram = (): RuntimeProgram => ({ main: VICTIM_FILE, files: [{ name: VICTIM_FILE, contents: [ "/// ", "import {", " handler,", " pattern,", " type Stream,", " Writable,", " WriteAuthorizedBy,", '} from "commonfabric";', "", "const setName = handler<", " { name: string },", " { name: Writable }", ">((event, state) => {", ' state.name.set("set:" + event.name);', "});", "", "type Output = {", " name: WriteAuthorizedBy;", " setName: Stream<{ name: string }>;", "};", "", "export default pattern<{ seed?: string }, Output>(() => {", " const name = new Writable<", " WriteAuthorizedBy", ' >("initial").for("name");', " return { name, setName: setName({ name }) };", "});", ].join("\n"), }], }); const victimHandlerIdentity = { kind: "verified", moduleIdentity: computeModuleHashes(victimProgram()).get(VICTIM_FILE)!, sourceFile: VICTIM_FILE, bindingPath: ["setName"], }; // The attacker is handed the victim's `name` cell and a `note` of its own. Its // handler runs `claim`, a function body that sees `tx`, what the victim's cell // has under that name. For an event whose `step` is `"probe"` it records // in `note` what `claim` returned, or the name and message of what it threw, // and writes nothing else, so that record commits whatever the claim did. For // any other step it swallows what `claim` threw and writes the event's `value` // into the victim's `name`. const attackerProgram = (claim: string): RuntimeProgram => ({ main: ATTACKER_FILE, files: [{ name: ATTACKER_FILE, contents: [ "/// ", 'import { handler, pattern, type Stream, Writable } from "commonfabric";', "", "type Event = { step: string; value?: string };", "", "const setName = handler<", " Event,", " { name: Writable; note: Writable }", ">((event, state) => {", " const tx: any = (state.name as any).tx;", " let outcome: string;", " try {", " outcome = String((() => {", claim, " })());", " } catch (error) {", " outcome = 'threw ' + (error as Error).name + ': ' +", " (error as Error).message;", " }", " if (event.step === 'probe') state.note.set(outcome);", " else state.name.set(event.value ?? '');", "});", "", "export default pattern<", " { name: Writable; note: Writable },", " { setName: Stream }", ">(({ name, note }) => ({ setName: setName({ name, note }) }));", ].join("\n"), }], }); describe("cfc-implementation-identity-authorization", () => { let storageManager: ReturnType; let runtime: Runtime; beforeEach(() => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager, }); }); afterEach(async () => { await runtime?.dispose(); await storageManager?.close(); }); const runVictim = async () => { const victimPattern = await runtime.patternManager.compilePattern( victimProgram(), { space }, ); // deno-lint-ignore no-explicit-any const victim = runtime.getCell( space, `victim-${crypto.randomUUID()}`, victimPattern.resultSchema, ) as Cell; await runtime.runSynced(victim, victimPattern, {}); await runtime.idle(); return victim; }; const runAttacker = async (claim: string) => { const victim = await runVictim(); const attackerPattern = await runtime.patternManager.compilePattern( attackerProgram(claim), { space }, ); // deno-lint-ignore no-explicit-any const attacker = runtime.getCell( space, `attacker-${crypto.randomUUID()}`, attackerPattern.resultSchema, ) as Cell; const note = runtime.getCell( space, `note-${crypto.randomUUID()}`, { type: "string" }, ); await runtime.runSynced(attacker, attackerPattern, { name: victim.key("name"), note, }); await runtime.idle(); const send = async (event: Record) => { await runtime.editWithRetry((tx) => attacker.key("setName").withTx(tx).send(event) ); await runtime.idle(); }; return { victim, probe: async () => { await send({ step: "probe" }); return await note.pull(); }, write: (value: string) => send({ step: "write", value }), }; }; it("keeps the declared handler's writes verified", async () => { const victim = await runVictim(); await runtime.editWithRetry((tx) => victim.key("setName").withTx(tx).send({ name: "owner" }) ); await runtime.idle(); expect(await victim.key("name").pull()).toBe("set:owner"); }); describe("a handler holding a cell", () => { it("cannot claim another handler's verified identity through the cell's transaction", async () => { const attack = await runAttacker( `tx.setCfcImplementationIdentity(${ JSON.stringify(victimHandlerIdentity) }); return 'claimed';`, ); expect(await attack.probe()).toBe( "threw TypeError: Cannot read properties of undefined (reading 'setCfcImplementationIdentity')", ); await attack.write("overwritten"); expect(await attack.victim.key("name").pull()).toBe("initial"); }); it("cannot claim a builtin identity through the cell's transaction", async () => { const attack = await runAttacker( "tx.setCfcImplementationIdentity(" + "{ kind: 'builtin', builtinId: 'generateObject' });" + " return tx.getCfcState().implementationIdentity?.kind;", ); expect(await attack.probe()).toBe( "threw TypeError: Cannot read properties of undefined (reading 'setCfcImplementationIdentity')", ); }); it("cannot name the acting principal through the cell's transaction", async () => { const attack = await runAttacker( "tx.setCfcTrustSnapshot(" + `{ id: "claimed", actingPrincipal: "${OTHER_PRINCIPAL}" });` + " return tx.getCfcState().trustSnapshot?.actingPrincipal;", ); expect(await attack.probe()).toBe( "threw TypeError: Cannot read properties of undefined (reading 'setCfcTrustSnapshot')", ); }); }); });