diff --git a/README.md b/README.md index a74d626..d440a3b 100644 --- a/README.md +++ b/README.md @@ -195,3 +195,29 @@ repository carries implementation and does not carry decisions. - `02-DECISIONS/0039-the-link-is-the-security-boundary.md` — a node owns no password - `02-DECISIONS/0041-the-host-depends-on-nothing.md` — why this is a static binary, and Go - `04-ISSUES/007-an-installed-package-is-not-a-capability` — why detection works this way + +## Checks that cross into the control plane's repository + +Two things are agreed between this repository and `novox/mesh-control`, and each is a separate +struct on each side. A field renamed on one of them fails **silently** — the crossing succeeds and +something is simply absent — so both are checked by handing one side's real output to the other's +real parser. Neither runs by default; each skips with a reason, because a repository that fails +without its neighbour checked out is a repository nobody can build. + +**What the mesh sends, read by this host:** + +``` +mesh-control: ./build/mesh-control plan --json > /tmp/d.json +mesh-host: MESH_EMITTED=/tmp/d.json go test ./internal/declaration/ -v +``` + +**What this node says when it joins, read by the mesh:** + +``` +mesh-host: MESH_ENROL_OUT=/tmp/enrol.json go test ./internal/link/ +mesh-control: MESH_ENROL=/tmp/enrol.json make check +``` + +The second writes the private half of the sealing key beside the request, so the mesh's suite can +prove that what it sealed is openable rather than merely present. A key that is correctly named +and simply *wrong* passes every check that only looks at the message. diff --git a/internal/link/enrol_shape_test.go b/internal/link/enrol_shape_test.go new file mode 100644 index 0000000..3100a4a --- /dev/null +++ b/internal/link/enrol_shape_test.go @@ -0,0 +1,62 @@ +package link + +import ( + "encoding/json" + "os" + "testing" + + "github.com/novox/mesh-host/internal/identity" +) + +// What this node says when it joins, written out so the mesh's own suite can accept it. +// +// The two ends are separate structs in separate repositories. Every field here is one somebody +// could rename on one side, and the failure would be silent: enrolment succeeds, a key is simply +// absent, and the node looks joined until the first thing sealed to it cannot be opened. That is +// exactly the shape of fault this project keeps finding late. +// +// Skipped unless MESH_ENROL_OUT names a file, so this is a check somebody runs deliberately +// rather than a dependency between two repositories. +func TestWhatThisNodeSaysWhenItJoins(t *testing.T) { + path := os.Getenv("MESH_ENROL_OUT") + if path == "" { + t.Skip("set MESH_ENROL_OUT to write the enrolment request the mesh's suite reads") + } + + // A real one. Generated the way enrolment generates them rather than typed as literals, so a + // key that stopped being a key would be caught here rather than travelling. + mine, err := identity.Generate("workstation") + if err != nil { + t.Fatal(err) + } + overlay, err := identity.GenerateOverlayKey() + if err != nil { + t.Fatal(err) + } + sealing, err := identity.GenerateSealingKey() + if err != nil { + t.Fatal(err) + } + + request := EnrolRequest{ + Node: "workstation", + Secret: "a-one-time-secret", + PublicKey: mine.Public, + OverlayKey: overlay.Public, + SealingKey: sealing.Public, + Profile: map[string]any{"seat": true}, + } + body, err := json.MarshalIndent(request, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, append(body, '\n'), 0o644); err != nil { + t.Fatal(err) + } + // The private half of the sealing key goes beside it, so the mesh's suite can prove what it + // sealed is openable rather than merely present. + if err := os.WriteFile(path+".sealing-private", []byte(sealing.Private), 0o600); err != nil { + t.Fatal(err) + } + t.Logf("wrote %s", path) +}