The vault's own on a real bus: a node-engine gets its own secrets sealed to its stated key, another machine cannot ask in its name, the controller is refused (issue 1126, ADR 0550 §5)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 87d94d96cca1 on da29f77b1081 | novox/mesh-catalog#505 in group feat/1126-nodes-compose

This commit is contained in:
2026-10-26 19:42:17 +01:00
parent 02ead0d7d2
commit 87d94d96cc
@@ -5,6 +5,8 @@ package main
// key (or a first one) raises `secret-changed` for the machine's core secrets, the same key says nothing.
import (
"git.novox.be/novox/mesh-sdk/go/seal"
"encoding/json"
"os"
"path/filepath"
@@ -100,14 +102,17 @@ authorization {
users = [
{ user: "novox.mesh-vault", password: "pw", permissions: {
publish: { deny: [">"] }
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*"] }
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*", "mesh.seat.mesh-vault.call.own.*"] }
allow_responses: true } }
{ user: "node.ace", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace"] }
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace", "mesh.seat.mesh-vault.call.own.node_ace"] }
subscribe: { allow: ["_INBOX.node.ace.>"] } } }
{ user: "node.g14", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14"] }
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14", "mesh.seat.mesh-vault.call.own.node_g14"] }
subscribe: { allow: ["_INBOX.node.g14.>"] } } }
{ user: "controller", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.own.controller"] }
subscribe: { allow: ["_INBOX.controller.>"] } } }
]
}
`
@@ -135,39 +140,47 @@ func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
t.Cleanup(s.Shutdown)
v := newVault(t, nil)
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key")
tools := map[string]toolDef{"node-key": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key"),
"own": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".own")}
vaultConn := connectAs(t, s.ClientURL(), "novox.mesh-vault")
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call.node-key.*", func(m *nats.Msg) {
serve := func(m *nats.Msg) {
parts := strings.Split(m.Subject, ".")
var args map[string]any
_ = json.Unmarshal(m.Data, &args)
answer := busAnswer{}
if got, err := tool.runAs(untoken(parts[len(parts)-1]), args); err != nil {
if got, err := tools[parts[len(parts)-2]].runAs(untoken(parts[len(parts)-1]), args); err != nil {
answer.Error = err.Error()
} else {
answer.Result, _ = json.Marshal(got)
}
raw, _ := json.Marshal(answer)
_ = m.Respond(raw)
}); err != nil {
t.Fatal(err)
}
for _, verb := range []string{"node-key", "own"} {
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call."+verb+".*", serve); err != nil {
t.Fatal(err)
}
}
_ = vaultConn.Flush()
say := func(user, subject, key string) (string, error) {
raw, _ := json.Marshal(map[string]string{"key": key})
call := func(user, subject string, args any) (json.RawMessage, error) {
raw, _ := json.Marshal(args)
msg, err := connectAs(t, s.ClientURL(), user).Request(subject, raw, 3*time.Second)
if err != nil {
return "", err
return nil, err
}
var a busAnswer
_ = json.Unmarshal(msg.Data, &a)
if a.Error != "" {
return a.Error, errString(a.Error)
return nil, errString(a.Error)
}
return "", nil
return a.Result, nil
}
aceKey, _ := aSealingKey(t)
say := func(user, subject, key string) (string, error) {
_, err := call(user, subject, map[string]string{"key": key})
return "", err
}
aceKey, acePriv := aSealingKey(t)
g14Key, _ := aSealingKey(t)
if _, err := say("node.ace", "mesh.seat.mesh-vault.call.node-key.node_ace", aceKey); err != nil {
t.Fatal(err)
@@ -188,4 +201,32 @@ func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
if got, held, _ := v.Store.NodeKey("g14"); !held || got != g14Key {
t.Errorf("g14's key is %q, %v", got, held)
}
// Issue 1126 (ADR 0547 §2, ADR 0550 §5): `own` on the same bus. node.ace asks its own and gets its module's secret
// sealed to the key it stated, opening with its key alone; g14 cannot ask in ace's name, and asking in its own gets
// none of ace's; the controller is refused, as no node-engine.
if _, err := moveIn(t, v, "node.ace", MoveOf{Node: "ace", Module: "web", Name: "password", Origin: "made", Generation: "g-1",
SealingKey: aceKey}, "ace-web-pw"); err != nil {
t.Fatal(err)
}
raw, err := call("node.ace", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{})
var own OwnAnswer
if err != nil || json.Unmarshal(raw, &own) != nil || own.Node != "ace" || len(own.Own) != 1 {
t.Fatalf("ace's own over the bus: %s, %v", raw, err)
}
if value, err := seal.Open(own.Own[0].Sealed, acePriv); err != nil || string(value) != "ace-web-pw" {
t.Errorf("ace's secret does not open with ace's key: %q, %v", value, err)
}
if strings.Contains(string(raw), "ace-web-pw") {
t.Error("ace's secret crossed the bus in the clear")
}
if _, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{}); err == nil {
t.Error("node.g14 asked ace's own in ace's name")
}
if raw, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_g14", map[string]any{}); err != nil || strings.Contains(string(raw), "web") {
t.Errorf("g14 asking its own was answered %s, %v", raw, err)
}
if _, err := call("controller", "mesh.seat.mesh-vault.call.own.controller", map[string]any{}); err == nil || !strings.Contains(err.Error(), "not a node-engine") {
t.Errorf("the controller asking own: %v", err)
}
}