/** Host-control regression tests use real client/router signatures and durable custody. */ // @ts-types="@types/ws" import type WebSocket from "ws"; import { assert, assertEquals, assertRejects } from "@std/assert"; import { expect } from "@std/expect"; import { Identity } from "@commonfabric/identity"; import { sha256 } from "@commonfabric/content-hash"; import { getMemoryProtocolFlags } from "../v2.ts"; import { RoutedEpochStore } from "../v2/routed-epochs.ts"; import { RoutedMemoryHost } from "../v2/routed-host.ts"; import { routedFlags } from "../v2/routed-parser.ts"; import { MAX_ROUTED_LEASE_SECONDS, readRoutedProof, RoutedReader, routedStatementPayload, RoutedWriter, verifyRoutedRecord, } from "../v2/routed-wire.ts"; import { Server } from "../v2/server.ts"; class Socket extends EventTarget { readyState = 1; bufferedAmount = 0; binaryType = "arraybuffer"; output: Uint8Array[] = []; changed = Promise.withResolvers(); send(bytes: string | Uint8Array, done?: (error?: Error) => void) { this.output.push( typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes, ); this.changed.resolve(); done?.(); } close() { if (this.readyState !== 3) { this.readyState = 3; this.changed.resolve(); this.dispatchEvent(new Event("close")); } } receive(bytes: Uint8Array) { this.dispatchEvent( new MessageEvent("message", { data: bytes.slice().buffer }), ); } async take(): Promise { while (!this.output.length) { assertEquals(this.readyState, 1); this.changed = Promise.withResolvers(); await this.changed.promise; } return this.output.shift()!; } } Deno.test("toolshed independently refuses context/epoch/proof replay and isolates router revocation", async () => { const root = Deno.makeTempDirSync(), now = Math.floor(Date.now() / 1000); const [toolshed, router, other, client, space] = await Promise.all( [91, 92, 93, 94, 95].map((seed) => Identity.fromRaw(new Uint8Array(32).fill(seed)) ), ); const store = new RoutedEpochStore(`${root}/ledger`); const server = new Server({ store: new URL("memory://routed-host-proof-custody"), acl: { mode: "enforce" }, ownsSpace: (did) => did === space.did(), requireExplicitAcl: true, authorizeSessionOpen: () => undefined, sessionOpenAuth: { audience: toolshed.did() }, }); const host = new RoutedMemoryHost({ server, identity: toolshed, deployment: "fixture", routers: new Map( [router, other].map((r) => [r.did(), new Set(["127.0.0.1"])]), ), ownership: (did) => did === space.did() ? 1 : undefined, epochs: store, }); const flags = routedFlags({ ...getMemoryProtocolFlags(), modernCellRep: true, connectionAuth: true, routedAuthV1: true, }); async function link(identity: Identity, epoch: Uint8Array) { const socket = new Socket(); host.accept( socket as unknown as WebSocket, "/memory/router-link", "127.0.0.1", ); const hello = new RoutedReader( await verifyRoutedRecord(await socket.take(), toolshed.did()), "rph1", ); assertEquals(hello.text(), toolshed.did()); const nonce = hello.fixed(32); hello.time(); expect(JSON.parse(new TextDecoder().decode(hello.blob()))) .toMatchObject({ routerEncodingNegotiation: true, routerRequestCredit: true, }); hello.end(); expect( Object.hasOwn(server.memoryProtocolFlags(), "routerEncodingNegotiation"), ) .toBe(false); expect( Object.hasOwn(server.memoryProtocolFlags(), "routerRequestCredit"), ) .toBe(false); socket.receive( await new RoutedWriter("rpc1").text("fixture").text(identity.did()).text( toolshed.did(), ).fixed(epoch).fixed(nonce).fixed(new Uint8Array([1])).sign(identity), ); assertEquals(new TextDecoder().decode(await socket.take()), "rpo1"); let sequence = 0; return { socket, request: async (op: number, payload: Uint8Array) => { socket.receive( new RoutedWriter("mlq1").time(++sequence).fixed(new Uint8Array([op])) .blob(payload).bytes, ); const reply = new RoutedReader(await socket.take(), "mls1"); assertEquals(reply.time(), sequence); const status = reply.fixed(1)[0], bytes = reply.blob(); reply.end(); return { status, bytes }; }, }; } const epoch = new Uint8Array(16).fill(1), context = new Uint8Array(16).fill(2), challenge = new Uint8Array(32).fill(3); const statement = await routedStatementPayload({ principal: client.did(), router: router.did(), deployment: "fixture", challenge, iat: now, exp: now + 600, }).sign(client); async function proof(ctx = context, ep = epoch) { const issuance = await new RoutedWriter("mrc1").text("fixture").text( router.did(), ).fixed(ep).fixed(ctx).fixed(challenge).time(now).time(now + 60).sign( router, ); const receipt = await new RoutedWriter("mrr1").fixed(sha256(issuance)).text( client.did(), ).fixed(sha256(statement)).time(now).sign(router); return new RoutedWriter("mrp1").blob(statement).blob(issuance).blob(receipt) .bytes; } const admit = (ctx: Uint8Array, p: Uint8Array) => new RoutedWriter("mvp1").fixed(ctx).blob(flags).blob(p).bytes; try { for (const capability of [[], [0], [1, 0]]) { const rejected = new Socket(); host.accept( rejected as unknown as WebSocket, "/memory/router-link", "127.0.0.1", ); const hello = new RoutedReader( await verifyRoutedRecord(await rejected.take(), toolshed.did()), "rph1", ); hello.text(); const nonce = hello.fixed(32); rejected.receive( await new RoutedWriter("rpc1").text("fixture").text(router.did()).text( toolshed.did(), ) .fixed(epoch).fixed(nonce).fixed(new Uint8Array(capability)).sign( router, ), ); await assertRejects(() => rejected.take()); expect(rejected.readyState).toBe(3); expect(rejected.output).toHaveLength(0); } const first = await link(router, epoch), independent = await link(other, new Uint8Array(16).fill(8)); const original = await proof(); readRoutedProof(original); assertEquals((await first.request(6, admit(context, original))).status, 0); assertEquals((await first.request(6, admit(context, original))).status, 0); assertEquals( (await first.request( 6, admit( new Uint8Array(16).fill(4), await proof(new Uint8Array(16).fill(4)), ), )).status, 1, ); assertEquals(first.socket.readyState, 1); assertEquals( (await first.request( 4, new RoutedWriter("mrl1").fixed(context).text(client.did()).bytes, )).status, 0, ); assertEquals((await first.request(6, admit(context, original))).status, 1); const renewed = await link(router, new Uint8Array(16).fill(5)); assertEquals(first.socket.readyState, 3); // The replaced link closed every context on it and dropped their // statements, so the router may present one again on its new link // within the statement's lease. This is deliberate: a router that holds // its contexts open keeps that authority anyway. assertEquals( (await renewed.request( 6, admit(context, await proof(context, new Uint8Array(16).fill(5))), )).status, 0, ); host.revokeRouter(router.did()); assertEquals(renewed.socket.readyState, 3); assertEquals(independent.socket.readyState, 1); assertEquals( (await independent.request(6, admit(context, original))).status, 1, ); assert(independent.socket.readyState === 1); // Losing durable custody must close every context, including ones other // than the context whose proof found the ledger gone. const otherProof = async (ctx: Uint8Array, seed: number) => { const challenge = new Uint8Array(32).fill(seed); const statement = await routedStatementPayload({ principal: client.did(), router: other.did(), deployment: "fixture", challenge, iat: now, exp: now + 600, }).sign(client); const issuance = await new RoutedWriter("mrc1").text("fixture").text( other.did(), ) .fixed(new Uint8Array(16).fill(8)).fixed(ctx).fixed(challenge).time(now) .time(now + 60).sign(other); const receipt = await new RoutedWriter("mrr1").fixed(sha256(issuance)) .text(client.did()).fixed(sha256(statement)).time(now).sign(other); return new RoutedWriter("mrp1").blob(statement).blob(issuance).blob( receipt, ).bytes; }; const contexts = [new Uint8Array(16).fill(31), new Uint8Array(16).fill(32)]; for (const ctx of contexts) { assertEquals( (await independent.request( 6, admit(ctx, await otherProof(ctx, ctx[0])), )) .status, 0, ); } store.close(); await assertRejects(async () => independent.request( 6, admit(contexts[0], await otherProof(contexts[0], 33)), ) ); assertEquals(independent.socket.readyState, 3); } finally { host.close(); await server.close(); store.close(); Deno.removeSync(root, { recursive: true }); } }); Deno.test("a closed context ID may open again at once", async () => { const root = Deno.makeTempDirSync(); const clock = Math.floor(Date.now() / 1000); const [toolshed, router, client, space] = await Promise.all( [101, 102, 103, 104].map((seed) => Identity.fromRaw(new Uint8Array(32).fill(seed)) ), ); const store = new RoutedEpochStore(`${root}/ledger`); const host = new RoutedMemoryHost({ server: new Server({ store: new URL("memory://routed-host-closed-context-expiry"), acl: { mode: "enforce" }, ownsSpace: (did) => did === space.did(), requireExplicitAcl: true, authorizeSessionOpen: () => undefined, sessionOpenAuth: { audience: toolshed.did() }, }), identity: toolshed, deployment: "fixture", routers: new Map([[router.did(), new Set(["127.0.0.1"])]]), ownership: (did) => did === space.did() ? 1 : undefined, epochs: store, now: () => clock, }); const flags = routedFlags({ ...getMemoryProtocolFlags(), modernCellRep: true, connectionAuth: true, routedAuthV1: true, }); const epoch = new Uint8Array(16).fill(1), context = new Uint8Array(16).fill(2); const socket = new Socket(); host.accept( socket as unknown as WebSocket, "/memory/router-link", "127.0.0.1", ); const hello = new RoutedReader((await socket.take()).slice(0, -64), "rph1"); hello.text(); socket.receive( await new RoutedWriter("rpc1").text("fixture").text(router.did()).text( toolshed.did(), ).fixed(epoch).fixed(hello.fixed(32)).fixed(new Uint8Array([1])).sign( router, ), ); assertEquals(new TextDecoder().decode(await socket.take()), "rpo1"); let sequence = 0; const request = async (op: number, payload: Uint8Array) => { socket.receive( new RoutedWriter("mlq1").time(++sequence).fixed(new Uint8Array([op])) .blob(payload).bytes, ); const reply = new RoutedReader(await socket.take(), "mls1"); assertEquals(reply.time(), sequence); return reply.fixed(1)[0]; }; /** A fresh client statement and router evidence for `context` at `clock`. */ const admit = async (seed: number) => { const challenge = new Uint8Array(32).fill(seed); const statement = await routedStatementPayload({ principal: client.did(), router: router.did(), deployment: "fixture", challenge, iat: clock, exp: clock + MAX_ROUTED_LEASE_SECONDS, }).sign(client); const issuance = await new RoutedWriter("mrc1").text("fixture").text( router.did(), ).fixed(epoch).fixed(context).fixed(challenge).time(clock) .time(clock + 60).sign(router); const receipt = await new RoutedWriter("mrr1").fixed(sha256(issuance)) .text(client.did()).fixed(sha256(statement)).time(clock).sign(router); const proof = new RoutedWriter("mrp1").blob(statement).blob(issuance) .blob(receipt).bytes; return await request( 6, new RoutedWriter("mvp1").fixed(context).blob(flags).blob(proof).bytes, ); }; try { assertEquals(await admit(10), 0); assertEquals(await request(3, context), 0); // A closed context's statements leave with it, so refusing its ID would // protect nothing: a router may present them in any new context anyway. assertEquals(await admit(11), 0); assertEquals(await request(3, context), 0); assertEquals(await admit(10), 0); } finally { host.close(); store.close(); Deno.removeSync(root, { recursive: true }); } });