WIP: toybox: OCapN vats on workerd, with ingress and egress as capabilities #4
No files matched your search
+3
-3
@@ -19,9 +19,9 @@ Every start states the vat's grants, as JSON labels in query parameters; absent
|
||||
|
||||
## Ingress
|
||||
|
||||
A request's labels are its query parameters, plus a `Toybox-Labels` header (JSON) for secret ones. A URL ends up in logs and `Referer`s, so a secret label belongs in the header: a webkey page keeps the secret in its fragment and sends it in-band. A key given twice is a 400.
|
||||
A request's labels are its query parameters; a key given twice is a 400. Routing carries no secrets: a URL ends up in logs and `Referer`s. A secret travels in-band, as on a webkey page: the page keeps it in its fragment, reaches the vat by public labels, and sends it in the body of a call, which the vat checks like a swissnum.
|
||||
|
||||
The request reaches the vat whose mount labels it carries, every one with the same value. That is stricter than Flower's match (no conflicting key): an ingress client holds no constrained handle, so a label it omits must not match, or a secret label would protect nothing. The path is the app's own. The app sees the mount's and the request's labels together in `Toybox-Connection-Labels` (JSON), as a Flower server gets its connection labels. Responses carry `Referrer-Policy: no-referrer`.
|
||||
The request reaches the vat whose mount labels it carries, every one with the same value. That is stricter than Flower's match (no conflicting key): an ingress client holds no constrained handle that supplies labels for it, so it reaches a vat only by naming every label of its mount. The path is the app's own. The app sees the mount's and the request's labels together in `Toybox-Connection-Labels` (JSON), as a Flower server gets its connection labels. Responses carry `Referrer-Policy: no-referrer`.
|
||||
|
||||
## Egress
|
||||
|
||||
@@ -40,7 +40,7 @@ The switchboard has two sockets, so two root handles: `public` (no loopback or p
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm test # real workerds: createVat, upgrade, mounts, secret labels, egress grants and constrain, fetch and WebSocket out, crash and restart
|
||||
pnpm test # real workerds: createVat, upgrade, mounts, egress grants and constrain, fetch and WebSocket out, crash and restart
|
||||
pnpm switchboard # systemd-socket-activate on ./switchboard-public.sock and ./switchboard-local.sock
|
||||
pnpm start # likewise on 127.0.0.1:8787 and ./admin.sock; state in ./state
|
||||
```
|
||||
|
||||
@@ -37,8 +37,8 @@ export const UNMOUNT_LABELS = "DELETE FROM mount_label WHERE vat = ?1";
|
||||
* the vat has. A mount with no labels matches any request.
|
||||
*
|
||||
* Stricter than Flower's match (no conflict): an ingress client holds no
|
||||
* constrained handle, so a label it omits must not match, or a secret
|
||||
* label would protect nothing.
|
||||
* constrained handle that supplies labels for it, so a request reaches a
|
||||
* vat only by naming every label of its mount.
|
||||
*/
|
||||
export const LOOKUP = `
|
||||
SELECT m.vat, m.labels FROM mount m JOIN (
|
||||
|
||||
+10
-9
@@ -320,21 +320,22 @@ export class Directory extends DurableObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* A request's labels: its query parameters, plus a `Toybox-Labels` header
|
||||
* (JSON) for labels too secret for a URL, which ends up in logs and
|
||||
* `Referer`s. A webkey page keeps such a secret in its fragment and sends
|
||||
* it in the header.
|
||||
* A request's labels: its query parameters.
|
||||
*
|
||||
* Routing carries no secrets: a URL ends up in logs and `Referer`s. A
|
||||
* secret travels in-band instead, as on a webkey page: the page keeps it
|
||||
* in its fragment, reaches the vat by public labels, and sends it in the
|
||||
* body of a call, which the vat checks like a swissnum.
|
||||
*
|
||||
* @param {Request} request
|
||||
*/
|
||||
const requestLabels = (request) => {
|
||||
const query = {};
|
||||
const labels = {};
|
||||
for (const [k, v] of new URL(request.url).searchParams) {
|
||||
if (k in query) throw new Error(`label ${k} given twice`);
|
||||
query[k] = v;
|
||||
if (k in labels) throw new Error(`label ${k} given twice`);
|
||||
labels[k] = v;
|
||||
}
|
||||
const header = request.headers.get("Toybox-Labels");
|
||||
return mergeLabels(query, header ? JSON.parse(header) : {});
|
||||
return Object.freeze(labels);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,11 +19,11 @@ const makeDb = () => {
|
||||
|
||||
test("a request reaches the mount whose every label it carries", () => {
|
||||
const { mount, lookup } = makeDb();
|
||||
mount("a", { vat: "alice", key: "s3cret" });
|
||||
mount("a", { vat: "alice", flavor: "plain" });
|
||||
mount("b", { vat: "bob" });
|
||||
assert.equal(lookup({ vat: "alice", key: "s3cret", page: "2" }), "a");
|
||||
assert.equal(lookup({ vat: "alice", flavor: "plain", page: "2" }), "a");
|
||||
assert.equal(lookup({ vat: "alice" }), null);
|
||||
assert.equal(lookup({ vat: "alice", key: "wrong" }), null);
|
||||
assert.equal(lookup({ vat: "alice", flavor: "fancy" }), null);
|
||||
assert.equal(lookup({ vat: "bob" }), "b");
|
||||
assert.equal(lookup({}), null);
|
||||
});
|
||||
|
||||
+10
-7
@@ -171,13 +171,16 @@ test("mounts that could both match a request are refused", async () => {
|
||||
assert.match((await admin.create(src, { mount: { vat: "y", n: "2" } })).vat, /^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("a secret label: in a header, not the URL; omitting it doesn't match", async () => {
|
||||
const secret = "k".repeat(43);
|
||||
await admin.create(await app("show-labels.js"), { mount: { vat: "safe", key: secret } });
|
||||
assert.equal((await pub.get("/?vat=safe")).status, 404);
|
||||
assert.equal((await pub.get("/?vat=safe&key=wrong")).status, 404);
|
||||
const got = await pub.get("/?vat=safe", { "Toybox-Labels": JSON.stringify({ key: secret }) });
|
||||
assert.equal(got.status, 200);
|
||||
test("a request reaches a vat only by naming every label of its mount", async () => {
|
||||
await admin.create(await app("show-labels.js"), { mount: { vat: "both", flavor: "plain" } });
|
||||
assert.equal((await pub.get("/?vat=both")).status, 404);
|
||||
assert.equal((await pub.get("/?vat=both&flavor=fancy")).status, 404);
|
||||
assert.equal((await pub.get("/?vat=both&flavor=plain")).status, 200);
|
||||
});
|
||||
|
||||
test("routing reads no labels from headers", async () => {
|
||||
const got = await pub.get("/?vat=both", { "Toybox-Labels": JSON.stringify({ flavor: "plain" }) });
|
||||
assert.equal(got.status, 404);
|
||||
});
|
||||
|
||||
test("code, state, and mount survive a restart", async () => {
|
||||
|
||||
Reference in new issue
Block a user